{"thread":{"id":"64670","subject":"[PATCH 0/9] Implement `promisor.storeFields` and `--filter=auto`","startedAt":"2025-12-23T11:11:33Z","lastAt":"2026-07-04T09:49:50Z","messageCount":111,"participants":["Christian Couder","Jean-Noël AVILA","Patrick Steinhardt","Junio C Hamano","Jeff King","Toon Claes","Kristoffer Haugsbakk"],"isPatch":true,"patchVersion":1,"patchTotal":9},"messages":[{"id":"532643","messageId":"20251223111113.47473-1-christian.couder@gmail.com","threadId":"64670","inReplyTo":null,"subject":"[PATCH 0/9] Implement `promisor.storeFields` and `--filter=auto`","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-12-23T11:11:04Z","receivedAt":"2025-12-23T11:11:33Z","isPatch":true,"body":"Introduction\n============\n\nA previous patch series added the possibility to pass additional\nfields, a \"partialCloneFilter\" and a \"token\" for each advertised\npromisor remote, from a server to a client through the\n\"promisor-remote\" capability.\n\nOn the client side though, it has so far only been possible to use\nthis new information to compare it with local information and then\ndecide if the corresponding advertised promisor remote is accepted or\nnot.\n\nFor the \"token\" it would be useful if it could be stored on the\nclient. For example in a setup where the client uses specialized\nremote helpers which need a token to access the promisor remotes\nadvertised by the server, storing the token would allow the token to\nbe used when the client directly accesses a promisor remote for\nexample to lazy fetch some blobs it now needs.\n\nTo enable such a workflow, where the server can rotate tokens and the\nclient can have updated tokens from the server by simply fetching from\nit, the first part of this series introduces a new\n\"promisor.storeFields\" configuration option on the client side,\nsimilar to the \"promisor.checkFields\" configuration option. When field\nnames, \"token\" or \"partialCloneFilter\", are listed in this new\nconfiguration option, then the values of these field names transmitted\nby the server are stored in the local configuration on the client\nside.\n\nNote that for security reasons, the corresponding remote name and url\nof the advertised promisor remotes must have already been configured\non the client side. No new remote name nor url are configured.\n\nFor the \"partialCloneFilter\" field, simply storing the value is not\nenough to enable dynamic updates. Currently, when a user initiates a\npartial clone with `--filter=<filter-spec>`, that specific\n<filter-spec> is saved in the client's local configuration (e.g.,\nremote.origin.partialCloneFilter). Subsequent fetches then reuse this\nvalue, ignoring suggestions from the server.\n\nTo avoid breaking this mechanism and still be able to use the\n<filter-spec> that the server suggests for the promisor remotes that\nthe client accepts, the second part of this series introduces a new\n`--filter=auto` mode for `git clone` and `git fetch`.\n\nWhen `--filter=auto` is used, then \"auto\" is still saved as the\n<filter-spec> for the server locally on the client, and then when a\nfetch-pack happens, instead of passing just \"auto\", the actual filter\nrequested by the client is computed by combining the <filter-spec>s\nthat the server suggested for the promisor remotes that the client\naccepted. This uses the \"combine\" filter mechanism that already exists\nin \"list-objects-filter-options.{c,h}\".\n\nThis way by just using `--filter=auto` when cloning, a client makes\nsure it will use the <filter-spec>s suggested by the server for the\npromisor remotes it accepts.\n\nThis work is part of the \"LOP\" effort documented in:\n\n  Documentation/technical/large-object-promisors.adoc\n\nSee that doc for more information on the broader context.\n\nOverview of the patches\n=======================\n\nPatches 1/9 and 2/9 are the first part of the series and implement the\nnew \"promisor.storeFields\" configuration option. Patch 1/9 is a small\npreparatory refactoring.\n\nPatches from 3/9 to 9/9 implement the `--filter=auto` option:\n\n  - Patches 3/9 and 4/9 are cleanups of \"builtin/clone.c\" and\n    \"builtin/fetch.c\" respectively that make the `filter_options`\n    variable local to cmd_clone() or cmd_fetch().\n\n  - Patch 5/9 is a doc update as `--filter=<filter-spec>` wasn't\n    documented for `git fetch`.\n\n  - Patches 6/9 and 7/9 improve \"list-objects-filter-options.{c,h}\" to\n    support the new 'auto' mode.\n\n  - Patch 8/9 improves \"promisor-remote.{c,h}\" to support the new\n    'auto' mode.\n\n  - Patch 9/9 make the new 'auto' mode actually work by wiring up\n    everything together.\n\nCI Report\n=========\n\nAll the tests pass, see:\n\nhttps://github.com/chriscool/git/actions/runs/20455758377\n\nChristian Couder (9):\n  promisor-remote: refactor initialising field lists\n  promisor-remote: allow a client to store fields\n  clone: make filter_options local to cmd_clone()\n  fetch: make filter_options local to cmd_fetch()\n  doc: fetch: document `--filter=<filter-spec>` option\n  list-objects-filter-options: support 'auto' mode for --filter\n  list-objects-filter-options: implement auto filter resolution\n  promisor-remote: keep advertised filter in memory\n  fetch-pack: wire up and enable auto filter logic\n\n Documentation/config/promisor.adoc           |  33 +++\n Documentation/fetch-options.adoc             |  19 ++\n Documentation/git-clone.adoc                 |  25 ++-\n Documentation/gitprotocol-v2.adoc            |  24 +-\n Makefile                                     |   1 +\n builtin/clone.c                              |  18 +-\n builtin/fetch.c                              |  50 +++--\n fetch-pack.c                                 |  20 ++\n list-objects-filter-options.c                |  71 +++++-\n list-objects-filter-options.h                |  25 +++\n list-objects-filter.c                        |   8 +\n promisor-remote.c                            | 222 +++++++++++++++++--\n promisor-remote.h                            |   6 +\n t/meson.build                                |   1 +\n t/t5710-promisor-remote-capability.sh        | 109 +++++++++\n t/unit-tests/u-list-objects-filter-options.c |  86 +++++++\n transport.c                                  |   1 +\n 17 files changed, 663 insertions(+), 56 deletions(-)\n create mode 100644 t/unit-tests/u-list-objects-filter-options.c\n\n-- \n2.52.0.319.gfcaffa7898\n\n"},{"id":"532644","messageId":"20251223111113.47473-2-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20251223111113.47473-1-christian.couder@gmail.com","subject":"[PATCH 1/9] promisor-remote: refactor initialising field lists","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-12-23T11:11:05Z","receivedAt":"2025-12-23T11:11:35Z","isPatch":true,"body":"In \"promisor-remote.c\", the fields_sent() and fields_checked()\nfunctions serve similar purposes and contain a small amount of\nduplicated code.\n\nAs we are going to add a similar function in a following commit,\nlet's refactor this common code into a new initialize_fields_list()\nfunction.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 28 ++++++++++++++--------------\n 1 file changed, 14 insertions(+), 14 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 77ebf537e2..5d8151cedb 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -375,18 +375,24 @@ static char *fields_from_config(struct string_list *fields_list, const char *con\n \treturn fields;\n }\n \n+static struct string_list *initialize_fields_list(struct string_list *fields_list, int *initialized,\n+\t\t\t\t\t\t  const char *config_key)\n+{\n+\tif (!*initialized) {\n+\t\tfields_list->cmp = strcasecmp;\n+\t\tfields_from_config(fields_list, config_key);\n+\t\t*initialized = 1;\n+\t}\n+\n+\treturn fields_list;\n+}\n+\n static struct string_list *fields_sent(void)\n {\n \tstatic struct string_list fields_list = STRING_LIST_INIT_NODUP;\n \tstatic int initialized;\n \n-\tif (!initialized) {\n-\t\tfields_list.cmp = strcasecmp;\n-\t\tfields_from_config(&fields_list, \"promisor.sendFields\");\n-\t\tinitialized = 1;\n-\t}\n-\n-\treturn &fields_list;\n+\treturn initialize_fields_list(&fields_list, &initialized, \"promisor.sendFields\");\n }\n \n static struct string_list *fields_checked(void)\n@@ -394,13 +400,7 @@ static struct string_list *fields_checked(void)\n \tstatic struct string_list fields_list = STRING_LIST_INIT_NODUP;\n \tstatic int initialized;\n \n-\tif (!initialized) {\n-\t\tfields_list.cmp = strcasecmp;\n-\t\tfields_from_config(&fields_list, \"promisor.checkFields\");\n-\t\tinitialized = 1;\n-\t}\n-\n-\treturn &fields_list;\n+\treturn initialize_fields_list(&fields_list, &initialized, \"promisor.checkFields\");\n }\n \n /*\n-- \n2.52.0.319.gfcaffa7898\n\n"},{"id":"532645","messageId":"20251223111113.47473-3-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20251223111113.47473-1-christian.couder@gmail.com","subject":"[PATCH 2/9] promisor-remote: allow a client to store fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-12-23T11:11:06Z","receivedAt":"2025-12-23T11:11:36Z","isPatch":true,"body":"A previous commit allowed a server to pass additional fields through\nthe \"promisor-remote\" protocol capability after the \"name\" and \"url\"\nfields, specifically the \"partialCloneFilter\" and \"token\" fields.\n\nAnother previous commit, c213820c51 (promisor-remote: allow a client\nto check fields, 2025-09-08), has made it possible for a client to\ndecide if it accepts a promisor remote advertised by a server based\non these additional fields.\n\nOften though, it would be interesting for the client to just store in\nits configuration files these additional fields passed by the server,\nso that it can use them when needed.\n\nFor example if a token is necessary to access a promisor remote, that\ntoken could be updated frequently only on the server side and then\npassed to all the clients through the \"promisor-remote\" capability,\navoiding the need to update it on all the clients manually.\n\nStoring the token on the client side makes sure that the token is\navailable when the client needs to access the promisor remotes for a\nlazy fetch.\n\nIn the same way, if it appears that it's better to use a different\nfilter to access a promisor remote, it could be helpful if the client\ncould automatically use it.\n\nTo allow this, let's introduce a new \"promisor.storeFields\"\nconfiguration variable.\n\nLike \"promisor.checkFields\" and \"promisor.sendFields\", it should\ncontain a comma or space separated list of field names. Only the\n\"partialCloneFilter\" and \"token\" field names are supported for now.\n\nWhen a server advertises a promisor remote, for example \"foo\", along\nwith for example \"token=XXXXX\" to a client, and on the client side\n\"promisor.storeFields\" contains \"token\", then the client will store\nXXXXX for the \"remote.foo.token\" variable in its configuration file\nand reload its configuration so it can immediately use this new\nconfiguration variable.\n\nA message is emitted on stderr to warn users when the config is\nchanged.\n\nNote that even if \"promisor.acceptFromServer\" is set to \"all\", a\npromisor remote has to be already configured on the client side for\nsome of its config to be changed. In any case no new remote is\nconfigured and no new URL is stored.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    |  33 ++++++\n Documentation/gitprotocol-v2.adoc     |  12 ++-\n promisor-remote.c                     | 148 +++++++++++++++++++++++++-\n t/t5710-promisor-remote-capability.sh |  49 +++++++++\n 4 files changed, 236 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex 93e5e0d9b5..b0fa43b839 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -89,3 +89,36 @@ variable. The fields are checked only if the\n `promisor.acceptFromServer` config variable is not set to \"None\". If\n set to \"None\", this config variable has no effect. See\n linkgit:gitprotocol-v2[5].\n+\n+promisor.storeFields::\n+\tA comma or space separated list of additional remote related\n+\tfield names. If a client accepts an advertised remote, the\n+\tclient will store the values associated with these field names\n+\ttaken from the remote advertisement into its configuration,\n+\tand then reload its remote configuration. Currently,\n+\t\"partialCloneFilter\" and \"token\" are the only supported field\n+\tnames.\n++\n+For example if a server advertises \"partialCloneFilter=blob:limit=20k\"\n+for remote \"foo\", and that remote is accepted, then \"blob:limit=20k\"\n+will be stored for the \"remote.foo.partialCloneFilter\" configuration\n+variable.\n++\n+If the new field value from an advertised remote is the same as the\n+existing field value for that remote on the client side, then no\n+change is made to the client configuration though.\n++\n+When a new value is stored, a message is printed to standard error to\n+let users know about this.\n++\n+Note that for security reasons, if the remote is not already\n+configured on the client side, nothing will be stored for that\n+remote. In any case, no new remote will be created and no URL will be\n+stored.\n++\n+Before storing a partial clone filter, it's parsed to check it's\n+valid. If it's not, a warning is emitted and it's not stored.\n++\n+Before storing a token, a check is performed to ensure it contains no\n+control character. If the check fails, a warning is emitted and it's\n+not stored.\ndiff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\nindex c7db103299..d93dd279ea 100644\n--- a/Documentation/gitprotocol-v2.adoc\n+++ b/Documentation/gitprotocol-v2.adoc\n@@ -826,9 +826,10 @@ are case-sensitive and MUST be transmitted exactly as specified\n above. Clients MUST ignore fields they don't recognize to allow for\n future protocol extensions.\n \n-For now, the client can only use information transmitted through these\n-fields to decide if it accepts the advertised promisor remote. In the\n-future that information might be used for other purposes though.\n+The client can use information transmitted through these fields to\n+decide if it accepts the advertised promisor remote. Also, the client\n+can be configured to store the values of these fields (see\n+\"promisor.storeFields\" in linkgit:git-config[1]).\n \n Field values MUST be urlencoded.\n \n@@ -856,8 +857,9 @@ the server advertised, the client shouldn't advertise the\n On the server side, the \"promisor.advertise\" and \"promisor.sendFields\"\n configuration options can be used to control what it advertises. On\n the client side, the \"promisor.acceptFromServer\" configuration option\n-can be used to control what it accepts. See the documentation of these\n-configuration options for more information.\n+can be used to control what it accepts, and the \"promisor.storeFields\"\n+option, to control what it stores. See the documentation of these\n+configuration options in linkgit:git-config[1] for more information.\n \n Note that in the future it would be nice if the \"promisor-remote\"\n protocol capability could be used by the server, when responding to\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 5d8151cedb..8d6d2d7b76 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -403,6 +403,14 @@ static struct string_list *fields_checked(void)\n \treturn initialize_fields_list(&fields_list, &initialized, \"promisor.checkFields\");\n }\n \n+static struct string_list *fields_stored(void)\n+{\n+\tstatic struct string_list fields_list = STRING_LIST_INIT_NODUP;\n+\tstatic int initialized;\n+\n+\treturn initialize_fields_list(&fields_list, &initialized, \"promisor.storeFields\");\n+}\n+\n /*\n  * Struct for promisor remotes involved in the \"promisor-remote\"\n  * protocol capability.\n@@ -692,6 +700,132 @@ static struct promisor_info *parse_one_advertised_remote(const char *remote_info\n \treturn info;\n }\n \n+static bool store_one_field(struct repository *repo, const char *remote_name,\n+\t\t\t    const char *field_name, const char *field_key,\n+\t\t\t    const char *advertised, const char *current)\n+{\n+\tif (advertised && (!current || strcmp(current, advertised))) {\n+\t\tchar *key = xstrfmt(\"remote.%s.%s\", remote_name, field_key);\n+\n+\t\tfprintf(stderr, _(\"Storing new %s from server for remote '%s'.\\n\"\n+\t\t\t\t  \"    '%s' -> '%s'\\n\"),\n+\t\t\tfield_name, remote_name,\n+\t\t\tcurrent ? current : \"\",\n+\t\t\tadvertised);\n+\n+\t\trepo_config_set_worktree_gently(repo, key, advertised);\n+\t\tfree(key);\n+\n+\t\treturn true;\n+\t}\n+\n+\treturn false;\n+}\n+\n+/* Check that a filter is valid by parsing it */\n+static bool valid_filter(const char *filter, const char *remote_name)\n+{\n+\tstruct list_objects_filter_options filter_opts = LIST_OBJECTS_FILTER_INIT;\n+\tstruct strbuf err = STRBUF_INIT;\n+\tint res = gently_parse_list_objects_filter(&filter_opts, filter, &err);\n+\n+\tif (res)\n+\t\twarning(_(\"invalid filter '%s' for remote '%s' \"\n+\t\t\t  \"will not be stored: %s\"),\n+\t\t\tfilter, remote_name, err.buf);\n+\n+\tlist_objects_filter_release(&filter_opts);\n+\tstrbuf_release(&err);\n+\n+\treturn !res;\n+}\n+\n+/* Check that a token doesn't contain any control character */\n+static bool valid_token(const char *token, const char *remote_name)\n+{\n+\tconst char *c = token;\n+\n+\tfor (; *c; c++)\n+\t\tif (iscntrl(*c)) {\n+\t\t\twarning(_(\"invalid token '%s' for remote '%s' \"\n+\t\t\t\t  \"will not be stored\"),\n+\t\t\t\ttoken, remote_name);\n+\t\t\treturn false;\n+\t\t}\n+\n+\treturn true;\n+}\n+\n+struct store_info {\n+\tstruct repository *repo;\n+\tstruct string_list config_info;\n+\tbool store_filter;\n+\tbool store_token;\n+};\n+\n+static struct store_info *new_store_info(struct repository *repo)\n+{\n+\tstruct string_list *fields_to_store = fields_stored();\n+\tstruct store_info *s = xmalloc(sizeof(*s));\n+\n+\ts->repo = repo;\n+\n+\tstring_list_init_nodup(&s->config_info);\n+\tpromisor_config_info_list(repo, &s->config_info, fields_to_store);\n+\tstring_list_sort(&s->config_info);\n+\n+\ts->store_filter = !!string_list_lookup(fields_to_store, promisor_field_filter);\n+\ts->store_token = !!string_list_lookup(fields_to_store, promisor_field_token);\n+\n+\treturn s;\n+}\n+\n+static void free_store_info(struct store_info *s)\n+{\n+\tif (s) {\n+\t\tpromisor_info_list_clear(&s->config_info);\n+\t\tfree(s);\n+\t}\n+}\n+\n+static bool promisor_store_advertised_fields(struct promisor_info *advertised,\n+\t\t\t\t\t     struct store_info *store_info)\n+{\n+\tstruct promisor_info *p;\n+\tstruct string_list_item *item;\n+\tconst char *remote_name = advertised->name;\n+\tbool reload_config = false;\n+\n+\tif (!(store_info->store_filter || store_info->store_token))\n+\t\treturn false;\n+\n+\t/*\n+\t * Get existing config info for the advertised promisor\n+\t * remote. This ensures the remote is already configured on\n+\t * the client side.\n+\t */\n+\titem = string_list_lookup(&store_info->config_info, remote_name);\n+\n+\tif (!item)\n+\t\treturn false;\n+\n+\tp = item->util;\n+\n+\tif (store_info->store_filter && advertised->filter &&\n+\t    valid_filter(advertised->filter, remote_name))\n+\t\treload_config |= store_one_field(store_info->repo, remote_name,\n+\t\t\t\t\t\t \"filter\", promisor_field_filter,\n+\t\t\t\t\t\t advertised->filter, p->filter);\n+\n+\tif (store_info->store_token && advertised->token &&\n+\t    valid_token(advertised->token, remote_name))\n+\t\treload_config |= store_one_field(store_info->repo, remote_name,\n+\t\t\t\t\t\t \"token\", promisor_field_token,\n+\t\t\t\t\t\t advertised->token, p->token);\n+\n+\treturn reload_config;\n+}\n+\n static void filter_promisor_remote(struct repository *repo,\n \t\t\t\t   struct strvec *accepted,\n \t\t\t\t   const char *info)\n@@ -700,7 +834,9 @@ static void filter_promisor_remote(struct repository *repo,\n \tenum accept_promisor accept = ACCEPT_NONE;\n \tstruct string_list config_info = STRING_LIST_INIT_NODUP;\n \tstruct string_list remote_info = STRING_LIST_INIT_DUP;\n+\tstruct store_info *store_info = NULL;\n \tstruct string_list_item *item;\n+\tbool reload_config = false;\n \n \tif (!repo_config_get_string_tmp(the_repository, \"promisor.acceptfromserver\", &accept_str)) {\n \t\tif (!*accept_str || !strcasecmp(\"None\", accept_str))\n@@ -736,14 +872,24 @@ static void filter_promisor_remote(struct repository *repo,\n \t\t\tstring_list_sort(&config_info);\n \t\t}\n \n-\t\tif (should_accept_remote(accept, advertised, &config_info))\n+\t\tif (should_accept_remote(accept, advertised, &config_info)) {\n+\t\t\tif (!store_info)\n+\t\t\t\tstore_info = new_store_info(repo);\n+\t\t\tif (promisor_store_advertised_fields(advertised, store_info))\n+\t\t\t\treload_config = true;\n+\n \t\t\tstrvec_push(accepted, advertised->name);\n+\t\t}\n \n \t\tpromisor_info_free(advertised);\n \t}\n \n \tpromisor_info_list_clear(&config_info);\n \tstring_list_clear(&remote_info, 0);\n+\tfree_store_info(store_info);\n+\n+\tif (reload_config)\n+\t\trepo_promisor_remote_reinit(repo);\n }\n \n char *promisor_remote_reply(const char *info)\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex 023735d6a8..a726af214a 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -360,6 +360,55 @@ test_expect_success \"clone with promisor.checkFields\" '\n \tcheck_missing_objects server 1 \"$oid\"\n '\n \n+test_expect_success \"clone with promisor.storeFields=partialCloneFilter\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\tgit -C server remote add otherLop \"https://invalid.invalid\"  &&\n+\tgit -C server config remote.otherLop.token \"fooBar\" &&\n+\tgit -C server config remote.otherLop.stuff \"baz\" &&\n+\tgit -C server config remote.otherLop.partialCloneFilter \"blob:limit=10k\" &&\n+\ttest_when_finished \"git -C server remote remove otherLop\" &&\n+\n+\tgit -C server config remote.lop.token \"fooXXX\" &&\n+\tgit -C server config remote.lop.partialCloneFilter \"blob:limit=8k\" &&\n+\n+\ttest_config -C server promisor.sendFields \"partialCloneFilter, token\" &&\n+\ttest_when_finished \"rm trace\" &&\n+\n+\t# Clone from server to create a client\n+\tGIT_TRACE_PACKET=\"$(pwd)/trace\" GIT_NO_LAZY_FETCH=0 git clone \\\n+\t\t-c remote.lop.promisor=true \\\n+\t\t-c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n+\t\t-c remote.lop.url=\"file://$(pwd)/lop\" \\\n+\t\t-c remote.lop.token=\"fooYYY\" \\\n+\t\t-c remote.lop.partialCloneFilter=\"blob:none\" \\\n+\t\t-c promisor.acceptfromserver=All \\\n+\t\t-c promisor.storeFields=partialcloneFilter \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client 2>err &&\n+\n+\t# Check that the filter from the server is stored\n+\techo \"blob:limit=8k\" >expected &&\n+\tgit -C client config remote.lop.partialCloneFilter >actual &&\n+\ttest_cmp expected actual &&\n+\n+\t# Check that user is notified when the filter is stored\n+\ttest_grep \"Storing new filter from server for remote '\\''lop'\\''\" err &&\n+\ttest_grep \"'\\''blob:none'\\'' -> '\\''blob:limit=8k'\\''\" err &&\n+\n+\t# Check that the token from the server is NOT stored\n+\techo \"fooYYY\" >expected &&\n+\tgit -C client config remote.lop.token >actual &&\n+\ttest_cmp expected actual &&\n+\ttest_grep ! \"Storing new token from server\" err &&\n+\n+\t# Check that the filter for an unknown remote is NOT stored\n+\ttest_must_fail git -C client config remote.otherLop.partialCloneFilter >actual &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\"\n+'\n+\n test_expect_success \"clone with promisor.advertise set to 'true' but don't delete the client\" '\n \tgit -C server config promisor.advertise true &&\n \n-- \n2.52.0.319.gfcaffa7898\n\n"},{"id":"532646","messageId":"20251223111113.47473-4-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20251223111113.47473-1-christian.couder@gmail.com","subject":"[PATCH 3/9] clone: make filter_options local to cmd_clone()","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-12-23T11:11:07Z","receivedAt":"2025-12-23T11:11:38Z","isPatch":true,"body":"The `struct list_objects_filter_options filter_options` variable used\nin \"builtin/clone.c\" to store the parsed filters specified by\n`--filter=<filterspec>` is currently a static variable global to the\nfile.\n\nAs we are going to use it more in a following commit, it could become\na bit less easy to understand how it's managed.\n\nTo avoid that, let's make it clear that it's owned by cmd_clone() by\nmoving its definition into that function and making it non-static.\n\nThe only additional change to make this work is to pass it as an\nargument to checkout(). So it's a small quite cheap cleanup anyway.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n builtin/clone.c | 16 +++++++++++-----\n 1 file changed, 11 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex b19b302b06..186e5498d4 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -77,7 +77,6 @@ static struct string_list option_required_reference = STRING_LIST_INIT_NODUP;\n static struct string_list option_optional_reference = STRING_LIST_INIT_NODUP;\n static int max_jobs = -1;\n static struct string_list option_recurse_submodules = STRING_LIST_INIT_NODUP;\n-static struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n static int config_filter_submodules = -1;    /* unspecified */\n static int option_remote_submodules;\n \n@@ -634,7 +633,9 @@ static int git_sparse_checkout_init(const char *repo)\n \treturn result;\n }\n \n-static int checkout(int submodule_progress, int filter_submodules,\n+static int checkout(int submodule_progress,\n+\t\t    struct list_objects_filter_options *filter_options,\n+\t\t    int filter_submodules,\n \t\t    enum ref_storage_format ref_storage_format)\n {\n \tstruct object_id oid;\n@@ -723,9 +724,9 @@ static int checkout(int submodule_progress, int filter_submodules,\n \t\t\tstrvec_pushf(&cmd.args, \"--ref-format=%s\",\n \t\t\t\t     ref_storage_format_to_name(ref_storage_format));\n \n-\t\tif (filter_submodules && filter_options.choice)\n+\t\tif (filter_submodules && filter_options->choice)\n \t\t\tstrvec_pushf(&cmd.args, \"--filter=%s\",\n-\t\t\t\t     expand_list_objects_filter_spec(&filter_options));\n+\t\t\t\t     expand_list_objects_filter_spec(filter_options));\n \n \t\tif (option_single_branch >= 0)\n \t\t\tstrvec_push(&cmd.args, option_single_branch ?\n@@ -903,6 +904,7 @@ int cmd_clone(int argc,\n \tenum transport_family family = TRANSPORT_FAMILY_ALL;\n \tstruct string_list option_config = STRING_LIST_INIT_DUP;\n \tint option_dissociate = 0;\n+\tstruct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n \tint option_filter_submodules = -1; /* unspecified */\n \tstruct string_list server_options = STRING_LIST_INIT_NODUP;\n \tconst char *bundle_uri = NULL;\n@@ -1625,9 +1627,13 @@ int cmd_clone(int argc,\n \t\treturn 1;\n \n \tjunk_mode = JUNK_LEAVE_REPO;\n-\terr = checkout(submodule_progress, filter_submodules,\n+\terr = checkout(submodule_progress,\n+\t\t       &filter_options,\n+\t\t       filter_submodules,\n \t\t       ref_storage_format);\n \n+\tlist_objects_filter_release(&filter_options);\n+\n \tstring_list_clear(&option_not, 0);\n \tstring_list_clear(&option_config, 0);\n \tstring_list_clear(&server_options, 0);\n-- \n2.52.0.319.gfcaffa7898\n\n"},{"id":"532647","messageId":"20251223111113.47473-5-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20251223111113.47473-1-christian.couder@gmail.com","subject":"[PATCH 4/9] fetch: make filter_options local to cmd_fetch()","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-12-23T11:11:08Z","receivedAt":"2025-12-23T11:11:40Z","isPatch":true,"body":"The `struct list_objects_filter_options filter_options` variable used\nin \"builtin/fetch.c\" to store the parsed filters specified by\n`--filter=<filterspec>` is currently a static variable global to the\nfile.\n\nAs we are going to use it more in a following commit, it could become a\nbit less easy to understand how it's managed.\n\nTo avoid that, let's make it clear that it's owned by cmd_fetch() by\nmoving its definition into that function and making it non-static.\n\nThis requires passing a pointer to it through the prepare_transport(),\ndo_fetch(), backfill_tags(), fetch_one_setup_partial(), and fetch_one()\nfunctions, but it's quite straightforward.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n builtin/fetch.c | 48 +++++++++++++++++++++++++++---------------------\n 1 file changed, 27 insertions(+), 21 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 288d3772ea..b984173447 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -97,7 +97,6 @@ static struct strbuf default_rla = STRBUF_INIT;\n static struct transport *gtransport;\n static struct transport *gsecondary;\n static struct refspec refmap = REFSPEC_INIT_FETCH;\n-static struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n static struct string_list server_options = STRING_LIST_INIT_DUP;\n static struct string_list negotiation_tip = STRING_LIST_INIT_NODUP;\n \n@@ -1449,7 +1448,8 @@ static void add_negotiation_tips(struct git_transport_options *smart_options)\n \tsmart_options->negotiation_tips = oids;\n }\n \n-static struct transport *prepare_transport(struct remote *remote, int deepen)\n+static struct transport *prepare_transport(struct remote *remote, int deepen,\n+\t\t\t\t\t   struct list_objects_filter_options *filter_options)\n {\n \tstruct transport *transport;\n \n@@ -1473,9 +1473,9 @@ static struct transport *prepare_transport(struct remote *remote, int deepen)\n \t\tset_option(transport, TRANS_OPT_UPDATE_SHALLOW, \"yes\");\n \tif (refetch)\n \t\tset_option(transport, TRANS_OPT_REFETCH, \"yes\");\n-\tif (filter_options.choice) {\n+\tif (filter_options->choice) {\n \t\tconst char *spec =\n-\t\t\texpand_list_objects_filter_spec(&filter_options);\n+\t\t\texpand_list_objects_filter_spec(filter_options);\n \t\tset_option(transport, TRANS_OPT_LIST_OBJECTS_FILTER, spec);\n \t\tset_option(transport, TRANS_OPT_FROM_PROMISOR, \"1\");\n \t}\n@@ -1493,7 +1493,8 @@ static int backfill_tags(struct display_state *display_state,\n \t\t\t struct ref_transaction *transaction,\n \t\t\t struct ref *ref_map,\n \t\t\t struct fetch_head *fetch_head,\n-\t\t\t const struct fetch_config *config)\n+\t\t\t const struct fetch_config *config,\n+\t\t\t struct list_objects_filter_options *filter_options)\n {\n \tint retcode, cannot_reuse;\n \n@@ -1507,7 +1508,7 @@ static int backfill_tags(struct display_state *display_state,\n \tcannot_reuse = transport->cannot_reuse ||\n \t\tdeepen_since || deepen_not.nr;\n \tif (cannot_reuse) {\n-\t\tgsecondary = prepare_transport(transport->remote, 0);\n+\t\tgsecondary = prepare_transport(transport->remote, 0, filter_options);\n \t\ttransport = gsecondary;\n \t}\n \n@@ -1713,7 +1714,8 @@ static int commit_ref_transaction(struct ref_transaction **transaction,\n \n static int do_fetch(struct transport *transport,\n \t\t    struct refspec *rs,\n-\t\t    const struct fetch_config *config)\n+\t\t    const struct fetch_config *config,\n+\t\t    struct list_objects_filter_options *filter_options)\n {\n \tstruct ref_transaction *transaction = NULL;\n \tstruct ref *ref_map = NULL;\n@@ -1873,7 +1875,7 @@ static int do_fetch(struct transport *transport,\n \t\t\t * the transaction and don't commit anything.\n \t\t\t */\n \t\t\tif (backfill_tags(&display_state, transport, transaction, tags_ref_map,\n-\t\t\t\t\t  &fetch_head, config))\n+\t\t\t\t\t  &fetch_head, config, filter_options))\n \t\t\t\tretcode = 1;\n \t\t}\n \n@@ -2198,20 +2200,21 @@ static int fetch_multiple(struct string_list *list, int max_children,\n  * Fetching from the promisor remote should use the given filter-spec\n  * or inherit the default filter-spec from the config.\n  */\n-static inline void fetch_one_setup_partial(struct remote *remote)\n+static inline void fetch_one_setup_partial(struct remote *remote,\n+\t\t\t\t\t   struct list_objects_filter_options *filter_options)\n {\n \t/*\n \t * Explicit --no-filter argument overrides everything, regardless\n \t * of any prior partial clones and fetches.\n \t */\n-\tif (filter_options.no_filter)\n+\tif (filter_options->no_filter)\n \t\treturn;\n \n \t/*\n \t * If no prior partial clone/fetch and the current fetch DID NOT\n \t * request a partial-fetch, do a normal fetch.\n \t */\n-\tif (!repo_has_promisor_remote(the_repository) && !filter_options.choice)\n+\tif (!repo_has_promisor_remote(the_repository) && !filter_options->choice)\n \t\treturn;\n \n \t/*\n@@ -2220,8 +2223,8 @@ static inline void fetch_one_setup_partial(struct remote *remote)\n \t * filter-spec as the default for subsequent fetches to this\n \t * remote if there is currently no default filter-spec.\n \t */\n-\tif (filter_options.choice) {\n-\t\tpartial_clone_register(remote->name, &filter_options);\n+\tif (filter_options->choice) {\n+\t\tpartial_clone_register(remote->name, filter_options);\n \t\treturn;\n \t}\n \n@@ -2230,14 +2233,15 @@ static inline void fetch_one_setup_partial(struct remote *remote)\n \t * explicitly given filter-spec or inherit the filter-spec from\n \t * the config.\n \t */\n-\tif (!filter_options.choice)\n-\t\tpartial_clone_get_default_filter_spec(&filter_options, remote->name);\n+\tif (!filter_options->choice)\n+\t\tpartial_clone_get_default_filter_spec(filter_options, remote->name);\n \treturn;\n }\n \n static int fetch_one(struct remote *remote, int argc, const char **argv,\n \t\t     int prune_tags_ok, int use_stdin_refspecs,\n-\t\t     const struct fetch_config *config)\n+\t\t     const struct fetch_config *config,\n+\t\t     struct list_objects_filter_options *filter_options)\n {\n \tstruct refspec rs = REFSPEC_INIT_FETCH;\n \tint i;\n@@ -2249,7 +2253,7 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,\n \t\tdie(_(\"no remote repository specified; please specify either a URL or a\\n\"\n \t\t      \"remote name from which new revisions should be fetched\"));\n \n-\tgtransport = prepare_transport(remote, 1);\n+\tgtransport = prepare_transport(remote, 1, filter_options);\n \n \tif (prune < 0) {\n \t\t/* no command line request */\n@@ -2304,7 +2308,7 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,\n \tsigchain_push_common(unlock_pack_on_signal);\n \tatexit(unlock_pack_atexit);\n \tsigchain_push(SIGPIPE, SIG_IGN);\n-\texit_code = do_fetch(gtransport, &rs, config);\n+\texit_code = do_fetch(gtransport, &rs, config, filter_options);\n \tsigchain_pop(SIGPIPE);\n \trefspec_clear(&rs);\n \ttransport_disconnect(gtransport);\n@@ -2329,6 +2333,7 @@ int cmd_fetch(int argc,\n \tconst char *submodule_prefix = \"\";\n \tconst char *bundle_uri;\n \tstruct string_list list = STRING_LIST_INIT_DUP;\n+\tstruct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n \tstruct remote *remote = NULL;\n \tint all = -1, multiple = 0;\n \tint result = 0;\n@@ -2594,7 +2599,7 @@ int cmd_fetch(int argc,\n \t\ttrace2_region_enter(\"fetch\", \"negotiate-only\", the_repository);\n \t\tif (!remote)\n \t\t\tdie(_(\"must supply remote when using --negotiate-only\"));\n-\t\tgtransport = prepare_transport(remote, 1);\n+\t\tgtransport = prepare_transport(remote, 1, &filter_options);\n \t\tif (gtransport->smart_options) {\n \t\t\tgtransport->smart_options->acked_commits = &acked_commits;\n \t\t} else {\n@@ -2616,12 +2621,12 @@ int cmd_fetch(int argc,\n \t} else if (remote) {\n \t\tif (filter_options.choice || repo_has_promisor_remote(the_repository)) {\n \t\t\ttrace2_region_enter(\"fetch\", \"setup-partial\", the_repository);\n-\t\t\tfetch_one_setup_partial(remote);\n+\t\t\tfetch_one_setup_partial(remote, &filter_options);\n \t\t\ttrace2_region_leave(\"fetch\", \"setup-partial\", the_repository);\n \t\t}\n \t\ttrace2_region_enter(\"fetch\", \"fetch-one\", the_repository);\n \t\tresult = fetch_one(remote, argc, argv, prune_tags_ok, stdin_refspecs,\n-\t\t\t\t   &config);\n+\t\t\t\t   &config, &filter_options);\n \t\ttrace2_region_leave(\"fetch\", \"fetch-one\", the_repository);\n \t} else {\n \t\tint max_children = max_jobs;\n@@ -2727,5 +2732,6 @@ int cmd_fetch(int argc,\n \n  cleanup:\n \tstring_list_clear(&list, 0);\n+\tlist_objects_filter_release(&filter_options);\n \treturn result;\n }\n-- \n2.52.0.319.gfcaffa7898\n\n"},{"id":"532648","messageId":"20251223111113.47473-6-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20251223111113.47473-1-christian.couder@gmail.com","subject":"[PATCH 5/9] doc: fetch: document `--filter=<filter-spec>` option","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-12-23T11:11:09Z","receivedAt":"2025-12-23T11:11:41Z","isPatch":true,"body":"The `--filter=<filter-spec>` option is documented in most commands that\nsupport it except `git fetch`.\n\nLet's fix that and document that option properly in the same way as it\nis already documented for `git clone`.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/fetch-options.adoc | 10 ++++++++++\n 1 file changed, 10 insertions(+)\n\ndiff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc\nindex fcba46ee9e..70a9818331 100644\n--- a/Documentation/fetch-options.adoc\n+++ b/Documentation/fetch-options.adoc\n@@ -88,6 +88,16 @@ linkgit:git-config[1].\n This is incompatible with `--recurse-submodules=(yes|on-demand)` and takes\n precedence over the `fetch.output` config option.\n \n+--filter=<filter-spec>::\n+\tUse the partial clone feature and request that the server sends\n+\ta subset of reachable objects according to a given object filter.\n+\tWhen using `--filter`, the supplied _<filter-spec>_ is used for\n+\tthe partial fetch. For example, `--filter=blob:none` will filter\n+\tout all blobs (file contents) until needed by Git. Also,\n+\t`--filter=blob:limit=<size>` will filter out all blobs of size\n+\tat least _<size>_. For more details on filter specifications, see\n+\tthe `--filter` option in linkgit:git-rev-list[1].\n+\n ifndef::git-pull[]\n `--write-fetch-head`::\n `--no-write-fetch-head`::\n-- \n2.52.0.319.gfcaffa7898\n\n"},{"id":"532649","messageId":"20251223111113.47473-7-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20251223111113.47473-1-christian.couder@gmail.com","subject":"[PATCH 6/9] list-objects-filter-options: support 'auto' mode for --filter","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-12-23T11:11:10Z","receivedAt":"2025-12-23T11:11:43Z","isPatch":true,"body":"In a following commit, we are going to allow passing \"auto\" as a\n<filterspec> to the `--filter=<filterspec>` option, but only for some\ncommands. Other commands that support the `--filter=<filterspec>`\noption should still die() when 'auto' is passed.\n\nLet's set up the \"list-objects-filter-options.{c,h}\" infrastructure to\nsupport that:\n\n- Add a new `unsigned int allow_auto_filter : 1;` flag to\n  `struct list_objects_filter_options` which specifies if \"auto\" is\n  accepted or not.\n- Change gently_parse_list_objects_filter() to parse \"auto\" if it's\n  accepted.\n- Make sure we die() if \"auto\" is combined with another filter.\n- Update list_objects_filter_release() to preserve the\n  allow_auto_filter flag, as this function is often called (via\n  opt_parse_list_objects_filter) to reset the struct before parsing a\n  new value.\n\nLet's also update `list-objects-filter.c` to recognize the new\n`LOFC_AUTO` choice. Since \"auto\" must be resolved to a concrete filter\nbefore filtering actually begins, initializing a filter with\n`LOFC_AUTO` is invalid and will trigger a BUG().\n\nNote that ideally combining \"auto\" with \"auto\" could be allowed, but in\npractice, it's probably not worth the added code complexity. And if we\nreally want it, nothing prevents us to allow it in future work.\n\nIf we ever want to give a meaning to combining \"auto\" with a different\nfilter too, nothing prevents us to do that in future work either.\n\nWhile at it, let's add a new \"u-list-objects-filter-options.c\" file for\n`struct list_objects_filter_options` related unit tests. For now it\nonly tests gently_parse_list_objects_filter() though.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Makefile                                     |  1 +\n list-objects-filter-options.c                | 36 +++++++++++--\n list-objects-filter-options.h                |  6 +++\n list-objects-filter.c                        |  8 +++\n t/meson.build                                |  1 +\n t/unit-tests/u-list-objects-filter-options.c | 53 ++++++++++++++++++++\n 6 files changed, 102 insertions(+), 3 deletions(-)\n create mode 100644 t/unit-tests/u-list-objects-filter-options.c\n\ndiff --git a/Makefile b/Makefile\nindex 89d8d73ec0..85b2ff09f4 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1507,6 +1507,7 @@ CLAR_TEST_SUITES += u-dir\n CLAR_TEST_SUITES += u-example-decorate\n CLAR_TEST_SUITES += u-hash\n CLAR_TEST_SUITES += u-hashmap\n+CLAR_TEST_SUITES += u-list-objects-filter-options\n CLAR_TEST_SUITES += u-mem-pool\n CLAR_TEST_SUITES += u-oid-array\n CLAR_TEST_SUITES += u-oidmap\ndiff --git a/list-objects-filter-options.c b/list-objects-filter-options.c\nindex 7420bf81fe..f13ae5caeb 100644\n--- a/list-objects-filter-options.c\n+++ b/list-objects-filter-options.c\n@@ -20,6 +20,8 @@ const char *list_object_filter_config_name(enum list_objects_filter_choice c)\n \tcase LOFC_DISABLED:\n \t\t/* we have no name for \"no filter at all\" */\n \t\tbreak;\n+\tcase LOFC_AUTO:\n+\t\treturn \"auto\";\n \tcase LOFC_BLOB_NONE:\n \t\treturn \"blob:none\";\n \tcase LOFC_BLOB_LIMIT:\n@@ -52,7 +54,17 @@ int gently_parse_list_objects_filter(\n \tif (filter_options->choice)\n \t\tBUG(\"filter_options already populated\");\n \n-\tif (!strcmp(arg, \"blob:none\")) {\n+\tif (!strcmp(arg, \"auto\")) {\n+\t\tif (!filter_options->allow_auto_filter) {\n+\t\t\tstrbuf_addstr(\n+\t\t\t\terrbuf,\n+\t\t\t\t_(\"'auto' filter not supported by this command\"));\n+\t\t\treturn 1;\n+\t\t}\n+\t\tfilter_options->choice = LOFC_AUTO;\n+\t\treturn 0;\n+\n+\t} else if (!strcmp(arg, \"blob:none\")) {\n \t\tfilter_options->choice = LOFC_BLOB_NONE;\n \t\treturn 0;\n \n@@ -146,10 +158,20 @@ static int parse_combine_subfilter(\n \n \tdecoded = url_percent_decode(subspec->buf);\n \n-\tresult = has_reserved_character(subspec, errbuf) ||\n-\t\tgently_parse_list_objects_filter(\n+\tresult = has_reserved_character(subspec, errbuf);\n+\tif (result)\n+\t\tgoto cleanup;\n+\n+\tresult = gently_parse_list_objects_filter(\n \t\t\t&filter_options->sub[new_index], decoded, errbuf);\n+\tif (result)\n+\t\tgoto cleanup;\n+\n+\tresult = (filter_options->sub[new_index].choice == LOFC_AUTO);\n+\tif (result)\n+\t\tstrbuf_addstr(errbuf, _(\"an 'auto' filter cannot be combined\"));\n \n+cleanup:\n \tfree(decoded);\n \treturn result;\n }\n@@ -263,6 +285,9 @@ void parse_list_objects_filter(\n \t} else {\n \t\tstruct list_objects_filter_options *sub;\n \n+\t\tif (filter_options->choice == LOFC_AUTO)\n+\t\t\tdie(_(\"an 'auto' filter is incompatible with any other filter\"));\n+\n \t\t/*\n \t\t * Make filter_options an LOFC_COMBINE spec so we can trivially\n \t\t * add subspecs to it.\n@@ -277,6 +302,9 @@ void parse_list_objects_filter(\n \t\tif (gently_parse_list_objects_filter(sub, arg, &errbuf))\n \t\t\tdie(\"%s\", errbuf.buf);\n \n+\t\tif (sub->choice == LOFC_AUTO)\n+\t\t\tdie(_(\"an 'auto' filter is incompatible with any other filter\"));\n+\n \t\tstrbuf_addch(&filter_options->filter_spec, '+');\n \t\tfilter_spec_append_urlencode(filter_options, arg);\n \t}\n@@ -317,6 +345,7 @@ void list_objects_filter_release(\n \tstruct list_objects_filter_options *filter_options)\n {\n \tsize_t sub;\n+\tunsigned int allow_auto_filter = filter_options->allow_auto_filter;\n \n \tif (!filter_options)\n \t\treturn;\n@@ -326,6 +355,7 @@ void list_objects_filter_release(\n \t\tlist_objects_filter_release(&filter_options->sub[sub]);\n \tfree(filter_options->sub);\n \tlist_objects_filter_init(filter_options);\n+\tfilter_options->allow_auto_filter = allow_auto_filter;\n }\n \n void partial_clone_register(\ndiff --git a/list-objects-filter-options.h b/list-objects-filter-options.h\nindex 7b2108b986..77d7bbc846 100644\n--- a/list-objects-filter-options.h\n+++ b/list-objects-filter-options.h\n@@ -18,6 +18,7 @@ enum list_objects_filter_choice {\n \tLOFC_SPARSE_OID,\n \tLOFC_OBJECT_TYPE,\n \tLOFC_COMBINE,\n+\tLOFC_AUTO,\n \tLOFC__COUNT /* must be last */\n };\n \n@@ -50,6 +51,11 @@ struct list_objects_filter_options {\n \t */\n \tunsigned int no_filter : 1;\n \n+\t/*\n+\t * Is LOFC_AUTO a valid option?\n+\t */\n+\tunsigned int allow_auto_filter : 1;\n+\n \t/*\n \t * BEGIN choice-specific parsed values from within the filter-spec. Only\n \t * some values will be defined for any given choice.\ndiff --git a/list-objects-filter.c b/list-objects-filter.c\nindex acd65ebb73..78316e7f90 100644\n--- a/list-objects-filter.c\n+++ b/list-objects-filter.c\n@@ -745,6 +745,13 @@ static void filter_combine__init(\n \tfilter->finalize_omits_fn = filter_combine__finalize_omits;\n }\n \n+static void filter_auto__init(\n+\tstruct list_objects_filter_options *filter_options UNUSED,\n+\tstruct filter *filter UNUSED)\n+{\n+\tBUG(\"LOFC_AUTO should have been resolved before initializing the filter\");\n+}\n+\n typedef void (*filter_init_fn)(\n \tstruct list_objects_filter_options *filter_options,\n \tstruct filter *filter);\n@@ -760,6 +767,7 @@ static filter_init_fn s_filters[] = {\n \tfilter_sparse_oid__init,\n \tfilter_object_type__init,\n \tfilter_combine__init,\n+\tfilter_auto__init,\n };\n \n struct filter *list_objects_filter__init(\ndiff --git a/t/meson.build b/t/meson.build\nindex 459c52a489..0bd66cc6ce 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -4,6 +4,7 @@ clar_test_suites = [\n   'unit-tests/u-example-decorate.c',\n   'unit-tests/u-hash.c',\n   'unit-tests/u-hashmap.c',\n+  'unit-tests/u-list-objects-filter-options.c',\n   'unit-tests/u-mem-pool.c',\n   'unit-tests/u-oid-array.c',\n   'unit-tests/u-oidmap.c',\ndiff --git a/t/unit-tests/u-list-objects-filter-options.c b/t/unit-tests/u-list-objects-filter-options.c\nnew file mode 100644\nindex 0000000000..f7d73701b5\n--- /dev/null\n+++ b/t/unit-tests/u-list-objects-filter-options.c\n@@ -0,0 +1,53 @@\n+#include \"unit-test.h\"\n+#include \"list-objects-filter-options.h\"\n+#include \"strbuf.h\"\n+\n+/* Helper to test gently_parse_list_objects_filter() */\n+static void check_gentle_parse(const char *filter_spec,\n+\t\t\t       int expect_success,\n+\t\t\t       int allow_auto,\n+\t\t\t       enum list_objects_filter_choice expected_choice)\n+{\n+\tstruct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n+\tstruct strbuf errbuf = STRBUF_INIT;\n+\tint ret;\n+\n+\tfilter_options.allow_auto_filter = allow_auto;\n+\n+\tret = gently_parse_list_objects_filter(&filter_options, filter_spec, &errbuf);\n+\n+\tif (expect_success) {\n+\t\tcl_assert_equal_i(ret, 0);\n+\t\tcl_assert_equal_i(expected_choice, filter_options.choice);\n+\t\tcl_assert_equal_i(errbuf.len, 0);\n+\t} else {\n+\t\tcl_assert(ret != 0);\n+\t\tcl_assert(errbuf.len > 0);\n+\t}\n+\n+\tstrbuf_release(&errbuf);\n+\tlist_objects_filter_release(&filter_options);\n+}\n+\n+void test_list_objects_filter_options__regular_filters(void)\n+{\n+\tcheck_gentle_parse(\"blob:none\", 1, 0, LOFC_BLOB_NONE);\n+\tcheck_gentle_parse(\"blob:none\", 1, 1, LOFC_BLOB_NONE);\n+\tcheck_gentle_parse(\"blob:limit=5k\", 1, 0, LOFC_BLOB_LIMIT);\n+\tcheck_gentle_parse(\"blob:limit=5k\", 1, 1, LOFC_BLOB_LIMIT);\n+\tcheck_gentle_parse(\"combine:blob:none+tree:0\", 1, 0, LOFC_COMBINE);\n+\tcheck_gentle_parse(\"combine:blob:none+tree:0\", 1, 1, LOFC_COMBINE);\n+}\n+\n+void test_list_objects_filter_options__auto_allowed(void)\n+{\n+\tcheck_gentle_parse(\"auto\", 1, 1, LOFC_AUTO);\n+\tcheck_gentle_parse(\"auto\", 0, 0, 0);\n+}\n+\n+void test_list_objects_filter_options__combine_auto_fails(void)\n+{\n+\tcheck_gentle_parse(\"combine:auto+blob:none\", 0, 1, 0);\n+\tcheck_gentle_parse(\"combine:blob:none+auto\", 0, 1, 0);\n+\tcheck_gentle_parse(\"combine:auto+auto\", 0, 1, 0);\n+}\n-- \n2.52.0.319.gfcaffa7898\n\n"},{"id":"532650","messageId":"20251223111113.47473-8-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20251223111113.47473-1-christian.couder@gmail.com","subject":"[PATCH 7/9] list-objects-filter-options: implement auto filter resolution","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-12-23T11:11:11Z","receivedAt":"2025-12-23T11:11:44Z","isPatch":true,"body":"In a following commit, we will need to aggregate filters from multiple\naccepted promisor remotes into a single filter.\n\nFor that purpose, let's add a `list_objects_filter_combine()` helper\nfunction that takes a list of filter specifications and combines them\ninto a single string. If multiple filters are provided, it constructs a\n\"combine:...\" filter, ensuring that sub-filters are properly\nURL-encoded using the existing `allow_unencoded` logic.\n\nIn a following commit, we will add a `--filter=auto` option that will\nenable a client to use the filters suggested by the server for the\npromisor remotes the client accepted.\n\nTo simplify the filter processing related to this new feature, let's\nalso add a small `list_objects_filter_resolve_auto()` function.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n list-objects-filter-options.c                | 35 ++++++++++++++++++++\n list-objects-filter-options.h                | 19 +++++++++++\n t/unit-tests/u-list-objects-filter-options.c | 33 ++++++++++++++++++\n 3 files changed, 87 insertions(+)\n\ndiff --git a/list-objects-filter-options.c b/list-objects-filter-options.c\nindex f13ae5caeb..4a9c1991c1 100644\n--- a/list-objects-filter-options.c\n+++ b/list-objects-filter-options.c\n@@ -230,6 +230,41 @@ static void filter_spec_append_urlencode(\n \t\t     filter->filter_spec.buf + orig_len);\n }\n \n+char *list_objects_filter_combine(const struct string_list *specs)\n+{\n+\tstruct strbuf buf = STRBUF_INIT;\n+\n+\tif (!specs->nr)\n+\t\treturn NULL;\n+\n+\tif (specs->nr == 1)\n+\t\treturn xstrdup(specs->items[0].string);\n+\n+\tstrbuf_addstr(&buf, \"combine:\");\n+\n+\tfor (size_t i = 0; i < specs->nr; i++) {\n+\t\tconst char *spec = specs->items[i].string;\n+\t\tif (i > 0)\n+\t\t\tstrbuf_addch(&buf, '+');\n+\n+\t\tstrbuf_addstr_urlencode(&buf, spec, allow_unencoded);\n+\t}\n+\n+\treturn strbuf_detach(&buf, NULL);\n+}\n+\n+void list_objects_filter_resolve_auto(struct list_objects_filter_options *filter_options,\n+\tchar *new_filter, struct strbuf *errbuf)\n+{\n+\tif (filter_options->choice != LOFC_AUTO)\n+\t\treturn;\n+\n+\tlist_objects_filter_release(filter_options);\n+\n+\tif (new_filter)\n+\t\tgently_parse_list_objects_filter(filter_options, new_filter, errbuf);\n+}\n+\n /*\n  * Changes filter_options into an equivalent LOFC_COMBINE filter options\n  * instance. Does not do anything if filter_options is already LOFC_COMBINE.\ndiff --git a/list-objects-filter-options.h b/list-objects-filter-options.h\nindex 77d7bbc846..832d615c17 100644\n--- a/list-objects-filter-options.h\n+++ b/list-objects-filter-options.h\n@@ -6,6 +6,7 @@\n #include \"strbuf.h\"\n \n struct option;\n+struct string_list;\n \n /*\n  * The list of defined filters for list-objects.\n@@ -168,4 +169,22 @@ void list_objects_filter_copy(\n \tstruct list_objects_filter_options *dest,\n \tconst struct list_objects_filter_options *src);\n \n+/*\n+ * Combine the filter specs in 'specs' into a combined filter string\n+ * like \"combine:<spec1>+<spec2>\", where <spec1>, <spec2>, etc are\n+ * properly urlencoded. If 'specs' contains no element, NULL is\n+ * returned. If 'specs' contains a single element, a copy of that\n+ * element is returned.\n+ */\n+char *list_objects_filter_combine(const struct string_list *specs);\n+\n+/*\n+ * Check if 'filter_options' are an 'auto' filter, and if that's the\n+ * case populate it with the filter specified by 'new_filter'.\n+ */\n+void list_objects_filter_resolve_auto(\n+\tstruct list_objects_filter_options *filter_options,\n+\tchar *new_filter,\n+\tstruct strbuf *errbuf);\n+\n #endif /* LIST_OBJECTS_FILTER_OPTIONS_H */\ndiff --git a/t/unit-tests/u-list-objects-filter-options.c b/t/unit-tests/u-list-objects-filter-options.c\nindex f7d73701b5..84a012af3c 100644\n--- a/t/unit-tests/u-list-objects-filter-options.c\n+++ b/t/unit-tests/u-list-objects-filter-options.c\n@@ -1,6 +1,7 @@\n #include \"unit-test.h\"\n #include \"list-objects-filter-options.h\"\n #include \"strbuf.h\"\n+#include \"string-list.h\"\n \n /* Helper to test gently_parse_list_objects_filter() */\n static void check_gentle_parse(const char *filter_spec,\n@@ -51,3 +52,35 @@ void test_list_objects_filter_options__combine_auto_fails(void)\n \tcheck_gentle_parse(\"combine:blob:none+auto\", 0, 1, 0);\n \tcheck_gentle_parse(\"combine:auto+auto\", 0, 1, 0);\n }\n+\n+/* Helper to test list_objects_filter_combine() */\n+static void check_combine(const char **specs, size_t nr, const char *expected)\n+{\n+\tstruct string_list spec_list = STRING_LIST_INIT_NODUP;\n+\tchar *actual;\n+\n+\tfor (size_t i = 0; i < nr; i++)\n+\t\tstring_list_append(&spec_list, specs[i]);\n+\n+\tactual = list_objects_filter_combine(&spec_list);\n+\n+\tcl_assert_equal_s(actual, expected);\n+\n+\tfree(actual);\n+\tstring_list_clear(&spec_list, 0);\n+}\n+\n+void test_list_objects_filter_options__combine_helper(void)\n+{\n+\tconst char *empty[] = { NULL };\n+\tconst char *one[] = { \"blob:none\" };\n+\tconst char *two[] = { \"blob:none\", \"tree:0\" };\n+\tconst char *complex[] = { \"blob:limit=1k\", \"object:type=tag\" };\n+\tconst char *needs_encoding[] = { \"blob:none\", \"combine:tree:0+blob:limit=1k\" };\n+\n+\tcheck_combine(empty, 0, NULL);\n+\tcheck_combine(one, 1, \"blob:none\");\n+\tcheck_combine(two, 2, \"combine:blob:none+tree:0\");\n+\tcheck_combine(complex, 2, \"combine:blob:limit=1k+object:type=tag\");\n+\tcheck_combine(needs_encoding, 2, \"combine:blob:none+combine:tree:0%2bblob:limit=1k\");\n+}\n-- \n2.52.0.319.gfcaffa7898\n\n"},{"id":"532651","messageId":"20251223111113.47473-9-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20251223111113.47473-1-christian.couder@gmail.com","subject":"[PATCH 8/9] promisor-remote: keep advertised filter in memory","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-12-23T11:11:12Z","receivedAt":"2025-12-23T11:11:46Z","isPatch":true,"body":"Currently, advertised filters are only kept in memory temporarily\nduring parsing, or persisted to disk if `promisor.storeFields`\ncontains 'partialCloneFilter'.\n\nIn a following commit though, we will add a `--filter=auto` option.\nThis option will enable the client to use the filters that the server\nis suggesting for the promisor remotes the client accepts.\n\nTo use them even if `promisor.storeFields` is not configured, these\nfilters should be stored somewhere for the current session.\n\nLet's add an `advertised_filter` field to `struct promisor_remote`\nfor that purpose.\n\nTo ensure that the filters are available in all cases,\nfilter_promisor_remote() captures them into a temporary list and\napplies them to the `promisor_remote` structs after the potential\nconfiguration reload.\n\nThen the accepted remotes are marked as `accepted` in the repository\nstate. This ensures that subsequent calls to look up accepted remotes\n(like in the filter construction below) actually find them.\n\nIn a following commit, we will add a `--filter=auto` option that will\nenable a client to use the filters suggested by the server for the\npromisor remotes the client accepted.\n\nTo enable the client to construct a filter spec based on these filters,\nlet's add a `promisor_remote_construct_filter(repo)` function.\n\nThis function:\n\n- iterates over all accepted promisor remotes in the repository,\n- collects the filters advertised for them (using `advertised_filter`\n  which a previous commit added to `struct promisor_remote`), and\n- generates a single filter spec for them (using the\n  `list_objects_filter_combine()` function added by a previous commit).\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 48 +++++++++++++++++++++++++++++++++++++++++++++++\n promisor-remote.h |  6 ++++++\n 2 files changed, 54 insertions(+)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 8d6d2d7b76..d5f3223cd0 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -193,6 +193,7 @@ void promisor_remote_clear(struct promisor_remote_config *config)\n \twhile (config->promisors) {\n \t\tstruct promisor_remote *r = config->promisors;\n \t\tfree(r->partial_clone_filter);\n+\t\tfree(r->advertised_filter);\n \t\tconfig->promisors = config->promisors->next;\n \t\tfree(r);\n \t}\n@@ -837,6 +838,7 @@ static void filter_promisor_remote(struct repository *repo,\n \tstruct store_info *store_info = NULL;\n \tstruct string_list_item *item;\n \tbool reload_config = false;\n+\tstruct string_list captured_filters = STRING_LIST_INIT_DUP;\n \n \tif (!repo_config_get_string_tmp(the_repository, \"promisor.acceptfromserver\", &accept_str)) {\n \t\tif (!*accept_str || !strcasecmp(\"None\", accept_str))\n@@ -879,6 +881,13 @@ static void filter_promisor_remote(struct repository *repo,\n \t\t\t\treload_config = true;\n \n \t\t\tstrvec_push(accepted, advertised->name);\n+\n+\t\t\t/* Capture advertised filters for accepted remotes */\n+\t\t\tif (advertised->filter) {\n+\t\t\t\tstruct string_list_item *i;\n+\t\t\t\ti = string_list_append(&captured_filters, advertised->name);\n+\t\t\t\ti->util = xstrdup(advertised->filter);\n+\t\t\t}\n \t\t}\n \n \t\tpromisor_info_free(advertised);\n@@ -890,6 +899,25 @@ static void filter_promisor_remote(struct repository *repo,\n \n \tif (reload_config)\n \t\trepo_promisor_remote_reinit(repo);\n+\n+\t/* Apply captured filters to the stable repo state */\n+\tfor_each_string_list_item(item, &captured_filters) {\n+\t\tstruct promisor_remote *r = repo_promisor_remote_find(repo, item->string);\n+\t\tif (r) {\n+\t\t\tfree(r->advertised_filter);\n+\t\t\tr->advertised_filter = item->util;\n+\t\t\titem->util = NULL;\n+\t\t}\n+\t}\n+\n+\tstring_list_clear(&captured_filters, 1);\n+\n+\t/* Mark the remotes as accepted in the repository state */\n+\tfor (size_t i = 0; i < accepted->nr; i++) {\n+\t\tstruct promisor_remote *r = repo_promisor_remote_find(repo, accepted->v[i]);\n+\t\tif (r)\n+\t\t\tr->accepted = 1;\n+\t}\n }\n \n char *promisor_remote_reply(const char *info)\n@@ -935,3 +963,23 @@ void mark_promisor_remotes_as_accepted(struct repository *r, const char *remotes\n \n \tstring_list_clear(&accepted_remotes, 0);\n }\n+\n+char *promisor_remote_construct_filter(struct repository *repo)\n+{\n+\tstruct string_list advertised_filters = STRING_LIST_INIT_NODUP;\n+\tstruct promisor_remote *r;\n+\tchar *result;\n+\n+\tpromisor_remote_init(repo);\n+\n+\tfor (r = repo->promisor_remote_config->promisors; r; r = r->next) {\n+\t\tif (r->accepted && r->advertised_filter)\n+\t\t\tstring_list_append(&advertised_filters, r->advertised_filter);\n+\t}\n+\n+\tresult = list_objects_filter_combine(&advertised_filters);\n+\n+\tstring_list_clear(&advertised_filters, 0);\n+\n+\treturn result;\n+}\ndiff --git a/promisor-remote.h b/promisor-remote.h\nindex 263d331a55..98a0f05e03 100644\n--- a/promisor-remote.h\n+++ b/promisor-remote.h\n@@ -15,6 +15,7 @@ struct object_id;\n struct promisor_remote {\n \tstruct promisor_remote *next;\n \tchar *partial_clone_filter;\n+\tchar *advertised_filter;\n \tunsigned int accepted : 1;\n \tconst char name[FLEX_ARRAY];\n };\n@@ -67,4 +68,9 @@ void mark_promisor_remotes_as_accepted(struct repository *repo, const char *remo\n  */\n int repo_has_accepted_promisor_remote(struct repository *r);\n \n+/*\n+ * Use the filters from the accepted remotes to create a filter.\n+ */\n+char *promisor_remote_construct_filter(struct repository *repo);\n+\n #endif /* PROMISOR_REMOTE_H */\n-- \n2.52.0.319.gfcaffa7898\n\n"},{"id":"532652","messageId":"20251223111113.47473-10-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20251223111113.47473-1-christian.couder@gmail.com","subject":"[PATCH 9/9] fetch-pack: wire up and enable auto filter logic","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-12-23T11:11:13Z","receivedAt":"2025-12-23T11:11:47Z","isPatch":true,"body":"Previous commits have set up an infrastructure for `--filter=auto` to\nautomatically prepare a partial clone filter based on what the server\nadvertised and the client accepted.\n\nUsing that infrastructure, let's now enable the `--filter=auto` option\nin `git clone` and `git fetch` by setting `allow_auto_filter` to 1.\n\nNote that these small changes mean that when `git clone --filter=auto`\nor `git fetch --filter=auto` are used, \"auto\" is automatically saved\nas the partial clone filter for the server on the client. Therefore\nsubsequent calls to `git fetch` on the client will automatically use\nthis \"auto\" mode even without `--filter=auto`.\n\nLet's also set `allow_auto_filter` to 1 in `transport.c`, as the\ntransport layer must be able to accept the \"auto\" filter spec even if\nthe invoking command hasn't fully parsed it yet.\n\nWhen an \"auto\" filter is requested, let's have the \"fetch-pack.c\" code\nin `do_fetch_pack_v2()` compute a filter and send it to the server.\n\nIn `do_fetch_pack_v2()` the logic also needs to check for the\n\"promisor-remote\" capability and call `promisor_remote_reply()` to\nparse advertised remotes and populate the list of those accepted (and\ntheir filters).\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/fetch-options.adoc      | 19 ++++++---\n Documentation/git-clone.adoc          | 25 ++++++++---\n Documentation/gitprotocol-v2.adoc     | 16 ++++---\n builtin/clone.c                       |  2 +\n builtin/fetch.c                       |  2 +\n fetch-pack.c                          | 20 +++++++++\n t/t5710-promisor-remote-capability.sh | 60 +++++++++++++++++++++++++++\n transport.c                           |  1 +\n 8 files changed, 130 insertions(+), 15 deletions(-)\n\ndiff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc\nindex 70a9818331..f7432d4b29 100644\n--- a/Documentation/fetch-options.adoc\n+++ b/Documentation/fetch-options.adoc\n@@ -92,11 +92,20 @@ precedence over the `fetch.output` config option.\n \tUse the partial clone feature and request that the server sends\n \ta subset of reachable objects according to a given object filter.\n \tWhen using `--filter`, the supplied _<filter-spec>_ is used for\n-\tthe partial fetch. For example, `--filter=blob:none` will filter\n-\tout all blobs (file contents) until needed by Git. Also,\n-\t`--filter=blob:limit=<size>` will filter out all blobs of size\n-\tat least _<size>_. For more details on filter specifications, see\n-\tthe `--filter` option in linkgit:git-rev-list[1].\n+\tthe partial fetch.\n++\n+If `--filter=auto` is used, the filter specification is determined\n+automatically by combining the filter specifications advertised by\n+the server for the promisor remotes that the client accepts (see\n+linkgit:gitprotocol-v2[5] and the `promisor.acceptFromServer`\n+configuration option in linkgit:git-config[1]).\n++\n+For details on all other available filter specifications, see the\n+`--filter=<filter-spec>` option in linkgit:git-rev-list[1].\n++\n+For example, `--filter=blob:none` will filter out all blobs (file\n+contents) until needed by Git. Also, `--filter=blob:limit=<size>` will\n+filter out all blobs of size at least _<size>_.\n \n ifndef::git-pull[]\n `--write-fetch-head`::\ndiff --git a/Documentation/git-clone.adoc b/Documentation/git-clone.adoc\nindex 57cdfb7620..0db2d1e5f0 100644\n--- a/Documentation/git-clone.adoc\n+++ b/Documentation/git-clone.adoc\n@@ -187,11 +187,26 @@ objects from the source repository into a pack in the cloned repository.\n \tUse the partial clone feature and request that the server sends\n \ta subset of reachable objects according to a given object filter.\n \tWhen using `--filter`, the supplied _<filter-spec>_ is used for\n-\tthe partial clone filter. For example, `--filter=blob:none` will\n-\tfilter out all blobs (file contents) until needed by Git. Also,\n-\t`--filter=blob:limit=<size>` will filter out all blobs of size\n-\tat least _<size>_. For more details on filter specifications, see\n-\tthe `--filter` option in linkgit:git-rev-list[1].\n+\tthe partial clone filter.\n++\n+If `--filter=auto` is used the filter specification is determined\n+automatically through the 'promisor-remote' protocol (see\n+linkgit:gitprotocol-v2[5]) by combining the filter specifications\n+advertised by the server for the promisor remotes that the client\n+accepts (see the `promisor.acceptFromServer` configuration option in\n+linkgit:git-config[1]). This allows the server to suggest the optimal\n+filter for the available promisor remotes.\n++\n+As with other filter specifications, the \"auto\" value is persisted in\n+the configuration. This ensures that future fetches will continue to\n+adapt to the server's current recommendation.\n++\n+For details on all other available filter specifications, see the\n+`--filter=<filter-spec>` option in linkgit:git-rev-list[1].\n++\n+For example, `--filter=blob:none` will filter out all blobs (file\n+contents) until needed by Git. Also, `--filter=blob:limit=<size>` will\n+filter out all blobs of size at least _<size>_.\n \n `--also-filter-submodules`::\n \tAlso apply the partial clone filter to any submodules in the repository.\ndiff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\nindex d93dd279ea..f985cb4c47 100644\n--- a/Documentation/gitprotocol-v2.adoc\n+++ b/Documentation/gitprotocol-v2.adoc\n@@ -812,10 +812,15 @@ MUST appear first in each pr-fields, in that order.\n After these mandatory fields, the server MAY advertise the following\n optional fields in any order:\n \n-`partialCloneFilter`:: The filter specification used by the remote.\n+`partialCloneFilter`:: The filter specification for the remote. It\n+corresponds to the \"remote.<name>.partialCloneFilter\" config setting.\n Clients can use this to determine if the remote's filtering strategy\n-is compatible with their needs (e.g., checking if both use \"blob:none\").\n-It corresponds to the \"remote.<name>.partialCloneFilter\" config setting.\n+is compatible with their needs (e.g., checking if both use\n+\"blob:none\"). Additionally they can use this through the\n+`--filter=auto` option in linkgit:git-clone[1]. With that option, the\n+filter specification of the clone will be automatically computed by\n+combining the filter specifications of the promisor remotes the client\n+accepts.\n \n `token`:: An authentication token that clients can use when\n connecting to the remote. It corresponds to the \"remote.<name>.token\"\n@@ -828,8 +833,9 @@ future protocol extensions.\n \n The client can use information transmitted through these fields to\n decide if it accepts the advertised promisor remote. Also, the client\n-can be configured to store the values of these fields (see\n-\"promisor.storeFields\" in linkgit:git-config[1]).\n+can be configured to store the values of these fields or use them\n+to automatically configure the repository (see \"promisor.storeFields\"\n+in linkgit:git-config[1] and `--filter=auto` in linkgit:git-clone[1]).\n \n Field values MUST be urlencoded.\n \ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 186e5498d4..41bbaea72a 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -1001,6 +1001,8 @@ int cmd_clone(int argc,\n \t\tNULL\n \t};\n \n+\tfilter_options.allow_auto_filter = 1;\n+\n \tpacket_trace_identity(\"clone\");\n \n \trepo_config(the_repository, git_clone_config, NULL);\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex b984173447..ddc30a0d30 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -2439,6 +2439,8 @@ int cmd_fetch(int argc,\n \t\tOPT_END()\n \t};\n \n+\tfilter_options.allow_auto_filter = 1;\n+\n \tpacket_trace_identity(\"fetch\");\n \n \t/* Record the command line for the reflog */\ndiff --git a/fetch-pack.c b/fetch-pack.c\nindex 40316c9a34..12ccea0dab 100644\n--- a/fetch-pack.c\n+++ b/fetch-pack.c\n@@ -35,6 +35,7 @@\n #include \"sigchain.h\"\n #include \"mergesort.h\"\n #include \"prio-queue.h\"\n+#include \"promisor-remote.h\"\n \n static int transfer_unpack_limit = -1;\n static int fetch_unpack_limit = -1;\n@@ -1661,6 +1662,25 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,\n \tstruct string_list packfile_uris = STRING_LIST_INIT_DUP;\n \tint i;\n \tstruct strvec index_pack_args = STRVEC_INIT;\n+\tconst char *promisor_remote_config;\n+\n+\tif (server_feature_v2(\"promisor-remote\", &promisor_remote_config)) {\n+\t\tchar *remote_name = promisor_remote_reply(promisor_remote_config);\n+\t\tfree(remote_name);\n+\t}\n+\n+\tif (args->filter_options.choice == LOFC_AUTO) {\n+\t\tstruct strbuf errbuf = STRBUF_INIT;\n+\t\tchar *constructed_filter = promisor_remote_construct_filter(r);\n+\n+\t\tlist_objects_filter_resolve_auto(&args->filter_options,\n+\t\t\t\t\t\t constructed_filter, &errbuf);\n+\t\tif (errbuf.len > 0)\n+\t\t\tdie(_(\"couldn't resolve 'auto' filter: %s\"), errbuf.buf);\n+\n+\t\tfree(constructed_filter);\n+\t\tstrbuf_release(&errbuf);\n+\t}\n \n \tnegotiator = &negotiator_alloc;\n \tif (args->refetch)\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex a726af214a..21543bce20 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -409,6 +409,66 @@ test_expect_success \"clone with promisor.storeFields=partialCloneFilter\" '\n \tcheck_missing_objects server 1 \"$oid\"\n '\n \n+test_expect_success \"clone and fetch with --filter=auto\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client trace\" &&\n+\n+\tgit -C server config remote.lop.partialCloneFilter \"blob:limit=9500\" &&\n+\ttest_config -C server promisor.sendFields \"partialCloneFilter\" &&\n+\n+\tGIT_TRACE_PACKET=\"$(pwd)/trace\" GIT_NO_LAZY_FETCH=0 git clone \\\n+\t\t-c remote.lop.promisor=true \\\n+\t\t-c remote.lop.url=\"file://$(pwd)/lop\" \\\n+\t\t-c promisor.acceptfromserver=All \\\n+\t\t--no-local --filter=auto server client 2>err &&\n+\n+\ttest_grep \"filter blob:limit=9500\" trace &&\n+\ttest_grep ! \"filter auto\" trace &&\n+\n+\t# Verify \"auto\" is persisted in config\n+\techo auto >expected &&\n+\tgit -C client config remote.origin.partialCloneFilter >actual &&\n+\ttest_cmp expected actual &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\" &&\n+\n+\t# Now change the filter on the server\n+\tgit -C server config remote.lop.partialCloneFilter \"blob:limit=5678\" &&\n+\n+\t# Get a new commit on the server to ensure \"git fetch\" actually runs fetch-pack\n+\ttest_commit -C template new-commit &&\n+\tgit -C template push --all \"$(pwd)/server\" &&\n+\n+\t# Perform a fetch WITH --filter=auto\n+\trm -rf trace &&\n+\tGIT_TRACE_PACKET=\"$(pwd)/trace\" git -C client fetch --filter=auto &&\n+\n+\t# Verify that the new filter was used\n+\ttest_grep \"filter blob:limit=5678\" trace &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\" &&\n+\n+\t# Change the filter on the server again\n+\tgit -C server config remote.lop.partialCloneFilter \"blob:limit=5432\" &&\n+\n+\t# Get yet a new commit on the server to ensure fetch-pack runs\n+\ttest_commit -C template yet-a-new-commit &&\n+\tgit -C template push --all \"$(pwd)/server\" &&\n+\n+\t# Perform a fetch WITHOUT --filter=auto\n+\t# Relies on \"auto\" being persisted in the client config\n+\trm -rf trace &&\n+\tGIT_TRACE_PACKET=\"$(pwd)/trace\" git -C client fetch &&\n+\n+\t# Verify that the new filter was used\n+\ttest_grep \"filter blob:limit=5432\" trace &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\"\n+'\n+\n test_expect_success \"clone with promisor.advertise set to 'true' but don't delete the client\" '\n \tgit -C server config promisor.advertise true &&\n \ndiff --git a/transport.c b/transport.c\nindex c7f06a7382..cde8d83a57 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1219,6 +1219,7 @@ struct transport *transport_get(struct remote *remote, const char *url)\n \t\t */\n \t\tstruct git_transport_data *data = xcalloc(1, sizeof(*data));\n \t\tlist_objects_filter_init(&data->options.filter_options);\n+\t\tdata->options.filter_options.allow_auto_filter = 1;\n \t\tret->data = data;\n \t\tret->vtable = &builtin_smart_vtable;\n \t\tret->smart_options = &(data->options);\n-- \n2.52.0.319.gfcaffa7898\n\n"},{"id":"532764","messageId":"4702585.LvFx2qVVIh@cayenne","threadId":"64670","inReplyTo":"20251223111113.47473-6-christian.couder@gmail.com","subject":"Re: [PATCH 5/9] doc: fetch: document `--filter=<filter-spec>` option","fromName":"Jean-Noël AVILA","fromEmail":"avila.jn@gmail.com","sentAt":"2025-12-26T13:33:38Z","receivedAt":"2025-12-26T13:34:01Z","isPatch":true,"body":"On Tuesday, 23 December 2025 12:11:09 CET Christian Couder wrote:\n> The `--filter=<filter-spec>` option is documented in most commands that\n> support it except `git fetch`.\n> \n> Let's fix that and document that option properly in the same way as it\n> is already documented for `git clone`.\n> \n> Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n> ---\n>  Documentation/fetch-options.adoc | 10 ++++++++++\n>  1 file changed, 10 insertions(+)\n> \n> diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-\noptions.adoc\n> index fcba46ee9e..70a9818331 100644\n> --- a/Documentation/fetch-options.adoc\n> +++ b/Documentation/fetch-options.adoc\n> @@ -88,6 +88,16 @@ linkgit:git-config[1].\n>  This is incompatible with `--recurse-submodules=(yes|on-demand)` and takes\n>  precedence over the `fetch.output` config option.\n> \n> +--filter=<filter-spec>::\n\nThe option itself must also be back-ticked.\n\n`--filter=<filter-spec>`::\n\n> +\tUse the partial clone feature and request that the server sends\n> +\ta subset of reachable objects according to a given object filter.\n> +\tWhen using `--filter`, the supplied _<filter-spec>_ is used for\n> +\tthe partial fetch. For example, `--filter=blob:none` will filter\n\n\nIsn't this second sentence redundant? What new information is brought?\n\n> +\tout all blobs (file contents) until needed by Git. Also,\n> +\t`--filter=blob:limit=<size>` will filter out all blobs of size\n> +\tat least _<size>_. For more details on filter specifications, see\n> +\tthe `--filter` option in linkgit:git-rev-list[1].\n> +\n>  ifndef::git-pull[]\n>  `--write-fetch-head`::\n>  `--no-write-fetch-head`::\n\nThank you\n\nJean-Noël\n\n\n"},{"id":"533189","messageId":"aV4v3JwW0S-c9Dn4@pks.im","threadId":"64670","inReplyTo":"20251223111113.47473-3-christian.couder@gmail.com","subject":"Re: [PATCH 2/9] promisor-remote: allow a client to store fields","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-07T10:05:16Z","receivedAt":"2026-01-07T10:05:28Z","isPatch":true,"body":"On Tue, Dec 23, 2025 at 12:11:06PM +0100, Christian Couder wrote:\n> A previous commit allowed a server to pass additional fields through\n> the \"promisor-remote\" protocol capability after the \"name\" and \"url\"\n> fields, specifically the \"partialCloneFilter\" and \"token\" fields.\n> \n> Another previous commit, c213820c51 (promisor-remote: allow a client\n> to check fields, 2025-09-08), has made it possible for a client to\n> decide if it accepts a promisor remote advertised by a server based\n> on these additional fields.\n> \n> Often though, it would be interesting for the client to just store in\n> its configuration files these additional fields passed by the server,\n> so that it can use them when needed.\n> \n> For example if a token is necessary to access a promisor remote, that\n> token could be updated frequently only on the server side and then\n> passed to all the clients through the \"promisor-remote\" capability,\n> avoiding the need to update it on all the clients manually.\n> \n> Storing the token on the client side makes sure that the token is\n> available when the client needs to access the promisor remotes for a\n> lazy fetch.\n\nI guess another use case is that a client performs a fresh clone and\ndoesn't know anything about the remote's promisors yet, right? In that\ncase, the client may want to tell git-clone(1) to accept any of the\nremote's advertised promisors, store it and then use that promisor's\nfilter to perform the actual clone.\n\n> In the same way, if it appears that it's better to use a different\n> filter to access a promisor remote, it could be helpful if the client\n> could automatically use it.\n> \n> To allow this, let's introduce a new \"promisor.storeFields\"\n> configuration variable.\n> \n> Like \"promisor.checkFields\" and \"promisor.sendFields\", it should\n> contain a comma or space separated list of field names. Only the\n> \"partialCloneFilter\" and \"token\" field names are supported for now.\n> \n> When a server advertises a promisor remote, for example \"foo\", along\n> with for example \"token=XXXXX\" to a client, and on the client side\n> \"promisor.storeFields\" contains \"token\", then the client will store\n> XXXXX for the \"remote.foo.token\" variable in its configuration file\n> and reload its configuration so it can immediately use this new\n> configuration variable.\n> \n> A message is emitted on stderr to warn users when the config is\n> changed.\n> \n> Note that even if \"promisor.acceptFromServer\" is set to \"all\", a\n> promisor remote has to be already configured on the client side for\n> some of its config to be changed. In any case no new remote is\n> configured and no new URL is stored.\n\nHm, okay, so that's not yet part of this series. I assume this is going\nto be part of a subsequent patch series then?\n\n> diff --git a/promisor-remote.c b/promisor-remote.c\n> index 5d8151cedb..8d6d2d7b76 100644\n> --- a/promisor-remote.c\n> +++ b/promisor-remote.c\n> @@ -403,6 +403,14 @@ static struct string_list *fields_checked(void)\n>  \treturn initialize_fields_list(&fields_list, &initialized, \"promisor.checkFields\");\n>  }\n>  \n> +static struct string_list *fields_stored(void)\n> +{\n> +\tstatic struct string_list fields_list = STRING_LIST_INIT_NODUP;\n> +\tstatic int initialized;\n> +\n> +\treturn initialize_fields_list(&fields_list, &initialized, \"promisor.storeFields\");\n> +}\n\nI'm a bit worried about all the function-local state that we're\naccumulating in those functions. Wouldn't it be preferable if we instead\nhad a `struct promisor_remote` that encapsulates the information?\n\n> @@ -692,6 +700,132 @@ static struct promisor_info *parse_one_advertised_remote(const char *remote_info\n>  \treturn info;\n>  }\n>  \n> +static bool store_one_field(struct repository *repo, const char *remote_name,\n> +\t\t\t    const char *field_name, const char *field_key,\n> +\t\t\t    const char *advertised, const char *current)\n> +{\n> +\tif (advertised && (!current || strcmp(current, advertised))) {\n> +\t\tchar *key = xstrfmt(\"remote.%s.%s\", remote_name, field_key);\n> +\n> +\t\tfprintf(stderr, _(\"Storing new %s from server for remote '%s'.\\n\"\n> +\t\t\t\t  \"    '%s' -> '%s'\\n\"),\n> +\t\t\tfield_name, remote_name,\n> +\t\t\tcurrent ? current : \"\",\n> +\t\t\tadvertised);\n> +\n> +\t\trepo_config_set_worktree_gently(repo, key, advertised);\n\nWhy do we store this information in the current per-worktree config? I'd\nexpect that this should be stored in the local config.\n\n> +\t\tfree(key);\n> +\n> +\t\treturn true;\n> +\t}\n> +\n> +\treturn false;\n> +}\n> +\n> +/* Check that a filter is valid by parsing it */\n> +static bool valid_filter(const char *filter, const char *remote_name)\n> +{\n> +\tstruct list_objects_filter_options filter_opts = LIST_OBJECTS_FILTER_INIT;\n> +\tstruct strbuf err = STRBUF_INIT;\n> +\tint res = gently_parse_list_objects_filter(&filter_opts, filter, &err);\n> +\n> +\tif (res)\n> +\t\twarning(_(\"invalid filter '%s' for remote '%s' \"\n> +\t\t\t  \"will not be stored: %s\"),\n> +\t\t\tfilter, remote_name, err.buf);\n> +\n> +\tlist_objects_filter_release(&filter_opts);\n> +\tstrbuf_release(&err);\n> +\n> +\treturn !res;\n> +}\n> +\n> +/* Check that a token doesn't contain any control character */\n> +static bool valid_token(const char *token, const char *remote_name)\n> +{\n> +\tconst char *c = token;\n> +\n> +\tfor (; *c; c++)\n> +\t\tif (iscntrl(*c)) {\n\nMakes sense. I was also wondering about whether we want to check for\nnon-space whitespace characters, like newlines.\n\n> +\t\t\twarning(_(\"invalid token '%s' for remote '%s' \"\n> +\t\t\t\t  \"will not be stored\"),\n> +\t\t\t\ttoken, remote_name);\n> +\t\t\treturn false;\n> +\t\t}\n> +\n> +\treturn true;\n> +}\n> +\n> +struct store_info {\n> +\tstruct repository *repo;\n> +\tstruct string_list config_info;\n> +\tbool store_filter;\n> +\tbool store_token;\n> +};\n> +\n> +static struct store_info *new_store_info(struct repository *repo)\n\nThis should be called `store_info_new()` according to our coding\nguidelines.\n\n> +{\n> +\tstruct string_list *fields_to_store = fields_stored();\n> +\tstruct store_info *s = xmalloc(sizeof(*s));\n> +\n> +\ts->repo = repo;\n> +\n> +\tstring_list_init_nodup(&s->config_info);\n> +\tpromisor_config_info_list(repo, &s->config_info, fields_to_store);\n> +\tstring_list_sort(&s->config_info);\n> +\n> +\ts->store_filter = !!string_list_lookup(fields_to_store, promisor_field_filter);\n> +\ts->store_token = !!string_list_lookup(fields_to_store, promisor_field_token);\n> +\n> +\treturn s;\n> +}\n> +\n> +static void free_store_info(struct store_info *s)\n\nLikewise, this would be `store_info_free()`.\n\n> diff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\n> index 023735d6a8..a726af214a 100755\n> --- a/t/t5710-promisor-remote-capability.sh\n> +++ b/t/t5710-promisor-remote-capability.sh\n> @@ -360,6 +360,55 @@ test_expect_success \"clone with promisor.checkFields\" '\n>  \tcheck_missing_objects server 1 \"$oid\"\n>  '\n>  \n> +test_expect_success \"clone with promisor.storeFields=partialCloneFilter\" '\n> +\tgit -C server config promisor.advertise true &&\n> +\ttest_when_finished \"rm -rf client\" &&\n> +\n> +\tgit -C server remote add otherLop \"https://invalid.invalid\"  &&\n> +\tgit -C server config remote.otherLop.token \"fooBar\" &&\n> +\tgit -C server config remote.otherLop.stuff \"baz\" &&\n> +\tgit -C server config remote.otherLop.partialCloneFilter \"blob:limit=10k\" &&\n> +\ttest_when_finished \"git -C server remote remove otherLop\" &&\n> +\n> +\tgit -C server config remote.lop.token \"fooXXX\" &&\n> +\tgit -C server config remote.lop.partialCloneFilter \"blob:limit=8k\" &&\n> +\n> +\ttest_config -C server promisor.sendFields \"partialCloneFilter, token\" &&\n> +\ttest_when_finished \"rm trace\" &&\n> +\n> +\t# Clone from server to create a client\n> +\tGIT_TRACE_PACKET=\"$(pwd)/trace\" GIT_NO_LAZY_FETCH=0 git clone \\\n> +\t\t-c remote.lop.promisor=true \\\n> +\t\t-c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n> +\t\t-c remote.lop.url=\"file://$(pwd)/lop\" \\\n> +\t\t-c remote.lop.token=\"fooYYY\" \\\n> +\t\t-c remote.lop.partialCloneFilter=\"blob:none\" \\\n> +\t\t-c promisor.acceptfromserver=All \\\n> +\t\t-c promisor.storeFields=partialcloneFilter \\\n> +\t\t--no-local --filter=\"blob:limit=5k\" server client 2>err &&\n\nOnet thing that's missing in these tests is to verify that a subsequent\ngit-fetch(1) updates the configuration.\n\nPatrick\n"},{"id":"533190","messageId":"aV4v5m29XfRcvI_9@pks.im","threadId":"64670","inReplyTo":"20251223111113.47473-5-christian.couder@gmail.com","subject":"Re: [PATCH 4/9] fetch: make filter_options local to cmd_fetch()","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-07T10:05:26Z","receivedAt":"2026-01-07T10:05:31Z","isPatch":true,"body":"On Tue, Dec 23, 2025 at 12:11:08PM +0100, Christian Couder wrote:\n> The `struct list_objects_filter_options filter_options` variable used\n> in \"builtin/fetch.c\" to store the parsed filters specified by\n> `--filter=<filterspec>` is currently a static variable global to the\n> file.\n> \n> As we are going to use it more in a following commit, it could become a\n> bit less easy to understand how it's managed.\n> \n> To avoid that, let's make it clear that it's owned by cmd_fetch() by\n> moving its definition into that function and making it non-static.\n> \n> This requires passing a pointer to it through the prepare_transport(),\n> do_fetch(), backfill_tags(), fetch_one_setup_partial(), and fetch_one()\n> functions, but it's quite straightforward.\n\nNice cleanups. I'm always happy to see less global state.\n\nPatrick\n"},{"id":"533191","messageId":"aV4v6_DFJtraLlPI@pks.im","threadId":"64670","inReplyTo":"20251223111113.47473-7-christian.couder@gmail.com","subject":"Re: [PATCH 6/9] list-objects-filter-options: support 'auto' mode for --filter","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-07T10:05:31Z","receivedAt":"2026-01-07T10:05:37Z","isPatch":true,"body":"On Tue, Dec 23, 2025 at 12:11:10PM +0100, Christian Couder wrote:\n> In a following commit, we are going to allow passing \"auto\" as a\n> <filterspec> to the `--filter=<filterspec>` option, but only for some\n> commands. Other commands that support the `--filter=<filterspec>`\n> option should still die() when 'auto' is passed.\n\nOkay. I assume the idea is that the user can eventually say `git clone\n--filter=auto`, and Git would automatically pick the best filter\nadvertised by the remote. Sounds reasonable to me.\n\n> Let's set up the \"list-objects-filter-options.{c,h}\" infrastructure to\n> support that:\n> \n> - Add a new `unsigned int allow_auto_filter : 1;` flag to\n>   `struct list_objects_filter_options` which specifies if \"auto\" is\n>   accepted or not.\n> - Change gently_parse_list_objects_filter() to parse \"auto\" if it's\n>   accepted.\n> - Make sure we die() if \"auto\" is combined with another filter.\n> - Update list_objects_filter_release() to preserve the\n>   allow_auto_filter flag, as this function is often called (via\n>   opt_parse_list_objects_filter) to reset the struct before parsing a\n>   new value.\n> \n> Let's also update `list-objects-filter.c` to recognize the new\n> `LOFC_AUTO` choice. Since \"auto\" must be resolved to a concrete filter\n> before filtering actually begins, initializing a filter with\n> `LOFC_AUTO` is invalid and will trigger a BUG().\n> \n> Note that ideally combining \"auto\" with \"auto\" could be allowed, but in\n> practice, it's probably not worth the added code complexity. And if we\n> really want it, nothing prevents us to allow it in future work.\n\nI guess the question is what this would even mean, and I cannot think\nof any benefit to allow `--filter=combine:auto+auto`. So agreed\n\n> If we ever want to give a meaning to combining \"auto\" with a different\n> filter too, nothing prevents us to do that in future work either.\n\nSo basically the case where the user knows that they definitely don't\nwant blobs, and in addition they want to pick the best filter advertised\nby the server? Yeah, that sounds like it could eventually be a nice\naddition.\n\n> diff --git a/list-objects-filter-options.c b/list-objects-filter-options.c\n> index 7420bf81fe..f13ae5caeb 100644\n> --- a/list-objects-filter-options.c\n> +++ b/list-objects-filter-options.c\n> @@ -52,7 +54,17 @@ int gently_parse_list_objects_filter(\n>  \tif (filter_options->choice)\n>  \t\tBUG(\"filter_options already populated\");\n>  \n> -\tif (!strcmp(arg, \"blob:none\")) {\n> +\tif (!strcmp(arg, \"auto\")) {\n> +\t\tif (!filter_options->allow_auto_filter) {\n> +\t\t\tstrbuf_addstr(\n> +\t\t\t\terrbuf,\n> +\t\t\t\t_(\"'auto' filter not supported by this command\"));\n\nTiny nit: the indentation looks a bit weird here.\n\n> @@ -146,10 +158,20 @@ static int parse_combine_subfilter(\n>  \n>  \tdecoded = url_percent_decode(subspec->buf);\n>  \n> -\tresult = has_reserved_character(subspec, errbuf) ||\n> -\t\tgently_parse_list_objects_filter(\n> +\tresult = has_reserved_character(subspec, errbuf);\n> +\tif (result)\n> +\t\tgoto cleanup;\n> +\n> +\tresult = gently_parse_list_objects_filter(\n>  \t\t\t&filter_options->sub[new_index], decoded, errbuf);\n> +\tif (result)\n> +\t\tgoto cleanup;\n> +\n> +\tresult = (filter_options->sub[new_index].choice == LOFC_AUTO);\n> +\tif (result)\n> +\t\tstrbuf_addstr(errbuf, _(\"an 'auto' filter cannot be combined\"));\n\nNit: let's maybe also add the `goto cleanup` here. I'm not a fan of\nleaving it away for the final statement as it makes it easy to forget\nbackfilling it in case this function needs to be extended in the future.\n\n> @@ -317,6 +345,7 @@ void list_objects_filter_release(\n>  \tstruct list_objects_filter_options *filter_options)\n>  {\n>  \tsize_t sub;\n> +\tunsigned int allow_auto_filter = filter_options->allow_auto_filter;\n>  \n>  \tif (!filter_options)\n>  \t\treturn;\n> @@ -326,6 +355,7 @@ void list_objects_filter_release(\n>  \t\tlist_objects_filter_release(&filter_options->sub[sub]);\n>  \tfree(filter_options->sub);\n>  \tlist_objects_filter_init(filter_options);\n> +\tfilter_options->allow_auto_filter = allow_auto_filter;\n>  }\n\nWhy do we do this extra step to restore the `allow_auto_filter` option\nhere? Are there any callers that reuse the filter after it has been\nreleased?\n\nIn any case, this function does have clearing semantics as it also knows\nto re-init the filter options. So it's somewhat misnamed and really\nshould be called `list_objects_filter_clear()` according to our coding\nguidelines. That's certainly outside the scope of this patch series\nthough.\n\nPatrick\n"},{"id":"533192","messageId":"aV4v8HCe6CLqXJ-1@pks.im","threadId":"64670","inReplyTo":"20251223111113.47473-8-christian.couder@gmail.com","subject":"Re: [PATCH 7/9] list-objects-filter-options: implement auto filter resolution","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-07T10:05:36Z","receivedAt":"2026-01-07T10:05:42Z","isPatch":true,"body":"On Tue, Dec 23, 2025 at 12:11:11PM +0100, Christian Couder wrote:\n> In a following commit, we will need to aggregate filters from multiple\n> accepted promisor remotes into a single filter.\n\nAh, interesting. I was always operating under the assumption that when\nthe server advertises multiple promisors, the client will pick only one\nof them. And that made me wonder how the client knows which one to pick\nin the first place.\n\nBut of course it's possible to just pick _all_ of them by combining the\nfilter.\n\n> diff --git a/list-objects-filter-options.c b/list-objects-filter-options.c\n> index f13ae5caeb..4a9c1991c1 100644\n> --- a/list-objects-filter-options.c\n> +++ b/list-objects-filter-options.c\n> @@ -230,6 +230,41 @@ static void filter_spec_append_urlencode(\n>  \t\t     filter->filter_spec.buf + orig_len);\n>  }\n>  \n> +char *list_objects_filter_combine(const struct string_list *specs)\n> +{\n> +\tstruct strbuf buf = STRBUF_INIT;\n> +\n> +\tif (!specs->nr)\n> +\t\treturn NULL;\n> +\n> +\tif (specs->nr == 1)\n> +\t\treturn xstrdup(specs->items[0].string);\n> +\n> +\tstrbuf_addstr(&buf, \"combine:\");\n> +\n> +\tfor (size_t i = 0; i < specs->nr; i++) {\n> +\t\tconst char *spec = specs->items[i].string;\n> +\t\tif (i > 0)\n> +\t\t\tstrbuf_addch(&buf, '+');\n> +\n> +\t\tstrbuf_addstr_urlencode(&buf, spec, allow_unencoded);\n\nShouldn't we use `filter_spec_append_urlencode()` to do this?\n\n> +\t}\n> +\n> +\treturn strbuf_detach(&buf, NULL);\n> +}\n\nI'm surprised we didn't have such a function yet.\n\n> +void list_objects_filter_resolve_auto(struct list_objects_filter_options *filter_options,\n> +\tchar *new_filter, struct strbuf *errbuf)\n> +{\n> +\tif (filter_options->choice != LOFC_AUTO)\n> +\t\treturn;\n\nI wonder whether we should rather `BUG()` in case the filter is not an\n\"auto\" filter. Otherwise it's easy to get the callsite wrong, as the\nuser may expect that the filter gets resolved tdo the new filter, but\nit's actually not because the original filter wasn't an \"auto\" filter in\nthe first place.\n\n> +\tlist_objects_filter_release(filter_options);\n> +\n> +\tif (new_filter)\n> +\t\tgently_parse_list_objects_filter(filter_options, new_filter, errbuf);\n> +}\n\nSo as menitoned in a preceding commit `list_objects_filter_release()`,\nwill retain the `allow_auto` option. But when resolving \"auto\" filters\nI'd expect us to not accept \"auto\" in the resolved filter anymore.\nOtherwise, if `new_filter` was \"auto\", we'd still end up with an auto\nfilter, wouldn't we? I'd rather expect us to abort in that case.\n\nPatrick\n"},{"id":"533194","messageId":"aV4v9WhL95Gcqr2t@pks.im","threadId":"64670","inReplyTo":"20251223111113.47473-9-christian.couder@gmail.com","subject":"Re: [PATCH 8/9] promisor-remote: keep advertised filter in memory","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-07T10:05:41Z","receivedAt":"2026-01-07T10:05:46Z","isPatch":true,"body":"On Tue, Dec 23, 2025 at 12:11:12PM +0100, Christian Couder wrote:\n> diff --git a/promisor-remote.c b/promisor-remote.c\n> index 8d6d2d7b76..d5f3223cd0 100644\n> --- a/promisor-remote.c\n> +++ b/promisor-remote.c\n> @@ -837,6 +838,7 @@ static void filter_promisor_remote(struct repository *repo,\n>  \tstruct store_info *store_info = NULL;\n>  \tstruct string_list_item *item;\n>  \tbool reload_config = false;\n> +\tstruct string_list captured_filters = STRING_LIST_INIT_DUP;\n>  \n>  \tif (!repo_config_get_string_tmp(the_repository, \"promisor.acceptfromserver\", &accept_str)) {\n>  \t\tif (!*accept_str || !strcasecmp(\"None\", accept_str))\n\nNit: I found the \"captured\" terminology to be somewhat confusing. Can we\nmaybe rename this to `advertised_filters` to clarify?\n\n> @@ -890,6 +899,25 @@ static void filter_promisor_remote(struct repository *repo,\n>  \n>  \tif (reload_config)\n>  \t\trepo_promisor_remote_reinit(repo);\n> +\n> +\t/* Apply captured filters to the stable repo state */\n> +\tfor_each_string_list_item(item, &captured_filters) {\n> +\t\tstruct promisor_remote *r = repo_promisor_remote_find(repo, item->string);\n> +\t\tif (r) {\n> +\t\t\tfree(r->advertised_filter);\n> +\t\t\tr->advertised_filter = item->util;\n> +\t\t\titem->util = NULL;\n> +\t\t}\n> +\t}\n> +\n> +\tstring_list_clear(&captured_filters, 1);\n\nAh, I was wondering about memory lifetime first because we ask\n`string_list_clear()` to free the `->util` pointers. But above we set\nthat pointer to `NULL` in case we retain it.\n\n> @@ -935,3 +963,23 @@ void mark_promisor_remotes_as_accepted(struct repository *r, const char *remotes\n>  \n>  \tstring_list_clear(&accepted_remotes, 0);\n>  }\n> +\n> +char *promisor_remote_construct_filter(struct repository *repo)\n> +{\n> +\tstruct string_list advertised_filters = STRING_LIST_INIT_NODUP;\n> +\tstruct promisor_remote *r;\n> +\tchar *result;\n> +\n> +\tpromisor_remote_init(repo);\n> +\n> +\tfor (r = repo->promisor_remote_config->promisors; r; r = r->next) {\n> +\t\tif (r->accepted && r->advertised_filter)\n> +\t\t\tstring_list_append(&advertised_filters, r->advertised_filter);\n\nWould we ever accept a promisor remote that _doesn't_ have an advertised\nfilter? If not, should we maybe `BUG()` in case the advertised filter\nhas not been set?\n\nPatrick\n"},{"id":"533193","messageId":"aV4v--FYaHCLLrPz@pks.im","threadId":"64670","inReplyTo":"20251223111113.47473-10-christian.couder@gmail.com","subject":"Re: [PATCH 9/9] fetch-pack: wire up and enable auto filter logic","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-07T10:05:47Z","receivedAt":"2026-01-07T10:05:52Z","isPatch":true,"body":"On Tue, Dec 23, 2025 at 12:11:13PM +0100, Christian Couder wrote:\n> diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc\n> index 70a9818331..f7432d4b29 100644\n> --- a/Documentation/fetch-options.adoc\n> +++ b/Documentation/fetch-options.adoc\n> @@ -92,11 +92,20 @@ precedence over the `fetch.output` config option.\n>  \tUse the partial clone feature and request that the server sends\n>  \ta subset of reachable objects according to a given object filter.\n>  \tWhen using `--filter`, the supplied _<filter-spec>_ is used for\n> -\tthe partial fetch. For example, `--filter=blob:none` will filter\n> -\tout all blobs (file contents) until needed by Git. Also,\n> -\t`--filter=blob:limit=<size>` will filter out all blobs of size\n> -\tat least _<size>_. For more details on filter specifications, see\n> -\tthe `--filter` option in linkgit:git-rev-list[1].\n> +\tthe partial fetch.\n> ++\n> +If `--filter=auto` is used, the filter specification is determined\n> +automatically by combining the filter specifications advertised by\n> +the server for the promisor remotes that the client accepts (see\n> +linkgit:gitprotocol-v2[5] and the `promisor.acceptFromServer`\n> +configuration option in linkgit:git-config[1]).\n\nOkay, so if \"promisor.acceptFromServer\" enables a subset of advertised\npromisors we will automatically use their advertised filters. But what\nabout the case where we already have a set of local promisors with their\nown filters, would those also honored by \"--filter=auto\"?\n\n> diff --git a/builtin/clone.c b/builtin/clone.c\n> index 186e5498d4..41bbaea72a 100644\n> --- a/builtin/clone.c\n> +++ b/builtin/clone.c\n> @@ -1001,6 +1001,8 @@ int cmd_clone(int argc,\n>  \t\tNULL\n>  \t};\n>  \n> +\tfilter_options.allow_auto_filter = 1;\n> +\n>  \tpacket_trace_identity(\"clone\");\n>  \n>  \trepo_config(the_repository, git_clone_config, NULL);\n> diff --git a/builtin/fetch.c b/builtin/fetch.c\n> index b984173447..ddc30a0d30 100644\n> --- a/builtin/fetch.c\n> +++ b/builtin/fetch.c\n> @@ -2439,6 +2439,8 @@ int cmd_fetch(int argc,\n>  \t\tOPT_END()\n>  \t};\n>  \n> +\tfilter_options.allow_auto_filter = 1;\n> +\n>  \tpacket_trace_identity(\"fetch\");\n>  \n>  \t/* Record the command line for the reflog */\n\nNice that both of these changes are so easy now.\n\n> diff --git a/fetch-pack.c b/fetch-pack.c\n> index 40316c9a34..12ccea0dab 100644\n> --- a/fetch-pack.c\n> +++ b/fetch-pack.c\n> @@ -1661,6 +1662,25 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,\n>  \tstruct string_list packfile_uris = STRING_LIST_INIT_DUP;\n>  \tint i;\n>  \tstruct strvec index_pack_args = STRVEC_INIT;\n> +\tconst char *promisor_remote_config;\n> +\n> +\tif (server_feature_v2(\"promisor-remote\", &promisor_remote_config)) {\n> +\t\tchar *remote_name = promisor_remote_reply(promisor_remote_config);\n> +\t\tfree(remote_name);\n> +\t}\n> +\n> +\tif (args->filter_options.choice == LOFC_AUTO) {\n> +\t\tstruct strbuf errbuf = STRBUF_INIT;\n> +\t\tchar *constructed_filter = promisor_remote_construct_filter(r);\n> +\n> +\t\tlist_objects_filter_resolve_auto(&args->filter_options,\n> +\t\t\t\t\t\t constructed_filter, &errbuf);\n> +\t\tif (errbuf.len > 0)\n> +\t\t\tdie(_(\"couldn't resolve 'auto' filter: %s\"), errbuf.buf);\n\nNow that I see it being used I think that the calling convention of this\nfunction is a bit weird. I would've expected the function to return an\nerror code that the caller can consult instead of having to check for\n`errbuf.len`.\n\nPatrick\n"},{"id":"535124","messageId":"CAP8UFD0Xmfi38=q5FBL6UeoJNqNTw7cD9pAqi_vAr3dFxv2w3Q@mail.gmail.com","threadId":"64670","inReplyTo":"aV4v3JwW0S-c9Dn4@pks.im","subject":"Re: [PATCH 2/9] promisor-remote: allow a client to store fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-04T10:20:21Z","receivedAt":"2026-02-04T10:20:33Z","isPatch":true,"body":"On Wed, Jan 7, 2026 at 11:05 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Tue, Dec 23, 2025 at 12:11:06PM +0100, Christian Couder wrote:\n> > A previous commit allowed a server to pass additional fields through\n> > the \"promisor-remote\" protocol capability after the \"name\" and \"url\"\n> > fields, specifically the \"partialCloneFilter\" and \"token\" fields.\n> >\n> > Another previous commit, c213820c51 (promisor-remote: allow a client\n> > to check fields, 2025-09-08), has made it possible for a client to\n> > decide if it accepts a promisor remote advertised by a server based\n> > on these additional fields.\n> >\n> > Often though, it would be interesting for the client to just store in\n> > its configuration files these additional fields passed by the server,\n> > so that it can use them when needed.\n> >\n> > For example if a token is necessary to access a promisor remote, that\n> > token could be updated frequently only on the server side and then\n> > passed to all the clients through the \"promisor-remote\" capability,\n> > avoiding the need to update it on all the clients manually.\n> >\n> > Storing the token on the client side makes sure that the token is\n> > available when the client needs to access the promisor remotes for a\n> > lazy fetch.\n>\n> I guess another use case is that a client performs a fresh clone and\n> doesn't know anything about the remote's promisors yet, right? In that\n> case, the client may want to tell git-clone(1) to accept any of the\n> remote's advertised promisors, store it and then use that promisor's\n> filter to perform the actual clone.\n\nActually there are two issues with this.\n\nThe first one is the security issue with the client adding a new\npromisor to its config that I will discuss below.\n\nThe second one is the fact that it's better if the filter suggested by\nthe server is used right away during the initial clone, but you have\nto pass a `--filter=<filter-spec>` to the clone option in the first\nplace when you start the initial clone and the filter suggested by the\nserver might be different than the one you pass. This is why the\nsecond part of the series implements `--filter=auto`.\n\n> > In the same way, if it appears that it's better to use a different\n> > filter to access a promisor remote, it could be helpful if the client\n> > could automatically use it.\n\nBy the way I have removed this in the version 2 I am going to send\nsoon, as it could be misleading.\n\n> > To allow this, let's introduce a new \"promisor.storeFields\"\n> > configuration variable.\n> >\n> > Like \"promisor.checkFields\" and \"promisor.sendFields\", it should\n> > contain a comma or space separated list of field names. Only the\n> > \"partialCloneFilter\" and \"token\" field names are supported for now.\n> >\n> > When a server advertises a promisor remote, for example \"foo\", along\n> > with for example \"token=XXXXX\" to a client, and on the client side\n> > \"promisor.storeFields\" contains \"token\", then the client will store\n> > XXXXX for the \"remote.foo.token\" variable in its configuration file\n> > and reload its configuration so it can immediately use this new\n> > configuration variable.\n> >\n> > A message is emitted on stderr to warn users when the config is\n> > changed.\n> >\n> > Note that even if \"promisor.acceptFromServer\" is set to \"all\", a\n> > promisor remote has to be already configured on the client side for\n> > some of its config to be changed. In any case no new remote is\n> > configured and no new URL is stored.\n>\n> Hm, okay, so that's not yet part of this series. I assume this is going\n> to be part of a subsequent patch series then?\n\nMy opinion is that we should indeed work on that in a future separate\nseries, as it could be very useful in setups where clients trust the\nserver, like corporate setups. For now I prefer to keep things safe by\ndefault and not make it possible.\n\n> > diff --git a/promisor-remote.c b/promisor-remote.c\n> > index 5d8151cedb..8d6d2d7b76 100644\n> > --- a/promisor-remote.c\n> > +++ b/promisor-remote.c\n> > @@ -403,6 +403,14 @@ static struct string_list *fields_checked(void)\n> >       return initialize_fields_list(&fields_list, &initialized, \"promisor.checkFields\");\n> >  }\n> >\n> > +static struct string_list *fields_stored(void)\n> > +{\n> > +     static struct string_list fields_list = STRING_LIST_INIT_NODUP;\n> > +     static int initialized;\n> > +\n> > +     return initialize_fields_list(&fields_list, &initialized, \"promisor.storeFields\");\n> > +}\n>\n> I'm a bit worried about all the function-local state that we're\n> accumulating in those functions. Wouldn't it be preferable if we instead\n> had a `struct promisor_remote` that encapsulates the information?\n\nI don't think we have a good standard way to manage information from\nthe config yet. Some suggestions have been made about using a new\nstruct for some config options, for example in:\n\nhttps://lore.kernel.org/git/8899016f-eeef-404b-8da6-ff3a90e81cea@gmail.com/\n\nand perhaps such a good standard way to manage config information will\nresult from these efforts, but I think it's too early to be sure.\n\nIn the meantime, I don't think it's a good idea to spend time on a\nspecialized way to do it just for promisor remotes.\n\n> > @@ -692,6 +700,132 @@ static struct promisor_info *parse_one_advertised_remote(const char *remote_info\n> >       return info;\n> >  }\n> >\n> > +static bool store_one_field(struct repository *repo, const char *remote_name,\n> > +                         const char *field_name, const char *field_key,\n> > +                         const char *advertised, const char *current)\n> > +{\n> > +     if (advertised && (!current || strcmp(current, advertised))) {\n> > +             char *key = xstrfmt(\"remote.%s.%s\", remote_name, field_key);\n> > +\n> > +             fprintf(stderr, _(\"Storing new %s from server for remote '%s'.\\n\"\n> > +                               \"    '%s' -> '%s'\\n\"),\n> > +                     field_name, remote_name,\n> > +                     current ? current : \"\",\n> > +                     advertised);\n> > +\n> > +             repo_config_set_worktree_gently(repo, key, advertised);\n>\n> Why do we store this information in the current per-worktree config? I'd\n> expect that this should be stored in the local config.\n\nRight, repo_config_set_gently() is used now instead.\n\n> > +             free(key);\n> > +\n> > +             return true;\n> > +     }\n\n[...]\n\n> > +struct store_info {\n> > +     struct repository *repo;\n> > +     struct string_list config_info;\n> > +     bool store_filter;\n> > +     bool store_token;\n> > +};\n> > +\n> > +static struct store_info *new_store_info(struct repository *repo)\n>\n> This should be called `store_info_new()` according to our coding\n> guidelines.\n\nFine, `store_info_new()` and `store_info_free()` are now used as you suggest.\n\n> > diff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\n> > index 023735d6a8..a726af214a 100755\n> > --- a/t/t5710-promisor-remote-capability.sh\n> > +++ b/t/t5710-promisor-remote-capability.sh\n> > @@ -360,6 +360,55 @@ test_expect_success \"clone with promisor.checkFields\" '\n> >       check_missing_objects server 1 \"$oid\"\n> >  '\n> >\n> > +test_expect_success \"clone with promisor.storeFields=partialCloneFilter\" '\n> > +     git -C server config promisor.advertise true &&\n> > +     test_when_finished \"rm -rf client\" &&\n> > +\n> > +     git -C server remote add otherLop \"https://invalid.invalid\"  &&\n> > +     git -C server config remote.otherLop.token \"fooBar\" &&\n> > +     git -C server config remote.otherLop.stuff \"baz\" &&\n> > +     git -C server config remote.otherLop.partialCloneFilter \"blob:limit=10k\" &&\n> > +     test_when_finished \"git -C server remote remove otherLop\" &&\n> > +\n> > +     git -C server config remote.lop.token \"fooXXX\" &&\n> > +     git -C server config remote.lop.partialCloneFilter \"blob:limit=8k\" &&\n> > +\n> > +     test_config -C server promisor.sendFields \"partialCloneFilter, token\" &&\n> > +     test_when_finished \"rm trace\" &&\n> > +\n> > +     # Clone from server to create a client\n> > +     GIT_TRACE_PACKET=\"$(pwd)/trace\" GIT_NO_LAZY_FETCH=0 git clone \\\n> > +             -c remote.lop.promisor=true \\\n> > +             -c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n> > +             -c remote.lop.url=\"file://$(pwd)/lop\" \\\n> > +             -c remote.lop.token=\"fooYYY\" \\\n> > +             -c remote.lop.partialCloneFilter=\"blob:none\" \\\n> > +             -c promisor.acceptfromserver=All \\\n> > +             -c promisor.storeFields=partialcloneFilter \\\n> > +             --no-local --filter=\"blob:limit=5k\" server client 2>err &&\n>\n> Onet thing that's missing in these tests is to verify that a subsequent\n> git-fetch(1) updates the configuration.\n\nOk, I have added a test using `git fetch`.\n\nThanks.\n"},{"id":"535125","messageId":"CAP8UFD2Nf4N2BUwZVtSDLH3Uu+UkGY67wmevwc4CoBN3jnqWCg@mail.gmail.com","threadId":"64670","inReplyTo":"aV4v6_DFJtraLlPI@pks.im","subject":"Re: [PATCH 6/9] list-objects-filter-options: support 'auto' mode for --filter","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-04T10:21:34Z","receivedAt":"2026-02-04T10:21:47Z","isPatch":true,"body":"On Wed, Jan 7, 2026 at 11:05 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Tue, Dec 23, 2025 at 12:11:10PM +0100, Christian Couder wrote:\n> > In a following commit, we are going to allow passing \"auto\" as a\n> > <filterspec> to the `--filter=<filterspec>` option, but only for some\n> > commands. Other commands that support the `--filter=<filterspec>`\n> > option should still die() when 'auto' is passed.\n>\n> Okay. I assume the idea is that the user can eventually say `git clone\n> --filter=auto`, and Git would automatically pick the best filter\n> advertised by the remote. Sounds reasonable to me.\n\nYeah, that's the idea.\n\n> > Let's set up the \"list-objects-filter-options.{c,h}\" infrastructure to\n> > support that:\n> >\n> > - Add a new `unsigned int allow_auto_filter : 1;` flag to\n> >   `struct list_objects_filter_options` which specifies if \"auto\" is\n> >   accepted or not.\n> > - Change gently_parse_list_objects_filter() to parse \"auto\" if it's\n> >   accepted.\n> > - Make sure we die() if \"auto\" is combined with another filter.\n> > - Update list_objects_filter_release() to preserve the\n> >   allow_auto_filter flag, as this function is often called (via\n> >   opt_parse_list_objects_filter) to reset the struct before parsing a\n> >   new value.\n> >\n> > Let's also update `list-objects-filter.c` to recognize the new\n> > `LOFC_AUTO` choice. Since \"auto\" must be resolved to a concrete filter\n> > before filtering actually begins, initializing a filter with\n> > `LOFC_AUTO` is invalid and will trigger a BUG().\n> >\n> > Note that ideally combining \"auto\" with \"auto\" could be allowed, but in\n> > practice, it's probably not worth the added code complexity. And if we\n> > really want it, nothing prevents us to allow it in future work.\n>\n> I guess the question is what this would even mean, and I cannot think\n> of any benefit to allow `--filter=combine:auto+auto`. So agreed\n\nWe could allow `--filter=combine:auto+auto` to mean the same as just\n`--filter=auto`. But I also don't see a benefit to allow this now.\n\n> > If we ever want to give a meaning to combining \"auto\" with a different\n> > filter too, nothing prevents us to do that in future work either.\n>\n> So basically the case where the user knows that they definitely don't\n> want blobs, and in addition they want to pick the best filter advertised\n> by the server? Yeah, that sounds like it could eventually be a nice\n> addition.\n\nYeah, but I think it's also not needed for now.\n\n> > diff --git a/list-objects-filter-options.c b/list-objects-filter-options.c\n> > index 7420bf81fe..f13ae5caeb 100644\n> > --- a/list-objects-filter-options.c\n> > +++ b/list-objects-filter-options.c\n> > @@ -52,7 +54,17 @@ int gently_parse_list_objects_filter(\n> >       if (filter_options->choice)\n> >               BUG(\"filter_options already populated\");\n> >\n> > -     if (!strcmp(arg, \"blob:none\")) {\n> > +     if (!strcmp(arg, \"auto\")) {\n> > +             if (!filter_options->allow_auto_filter) {\n> > +                     strbuf_addstr(\n> > +                             errbuf,\n> > +                             _(\"'auto' filter not supported by this command\"));\n>\n> Tiny nit: the indentation looks a bit weird here.\n\nI have changed it. Hope it's better now.\n\n> > @@ -146,10 +158,20 @@ static int parse_combine_subfilter(\n> >\n> >       decoded = url_percent_decode(subspec->buf);\n> >\n> > -     result = has_reserved_character(subspec, errbuf) ||\n> > -             gently_parse_list_objects_filter(\n> > +     result = has_reserved_character(subspec, errbuf);\n> > +     if (result)\n> > +             goto cleanup;\n> > +\n> > +     result = gently_parse_list_objects_filter(\n> >                       &filter_options->sub[new_index], decoded, errbuf);\n> > +     if (result)\n> > +             goto cleanup;\n> > +\n> > +     result = (filter_options->sub[new_index].choice == LOFC_AUTO);\n> > +     if (result)\n> > +             strbuf_addstr(errbuf, _(\"an 'auto' filter cannot be combined\"));\n>\n> Nit: let's maybe also add the `goto cleanup` here. I'm not a fan of\n> leaving it away for the final statement as it makes it easy to forget\n> backfilling it in case this function needs to be extended in the future.\n\nOk, I have added the `goto cleanup`.\n\n> > @@ -317,6 +345,7 @@ void list_objects_filter_release(\n> >       struct list_objects_filter_options *filter_options)\n> >  {\n> >       size_t sub;\n> > +     unsigned int allow_auto_filter = filter_options->allow_auto_filter;\n> >\n> >       if (!filter_options)\n> >               return;\n> > @@ -326,6 +355,7 @@ void list_objects_filter_release(\n> >               list_objects_filter_release(&filter_options->sub[sub]);\n> >       free(filter_options->sub);\n> >       list_objects_filter_init(filter_options);\n> > +     filter_options->allow_auto_filter = allow_auto_filter;\n> >  }\n>\n> Why do we do this extra step to restore the `allow_auto_filter` option\n> here? Are there any callers that reuse the filter after it has been\n> released?\n\nAs you noticed below, list_objects_filter_release() doesn't just\nrelease resources but actually resets the state. That's because the\nfilter options are indeed reused during command-line parsing.\n\nIn cmd_clone() a single `struct list_objects_filter_options` called\n\"filter_options\" is declared and then pointers to it are passed to a\nnumber of functions. In particular, opt_parse_list_objects_filter()\nhandles the `--no-filter` case by calling\nlist_objects_filter_set_no_filter() which calls\nlist_objects_filter_release().\n\nSo yeah, if the user runs something like `git fetch --no-filter\n--filter=auto`, then \"filter_options\" is reused when `--filter=auto`\nis processed, so after it has been released.\n\nAlso note that the `allow_auto_filter` field is a configuration bit\nset by the command (e.g., cmd_fetch) before parsing begins. It\nindicates that the command supports the 'auto' mode. It's not data\nprovided by users, so it doesn't change depending on which filter\nrelated options are passed.\n\nI have added the following to the commit message:\n\n\"Also note that the new `allow_auto_filter` flag depends on the command,\nnot user choices, so it should be reset to the command default when\n`struct list_objects_filter_options` instances are reset.\"\n\n> In any case, this function does have clearing semantics as it also knows\n> to re-init the filter options. So it's somewhat misnamed and really\n> should be called `list_objects_filter_clear()` according to our coding\n> guidelines. That's certainly outside the scope of this patch series\n> though.\n\nYeah, it can be done separately.\n"},{"id":"535126","messageId":"CAP8UFD0iBxn6cPFKLAkSW7O3To1ago60MWYwV7YxjxOVxni1Kw@mail.gmail.com","threadId":"64670","inReplyTo":"aV4v8HCe6CLqXJ-1@pks.im","subject":"Re: [PATCH 7/9] list-objects-filter-options: implement auto filter resolution","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-04T10:29:43Z","receivedAt":"2026-02-04T10:29:56Z","isPatch":true,"body":"On Wed, Jan 7, 2026 at 11:05 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Tue, Dec 23, 2025 at 12:11:11PM +0100, Christian Couder wrote:\n> > In a following commit, we will need to aggregate filters from multiple\n> > accepted promisor remotes into a single filter.\n>\n> Ah, interesting. I was always operating under the assumption that when\n> the server advertises multiple promisors, the client will pick only one\n> of them. And that made me wonder how the client knows which one to pick\n> in the first place.\n>\n> But of course it's possible to just pick _all_ of them by combining the\n> filter.\n\nYeah, that's the idea.\n\n> > diff --git a/list-objects-filter-options.c b/list-objects-filter-options.c\n> > index f13ae5caeb..4a9c1991c1 100644\n> > --- a/list-objects-filter-options.c\n> > +++ b/list-objects-filter-options.c\n> > @@ -230,6 +230,41 @@ static void filter_spec_append_urlencode(\n> >                    filter->filter_spec.buf + orig_len);\n> >  }\n> >\n> > +char *list_objects_filter_combine(const struct string_list *specs)\n> > +{\n> > +     struct strbuf buf = STRBUF_INIT;\n> > +\n> > +     if (!specs->nr)\n> > +             return NULL;\n> > +\n> > +     if (specs->nr == 1)\n> > +             return xstrdup(specs->items[0].string);\n> > +\n> > +     strbuf_addstr(&buf, \"combine:\");\n> > +\n> > +     for (size_t i = 0; i < specs->nr; i++) {\n> > +             const char *spec = specs->items[i].string;\n> > +             if (i > 0)\n> > +                     strbuf_addch(&buf, '+');\n> > +\n> > +             strbuf_addstr_urlencode(&buf, spec, allow_unencoded);\n>\n> Shouldn't we use `filter_spec_append_urlencode()` to do this?\n\nYeah, probably, see below.\n\n> > +     }\n> > +\n> > +     return strbuf_detach(&buf, NULL);\n> > +}\n>\n> I'm surprised we didn't have such a function yet.\n\nI have refactored the code so that we use a temporary `struct\nlist_objects_filter_options` and `gently_parse_list_objects_filter()`\nto construct a combined filter in the next commit instead of this\nfunction.\n\nThis also takes care of your comment above about\n`strbuf_addstr_urlencode()` vs `filter_spec_append_urlencode()`.\n\n> > +void list_objects_filter_resolve_auto(struct list_objects_filter_options *filter_options,\n> > +     char *new_filter, struct strbuf *errbuf)\n> > +{\n> > +     if (filter_options->choice != LOFC_AUTO)\n> > +             return;\n>\n> I wonder whether we should rather `BUG()` in case the filter is not an\n> \"auto\" filter. Otherwise it's easy to get the callsite wrong, as the\n> user may expect that the filter gets resolved tdo the new filter, but\n> it's actually not because the original filter wasn't an \"auto\" filter in\n> the first place.\n\nActually the list_objects_filter_resolve_auto() function is not very\nuseful, so I have just removed it too in the v2 I will send.\n\nThis way this whole patch is not necessary and has been removed in v2.\n\n> > +     list_objects_filter_release(filter_options);\n> > +\n> > +     if (new_filter)\n> > +             gently_parse_list_objects_filter(filter_options, new_filter, errbuf);\n> > +}\n>\n> So as menitoned in a preceding commit `list_objects_filter_release()`,\n> will retain the `allow_auto` option. But when resolving \"auto\" filters\n> I'd expect us to not accept \"auto\" in the resolved filter anymore.\n> Otherwise, if `new_filter` was \"auto\", we'd still end up with an auto\n> filter, wouldn't we? I'd rather expect us to abort in that case.\n\nRight, I have fixed this by adding the following in the next commit:\n\n         /* The result of resolving an 'auto' filter must not be 'auto' */\n         args->filter_options.allow_auto_filter = 0;\n\nThanks!\n"},{"id":"535130","messageId":"CAP8UFD1za=FowTWBqjanyRFANKBsc-+LOcbSsuBzjeiK8T_fkw@mail.gmail.com","threadId":"64670","inReplyTo":"aV4v9WhL95Gcqr2t@pks.im","subject":"Re: [PATCH 8/9] promisor-remote: keep advertised filter in memory","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-04T10:57:42Z","receivedAt":"2026-02-04T10:57:54Z","isPatch":true,"body":"On Wed, Jan 7, 2026 at 11:05 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Tue, Dec 23, 2025 at 12:11:12PM +0100, Christian Couder wrote:\n> > diff --git a/promisor-remote.c b/promisor-remote.c\n> > index 8d6d2d7b76..d5f3223cd0 100644\n> > --- a/promisor-remote.c\n> > +++ b/promisor-remote.c\n> > @@ -837,6 +838,7 @@ static void filter_promisor_remote(struct repository *repo,\n> >       struct store_info *store_info = NULL;\n> >       struct string_list_item *item;\n> >       bool reload_config = false;\n> > +     struct string_list captured_filters = STRING_LIST_INIT_DUP;\n> >\n> >       if (!repo_config_get_string_tmp(the_repository, \"promisor.acceptfromserver\", &accept_str)) {\n> >               if (!*accept_str || !strcasecmp(\"None\", accept_str))\n>\n> Nit: I found the \"captured\" terminology to be somewhat confusing. Can we\n> maybe rename this to `advertised_filters` to clarify?\n\nWell \"advertised_filter\" is already used and I think it might be\nconfusing to use a very similar name, so for now until we find a\nbetter name, I kept \"captured\" in v2 even if it's not the best.\n\nWhat about using `server_filters`?\n\n> > @@ -935,3 +963,23 @@ void mark_promisor_remotes_as_accepted(struct repository *r, const char *remotes\n> >\n> >       string_list_clear(&accepted_remotes, 0);\n> >  }\n> > +\n> > +char *promisor_remote_construct_filter(struct repository *repo)\n> > +{\n> > +     struct string_list advertised_filters = STRING_LIST_INIT_NODUP;\n> > +     struct promisor_remote *r;\n> > +     char *result;\n> > +\n> > +     promisor_remote_init(repo);\n> > +\n> > +     for (r = repo->promisor_remote_config->promisors; r; r = r->next) {\n> > +             if (r->accepted && r->advertised_filter)\n> > +                     string_list_append(&advertised_filters, r->advertised_filter);\n>\n> Would we ever accept a promisor remote that _doesn't_ have an advertised\n> filter? If not, should we maybe `BUG()` in case the advertised filter\n> has not been set?\n\nI think it should be fine to accept a promisor remote without an\nadvertised filter. The server might prefer to not advertise filters\nbecause it thinks that the client should determine the best filter\nbased on the client needs. That's how it works now.\n"},{"id":"535131","messageId":"CAP8UFD0kLyLCSXYA1Zw8BvBDTxYmTZeo0L44dz7_HC6uY683sg@mail.gmail.com","threadId":"64670","inReplyTo":"aV4v--FYaHCLLrPz@pks.im","subject":"Re: [PATCH 9/9] fetch-pack: wire up and enable auto filter logic","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-04T11:06:10Z","receivedAt":"2026-02-04T11:06:23Z","isPatch":true,"body":"On Wed, Jan 7, 2026 at 11:05 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Tue, Dec 23, 2025 at 12:11:13PM +0100, Christian Couder wrote:\n> > diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc\n> > index 70a9818331..f7432d4b29 100644\n> > --- a/Documentation/fetch-options.adoc\n> > +++ b/Documentation/fetch-options.adoc\n> > @@ -92,11 +92,20 @@ precedence over the `fetch.output` config option.\n> >       Use the partial clone feature and request that the server sends\n> >       a subset of reachable objects according to a given object filter.\n> >       When using `--filter`, the supplied _<filter-spec>_ is used for\n> > -     the partial fetch. For example, `--filter=blob:none` will filter\n> > -     out all blobs (file contents) until needed by Git. Also,\n> > -     `--filter=blob:limit=<size>` will filter out all blobs of size\n> > -     at least _<size>_. For more details on filter specifications, see\n> > -     the `--filter` option in linkgit:git-rev-list[1].\n> > +     the partial fetch.\n> > ++\n> > +If `--filter=auto` is used, the filter specification is determined\n> > +automatically by combining the filter specifications advertised by\n> > +the server for the promisor remotes that the client accepts (see\n> > +linkgit:gitprotocol-v2[5] and the `promisor.acceptFromServer`\n> > +configuration option in linkgit:git-config[1]).\n>\n> Okay, so if \"promisor.acceptFromServer\" enables a subset of advertised\n> promisors we will automatically use their advertised filters. But what\n> about the case where we already have a set of local promisors with their\n> own filters, would those also honored by \"--filter=auto\"?\n\nNo, they wouldn't be honored. 'auto' means that the client fully\naccepts the filters advertised by the server. Maybe we could add a new\nmode for using the locally configured filter by default and only using\nthe advertised filter if there is no locally configured filter for the\nremote, but we can do that later.\n\n> > diff --git a/fetch-pack.c b/fetch-pack.c\n> > index 40316c9a34..12ccea0dab 100644\n> > --- a/fetch-pack.c\n> > +++ b/fetch-pack.c\n> > @@ -1661,6 +1662,25 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,\n> >       struct string_list packfile_uris = STRING_LIST_INIT_DUP;\n> >       int i;\n> >       struct strvec index_pack_args = STRVEC_INIT;\n> > +     const char *promisor_remote_config;\n> > +\n> > +     if (server_feature_v2(\"promisor-remote\", &promisor_remote_config)) {\n> > +             char *remote_name = promisor_remote_reply(promisor_remote_config);\n> > +             free(remote_name);\n> > +     }\n> > +\n> > +     if (args->filter_options.choice == LOFC_AUTO) {\n> > +             struct strbuf errbuf = STRBUF_INIT;\n> > +             char *constructed_filter = promisor_remote_construct_filter(r);\n> > +\n> > +             list_objects_filter_resolve_auto(&args->filter_options,\n> > +                                              constructed_filter, &errbuf);\n> > +             if (errbuf.len > 0)\n> > +                     die(_(\"couldn't resolve 'auto' filter: %s\"), errbuf.buf);\n>\n> Now that I see it being used I think that the calling convention of this\n> function is a bit weird. I would've expected the function to return an\n> error code that the caller can consult instead of having to check for\n> `errbuf.len`.\n\nRight, anyway I have removed that `list_objects_filter_resolve_auto()`\nfunction altogether by removing the patch that introduced it in v2.\n\nThanks!\n"},{"id":"535132","messageId":"20260204110818.2919273-1-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20251223111113.47473-1-christian.couder@gmail.com","subject":"[PATCH v2 0/8] Implement `promisor.storeFields` and `--filter=auto`","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-04T11:08:05Z","receivedAt":"2026-02-04T11:08:35Z","isPatch":true,"body":"Introduction\n============\n\nA previous patch series added the possibility to pass additional\nfields, a \"partialCloneFilter\" and a \"token\" for each advertised\npromisor remote, from a server to a client through the\n\"promisor-remote\" capability.\n\nOn the client side though, it has so far only been possible to use\nthis new information to compare it with local information and then\ndecide if the corresponding advertised promisor remote is accepted or\nnot.\n\nFor the \"token\" it would be useful if it could be stored on the\nclient. For example in a setup where the client uses specialized\nremote helpers which need a token to access the promisor remotes\nadvertised by the server, storing the token would allow the token to\nbe used when the client directly accesses a promisor remote for\nexample to lazy fetch some blobs it now needs.\n\nTo enable such a workflow, where the server can rotate tokens and the\nclient can have updated tokens from the server by simply fetching from\nit, the first part of this series introduces a new\n\"promisor.storeFields\" configuration option on the client side,\nsimilar to the \"promisor.checkFields\" configuration option. When field\nnames, \"token\" or \"partialCloneFilter\", are listed in this new\nconfiguration option, then the values of these field names transmitted\nby the server are stored in the local configuration on the client\nside.\n\nNote that for security reasons, the corresponding remote name and url\nof the advertised promisor remotes must have already been configured\non the client side. No new remote name nor url are configured.\n\nFor the \"partialCloneFilter\" field, simply storing the value is not\nenough to enable dynamic updates. Currently, when a user initiates a\npartial clone with `--filter=<filter-spec>`, that specific\n<filter-spec> is saved in the client's local configuration (e.g.,\nremote.origin.partialCloneFilter). Subsequent fetches then reuse this\nvalue, ignoring suggestions from the server.\n\nTo avoid breaking this mechanism and still be able to use the\n<filter-spec> that the server suggests for the promisor remotes that\nthe client accepts, the second part of this series introduces a new\n`--filter=auto` mode for `git clone` and `git fetch`.\n\nWhen `--filter=auto` is used, then \"auto\" is still saved as the\n<filter-spec> for the server locally on the client, and then when a\nfetch-pack happens, instead of passing just \"auto\", the actual filter\nrequested by the client is computed by combining the <filter-spec>s\nthat the server suggested for the promisor remotes that the client\naccepted. This uses the \"combine\" filter mechanism that already exists\nin \"list-objects-filter-options.{c,h}\".\n\nThis way by just using `--filter=auto` when cloning, a client makes\nsure it will use the <filter-spec>s suggested by the server for the\npromisor remotes it accepts.\n\nThis work is part of the \"LOP\" effort documented in:\n\n  Documentation/technical/large-object-promisors.adoc\n\nSee that doc for more information on the broader context.\n\nOverview of the patches\n=======================\n\nPatches 1/8 and 2/8 are the first part of the series and implement the\nnew \"promisor.storeFields\" configuration option. Patch 1/8 is a small\npreparatory refactoring.\n\nPatches from 3/8 to 8/8 implement the `--filter=auto` option:\n\n  - Patches 3/8 and 4/8 are cleanups of \"builtin/clone.c\" and\n    \"builtin/fetch.c\" respectively that make the `filter_options`\n    variable local to cmd_clone() or cmd_fetch().\n\n  - Patch 5/8 is a doc update as `--filter=<filter-spec>` wasn't\n    documented for `git fetch`.\n\n  - Patch 6/8 improves \"list-objects-filter-options.{c,h}\" to\n    support the new 'auto' mode.\n\n  - Patch 7/8 improves \"promisor-remote.{c,h}\" to support the new\n    'auto' mode.\n\n  - Patch 8/8 make the new 'auto' mode actually work by wiring up\n    everything together.\n\nCI Report\n=========\n\nAll the tests pass, see:\n\nhttps://github.com/chriscool/git/actions/runs/21665784738\n\nChanges since v1\n================\n\nThanks to Patrick Steinhardt and Jean-Noël Avila for reviewing the\nprevious version!\n\nIn patch 2/8:\n\n  - A note has been added to the commit message to clarify why the new\n    \"promisor.storeFields\" configuration variable might not be very\n    useful for partial cloçne filters.\n    \n  - repo_config_set_gently() is used instead of\n    repo_config_set_worktree_gently().\n\n  - new_store_info() and free_store_info() have been renamed\n    store_info_new() and store_info_free() respectively.\n\nIn patch 5/8:\n\n  - The commit message has been clarified to say that we use the same\n    words as in the `git clone`documentation and that we are not\n    trying to improve on them.\n\n  - Backticks have been added around \"--filter=<filter-spec>\".\n\nIn patch 6/8:\n\n  - The commit message has been clarified to note that the new\n    `allow_auto_filter` flag depends on the command not on user input.\n\n  - A call to strbuf_addstr() has been indented better.\n\n  - A `goto cleanup;` instruction has been added.\n\nPatch 7/9 in v1 has been removed, as another way to combine filter has\nbeen implemented (see below).\n\nIn patch 7/8:\n\n  - A typo in the commit message subject has been fixed (missing 's').\n\n  - The commit message has been fixed to say that `advertised_filte`\n    is added in the current commit, not a previous one, and to remove\n    a mention of `list_objects_filter_combine()` as the previous\n    commit that introduced that function has been removed.\n\n  - promisor_remote_construct_filter() now uses a temporary `struct\n    list_objects_filter_options` to construct a combined filter\n    (instead of `list_objects_filter_combine()`).\n\n  - The comment on top of the declaration of\n    promisor_remote_construct_filter() in \"promisor-remote.h\" has been\n    improved a bit.\n\nIn patch 8/8:\n\n  - Instead of using list_objects_filter_resolve_auto(), we use\n    gently_parse_list_objects_filter() directly.\n\n  - We unset `allow_auto_filter` before parsing the combined filter as\n    it must not be 'auto'.\n\n  - A die() message has been improved a bit.\n\nRange diff since v1\n===================\n\n 1:  fcaffa7898 =  1:  e19b1518cd promisor-remote: refactor initialising field lists\n 2:  9bcfa03987 !  2:  8f20baac17 promisor-remote: allow a client to store fields\n    @@ Commit message\n         available when the client needs to access the promisor remotes for a\n         lazy fetch.\n     \n    -    In the same way, if it appears that it's better to use a different\n    -    filter to access a promisor remote, it could be helpful if the client\n    -    could automatically use it.\n    -\n         To allow this, let's introduce a new \"promisor.storeFields\"\n         configuration variable.\n     \n    -    Like \"promisor.checkFields\" and \"promisor.sendFields\", it should\n    -    contain a comma or space separated list of field names. Only the\n    -    \"partialCloneFilter\" and \"token\" field names are supported for now.\n    +    Note that for a partial clone filter, it's less interesting to have\n    +    it stored on the client. This is because a filter should be used\n    +    right away and we already pass a `--filter=<filter-spec>` option to\n    +    `git clone` when starting a partial clone. Storing the filter could\n    +    perhaps still be interesting for information purposes.\n    +\n    +    Like \"promisor.checkFields\" and \"promisor.sendFields\", the new\n    +    configuration variable should contain a comma or space separated list\n    +    of field names. Only the \"partialCloneFilter\" and \"token\" field names\n    +    are supported for now.\n     \n         When a server advertises a promisor remote, for example \"foo\", along\n         with for example \"token=XXXXX\" to a client, and on the client side\n    @@ promisor-remote.c: static struct promisor_info *parse_one_advertised_remote(cons\n     +\t\t\tcurrent ? current : \"\",\n     +\t\t\tadvertised);\n     +\n    -+\t\trepo_config_set_worktree_gently(repo, key, advertised);\n    ++\t\trepo_config_set_gently(repo, key, advertised);\n     +\t\tfree(key);\n     +\n     +\t\treturn true;\n    @@ promisor-remote.c: static struct promisor_info *parse_one_advertised_remote(cons\n     +\tbool store_token;\n     +};\n     +\n    -+static struct store_info *new_store_info(struct repository *repo)\n    ++static struct store_info *store_info_new(struct repository *repo)\n     +{\n     +\tstruct string_list *fields_to_store = fields_stored();\n     +\tstruct store_info *s = xmalloc(sizeof(*s));\n    @@ promisor-remote.c: static struct promisor_info *parse_one_advertised_remote(cons\n     +\treturn s;\n     +}\n     +\n    -+static void free_store_info(struct store_info *s)\n    ++static void store_info_free(struct store_info *s)\n     +{\n     +\tif (s) {\n     +\t\tpromisor_info_list_clear(&s->config_info);\n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n     -\t\tif (should_accept_remote(accept, advertised, &config_info))\n     +\t\tif (should_accept_remote(accept, advertised, &config_info)) {\n     +\t\t\tif (!store_info)\n    -+\t\t\t\tstore_info = new_store_info(repo);\n    ++\t\t\t\tstore_info = store_info_new(repo);\n     +\t\t\tif (promisor_store_advertised_fields(advertised, store_info))\n     +\t\t\t\treload_config = true;\n     +\n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n      \n      \tpromisor_info_list_clear(&config_info);\n      \tstring_list_clear(&remote_info, 0);\n    -+\tfree_store_info(store_info);\n    ++\tstore_info_free(store_info);\n     +\n     +\tif (reload_config)\n     +\t\trepo_promisor_remote_reinit(repo);\n 3:  629b1ba1af =  3:  9d53a79600 clone: make filter_options local to cmd_clone()\n 4:  ab9105062d =  4:  b24907e6dc fetch: make filter_options local to cmd_fetch()\n 5:  72924115c1 !  5:  90fb77360b doc: fetch: document `--filter=<filter-spec>` option\n    @@ Commit message\n         The `--filter=<filter-spec>` option is documented in most commands that\n         support it except `git fetch`.\n     \n    -    Let's fix that and document that option properly in the same way as it\n    -    is already documented for `git clone`.\n    +    Let's fix that and document that option using the same words already\n    +    used to document it for `git clone`.\n    +\n    +    Those words could probably be improved, but they are not wrong, so\n    +    let's just use them for now and leave improving them for future work.\n     \n         Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n     \n    @@ Documentation/fetch-options.adoc: linkgit:git-config[1].\n      This is incompatible with `--recurse-submodules=(yes|on-demand)` and takes\n      precedence over the `fetch.output` config option.\n      \n    -+--filter=<filter-spec>::\n    ++`--filter=<filter-spec>`::\n     +\tUse the partial clone feature and request that the server sends\n     +\ta subset of reachable objects according to a given object filter.\n     +\tWhen using `--filter`, the supplied _<filter-spec>_ is used for\n 6:  c2f18b2055 !  6:  b524b24024 list-objects-filter-options: support 'auto' mode for --filter\n    @@ Commit message\n     \n         - Add a new `unsigned int allow_auto_filter : 1;` flag to\n           `struct list_objects_filter_options` which specifies if \"auto\" is\n    -      accepted or not.\n    +      accepted or not by the current command.\n         - Change gently_parse_list_objects_filter() to parse \"auto\" if it's\n           accepted.\n         - Make sure we die() if \"auto\" is combined with another filter.\n    @@ Commit message\n         If we ever want to give a meaning to combining \"auto\" with a different\n         filter too, nothing prevents us to do that in future work either.\n     \n    +    Also note that the new `allow_auto_filter` flag depends on the command,\n    +    not user choices, so it should be reset to the command default when\n    +    `struct list_objects_filter_options` instances are reset.\n    +\n         While at it, let's add a new \"u-list-objects-filter-options.c\" file for\n         `struct list_objects_filter_options` related unit tests. For now it\n         only tests gently_parse_list_objects_filter() though.\n    @@ list-objects-filter-options.c: int gently_parse_list_objects_filter(\n     -\tif (!strcmp(arg, \"blob:none\")) {\n     +\tif (!strcmp(arg, \"auto\")) {\n     +\t\tif (!filter_options->allow_auto_filter) {\n    -+\t\t\tstrbuf_addstr(\n    -+\t\t\t\terrbuf,\n    -+\t\t\t\t_(\"'auto' filter not supported by this command\"));\n    ++\t\t\tstrbuf_addstr(errbuf,\n    ++\t\t\t\t      _(\"'auto' filter not supported by this command\"));\n     +\t\t\treturn 1;\n     +\t\t}\n     +\t\tfilter_options->choice = LOFC_AUTO;\n    @@ list-objects-filter-options.c: static int parse_combine_subfilter(\n     +\t\tgoto cleanup;\n     +\n     +\tresult = (filter_options->sub[new_index].choice == LOFC_AUTO);\n    -+\tif (result)\n    ++\tif (result) {\n     +\t\tstrbuf_addstr(errbuf, _(\"an 'auto' filter cannot be combined\"));\n    ++\t\tgoto cleanup;\n    ++\t}\n      \n     +cleanup:\n      \tfree(decoded);\n 7:  1b88355e66 <  -:  ---------- list-objects-filter-options: implement auto filter resolution\n 8:  6ac8bf81f0 !  7:  4ec51ee88f promisor-remote: keep advertised filter in memory\n    @@ Metadata\n     Author: Christian Couder <chriscool@tuxfamily.org>\n     \n      ## Commit message ##\n    -    promisor-remote: keep advertised filter in memory\n    +    promisor-remote: keep advertised filters in memory\n     \n         Currently, advertised filters are only kept in memory temporarily\n         during parsing, or persisted to disk if `promisor.storeFields`\n    @@ Commit message\n         promisor remotes the client accepted.\n     \n         To enable the client to construct a filter spec based on these filters,\n    -    let's add a `promisor_remote_construct_filter(repo)` function.\n    +    let's also add a `promisor_remote_construct_filter(repo)` function.\n     \n         This function:\n     \n         - iterates over all accepted promisor remotes in the repository,\n         - collects the filters advertised for them (using `advertised_filter`\n    -      which a previous commit added to `struct promisor_remote`), and\n    -    - generates a single filter spec for them (using the\n    -      `list_objects_filter_combine()` function added by a previous commit).\n    +      added in this commit, and\n    +    - generates a single filter spec for them.\n     \n         Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n     \n    @@ promisor-remote.c: void mark_promisor_remotes_as_accepted(struct repository *r,\n     +\n     +char *promisor_remote_construct_filter(struct repository *repo)\n     +{\n    -+\tstruct string_list advertised_filters = STRING_LIST_INIT_NODUP;\n     +\tstruct promisor_remote *r;\n    -+\tchar *result;\n    ++\tstruct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n    ++\tstruct strbuf err = STRBUF_INIT;\n    ++\tchar *result = NULL;\n     +\n     +\tpromisor_remote_init(repo);\n     +\n     +\tfor (r = repo->promisor_remote_config->promisors; r; r = r->next) {\n     +\t\tif (r->accepted && r->advertised_filter)\n    -+\t\t\tstring_list_append(&advertised_filters, r->advertised_filter);\n    ++\t\t\tif (gently_parse_list_objects_filter(&filter_options,\n    ++\t\t\t\t\t\t\t     r->advertised_filter,\n    ++\t\t\t\t\t\t\t     &err)) {\n    ++\t\t\t\twarning(_(\"promisor remote '%s' advertised invalid filter '%s': %s\"),\n    ++\t\t\t\t\tr->name, r->advertised_filter, err.buf);\n    ++\t\t\t\tstrbuf_reset(&err);\n    ++\t\t\t\tcontinue;\n    ++\t\t\t}\n     +\t}\n     +\n    -+\tresult = list_objects_filter_combine(&advertised_filters);\n    ++\tif (filter_options.choice)\n    ++\t\tresult = xstrdup(expand_list_objects_filter_spec(&filter_options));\n     +\n    -+\tstring_list_clear(&advertised_filters, 0);\n    ++\tlist_objects_filter_release(&filter_options);\n    ++\tstrbuf_release(&err);\n     +\n     +\treturn result;\n     +}\n    @@ promisor-remote.h: void mark_promisor_remotes_as_accepted(struct repository *rep\n      int repo_has_accepted_promisor_remote(struct repository *r);\n      \n     +/*\n    -+ * Use the filters from the accepted remotes to create a filter.\n    ++ * Use the filters from the accepted remotes to create a combined\n    ++ * filter (useful in `--filter=auto` mode).\n     + */\n     +char *promisor_remote_construct_filter(struct repository *repo);\n     +\n 9:  7c822499e2 !  8:  994ecb3317 fetch-pack: wire up and enable auto filter logic\n    @@ fetch-pack.c: static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,\n     +\t\tstruct strbuf errbuf = STRBUF_INIT;\n     +\t\tchar *constructed_filter = promisor_remote_construct_filter(r);\n     +\n    -+\t\tlist_objects_filter_resolve_auto(&args->filter_options,\n    -+\t\t\t\t\t\t constructed_filter, &errbuf);\n    ++\t\tlist_objects_filter_release(&args->filter_options);\n    ++\t\t/* The result of resolving an 'auto' filter must not be 'auto' */\n    ++\t\targs->filter_options.allow_auto_filter = 0;\n    ++\n    ++\t\tif (constructed_filter)\n    ++\t\t\tgently_parse_list_objects_filter(&args->filter_options,\n    ++\t\t\t\t\t\t\t constructed_filter,\n    ++\t\t\t\t\t\t\t &errbuf);\n    ++\n     +\t\tif (errbuf.len > 0)\n    -+\t\t\tdie(_(\"couldn't resolve 'auto' filter: %s\"), errbuf.buf);\n    ++\t\t\tdie(_(\"couldn't resolve 'auto' filter '%s': %s\"),\n    ++\t\t\t    constructed_filter, errbuf.buf);\n     +\n     +\t\tfree(constructed_filter);\n     +\t\tstrbuf_release(&errbuf);\n\n\nChristian Couder (8):\n  promisor-remote: refactor initialising field lists\n  promisor-remote: allow a client to store fields\n  clone: make filter_options local to cmd_clone()\n  fetch: make filter_options local to cmd_fetch()\n  doc: fetch: document `--filter=<filter-spec>` option\n  list-objects-filter-options: support 'auto' mode for --filter\n  promisor-remote: keep advertised filters in memory\n  fetch-pack: wire up and enable auto filter logic\n\n Documentation/config/promisor.adoc           |  33 +++\n Documentation/fetch-options.adoc             |  19 ++\n Documentation/git-clone.adoc                 |  25 +-\n Documentation/gitprotocol-v2.adoc            |  24 +-\n Makefile                                     |   1 +\n builtin/clone.c                              |  18 +-\n builtin/fetch.c                              |  50 ++--\n fetch-pack.c                                 |  28 +++\n list-objects-filter-options.c                |  37 ++-\n list-objects-filter-options.h                |   6 +\n list-objects-filter.c                        |   8 +\n promisor-remote.c                            | 232 +++++++++++++++++--\n promisor-remote.h                            |   7 +\n t/meson.build                                |   1 +\n t/t5710-promisor-remote-capability.sh        | 109 +++++++++\n t/unit-tests/u-list-objects-filter-options.c |  53 +++++\n transport.c                                  |   1 +\n 17 files changed, 596 insertions(+), 56 deletions(-)\n create mode 100644 t/unit-tests/u-list-objects-filter-options.c\n\n-- \n2.53.0.rc2.10.g12663a1c75.dirty\n\n"},{"id":"535133","messageId":"20260204110818.2919273-2-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260204110818.2919273-1-christian.couder@gmail.com","subject":"[PATCH v2 1/8] promisor-remote: refactor initialising field lists","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-04T11:08:06Z","receivedAt":"2026-02-04T11:08:37Z","isPatch":true,"body":"In \"promisor-remote.c\", the fields_sent() and fields_checked()\nfunctions serve similar purposes and contain a small amount of\nduplicated code.\n\nAs we are going to add a similar function in a following commit,\nlet's refactor this common code into a new initialize_fields_list()\nfunction.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 28 ++++++++++++++--------------\n 1 file changed, 14 insertions(+), 14 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 77ebf537e2..5d8151cedb 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -375,18 +375,24 @@ static char *fields_from_config(struct string_list *fields_list, const char *con\n \treturn fields;\n }\n \n+static struct string_list *initialize_fields_list(struct string_list *fields_list, int *initialized,\n+\t\t\t\t\t\t  const char *config_key)\n+{\n+\tif (!*initialized) {\n+\t\tfields_list->cmp = strcasecmp;\n+\t\tfields_from_config(fields_list, config_key);\n+\t\t*initialized = 1;\n+\t}\n+\n+\treturn fields_list;\n+}\n+\n static struct string_list *fields_sent(void)\n {\n \tstatic struct string_list fields_list = STRING_LIST_INIT_NODUP;\n \tstatic int initialized;\n \n-\tif (!initialized) {\n-\t\tfields_list.cmp = strcasecmp;\n-\t\tfields_from_config(&fields_list, \"promisor.sendFields\");\n-\t\tinitialized = 1;\n-\t}\n-\n-\treturn &fields_list;\n+\treturn initialize_fields_list(&fields_list, &initialized, \"promisor.sendFields\");\n }\n \n static struct string_list *fields_checked(void)\n@@ -394,13 +400,7 @@ static struct string_list *fields_checked(void)\n \tstatic struct string_list fields_list = STRING_LIST_INIT_NODUP;\n \tstatic int initialized;\n \n-\tif (!initialized) {\n-\t\tfields_list.cmp = strcasecmp;\n-\t\tfields_from_config(&fields_list, \"promisor.checkFields\");\n-\t\tinitialized = 1;\n-\t}\n-\n-\treturn &fields_list;\n+\treturn initialize_fields_list(&fields_list, &initialized, \"promisor.checkFields\");\n }\n \n /*\n-- \n2.53.0.rc2.10.g12663a1c75.dirty\n\n"},{"id":"535134","messageId":"20260204110818.2919273-3-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260204110818.2919273-1-christian.couder@gmail.com","subject":"[PATCH v2 2/8] promisor-remote: allow a client to store fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-04T11:08:07Z","receivedAt":"2026-02-04T11:08:38Z","isPatch":true,"body":"A previous commit allowed a server to pass additional fields through\nthe \"promisor-remote\" protocol capability after the \"name\" and \"url\"\nfields, specifically the \"partialCloneFilter\" and \"token\" fields.\n\nAnother previous commit, c213820c51 (promisor-remote: allow a client\nto check fields, 2025-09-08), has made it possible for a client to\ndecide if it accepts a promisor remote advertised by a server based\non these additional fields.\n\nOften though, it would be interesting for the client to just store in\nits configuration files these additional fields passed by the server,\nso that it can use them when needed.\n\nFor example if a token is necessary to access a promisor remote, that\ntoken could be updated frequently only on the server side and then\npassed to all the clients through the \"promisor-remote\" capability,\navoiding the need to update it on all the clients manually.\n\nStoring the token on the client side makes sure that the token is\navailable when the client needs to access the promisor remotes for a\nlazy fetch.\n\nTo allow this, let's introduce a new \"promisor.storeFields\"\nconfiguration variable.\n\nNote that for a partial clone filter, it's less interesting to have\nit stored on the client. This is because a filter should be used\nright away and we already pass a `--filter=<filter-spec>` option to\n`git clone` when starting a partial clone. Storing the filter could\nperhaps still be interesting for information purposes.\n\nLike \"promisor.checkFields\" and \"promisor.sendFields\", the new\nconfiguration variable should contain a comma or space separated list\nof field names. Only the \"partialCloneFilter\" and \"token\" field names\nare supported for now.\n\nWhen a server advertises a promisor remote, for example \"foo\", along\nwith for example \"token=XXXXX\" to a client, and on the client side\n\"promisor.storeFields\" contains \"token\", then the client will store\nXXXXX for the \"remote.foo.token\" variable in its configuration file\nand reload its configuration so it can immediately use this new\nconfiguration variable.\n\nA message is emitted on stderr to warn users when the config is\nchanged.\n\nNote that even if \"promisor.acceptFromServer\" is set to \"all\", a\npromisor remote has to be already configured on the client side for\nsome of its config to be changed. In any case no new remote is\nconfigured and no new URL is stored.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    |  33 ++++++\n Documentation/gitprotocol-v2.adoc     |  12 ++-\n promisor-remote.c                     | 148 +++++++++++++++++++++++++-\n t/t5710-promisor-remote-capability.sh |  49 +++++++++\n 4 files changed, 236 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex 93e5e0d9b5..b0fa43b839 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -89,3 +89,36 @@ variable. The fields are checked only if the\n `promisor.acceptFromServer` config variable is not set to \"None\". If\n set to \"None\", this config variable has no effect. See\n linkgit:gitprotocol-v2[5].\n+\n+promisor.storeFields::\n+\tA comma or space separated list of additional remote related\n+\tfield names. If a client accepts an advertised remote, the\n+\tclient will store the values associated with these field names\n+\ttaken from the remote advertisement into its configuration,\n+\tand then reload its remote configuration. Currently,\n+\t\"partialCloneFilter\" and \"token\" are the only supported field\n+\tnames.\n++\n+For example if a server advertises \"partialCloneFilter=blob:limit=20k\"\n+for remote \"foo\", and that remote is accepted, then \"blob:limit=20k\"\n+will be stored for the \"remote.foo.partialCloneFilter\" configuration\n+variable.\n++\n+If the new field value from an advertised remote is the same as the\n+existing field value for that remote on the client side, then no\n+change is made to the client configuration though.\n++\n+When a new value is stored, a message is printed to standard error to\n+let users know about this.\n++\n+Note that for security reasons, if the remote is not already\n+configured on the client side, nothing will be stored for that\n+remote. In any case, no new remote will be created and no URL will be\n+stored.\n++\n+Before storing a partial clone filter, it's parsed to check it's\n+valid. If it's not, a warning is emitted and it's not stored.\n++\n+Before storing a token, a check is performed to ensure it contains no\n+control character. If the check fails, a warning is emitted and it's\n+not stored.\ndiff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\nindex c7db103299..d93dd279ea 100644\n--- a/Documentation/gitprotocol-v2.adoc\n+++ b/Documentation/gitprotocol-v2.adoc\n@@ -826,9 +826,10 @@ are case-sensitive and MUST be transmitted exactly as specified\n above. Clients MUST ignore fields they don't recognize to allow for\n future protocol extensions.\n \n-For now, the client can only use information transmitted through these\n-fields to decide if it accepts the advertised promisor remote. In the\n-future that information might be used for other purposes though.\n+The client can use information transmitted through these fields to\n+decide if it accepts the advertised promisor remote. Also, the client\n+can be configured to store the values of these fields (see\n+\"promisor.storeFields\" in linkgit:git-config[1]).\n \n Field values MUST be urlencoded.\n \n@@ -856,8 +857,9 @@ the server advertised, the client shouldn't advertise the\n On the server side, the \"promisor.advertise\" and \"promisor.sendFields\"\n configuration options can be used to control what it advertises. On\n the client side, the \"promisor.acceptFromServer\" configuration option\n-can be used to control what it accepts. See the documentation of these\n-configuration options for more information.\n+can be used to control what it accepts, and the \"promisor.storeFields\"\n+option, to control what it stores. See the documentation of these\n+configuration options in linkgit:git-config[1] for more information.\n \n Note that in the future it would be nice if the \"promisor-remote\"\n protocol capability could be used by the server, when responding to\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 5d8151cedb..59997dd4c7 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -403,6 +403,14 @@ static struct string_list *fields_checked(void)\n \treturn initialize_fields_list(&fields_list, &initialized, \"promisor.checkFields\");\n }\n \n+static struct string_list *fields_stored(void)\n+{\n+\tstatic struct string_list fields_list = STRING_LIST_INIT_NODUP;\n+\tstatic int initialized;\n+\n+\treturn initialize_fields_list(&fields_list, &initialized, \"promisor.storeFields\");\n+}\n+\n /*\n  * Struct for promisor remotes involved in the \"promisor-remote\"\n  * protocol capability.\n@@ -692,6 +700,132 @@ static struct promisor_info *parse_one_advertised_remote(const char *remote_info\n \treturn info;\n }\n \n+static bool store_one_field(struct repository *repo, const char *remote_name,\n+\t\t\t    const char *field_name, const char *field_key,\n+\t\t\t    const char *advertised, const char *current)\n+{\n+\tif (advertised && (!current || strcmp(current, advertised))) {\n+\t\tchar *key = xstrfmt(\"remote.%s.%s\", remote_name, field_key);\n+\n+\t\tfprintf(stderr, _(\"Storing new %s from server for remote '%s'.\\n\"\n+\t\t\t\t  \"    '%s' -> '%s'\\n\"),\n+\t\t\tfield_name, remote_name,\n+\t\t\tcurrent ? current : \"\",\n+\t\t\tadvertised);\n+\n+\t\trepo_config_set_gently(repo, key, advertised);\n+\t\tfree(key);\n+\n+\t\treturn true;\n+\t}\n+\n+\treturn false;\n+}\n+\n+/* Check that a filter is valid by parsing it */\n+static bool valid_filter(const char *filter, const char *remote_name)\n+{\n+\tstruct list_objects_filter_options filter_opts = LIST_OBJECTS_FILTER_INIT;\n+\tstruct strbuf err = STRBUF_INIT;\n+\tint res = gently_parse_list_objects_filter(&filter_opts, filter, &err);\n+\n+\tif (res)\n+\t\twarning(_(\"invalid filter '%s' for remote '%s' \"\n+\t\t\t  \"will not be stored: %s\"),\n+\t\t\tfilter, remote_name, err.buf);\n+\n+\tlist_objects_filter_release(&filter_opts);\n+\tstrbuf_release(&err);\n+\n+\treturn !res;\n+}\n+\n+/* Check that a token doesn't contain any control character */\n+static bool valid_token(const char *token, const char *remote_name)\n+{\n+\tconst char *c = token;\n+\n+\tfor (; *c; c++)\n+\t\tif (iscntrl(*c)) {\n+\t\t\twarning(_(\"invalid token '%s' for remote '%s' \"\n+\t\t\t\t  \"will not be stored\"),\n+\t\t\t\ttoken, remote_name);\n+\t\t\treturn false;\n+\t\t}\n+\n+\treturn true;\n+}\n+\n+struct store_info {\n+\tstruct repository *repo;\n+\tstruct string_list config_info;\n+\tbool store_filter;\n+\tbool store_token;\n+};\n+\n+static struct store_info *store_info_new(struct repository *repo)\n+{\n+\tstruct string_list *fields_to_store = fields_stored();\n+\tstruct store_info *s = xmalloc(sizeof(*s));\n+\n+\ts->repo = repo;\n+\n+\tstring_list_init_nodup(&s->config_info);\n+\tpromisor_config_info_list(repo, &s->config_info, fields_to_store);\n+\tstring_list_sort(&s->config_info);\n+\n+\ts->store_filter = !!string_list_lookup(fields_to_store, promisor_field_filter);\n+\ts->store_token = !!string_list_lookup(fields_to_store, promisor_field_token);\n+\n+\treturn s;\n+}\n+\n+static void store_info_free(struct store_info *s)\n+{\n+\tif (s) {\n+\t\tpromisor_info_list_clear(&s->config_info);\n+\t\tfree(s);\n+\t}\n+}\n+\n+static bool promisor_store_advertised_fields(struct promisor_info *advertised,\n+\t\t\t\t\t     struct store_info *store_info)\n+{\n+\tstruct promisor_info *p;\n+\tstruct string_list_item *item;\n+\tconst char *remote_name = advertised->name;\n+\tbool reload_config = false;\n+\n+\tif (!(store_info->store_filter || store_info->store_token))\n+\t\treturn false;\n+\n+\t/*\n+\t * Get existing config info for the advertised promisor\n+\t * remote. This ensures the remote is already configured on\n+\t * the client side.\n+\t */\n+\titem = string_list_lookup(&store_info->config_info, remote_name);\n+\n+\tif (!item)\n+\t\treturn false;\n+\n+\tp = item->util;\n+\n+\tif (store_info->store_filter && advertised->filter &&\n+\t    valid_filter(advertised->filter, remote_name))\n+\t\treload_config |= store_one_field(store_info->repo, remote_name,\n+\t\t\t\t\t\t \"filter\", promisor_field_filter,\n+\t\t\t\t\t\t advertised->filter, p->filter);\n+\n+\tif (store_info->store_token && advertised->token &&\n+\t    valid_token(advertised->token, remote_name))\n+\t\treload_config |= store_one_field(store_info->repo, remote_name,\n+\t\t\t\t\t\t \"token\", promisor_field_token,\n+\t\t\t\t\t\t advertised->token, p->token);\n+\n+\treturn reload_config;\n+}\n+\n static void filter_promisor_remote(struct repository *repo,\n \t\t\t\t   struct strvec *accepted,\n \t\t\t\t   const char *info)\n@@ -700,7 +834,9 @@ static void filter_promisor_remote(struct repository *repo,\n \tenum accept_promisor accept = ACCEPT_NONE;\n \tstruct string_list config_info = STRING_LIST_INIT_NODUP;\n \tstruct string_list remote_info = STRING_LIST_INIT_DUP;\n+\tstruct store_info *store_info = NULL;\n \tstruct string_list_item *item;\n+\tbool reload_config = false;\n \n \tif (!repo_config_get_string_tmp(the_repository, \"promisor.acceptfromserver\", &accept_str)) {\n \t\tif (!*accept_str || !strcasecmp(\"None\", accept_str))\n@@ -736,14 +872,24 @@ static void filter_promisor_remote(struct repository *repo,\n \t\t\tstring_list_sort(&config_info);\n \t\t}\n \n-\t\tif (should_accept_remote(accept, advertised, &config_info))\n+\t\tif (should_accept_remote(accept, advertised, &config_info)) {\n+\t\t\tif (!store_info)\n+\t\t\t\tstore_info = store_info_new(repo);\n+\t\t\tif (promisor_store_advertised_fields(advertised, store_info))\n+\t\t\t\treload_config = true;\n+\n \t\t\tstrvec_push(accepted, advertised->name);\n+\t\t}\n \n \t\tpromisor_info_free(advertised);\n \t}\n \n \tpromisor_info_list_clear(&config_info);\n \tstring_list_clear(&remote_info, 0);\n+\tstore_info_free(store_info);\n+\n+\tif (reload_config)\n+\t\trepo_promisor_remote_reinit(repo);\n }\n \n char *promisor_remote_reply(const char *info)\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex 023735d6a8..a726af214a 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -360,6 +360,55 @@ test_expect_success \"clone with promisor.checkFields\" '\n \tcheck_missing_objects server 1 \"$oid\"\n '\n \n+test_expect_success \"clone with promisor.storeFields=partialCloneFilter\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\tgit -C server remote add otherLop \"https://invalid.invalid\"  &&\n+\tgit -C server config remote.otherLop.token \"fooBar\" &&\n+\tgit -C server config remote.otherLop.stuff \"baz\" &&\n+\tgit -C server config remote.otherLop.partialCloneFilter \"blob:limit=10k\" &&\n+\ttest_when_finished \"git -C server remote remove otherLop\" &&\n+\n+\tgit -C server config remote.lop.token \"fooXXX\" &&\n+\tgit -C server config remote.lop.partialCloneFilter \"blob:limit=8k\" &&\n+\n+\ttest_config -C server promisor.sendFields \"partialCloneFilter, token\" &&\n+\ttest_when_finished \"rm trace\" &&\n+\n+\t# Clone from server to create a client\n+\tGIT_TRACE_PACKET=\"$(pwd)/trace\" GIT_NO_LAZY_FETCH=0 git clone \\\n+\t\t-c remote.lop.promisor=true \\\n+\t\t-c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n+\t\t-c remote.lop.url=\"file://$(pwd)/lop\" \\\n+\t\t-c remote.lop.token=\"fooYYY\" \\\n+\t\t-c remote.lop.partialCloneFilter=\"blob:none\" \\\n+\t\t-c promisor.acceptfromserver=All \\\n+\t\t-c promisor.storeFields=partialcloneFilter \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client 2>err &&\n+\n+\t# Check that the filter from the server is stored\n+\techo \"blob:limit=8k\" >expected &&\n+\tgit -C client config remote.lop.partialCloneFilter >actual &&\n+\ttest_cmp expected actual &&\n+\n+\t# Check that user is notified when the filter is stored\n+\ttest_grep \"Storing new filter from server for remote '\\''lop'\\''\" err &&\n+\ttest_grep \"'\\''blob:none'\\'' -> '\\''blob:limit=8k'\\''\" err &&\n+\n+\t# Check that the token from the server is NOT stored\n+\techo \"fooYYY\" >expected &&\n+\tgit -C client config remote.lop.token >actual &&\n+\ttest_cmp expected actual &&\n+\ttest_grep ! \"Storing new token from server\" err &&\n+\n+\t# Check that the filter for an unknown remote is NOT stored\n+\ttest_must_fail git -C client config remote.otherLop.partialCloneFilter >actual &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\"\n+'\n+\n test_expect_success \"clone with promisor.advertise set to 'true' but don't delete the client\" '\n \tgit -C server config promisor.advertise true &&\n \n-- \n2.53.0.rc2.10.g12663a1c75.dirty\n\n"},{"id":"535135","messageId":"20260204110818.2919273-4-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260204110818.2919273-1-christian.couder@gmail.com","subject":"[PATCH v2 3/8] clone: make filter_options local to cmd_clone()","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-04T11:08:08Z","receivedAt":"2026-02-04T11:08:40Z","isPatch":true,"body":"The `struct list_objects_filter_options filter_options` variable used\nin \"builtin/clone.c\" to store the parsed filters specified by\n`--filter=<filterspec>` is currently a static variable global to the\nfile.\n\nAs we are going to use it more in a following commit, it could become\na bit less easy to understand how it's managed.\n\nTo avoid that, let's make it clear that it's owned by cmd_clone() by\nmoving its definition into that function and making it non-static.\n\nThe only additional change to make this work is to pass it as an\nargument to checkout(). So it's a small quite cheap cleanup anyway.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n builtin/clone.c | 16 +++++++++++-----\n 1 file changed, 11 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex b40cee5968..51f4b5809d 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -77,7 +77,6 @@ static struct string_list option_required_reference = STRING_LIST_INIT_NODUP;\n static struct string_list option_optional_reference = STRING_LIST_INIT_NODUP;\n static int max_jobs = -1;\n static struct string_list option_recurse_submodules = STRING_LIST_INIT_NODUP;\n-static struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n static int config_filter_submodules = -1;    /* unspecified */\n static int option_remote_submodules;\n \n@@ -634,7 +633,9 @@ static int git_sparse_checkout_init(const char *repo)\n \treturn result;\n }\n \n-static int checkout(int submodule_progress, int filter_submodules,\n+static int checkout(int submodule_progress,\n+\t\t    struct list_objects_filter_options *filter_options,\n+\t\t    int filter_submodules,\n \t\t    enum ref_storage_format ref_storage_format)\n {\n \tstruct object_id oid;\n@@ -723,9 +724,9 @@ static int checkout(int submodule_progress, int filter_submodules,\n \t\t\tstrvec_pushf(&cmd.args, \"--ref-format=%s\",\n \t\t\t\t     ref_storage_format_to_name(ref_storage_format));\n \n-\t\tif (filter_submodules && filter_options.choice)\n+\t\tif (filter_submodules && filter_options->choice)\n \t\t\tstrvec_pushf(&cmd.args, \"--filter=%s\",\n-\t\t\t\t     expand_list_objects_filter_spec(&filter_options));\n+\t\t\t\t     expand_list_objects_filter_spec(filter_options));\n \n \t\tif (option_single_branch >= 0)\n \t\t\tstrvec_push(&cmd.args, option_single_branch ?\n@@ -903,6 +904,7 @@ int cmd_clone(int argc,\n \tenum transport_family family = TRANSPORT_FAMILY_ALL;\n \tstruct string_list option_config = STRING_LIST_INIT_DUP;\n \tint option_dissociate = 0;\n+\tstruct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n \tint option_filter_submodules = -1; /* unspecified */\n \tstruct string_list server_options = STRING_LIST_INIT_NODUP;\n \tconst char *bundle_uri = NULL;\n@@ -1625,9 +1627,13 @@ int cmd_clone(int argc,\n \t\treturn 1;\n \n \tjunk_mode = JUNK_LEAVE_REPO;\n-\terr = checkout(submodule_progress, filter_submodules,\n+\terr = checkout(submodule_progress,\n+\t\t       &filter_options,\n+\t\t       filter_submodules,\n \t\t       ref_storage_format);\n \n+\tlist_objects_filter_release(&filter_options);\n+\n \tstring_list_clear(&option_not, 0);\n \tstring_list_clear(&option_config, 0);\n \tstring_list_clear(&server_options, 0);\n-- \n2.53.0.rc2.10.g12663a1c75.dirty\n\n"},{"id":"535136","messageId":"20260204110818.2919273-5-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260204110818.2919273-1-christian.couder@gmail.com","subject":"[PATCH v2 4/8] fetch: make filter_options local to cmd_fetch()","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-04T11:08:09Z","receivedAt":"2026-02-04T11:08:41Z","isPatch":true,"body":"The `struct list_objects_filter_options filter_options` variable used\nin \"builtin/fetch.c\" to store the parsed filters specified by\n`--filter=<filterspec>` is currently a static variable global to the\nfile.\n\nAs we are going to use it more in a following commit, it could become a\nbit less easy to understand how it's managed.\n\nTo avoid that, let's make it clear that it's owned by cmd_fetch() by\nmoving its definition into that function and making it non-static.\n\nThis requires passing a pointer to it through the prepare_transport(),\ndo_fetch(), backfill_tags(), fetch_one_setup_partial(), and fetch_one()\nfunctions, but it's quite straightforward.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n builtin/fetch.c | 48 +++++++++++++++++++++++++++---------------------\n 1 file changed, 27 insertions(+), 21 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 288d3772ea..b984173447 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -97,7 +97,6 @@ static struct strbuf default_rla = STRBUF_INIT;\n static struct transport *gtransport;\n static struct transport *gsecondary;\n static struct refspec refmap = REFSPEC_INIT_FETCH;\n-static struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n static struct string_list server_options = STRING_LIST_INIT_DUP;\n static struct string_list negotiation_tip = STRING_LIST_INIT_NODUP;\n \n@@ -1449,7 +1448,8 @@ static void add_negotiation_tips(struct git_transport_options *smart_options)\n \tsmart_options->negotiation_tips = oids;\n }\n \n-static struct transport *prepare_transport(struct remote *remote, int deepen)\n+static struct transport *prepare_transport(struct remote *remote, int deepen,\n+\t\t\t\t\t   struct list_objects_filter_options *filter_options)\n {\n \tstruct transport *transport;\n \n@@ -1473,9 +1473,9 @@ static struct transport *prepare_transport(struct remote *remote, int deepen)\n \t\tset_option(transport, TRANS_OPT_UPDATE_SHALLOW, \"yes\");\n \tif (refetch)\n \t\tset_option(transport, TRANS_OPT_REFETCH, \"yes\");\n-\tif (filter_options.choice) {\n+\tif (filter_options->choice) {\n \t\tconst char *spec =\n-\t\t\texpand_list_objects_filter_spec(&filter_options);\n+\t\t\texpand_list_objects_filter_spec(filter_options);\n \t\tset_option(transport, TRANS_OPT_LIST_OBJECTS_FILTER, spec);\n \t\tset_option(transport, TRANS_OPT_FROM_PROMISOR, \"1\");\n \t}\n@@ -1493,7 +1493,8 @@ static int backfill_tags(struct display_state *display_state,\n \t\t\t struct ref_transaction *transaction,\n \t\t\t struct ref *ref_map,\n \t\t\t struct fetch_head *fetch_head,\n-\t\t\t const struct fetch_config *config)\n+\t\t\t const struct fetch_config *config,\n+\t\t\t struct list_objects_filter_options *filter_options)\n {\n \tint retcode, cannot_reuse;\n \n@@ -1507,7 +1508,7 @@ static int backfill_tags(struct display_state *display_state,\n \tcannot_reuse = transport->cannot_reuse ||\n \t\tdeepen_since || deepen_not.nr;\n \tif (cannot_reuse) {\n-\t\tgsecondary = prepare_transport(transport->remote, 0);\n+\t\tgsecondary = prepare_transport(transport->remote, 0, filter_options);\n \t\ttransport = gsecondary;\n \t}\n \n@@ -1713,7 +1714,8 @@ static int commit_ref_transaction(struct ref_transaction **transaction,\n \n static int do_fetch(struct transport *transport,\n \t\t    struct refspec *rs,\n-\t\t    const struct fetch_config *config)\n+\t\t    const struct fetch_config *config,\n+\t\t    struct list_objects_filter_options *filter_options)\n {\n \tstruct ref_transaction *transaction = NULL;\n \tstruct ref *ref_map = NULL;\n@@ -1873,7 +1875,7 @@ static int do_fetch(struct transport *transport,\n \t\t\t * the transaction and don't commit anything.\n \t\t\t */\n \t\t\tif (backfill_tags(&display_state, transport, transaction, tags_ref_map,\n-\t\t\t\t\t  &fetch_head, config))\n+\t\t\t\t\t  &fetch_head, config, filter_options))\n \t\t\t\tretcode = 1;\n \t\t}\n \n@@ -2198,20 +2200,21 @@ static int fetch_multiple(struct string_list *list, int max_children,\n  * Fetching from the promisor remote should use the given filter-spec\n  * or inherit the default filter-spec from the config.\n  */\n-static inline void fetch_one_setup_partial(struct remote *remote)\n+static inline void fetch_one_setup_partial(struct remote *remote,\n+\t\t\t\t\t   struct list_objects_filter_options *filter_options)\n {\n \t/*\n \t * Explicit --no-filter argument overrides everything, regardless\n \t * of any prior partial clones and fetches.\n \t */\n-\tif (filter_options.no_filter)\n+\tif (filter_options->no_filter)\n \t\treturn;\n \n \t/*\n \t * If no prior partial clone/fetch and the current fetch DID NOT\n \t * request a partial-fetch, do a normal fetch.\n \t */\n-\tif (!repo_has_promisor_remote(the_repository) && !filter_options.choice)\n+\tif (!repo_has_promisor_remote(the_repository) && !filter_options->choice)\n \t\treturn;\n \n \t/*\n@@ -2220,8 +2223,8 @@ static inline void fetch_one_setup_partial(struct remote *remote)\n \t * filter-spec as the default for subsequent fetches to this\n \t * remote if there is currently no default filter-spec.\n \t */\n-\tif (filter_options.choice) {\n-\t\tpartial_clone_register(remote->name, &filter_options);\n+\tif (filter_options->choice) {\n+\t\tpartial_clone_register(remote->name, filter_options);\n \t\treturn;\n \t}\n \n@@ -2230,14 +2233,15 @@ static inline void fetch_one_setup_partial(struct remote *remote)\n \t * explicitly given filter-spec or inherit the filter-spec from\n \t * the config.\n \t */\n-\tif (!filter_options.choice)\n-\t\tpartial_clone_get_default_filter_spec(&filter_options, remote->name);\n+\tif (!filter_options->choice)\n+\t\tpartial_clone_get_default_filter_spec(filter_options, remote->name);\n \treturn;\n }\n \n static int fetch_one(struct remote *remote, int argc, const char **argv,\n \t\t     int prune_tags_ok, int use_stdin_refspecs,\n-\t\t     const struct fetch_config *config)\n+\t\t     const struct fetch_config *config,\n+\t\t     struct list_objects_filter_options *filter_options)\n {\n \tstruct refspec rs = REFSPEC_INIT_FETCH;\n \tint i;\n@@ -2249,7 +2253,7 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,\n \t\tdie(_(\"no remote repository specified; please specify either a URL or a\\n\"\n \t\t      \"remote name from which new revisions should be fetched\"));\n \n-\tgtransport = prepare_transport(remote, 1);\n+\tgtransport = prepare_transport(remote, 1, filter_options);\n \n \tif (prune < 0) {\n \t\t/* no command line request */\n@@ -2304,7 +2308,7 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,\n \tsigchain_push_common(unlock_pack_on_signal);\n \tatexit(unlock_pack_atexit);\n \tsigchain_push(SIGPIPE, SIG_IGN);\n-\texit_code = do_fetch(gtransport, &rs, config);\n+\texit_code = do_fetch(gtransport, &rs, config, filter_options);\n \tsigchain_pop(SIGPIPE);\n \trefspec_clear(&rs);\n \ttransport_disconnect(gtransport);\n@@ -2329,6 +2333,7 @@ int cmd_fetch(int argc,\n \tconst char *submodule_prefix = \"\";\n \tconst char *bundle_uri;\n \tstruct string_list list = STRING_LIST_INIT_DUP;\n+\tstruct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n \tstruct remote *remote = NULL;\n \tint all = -1, multiple = 0;\n \tint result = 0;\n@@ -2594,7 +2599,7 @@ int cmd_fetch(int argc,\n \t\ttrace2_region_enter(\"fetch\", \"negotiate-only\", the_repository);\n \t\tif (!remote)\n \t\t\tdie(_(\"must supply remote when using --negotiate-only\"));\n-\t\tgtransport = prepare_transport(remote, 1);\n+\t\tgtransport = prepare_transport(remote, 1, &filter_options);\n \t\tif (gtransport->smart_options) {\n \t\t\tgtransport->smart_options->acked_commits = &acked_commits;\n \t\t} else {\n@@ -2616,12 +2621,12 @@ int cmd_fetch(int argc,\n \t} else if (remote) {\n \t\tif (filter_options.choice || repo_has_promisor_remote(the_repository)) {\n \t\t\ttrace2_region_enter(\"fetch\", \"setup-partial\", the_repository);\n-\t\t\tfetch_one_setup_partial(remote);\n+\t\t\tfetch_one_setup_partial(remote, &filter_options);\n \t\t\ttrace2_region_leave(\"fetch\", \"setup-partial\", the_repository);\n \t\t}\n \t\ttrace2_region_enter(\"fetch\", \"fetch-one\", the_repository);\n \t\tresult = fetch_one(remote, argc, argv, prune_tags_ok, stdin_refspecs,\n-\t\t\t\t   &config);\n+\t\t\t\t   &config, &filter_options);\n \t\ttrace2_region_leave(\"fetch\", \"fetch-one\", the_repository);\n \t} else {\n \t\tint max_children = max_jobs;\n@@ -2727,5 +2732,6 @@ int cmd_fetch(int argc,\n \n  cleanup:\n \tstring_list_clear(&list, 0);\n+\tlist_objects_filter_release(&filter_options);\n \treturn result;\n }\n-- \n2.53.0.rc2.10.g12663a1c75.dirty\n\n"},{"id":"535137","messageId":"20260204110818.2919273-6-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260204110818.2919273-1-christian.couder@gmail.com","subject":"[PATCH v2 5/8] doc: fetch: document `--filter=<filter-spec>` option","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-04T11:08:10Z","receivedAt":"2026-02-04T11:08:43Z","isPatch":true,"body":"The `--filter=<filter-spec>` option is documented in most commands that\nsupport it except `git fetch`.\n\nLet's fix that and document that option using the same words already\nused to document it for `git clone`.\n\nThose words could probably be improved, but they are not wrong, so\nlet's just use them for now and leave improving them for future work.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/fetch-options.adoc | 10 ++++++++++\n 1 file changed, 10 insertions(+)\n\ndiff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc\nindex fcba46ee9e..1ef9807d00 100644\n--- a/Documentation/fetch-options.adoc\n+++ b/Documentation/fetch-options.adoc\n@@ -88,6 +88,16 @@ linkgit:git-config[1].\n This is incompatible with `--recurse-submodules=(yes|on-demand)` and takes\n precedence over the `fetch.output` config option.\n \n+`--filter=<filter-spec>`::\n+\tUse the partial clone feature and request that the server sends\n+\ta subset of reachable objects according to a given object filter.\n+\tWhen using `--filter`, the supplied _<filter-spec>_ is used for\n+\tthe partial fetch. For example, `--filter=blob:none` will filter\n+\tout all blobs (file contents) until needed by Git. Also,\n+\t`--filter=blob:limit=<size>` will filter out all blobs of size\n+\tat least _<size>_. For more details on filter specifications, see\n+\tthe `--filter` option in linkgit:git-rev-list[1].\n+\n ifndef::git-pull[]\n `--write-fetch-head`::\n `--no-write-fetch-head`::\n-- \n2.53.0.rc2.10.g12663a1c75.dirty\n\n"},{"id":"535138","messageId":"20260204110818.2919273-7-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260204110818.2919273-1-christian.couder@gmail.com","subject":"[PATCH v2 6/8] list-objects-filter-options: support 'auto' mode for --filter","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-04T11:08:11Z","receivedAt":"2026-02-04T11:08:44Z","isPatch":true,"body":"In a following commit, we are going to allow passing \"auto\" as a\n<filterspec> to the `--filter=<filterspec>` option, but only for some\ncommands. Other commands that support the `--filter=<filterspec>`\noption should still die() when 'auto' is passed.\n\nLet's set up the \"list-objects-filter-options.{c,h}\" infrastructure to\nsupport that:\n\n- Add a new `unsigned int allow_auto_filter : 1;` flag to\n  `struct list_objects_filter_options` which specifies if \"auto\" is\n  accepted or not by the current command.\n- Change gently_parse_list_objects_filter() to parse \"auto\" if it's\n  accepted.\n- Make sure we die() if \"auto\" is combined with another filter.\n- Update list_objects_filter_release() to preserve the\n  allow_auto_filter flag, as this function is often called (via\n  opt_parse_list_objects_filter) to reset the struct before parsing a\n  new value.\n\nLet's also update `list-objects-filter.c` to recognize the new\n`LOFC_AUTO` choice. Since \"auto\" must be resolved to a concrete filter\nbefore filtering actually begins, initializing a filter with\n`LOFC_AUTO` is invalid and will trigger a BUG().\n\nNote that ideally combining \"auto\" with \"auto\" could be allowed, but in\npractice, it's probably not worth the added code complexity. And if we\nreally want it, nothing prevents us to allow it in future work.\n\nIf we ever want to give a meaning to combining \"auto\" with a different\nfilter too, nothing prevents us to do that in future work either.\n\nAlso note that the new `allow_auto_filter` flag depends on the command,\nnot user choices, so it should be reset to the command default when\n`struct list_objects_filter_options` instances are reset.\n\nWhile at it, let's add a new \"u-list-objects-filter-options.c\" file for\n`struct list_objects_filter_options` related unit tests. For now it\nonly tests gently_parse_list_objects_filter() though.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Makefile                                     |  1 +\n list-objects-filter-options.c                | 37 ++++++++++++--\n list-objects-filter-options.h                |  6 +++\n list-objects-filter.c                        |  8 +++\n t/meson.build                                |  1 +\n t/unit-tests/u-list-objects-filter-options.c | 53 ++++++++++++++++++++\n 6 files changed, 103 insertions(+), 3 deletions(-)\n create mode 100644 t/unit-tests/u-list-objects-filter-options.c\n\ndiff --git a/Makefile b/Makefile\nindex 8aa489f3b6..04256f747c 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1516,6 +1516,7 @@ CLAR_TEST_SUITES += u-dir\n CLAR_TEST_SUITES += u-example-decorate\n CLAR_TEST_SUITES += u-hash\n CLAR_TEST_SUITES += u-hashmap\n+CLAR_TEST_SUITES += u-list-objects-filter-options\n CLAR_TEST_SUITES += u-mem-pool\n CLAR_TEST_SUITES += u-oid-array\n CLAR_TEST_SUITES += u-oidmap\ndiff --git a/list-objects-filter-options.c b/list-objects-filter-options.c\nindex 7420bf81fe..ad92cbaa37 100644\n--- a/list-objects-filter-options.c\n+++ b/list-objects-filter-options.c\n@@ -20,6 +20,8 @@ const char *list_object_filter_config_name(enum list_objects_filter_choice c)\n \tcase LOFC_DISABLED:\n \t\t/* we have no name for \"no filter at all\" */\n \t\tbreak;\n+\tcase LOFC_AUTO:\n+\t\treturn \"auto\";\n \tcase LOFC_BLOB_NONE:\n \t\treturn \"blob:none\";\n \tcase LOFC_BLOB_LIMIT:\n@@ -52,7 +54,16 @@ int gently_parse_list_objects_filter(\n \tif (filter_options->choice)\n \t\tBUG(\"filter_options already populated\");\n \n-\tif (!strcmp(arg, \"blob:none\")) {\n+\tif (!strcmp(arg, \"auto\")) {\n+\t\tif (!filter_options->allow_auto_filter) {\n+\t\t\tstrbuf_addstr(errbuf,\n+\t\t\t\t      _(\"'auto' filter not supported by this command\"));\n+\t\t\treturn 1;\n+\t\t}\n+\t\tfilter_options->choice = LOFC_AUTO;\n+\t\treturn 0;\n+\n+\t} else if (!strcmp(arg, \"blob:none\")) {\n \t\tfilter_options->choice = LOFC_BLOB_NONE;\n \t\treturn 0;\n \n@@ -146,10 +157,22 @@ static int parse_combine_subfilter(\n \n \tdecoded = url_percent_decode(subspec->buf);\n \n-\tresult = has_reserved_character(subspec, errbuf) ||\n-\t\tgently_parse_list_objects_filter(\n+\tresult = has_reserved_character(subspec, errbuf);\n+\tif (result)\n+\t\tgoto cleanup;\n+\n+\tresult = gently_parse_list_objects_filter(\n \t\t\t&filter_options->sub[new_index], decoded, errbuf);\n+\tif (result)\n+\t\tgoto cleanup;\n+\n+\tresult = (filter_options->sub[new_index].choice == LOFC_AUTO);\n+\tif (result) {\n+\t\tstrbuf_addstr(errbuf, _(\"an 'auto' filter cannot be combined\"));\n+\t\tgoto cleanup;\n+\t}\n \n+cleanup:\n \tfree(decoded);\n \treturn result;\n }\n@@ -263,6 +286,9 @@ void parse_list_objects_filter(\n \t} else {\n \t\tstruct list_objects_filter_options *sub;\n \n+\t\tif (filter_options->choice == LOFC_AUTO)\n+\t\t\tdie(_(\"an 'auto' filter is incompatible with any other filter\"));\n+\n \t\t/*\n \t\t * Make filter_options an LOFC_COMBINE spec so we can trivially\n \t\t * add subspecs to it.\n@@ -277,6 +303,9 @@ void parse_list_objects_filter(\n \t\tif (gently_parse_list_objects_filter(sub, arg, &errbuf))\n \t\t\tdie(\"%s\", errbuf.buf);\n \n+\t\tif (sub->choice == LOFC_AUTO)\n+\t\t\tdie(_(\"an 'auto' filter is incompatible with any other filter\"));\n+\n \t\tstrbuf_addch(&filter_options->filter_spec, '+');\n \t\tfilter_spec_append_urlencode(filter_options, arg);\n \t}\n@@ -317,6 +346,7 @@ void list_objects_filter_release(\n \tstruct list_objects_filter_options *filter_options)\n {\n \tsize_t sub;\n+\tunsigned int allow_auto_filter = filter_options->allow_auto_filter;\n \n \tif (!filter_options)\n \t\treturn;\n@@ -326,6 +356,7 @@ void list_objects_filter_release(\n \t\tlist_objects_filter_release(&filter_options->sub[sub]);\n \tfree(filter_options->sub);\n \tlist_objects_filter_init(filter_options);\n+\tfilter_options->allow_auto_filter = allow_auto_filter;\n }\n \n void partial_clone_register(\ndiff --git a/list-objects-filter-options.h b/list-objects-filter-options.h\nindex 7b2108b986..77d7bbc846 100644\n--- a/list-objects-filter-options.h\n+++ b/list-objects-filter-options.h\n@@ -18,6 +18,7 @@ enum list_objects_filter_choice {\n \tLOFC_SPARSE_OID,\n \tLOFC_OBJECT_TYPE,\n \tLOFC_COMBINE,\n+\tLOFC_AUTO,\n \tLOFC__COUNT /* must be last */\n };\n \n@@ -50,6 +51,11 @@ struct list_objects_filter_options {\n \t */\n \tunsigned int no_filter : 1;\n \n+\t/*\n+\t * Is LOFC_AUTO a valid option?\n+\t */\n+\tunsigned int allow_auto_filter : 1;\n+\n \t/*\n \t * BEGIN choice-specific parsed values from within the filter-spec. Only\n \t * some values will be defined for any given choice.\ndiff --git a/list-objects-filter.c b/list-objects-filter.c\nindex acd65ebb73..78316e7f90 100644\n--- a/list-objects-filter.c\n+++ b/list-objects-filter.c\n@@ -745,6 +745,13 @@ static void filter_combine__init(\n \tfilter->finalize_omits_fn = filter_combine__finalize_omits;\n }\n \n+static void filter_auto__init(\n+\tstruct list_objects_filter_options *filter_options UNUSED,\n+\tstruct filter *filter UNUSED)\n+{\n+\tBUG(\"LOFC_AUTO should have been resolved before initializing the filter\");\n+}\n+\n typedef void (*filter_init_fn)(\n \tstruct list_objects_filter_options *filter_options,\n \tstruct filter *filter);\n@@ -760,6 +767,7 @@ static filter_init_fn s_filters[] = {\n \tfilter_sparse_oid__init,\n \tfilter_object_type__init,\n \tfilter_combine__init,\n+\tfilter_auto__init,\n };\n \n struct filter *list_objects_filter__init(\ndiff --git a/t/meson.build b/t/meson.build\nindex 459c52a489..0bd66cc6ce 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -4,6 +4,7 @@ clar_test_suites = [\n   'unit-tests/u-example-decorate.c',\n   'unit-tests/u-hash.c',\n   'unit-tests/u-hashmap.c',\n+  'unit-tests/u-list-objects-filter-options.c',\n   'unit-tests/u-mem-pool.c',\n   'unit-tests/u-oid-array.c',\n   'unit-tests/u-oidmap.c',\ndiff --git a/t/unit-tests/u-list-objects-filter-options.c b/t/unit-tests/u-list-objects-filter-options.c\nnew file mode 100644\nindex 0000000000..f7d73701b5\n--- /dev/null\n+++ b/t/unit-tests/u-list-objects-filter-options.c\n@@ -0,0 +1,53 @@\n+#include \"unit-test.h\"\n+#include \"list-objects-filter-options.h\"\n+#include \"strbuf.h\"\n+\n+/* Helper to test gently_parse_list_objects_filter() */\n+static void check_gentle_parse(const char *filter_spec,\n+\t\t\t       int expect_success,\n+\t\t\t       int allow_auto,\n+\t\t\t       enum list_objects_filter_choice expected_choice)\n+{\n+\tstruct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n+\tstruct strbuf errbuf = STRBUF_INIT;\n+\tint ret;\n+\n+\tfilter_options.allow_auto_filter = allow_auto;\n+\n+\tret = gently_parse_list_objects_filter(&filter_options, filter_spec, &errbuf);\n+\n+\tif (expect_success) {\n+\t\tcl_assert_equal_i(ret, 0);\n+\t\tcl_assert_equal_i(expected_choice, filter_options.choice);\n+\t\tcl_assert_equal_i(errbuf.len, 0);\n+\t} else {\n+\t\tcl_assert(ret != 0);\n+\t\tcl_assert(errbuf.len > 0);\n+\t}\n+\n+\tstrbuf_release(&errbuf);\n+\tlist_objects_filter_release(&filter_options);\n+}\n+\n+void test_list_objects_filter_options__regular_filters(void)\n+{\n+\tcheck_gentle_parse(\"blob:none\", 1, 0, LOFC_BLOB_NONE);\n+\tcheck_gentle_parse(\"blob:none\", 1, 1, LOFC_BLOB_NONE);\n+\tcheck_gentle_parse(\"blob:limit=5k\", 1, 0, LOFC_BLOB_LIMIT);\n+\tcheck_gentle_parse(\"blob:limit=5k\", 1, 1, LOFC_BLOB_LIMIT);\n+\tcheck_gentle_parse(\"combine:blob:none+tree:0\", 1, 0, LOFC_COMBINE);\n+\tcheck_gentle_parse(\"combine:blob:none+tree:0\", 1, 1, LOFC_COMBINE);\n+}\n+\n+void test_list_objects_filter_options__auto_allowed(void)\n+{\n+\tcheck_gentle_parse(\"auto\", 1, 1, LOFC_AUTO);\n+\tcheck_gentle_parse(\"auto\", 0, 0, 0);\n+}\n+\n+void test_list_objects_filter_options__combine_auto_fails(void)\n+{\n+\tcheck_gentle_parse(\"combine:auto+blob:none\", 0, 1, 0);\n+\tcheck_gentle_parse(\"combine:blob:none+auto\", 0, 1, 0);\n+\tcheck_gentle_parse(\"combine:auto+auto\", 0, 1, 0);\n+}\n-- \n2.53.0.rc2.10.g12663a1c75.dirty\n\n"},{"id":"535139","messageId":"20260204110818.2919273-8-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260204110818.2919273-1-christian.couder@gmail.com","subject":"[PATCH v2 7/8] promisor-remote: keep advertised filters in memory","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-04T11:08:12Z","receivedAt":"2026-02-04T11:08:45Z","isPatch":true,"body":"Currently, advertised filters are only kept in memory temporarily\nduring parsing, or persisted to disk if `promisor.storeFields`\ncontains 'partialCloneFilter'.\n\nIn a following commit though, we will add a `--filter=auto` option.\nThis option will enable the client to use the filters that the server\nis suggesting for the promisor remotes the client accepts.\n\nTo use them even if `promisor.storeFields` is not configured, these\nfilters should be stored somewhere for the current session.\n\nLet's add an `advertised_filter` field to `struct promisor_remote`\nfor that purpose.\n\nTo ensure that the filters are available in all cases,\nfilter_promisor_remote() captures them into a temporary list and\napplies them to the `promisor_remote` structs after the potential\nconfiguration reload.\n\nThen the accepted remotes are marked as `accepted` in the repository\nstate. This ensures that subsequent calls to look up accepted remotes\n(like in the filter construction below) actually find them.\n\nIn a following commit, we will add a `--filter=auto` option that will\nenable a client to use the filters suggested by the server for the\npromisor remotes the client accepted.\n\nTo enable the client to construct a filter spec based on these filters,\nlet's also add a `promisor_remote_construct_filter(repo)` function.\n\nThis function:\n\n- iterates over all accepted promisor remotes in the repository,\n- collects the filters advertised for them (using `advertised_filter`\n  added in this commit, and\n- generates a single filter spec for them.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 58 +++++++++++++++++++++++++++++++++++++++++++++++\n promisor-remote.h |  7 ++++++\n 2 files changed, 65 insertions(+)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 59997dd4c7..d0bfb209dc 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -193,6 +193,7 @@ void promisor_remote_clear(struct promisor_remote_config *config)\n \twhile (config->promisors) {\n \t\tstruct promisor_remote *r = config->promisors;\n \t\tfree(r->partial_clone_filter);\n+\t\tfree(r->advertised_filter);\n \t\tconfig->promisors = config->promisors->next;\n \t\tfree(r);\n \t}\n@@ -837,6 +838,7 @@ static void filter_promisor_remote(struct repository *repo,\n \tstruct store_info *store_info = NULL;\n \tstruct string_list_item *item;\n \tbool reload_config = false;\n+\tstruct string_list captured_filters = STRING_LIST_INIT_DUP;\n \n \tif (!repo_config_get_string_tmp(the_repository, \"promisor.acceptfromserver\", &accept_str)) {\n \t\tif (!*accept_str || !strcasecmp(\"None\", accept_str))\n@@ -879,6 +881,13 @@ static void filter_promisor_remote(struct repository *repo,\n \t\t\t\treload_config = true;\n \n \t\t\tstrvec_push(accepted, advertised->name);\n+\n+\t\t\t/* Capture advertised filters for accepted remotes */\n+\t\t\tif (advertised->filter) {\n+\t\t\t\tstruct string_list_item *i;\n+\t\t\t\ti = string_list_append(&captured_filters, advertised->name);\n+\t\t\t\ti->util = xstrdup(advertised->filter);\n+\t\t\t}\n \t\t}\n \n \t\tpromisor_info_free(advertised);\n@@ -890,6 +899,25 @@ static void filter_promisor_remote(struct repository *repo,\n \n \tif (reload_config)\n \t\trepo_promisor_remote_reinit(repo);\n+\n+\t/* Apply captured filters to the stable repo state */\n+\tfor_each_string_list_item(item, &captured_filters) {\n+\t\tstruct promisor_remote *r = repo_promisor_remote_find(repo, item->string);\n+\t\tif (r) {\n+\t\t\tfree(r->advertised_filter);\n+\t\t\tr->advertised_filter = item->util;\n+\t\t\titem->util = NULL;\n+\t\t}\n+\t}\n+\n+\tstring_list_clear(&captured_filters, 1);\n+\n+\t/* Mark the remotes as accepted in the repository state */\n+\tfor (size_t i = 0; i < accepted->nr; i++) {\n+\t\tstruct promisor_remote *r = repo_promisor_remote_find(repo, accepted->v[i]);\n+\t\tif (r)\n+\t\t\tr->accepted = 1;\n+\t}\n }\n \n char *promisor_remote_reply(const char *info)\n@@ -935,3 +963,33 @@ void mark_promisor_remotes_as_accepted(struct repository *r, const char *remotes\n \n \tstring_list_clear(&accepted_remotes, 0);\n }\n+\n+char *promisor_remote_construct_filter(struct repository *repo)\n+{\n+\tstruct promisor_remote *r;\n+\tstruct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n+\tstruct strbuf err = STRBUF_INIT;\n+\tchar *result = NULL;\n+\n+\tpromisor_remote_init(repo);\n+\n+\tfor (r = repo->promisor_remote_config->promisors; r; r = r->next) {\n+\t\tif (r->accepted && r->advertised_filter)\n+\t\t\tif (gently_parse_list_objects_filter(&filter_options,\n+\t\t\t\t\t\t\t     r->advertised_filter,\n+\t\t\t\t\t\t\t     &err)) {\n+\t\t\t\twarning(_(\"promisor remote '%s' advertised invalid filter '%s': %s\"),\n+\t\t\t\t\tr->name, r->advertised_filter, err.buf);\n+\t\t\t\tstrbuf_reset(&err);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\t}\n+\n+\tif (filter_options.choice)\n+\t\tresult = xstrdup(expand_list_objects_filter_spec(&filter_options));\n+\n+\tlist_objects_filter_release(&filter_options);\n+\tstrbuf_release(&err);\n+\n+\treturn result;\n+}\ndiff --git a/promisor-remote.h b/promisor-remote.h\nindex 263d331a55..d227299fd0 100644\n--- a/promisor-remote.h\n+++ b/promisor-remote.h\n@@ -15,6 +15,7 @@ struct object_id;\n struct promisor_remote {\n \tstruct promisor_remote *next;\n \tchar *partial_clone_filter;\n+\tchar *advertised_filter;\n \tunsigned int accepted : 1;\n \tconst char name[FLEX_ARRAY];\n };\n@@ -67,4 +68,10 @@ void mark_promisor_remotes_as_accepted(struct repository *repo, const char *remo\n  */\n int repo_has_accepted_promisor_remote(struct repository *r);\n \n+/*\n+ * Use the filters from the accepted remotes to create a combined\n+ * filter (useful in `--filter=auto` mode).\n+ */\n+char *promisor_remote_construct_filter(struct repository *repo);\n+\n #endif /* PROMISOR_REMOTE_H */\n-- \n2.53.0.rc2.10.g12663a1c75.dirty\n\n"},{"id":"535140","messageId":"20260204110818.2919273-9-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260204110818.2919273-1-christian.couder@gmail.com","subject":"[PATCH v2 8/8] fetch-pack: wire up and enable auto filter logic","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-04T11:08:13Z","receivedAt":"2026-02-04T11:08:46Z","isPatch":true,"body":"Previous commits have set up an infrastructure for `--filter=auto` to\nautomatically prepare a partial clone filter based on what the server\nadvertised and the client accepted.\n\nUsing that infrastructure, let's now enable the `--filter=auto` option\nin `git clone` and `git fetch` by setting `allow_auto_filter` to 1.\n\nNote that these small changes mean that when `git clone --filter=auto`\nor `git fetch --filter=auto` are used, \"auto\" is automatically saved\nas the partial clone filter for the server on the client. Therefore\nsubsequent calls to `git fetch` on the client will automatically use\nthis \"auto\" mode even without `--filter=auto`.\n\nLet's also set `allow_auto_filter` to 1 in `transport.c`, as the\ntransport layer must be able to accept the \"auto\" filter spec even if\nthe invoking command hasn't fully parsed it yet.\n\nWhen an \"auto\" filter is requested, let's have the \"fetch-pack.c\" code\nin `do_fetch_pack_v2()` compute a filter and send it to the server.\n\nIn `do_fetch_pack_v2()` the logic also needs to check for the\n\"promisor-remote\" capability and call `promisor_remote_reply()` to\nparse advertised remotes and populate the list of those accepted (and\ntheir filters).\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/fetch-options.adoc      | 19 ++++++---\n Documentation/git-clone.adoc          | 25 ++++++++---\n Documentation/gitprotocol-v2.adoc     | 16 ++++---\n builtin/clone.c                       |  2 +\n builtin/fetch.c                       |  2 +\n fetch-pack.c                          | 28 +++++++++++++\n t/t5710-promisor-remote-capability.sh | 60 +++++++++++++++++++++++++++\n transport.c                           |  1 +\n 8 files changed, 138 insertions(+), 15 deletions(-)\n\ndiff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc\nindex 1ef9807d00..a0cfb50d89 100644\n--- a/Documentation/fetch-options.adoc\n+++ b/Documentation/fetch-options.adoc\n@@ -92,11 +92,20 @@ precedence over the `fetch.output` config option.\n \tUse the partial clone feature and request that the server sends\n \ta subset of reachable objects according to a given object filter.\n \tWhen using `--filter`, the supplied _<filter-spec>_ is used for\n-\tthe partial fetch. For example, `--filter=blob:none` will filter\n-\tout all blobs (file contents) until needed by Git. Also,\n-\t`--filter=blob:limit=<size>` will filter out all blobs of size\n-\tat least _<size>_. For more details on filter specifications, see\n-\tthe `--filter` option in linkgit:git-rev-list[1].\n+\tthe partial fetch.\n++\n+If `--filter=auto` is used, the filter specification is determined\n+automatically by combining the filter specifications advertised by\n+the server for the promisor remotes that the client accepts (see\n+linkgit:gitprotocol-v2[5] and the `promisor.acceptFromServer`\n+configuration option in linkgit:git-config[1]).\n++\n+For details on all other available filter specifications, see the\n+`--filter=<filter-spec>` option in linkgit:git-rev-list[1].\n++\n+For example, `--filter=blob:none` will filter out all blobs (file\n+contents) until needed by Git. Also, `--filter=blob:limit=<size>` will\n+filter out all blobs of size at least _<size>_.\n \n ifndef::git-pull[]\n `--write-fetch-head`::\ndiff --git a/Documentation/git-clone.adoc b/Documentation/git-clone.adoc\nindex 57cdfb7620..0db2d1e5f0 100644\n--- a/Documentation/git-clone.adoc\n+++ b/Documentation/git-clone.adoc\n@@ -187,11 +187,26 @@ objects from the source repository into a pack in the cloned repository.\n \tUse the partial clone feature and request that the server sends\n \ta subset of reachable objects according to a given object filter.\n \tWhen using `--filter`, the supplied _<filter-spec>_ is used for\n-\tthe partial clone filter. For example, `--filter=blob:none` will\n-\tfilter out all blobs (file contents) until needed by Git. Also,\n-\t`--filter=blob:limit=<size>` will filter out all blobs of size\n-\tat least _<size>_. For more details on filter specifications, see\n-\tthe `--filter` option in linkgit:git-rev-list[1].\n+\tthe partial clone filter.\n++\n+If `--filter=auto` is used the filter specification is determined\n+automatically through the 'promisor-remote' protocol (see\n+linkgit:gitprotocol-v2[5]) by combining the filter specifications\n+advertised by the server for the promisor remotes that the client\n+accepts (see the `promisor.acceptFromServer` configuration option in\n+linkgit:git-config[1]). This allows the server to suggest the optimal\n+filter for the available promisor remotes.\n++\n+As with other filter specifications, the \"auto\" value is persisted in\n+the configuration. This ensures that future fetches will continue to\n+adapt to the server's current recommendation.\n++\n+For details on all other available filter specifications, see the\n+`--filter=<filter-spec>` option in linkgit:git-rev-list[1].\n++\n+For example, `--filter=blob:none` will filter out all blobs (file\n+contents) until needed by Git. Also, `--filter=blob:limit=<size>` will\n+filter out all blobs of size at least _<size>_.\n \n `--also-filter-submodules`::\n \tAlso apply the partial clone filter to any submodules in the repository.\ndiff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\nindex d93dd279ea..f985cb4c47 100644\n--- a/Documentation/gitprotocol-v2.adoc\n+++ b/Documentation/gitprotocol-v2.adoc\n@@ -812,10 +812,15 @@ MUST appear first in each pr-fields, in that order.\n After these mandatory fields, the server MAY advertise the following\n optional fields in any order:\n \n-`partialCloneFilter`:: The filter specification used by the remote.\n+`partialCloneFilter`:: The filter specification for the remote. It\n+corresponds to the \"remote.<name>.partialCloneFilter\" config setting.\n Clients can use this to determine if the remote's filtering strategy\n-is compatible with their needs (e.g., checking if both use \"blob:none\").\n-It corresponds to the \"remote.<name>.partialCloneFilter\" config setting.\n+is compatible with their needs (e.g., checking if both use\n+\"blob:none\"). Additionally they can use this through the\n+`--filter=auto` option in linkgit:git-clone[1]. With that option, the\n+filter specification of the clone will be automatically computed by\n+combining the filter specifications of the promisor remotes the client\n+accepts.\n \n `token`:: An authentication token that clients can use when\n connecting to the remote. It corresponds to the \"remote.<name>.token\"\n@@ -828,8 +833,9 @@ future protocol extensions.\n \n The client can use information transmitted through these fields to\n decide if it accepts the advertised promisor remote. Also, the client\n-can be configured to store the values of these fields (see\n-\"promisor.storeFields\" in linkgit:git-config[1]).\n+can be configured to store the values of these fields or use them\n+to automatically configure the repository (see \"promisor.storeFields\"\n+in linkgit:git-config[1] and `--filter=auto` in linkgit:git-clone[1]).\n \n Field values MUST be urlencoded.\n \ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 51f4b5809d..67c7db104f 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -1001,6 +1001,8 @@ int cmd_clone(int argc,\n \t\tNULL\n \t};\n \n+\tfilter_options.allow_auto_filter = 1;\n+\n \tpacket_trace_identity(\"clone\");\n \n \trepo_config(the_repository, git_clone_config, NULL);\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex b984173447..ddc30a0d30 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -2439,6 +2439,8 @@ int cmd_fetch(int argc,\n \t\tOPT_END()\n \t};\n \n+\tfilter_options.allow_auto_filter = 1;\n+\n \tpacket_trace_identity(\"fetch\");\n \n \t/* Record the command line for the reflog */\ndiff --git a/fetch-pack.c b/fetch-pack.c\nindex 40316c9a34..5e9a969e31 100644\n--- a/fetch-pack.c\n+++ b/fetch-pack.c\n@@ -35,6 +35,7 @@\n #include \"sigchain.h\"\n #include \"mergesort.h\"\n #include \"prio-queue.h\"\n+#include \"promisor-remote.h\"\n \n static int transfer_unpack_limit = -1;\n static int fetch_unpack_limit = -1;\n@@ -1661,6 +1662,33 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,\n \tstruct string_list packfile_uris = STRING_LIST_INIT_DUP;\n \tint i;\n \tstruct strvec index_pack_args = STRVEC_INIT;\n+\tconst char *promisor_remote_config;\n+\n+\tif (server_feature_v2(\"promisor-remote\", &promisor_remote_config)) {\n+\t\tchar *remote_name = promisor_remote_reply(promisor_remote_config);\n+\t\tfree(remote_name);\n+\t}\n+\n+\tif (args->filter_options.choice == LOFC_AUTO) {\n+\t\tstruct strbuf errbuf = STRBUF_INIT;\n+\t\tchar *constructed_filter = promisor_remote_construct_filter(r);\n+\n+\t\tlist_objects_filter_release(&args->filter_options);\n+\t\t/* The result of resolving an 'auto' filter must not be 'auto' */\n+\t\targs->filter_options.allow_auto_filter = 0;\n+\n+\t\tif (constructed_filter)\n+\t\t\tgently_parse_list_objects_filter(&args->filter_options,\n+\t\t\t\t\t\t\t constructed_filter,\n+\t\t\t\t\t\t\t &errbuf);\n+\n+\t\tif (errbuf.len > 0)\n+\t\t\tdie(_(\"couldn't resolve 'auto' filter '%s': %s\"),\n+\t\t\t    constructed_filter, errbuf.buf);\n+\n+\t\tfree(constructed_filter);\n+\t\tstrbuf_release(&errbuf);\n+\t}\n \n \tnegotiator = &negotiator_alloc;\n \tif (args->refetch)\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex a726af214a..21543bce20 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -409,6 +409,66 @@ test_expect_success \"clone with promisor.storeFields=partialCloneFilter\" '\n \tcheck_missing_objects server 1 \"$oid\"\n '\n \n+test_expect_success \"clone and fetch with --filter=auto\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client trace\" &&\n+\n+\tgit -C server config remote.lop.partialCloneFilter \"blob:limit=9500\" &&\n+\ttest_config -C server promisor.sendFields \"partialCloneFilter\" &&\n+\n+\tGIT_TRACE_PACKET=\"$(pwd)/trace\" GIT_NO_LAZY_FETCH=0 git clone \\\n+\t\t-c remote.lop.promisor=true \\\n+\t\t-c remote.lop.url=\"file://$(pwd)/lop\" \\\n+\t\t-c promisor.acceptfromserver=All \\\n+\t\t--no-local --filter=auto server client 2>err &&\n+\n+\ttest_grep \"filter blob:limit=9500\" trace &&\n+\ttest_grep ! \"filter auto\" trace &&\n+\n+\t# Verify \"auto\" is persisted in config\n+\techo auto >expected &&\n+\tgit -C client config remote.origin.partialCloneFilter >actual &&\n+\ttest_cmp expected actual &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\" &&\n+\n+\t# Now change the filter on the server\n+\tgit -C server config remote.lop.partialCloneFilter \"blob:limit=5678\" &&\n+\n+\t# Get a new commit on the server to ensure \"git fetch\" actually runs fetch-pack\n+\ttest_commit -C template new-commit &&\n+\tgit -C template push --all \"$(pwd)/server\" &&\n+\n+\t# Perform a fetch WITH --filter=auto\n+\trm -rf trace &&\n+\tGIT_TRACE_PACKET=\"$(pwd)/trace\" git -C client fetch --filter=auto &&\n+\n+\t# Verify that the new filter was used\n+\ttest_grep \"filter blob:limit=5678\" trace &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\" &&\n+\n+\t# Change the filter on the server again\n+\tgit -C server config remote.lop.partialCloneFilter \"blob:limit=5432\" &&\n+\n+\t# Get yet a new commit on the server to ensure fetch-pack runs\n+\ttest_commit -C template yet-a-new-commit &&\n+\tgit -C template push --all \"$(pwd)/server\" &&\n+\n+\t# Perform a fetch WITHOUT --filter=auto\n+\t# Relies on \"auto\" being persisted in the client config\n+\trm -rf trace &&\n+\tGIT_TRACE_PACKET=\"$(pwd)/trace\" git -C client fetch &&\n+\n+\t# Verify that the new filter was used\n+\ttest_grep \"filter blob:limit=5432\" trace &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\"\n+'\n+\n test_expect_success \"clone with promisor.advertise set to 'true' but don't delete the client\" '\n \tgit -C server config promisor.advertise true &&\n \ndiff --git a/transport.c b/transport.c\nindex c7f06a7382..cde8d83a57 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1219,6 +1219,7 @@ struct transport *transport_get(struct remote *remote, const char *url)\n \t\t */\n \t\tstruct git_transport_data *data = xcalloc(1, sizeof(*data));\n \t\tlist_objects_filter_init(&data->options.filter_options);\n+\t\tdata->options.filter_options.allow_auto_filter = 1;\n \t\tret->data = data;\n \t\tret->vtable = &builtin_smart_vtable;\n \t\tret->smart_options = &(data->options);\n-- \n2.53.0.rc2.10.g12663a1c75.dirty\n\n"},{"id":"535142","messageId":"20260204111908.2920406-1-christian.couder@gmail.com","threadId":"64670","inReplyTo":"4702585.LvFx2qVVIh@cayenne","subject":"Re: [PATCH 5/9] doc: fetch: document `--filter=<filter-spec>` option","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-04T11:19:07Z","receivedAt":"2026-02-04T11:19:18Z","isPatch":true,"body":"(Sorry but I cannot find the email send by Jean-Noël in Gmail so I am\nusing `git send-email` instead of Gmail to reply.)\n\nOn Fri, 26 Dec 2025 14:33:38 Jean-Noël AVILA wrote:\nOn Tuesday, 23 December 2025 12:11:09 CET Christian Couder wrote:\n\n> > diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-\n> options.adoc\n> > index fcba46ee9e..70a9818331 100644\n> > --- a/Documentation/fetch-options.adoc\n> > +++ b/Documentation/fetch-options.adoc\n> > @@ -88,6 +88,16 @@ linkgit:git-config[1].\n> >  This is incompatible with `--recurse-submodules=(yes|on-demand)` and takes\n> >  precedence over the `fetch.output` config option.\n> > \n> > +--filter=<filter-spec>::\n> \n> The option itself must also be back-ticked.\n> \n> `--filter=<filter-spec>`::\n\nYeah, I have back-ticked it in v2.\n\n> +\tUse the partial clone feature and request that the server sends\n> +\ta subset of reachable objects according to a given object filter.\n> +\tWhen using `--filter`, the supplied _<filter-spec>_ is used for\n> +\tthe partial fetch. For example, `--filter=blob:none` will filter\n> \n> Isn't this second sentence redundant? What new information is brought?\n\nI agree it's redundant, but I copied it from the `git-clone`\ndocumentation as-is because the goal here is not to improve on the\nexisting documentation but to fix the fact that some documentation is\nmissing.\n\nThat's why the commit message said \"in the same way as it is already\ndocumented for `git clone`\". I have improved the commit message to\nmake the commit goal clearer though.\n\nIf the documentation was wrong, I agree that copying it as-is would\nnot be the right thing to do, but here it's not wrong. And it's better\nto have some docs that are a bit redundant than to miss some docs.\n\nAlso I think it's better to improve on the documentation in a separate\ncommit because this way:\n\n- the `git-clone` documentation could be improved like the `git-fetch`\n  documentation in a single commit (so we get consistent documentation\n  using consistent documentation changes),\n  \n- how to best remove the redundancy is just a separate topic that I\n  prefer to avoid at least for now.\n\nThanks.\n"},{"id":"535757","messageId":"aYxsgL3uMpNlLe8o@pks.im","threadId":"64670","inReplyTo":"CAP8UFD0iBxn6cPFKLAkSW7O3To1ago60MWYwV7YxjxOVxni1Kw@mail.gmail.com","subject":"Re: [PATCH 7/9] list-objects-filter-options: implement auto filter resolution","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-11T11:48:16Z","receivedAt":"2026-02-11T11:48:28Z","isPatch":true,"body":"On Wed, Feb 04, 2026 at 11:29:43AM +0100, Christian Couder wrote:\n> On Wed, Jan 7, 2026 at 11:05 AM Patrick Steinhardt <ps@pks.im> wrote:\n> >\n> > On Tue, Dec 23, 2025 at 12:11:11PM +0100, Christian Couder wrote:\n> > > In a following commit, we will need to aggregate filters from multiple\n> > > accepted promisor remotes into a single filter.\n> >\n> > Ah, interesting. I was always operating under the assumption that when\n> > the server advertises multiple promisors, the client will pick only one\n> > of them. And that made me wonder how the client knows which one to pick\n> > in the first place.\n> >\n> > But of course it's possible to just pick _all_ of them by combining the\n> > filter.\n> \n> Yeah, that's the idea.\n\nOne thought I recently had: if one selects multiple promisor remotes,\nhow does the client know which promisor remote to fetch a certain object\nfrom? We don't always have enough information about a missing object to\nbe able to tell which of the filters would have excluded it, so it's not\npossible to basically \"reverse\" the filtering and deduce from them which\nremote should have them.\n\nPatrick\n"},{"id":"535758","messageId":"aYxsippsLqPnfIQ1@pks.im","threadId":"64670","inReplyTo":"20260204110818.2919273-6-christian.couder@gmail.com","subject":"Re: [PATCH v2 5/8] doc: fetch: document `--filter=<filter-spec>` option","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-11T11:48:26Z","receivedAt":"2026-02-11T11:48:32Z","isPatch":true,"body":"On Wed, Feb 04, 2026 at 12:08:10PM +0100, Christian Couder wrote:\n> The `--filter=<filter-spec>` option is documented in most commands that\n> support it except `git fetch`.\n> \n> Let's fix that and document that option using the same words already\n> used to document it for `git clone`.\n> \n> Those words could probably be improved, but they are not wrong, so\n> let's just use them for now and leave improving them for future work.\n\nHeh, this reads quite funny to me. I prefer the commit message from v1\nmyself, but don't care strongly about this.\n\nPatrick\n"},{"id":"535759","messageId":"aYxslPnqyKP-mgcM@pks.im","threadId":"64670","inReplyTo":"CAP8UFD1za=FowTWBqjanyRFANKBsc-+LOcbSsuBzjeiK8T_fkw@mail.gmail.com","subject":"Re: [PATCH 8/9] promisor-remote: keep advertised filter in memory","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-11T11:48:36Z","receivedAt":"2026-02-11T11:48:42Z","isPatch":true,"body":"On Wed, Feb 04, 2026 at 11:57:42AM +0100, Christian Couder wrote:\n> On Wed, Jan 7, 2026 at 11:05 AM Patrick Steinhardt <ps@pks.im> wrote:\n> >\n> > On Tue, Dec 23, 2025 at 12:11:12PM +0100, Christian Couder wrote:\n> > > diff --git a/promisor-remote.c b/promisor-remote.c\n> > > index 8d6d2d7b76..d5f3223cd0 100644\n> > > --- a/promisor-remote.c\n> > > +++ b/promisor-remote.c\n> > > @@ -837,6 +838,7 @@ static void filter_promisor_remote(struct repository *repo,\n> > >       struct store_info *store_info = NULL;\n> > >       struct string_list_item *item;\n> > >       bool reload_config = false;\n> > > +     struct string_list captured_filters = STRING_LIST_INIT_DUP;\n> > >\n> > >       if (!repo_config_get_string_tmp(the_repository, \"promisor.acceptfromserver\", &accept_str)) {\n> > >               if (!*accept_str || !strcasecmp(\"None\", accept_str))\n> >\n> > Nit: I found the \"captured\" terminology to be somewhat confusing. Can we\n> > maybe rename this to `advertised_filters` to clarify?\n> \n> Well \"advertised_filter\" is already used and I think it might be\n> confusing to use a very similar name, so for now until we find a\n> better name, I kept \"captured\" in v2 even if it's not the best.\n> \n> What about using `server_filters`?\n\nI think that'd work better than \"captured\". But we should probably not\ncall it \"sever\" but \"remote\" instead, so `remote_filters`. I would be\nhappy with such a rename.\n\nAnother alternative would be `accepted_filters` to stress the fact that\nit's not the complete list of filters. I'd be happy with either though.\n\n> > > @@ -935,3 +963,23 @@ void mark_promisor_remotes_as_accepted(struct repository *r, const char *remotes\n> > >\n> > >       string_list_clear(&accepted_remotes, 0);\n> > >  }\n> > > +\n> > > +char *promisor_remote_construct_filter(struct repository *repo)\n> > > +{\n> > > +     struct string_list advertised_filters = STRING_LIST_INIT_NODUP;\n> > > +     struct promisor_remote *r;\n> > > +     char *result;\n> > > +\n> > > +     promisor_remote_init(repo);\n> > > +\n> > > +     for (r = repo->promisor_remote_config->promisors; r; r = r->next) {\n> > > +             if (r->accepted && r->advertised_filter)\n> > > +                     string_list_append(&advertised_filters, r->advertised_filter);\n> >\n> > Would we ever accept a promisor remote that _doesn't_ have an advertised\n> > filter? If not, should we maybe `BUG()` in case the advertised filter\n> > has not been set?\n> \n> I think it should be fine to accept a promisor remote without an\n> advertised filter. The server might prefer to not advertise filters\n> because it thinks that the client should determine the best filter\n> based on the client needs. That's how it works now.\n\nOkay, fair.\n\nPatrick\n"},{"id":"535760","messageId":"aYxsmsQlbm4t2zLY@pks.im","threadId":"64670","inReplyTo":"20260204110818.2919273-9-christian.couder@gmail.com","subject":"Re: [PATCH v2 8/8] fetch-pack: wire up and enable auto filter logic","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-11T11:48:42Z","receivedAt":"2026-02-11T11:48:48Z","isPatch":true,"body":"On Wed, Feb 04, 2026 at 12:08:13PM +0100, Christian Couder wrote:\n> diff --git a/fetch-pack.c b/fetch-pack.c\n> index 40316c9a34..5e9a969e31 100644\n> --- a/fetch-pack.c\n> +++ b/fetch-pack.c\n> @@ -1661,6 +1662,33 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,\n>  \tstruct string_list packfile_uris = STRING_LIST_INIT_DUP;\n>  \tint i;\n>  \tstruct strvec index_pack_args = STRVEC_INIT;\n> +\tconst char *promisor_remote_config;\n> +\n> +\tif (server_feature_v2(\"promisor-remote\", &promisor_remote_config)) {\n> +\t\tchar *remote_name = promisor_remote_reply(promisor_remote_config);\n> +\t\tfree(remote_name);\n> +\t}\n\nHuh. Do we only call this function because it calls\n`filter_promisor_remote()`? We don't seem to care about anything else\nand do some more work to assemble the `remote_name` string that\nultimately ends up being pointless.\n\nMaybe we should instead expose that function?\n\n> +\tif (args->filter_options.choice == LOFC_AUTO) {\n> +\t\tstruct strbuf errbuf = STRBUF_INIT;\n> +\t\tchar *constructed_filter = promisor_remote_construct_filter(r);\n> +\n> +\t\tlist_objects_filter_release(&args->filter_options);\n> +\t\t/* The result of resolving an 'auto' filter must not be 'auto' */\n> +\t\targs->filter_options.allow_auto_filter = 0;\n\nWe didn't resolve though, we only released it. So the commend doesn't\nseem accurate to me anymore.\n\n> +\t\tif (constructed_filter)\n> +\t\t\tgently_parse_list_objects_filter(&args->filter_options,\n> +\t\t\t\t\t\t\t constructed_filter,\n> +\t\t\t\t\t\t\t &errbuf);\n> +\n> +\t\tif (errbuf.len > 0)\n> +\t\t\tdie(_(\"couldn't resolve 'auto' filter '%s': %s\"),\n> +\t\t\t    constructed_filter, errbuf.buf);\n\nI think `gently_parse_list_objects_filter()` already returns non-zero in\nall failure cases, so shouldn't we rather:\n\n\tif (constructed_filter &&\n\t    gently_parse_list_objects_filter(&args->filter_options,\n\t\t\t\t\t     constructed_filter,\n\t\t\t\t\t     &errbuf);\n\t\tdie(_(\"couldn't resolve 'auto' filter '%s': %s\"),\n\t\t    constructed_filter, errbuf.buf);\n\nPatrick\n"},{"id":"535784","messageId":"xmqqwm0jush3.fsf@gitster.g","threadId":"64670","inReplyTo":"aYxslPnqyKP-mgcM@pks.im","subject":"Re: [PATCH 8/9] promisor-remote: keep advertised filter in memory","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-11T16:59:04Z","receivedAt":"2026-02-11T16:59:06Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Another alternative would be `accepted_filters` to stress the fact that\n> it's not the complete list of filters. I'd be happy with either though.\n\nSo advertised is a superset, from which we chose some and becomes accepted?\nSounds very logical to me.\n\n;-)\n"},{"id":"535837","messageId":"CAP8UFD2SdgB7dAa3O-ZwzA0aO-NEFjgWOUgYFKnFkzzPzEyjfQ@mail.gmail.com","threadId":"64670","inReplyTo":"aYxsippsLqPnfIQ1@pks.im","subject":"Re: [PATCH v2 5/8] doc: fetch: document `--filter=<filter-spec>` option","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-12T10:06:54Z","receivedAt":"2026-02-12T10:07:07Z","isPatch":true,"body":"On Wed, Feb 11, 2026 at 12:48 PM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Wed, Feb 04, 2026 at 12:08:10PM +0100, Christian Couder wrote:\n> > The `--filter=<filter-spec>` option is documented in most commands that\n> > support it except `git fetch`.\n> >\n> > Let's fix that and document that option using the same words already\n> > used to document it for `git clone`.\n> >\n> > Those words could probably be improved, but they are not wrong, so\n> > let's just use them for now and leave improving them for future work.\n>\n> Heh, this reads quite funny to me. I prefer the commit message from v1\n> myself, but don't care strongly about this.\n\nYeah, what about the following then:\n\n   The `--filter=<filter-spec>` option is documented in most commands that\n   support it except `git fetch`.\n\n   Let's fix that and document this option. To ensure consistency across\n   commands, let's reuse the exact description currently found in\n   `git clone`.\n"},{"id":"535838","messageId":"CAP8UFD04BTTjXhTz_6HbSZiWZC77k0XfnusaP+V9h_3bzH9=+w@mail.gmail.com","threadId":"64670","inReplyTo":"aYxsgL3uMpNlLe8o@pks.im","subject":"Re: [PATCH 7/9] list-objects-filter-options: implement auto filter resolution","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-12T10:07:13Z","receivedAt":"2026-02-12T10:07:26Z","isPatch":true,"body":"On Wed, Feb 11, 2026 at 12:48 PM Patrick Steinhardt <ps@pks.im> wrote:\n\n> One thought I recently had: if one selects multiple promisor remotes,\n> how does the client know which promisor remote to fetch a certain object\n> from? We don't always have enough information about a missing object to\n> be able to tell which of the filters would have excluded it, so it's not\n> possible to basically \"reverse\" the filtering and deduce from them which\n> remote should have them.\n\nWhen there are multiple promisor remotes, the client will try to fetch\nthe missing objects from the promisor remotes in the order they appear\nin the config file, then it will try the \"main remote\" if it still\ncouldn't fetch some objects.\n\nNote that this isn't changed by this patch series. This is how it\nworks since it has been possible to configure multiple promisor\nremotes. It's also documented in the \"Using many promisor remotes\" of\n\"Documentation/technical/partial-clone.adoc\".\n\nBy the way the doc says \"the long term plan should be to make the\norder somehow fully configurable\" and this is what the \"Implement\npromisor remote fetch ordering\" GSoC 2026 project is about. See:\n\nhttps://git.github.io/SoC-2026-Ideas/\n\n(Thanks to Kaartic Sivaraam who recently submitted the PR to add this\nand other projects to that page.)\n"},{"id":"535839","messageId":"CAP8UFD3XO65TpLyk1B8nCFKqR4b6=zeEAFnq_Cbi8Cwo+1CntQ@mail.gmail.com","threadId":"64670","inReplyTo":"xmqqwm0jush3.fsf@gitster.g","subject":"Re: [PATCH 8/9] promisor-remote: keep advertised filter in memory","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-12T10:07:25Z","receivedAt":"2026-02-12T10:07:37Z","isPatch":true,"body":"On Wed, Feb 11, 2026 at 5:59 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Patrick Steinhardt <ps@pks.im> writes:\n>\n> > Another alternative would be `accepted_filters` to stress the fact that\n> > it's not the complete list of filters. I'd be happy with either though.\n>\n> So advertised is a superset, from which we chose some and becomes accepted?\n> Sounds very logical to me.\n\nFine, `accepted_filters` it is now.\n"},{"id":"535840","messageId":"CAP8UFD3jw0Lz_58ejfwyeE=VOhrZYq67495Gx+7fRKgzakDkkw@mail.gmail.com","threadId":"64670","inReplyTo":"aYxsmsQlbm4t2zLY@pks.im","subject":"Re: [PATCH v2 8/8] fetch-pack: wire up and enable auto filter logic","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-12T10:07:56Z","receivedAt":"2026-02-12T10:08:08Z","isPatch":true,"body":"On Wed, Feb 11, 2026 at 12:48 PM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Wed, Feb 04, 2026 at 12:08:13PM +0100, Christian Couder wrote:\n> > diff --git a/fetch-pack.c b/fetch-pack.c\n> > index 40316c9a34..5e9a969e31 100644\n> > --- a/fetch-pack.c\n> > +++ b/fetch-pack.c\n> > @@ -1661,6 +1662,33 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,\n> >       struct string_list packfile_uris = STRING_LIST_INIT_DUP;\n> >       int i;\n> >       struct strvec index_pack_args = STRVEC_INIT;\n> > +     const char *promisor_remote_config;\n> > +\n> > +     if (server_feature_v2(\"promisor-remote\", &promisor_remote_config)) {\n> > +             char *remote_name = promisor_remote_reply(promisor_remote_config);\n> > +             free(remote_name);\n> > +     }\n>\n> Huh. Do we only call this function because it calls\n> `filter_promisor_remote()`? We don't seem to care about anything else\n> and do some more work to assemble the `remote_name` string that\n> ultimately ends up being pointless.\n>\n> Maybe we should instead expose that function?\n\nYeah, we could expose that function, but then we would discard the\n`struct strvec` that the function requires and populates, so the \"huh\"\nfactor might in some way be even bigger.\n\nI think it would be better to change the signature of\npromisor_remote_reply() to:\n\nvoid promisor_remote_reply(const char *info, char **accepted)\n\nThis way we could pass NULL as the second argument and the function\nwould not assemble a string in that case.\n\n> > +     if (args->filter_options.choice == LOFC_AUTO) {\n> > +             struct strbuf errbuf = STRBUF_INIT;\n> > +             char *constructed_filter = promisor_remote_construct_filter(r);\n> > +\n> > +             list_objects_filter_release(&args->filter_options);\n> > +             /* The result of resolving an 'auto' filter must not be 'auto' */\n> > +             args->filter_options.allow_auto_filter = 0;\n>\n> We didn't resolve though, we only released it. So the commend doesn't\n> seem accurate to me anymore.\n\nWhat the comment wanted to say is that when we are going to resolve an\nauto filter, in gently_parse_list_objects_filter() below, the result\nmust not be 'auto', so we disallow 'auto'.\n\nSo maybe something like /* Disallow 'auto' as a result of the\nresolution of this 'auto' filter below */ ?\n\n> > +             if (constructed_filter)\n> > +                     gently_parse_list_objects_filter(&args->filter_options,\n> > +                                                      constructed_filter,\n> > +                                                      &errbuf);\n> > +\n> > +             if (errbuf.len > 0)\n> > +                     die(_(\"couldn't resolve 'auto' filter '%s': %s\"),\n> > +                         constructed_filter, errbuf.buf);\n>\n> I think `gently_parse_list_objects_filter()` already returns non-zero in\n> all failure cases, so shouldn't we rather:\n>\n>         if (constructed_filter &&\n>             gently_parse_list_objects_filter(&args->filter_options,\n>                                              constructed_filter,\n>                                              &errbuf);\n>                 die(_(\"couldn't resolve 'auto' filter '%s': %s\"),\n>                     constructed_filter, errbuf.buf);\n\nYeah, it might be easier to understand. I will use your suggestion.\n\nThanks.\n"},{"id":"535841","messageId":"20260212100843.883623-1-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260204110818.2919273-1-christian.couder@gmail.com","subject":"[PATCH v3 0/9] Implement `promisor.storeFields` and `--filter=auto`","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-12T10:08:31Z","receivedAt":"2026-02-12T10:09:05Z","isPatch":true,"body":"Introduction\n============\n\nA previous patch series added the possibility to pass additional\nfields, a \"partialCloneFilter\" and a \"token\" for each advertised\npromisor remote, from a server to a client through the\n\"promisor-remote\" capability.\n\nOn the client side though, it has so far only been possible to use\nthis new information to compare it with local information and then\ndecide if the corresponding advertised promisor remote is accepted or\nnot.\n\nFor the \"token\" it would be useful if it could be stored on the\nclient. For example in a setup where the client uses specialized\nremote helpers which need a token to access the promisor remotes\nadvertised by the server, storing the token would allow the token to\nbe used when the client directly accesses a promisor remote for\nexample to lazy fetch some blobs it now needs.\n\nTo enable such a workflow, where the server can rotate tokens and the\nclient can have updated tokens from the server by simply fetching from\nit, the first part of this series introduces a new\n\"promisor.storeFields\" configuration option on the client side,\nsimilar to the \"promisor.checkFields\" configuration option. When field\nnames, \"token\" or \"partialCloneFilter\", are listed in this new\nconfiguration option, then the values of these field names transmitted\nby the server are stored in the local configuration on the client\nside.\n\nNote that for security reasons, the corresponding remote name and url\nof the advertised promisor remotes must have already been configured\non the client side. No new remote name nor url are configured.\n\nFor the \"partialCloneFilter\" field, simply storing the value is not\nenough to enable dynamic updates. Currently, when a user initiates a\npartial clone with `--filter=<filter-spec>`, that specific\n<filter-spec> is saved in the client's local configuration (e.g.,\nremote.origin.partialCloneFilter). Subsequent fetches then reuse this\nvalue, ignoring suggestions from the server.\n\nTo avoid breaking this mechanism and still be able to use the\n<filter-spec> that the server suggests for the promisor remotes that\nthe client accepts, the second part of this series introduces a new\n`--filter=auto` mode for `git clone` and `git fetch`.\n\nWhen `--filter=auto` is used, then \"auto\" is still saved as the\n<filter-spec> for the server locally on the client, and then when a\nfetch-pack happens, instead of passing just \"auto\", the actual filter\nrequested by the client is computed by combining the <filter-spec>s\nthat the server suggested for the promisor remotes that the client\naccepted. This uses the \"combine\" filter mechanism that already exists\nin \"list-objects-filter-options.{c,h}\".\n\nThis way by just using `--filter=auto` when cloning, a client makes\nsure it will use the <filter-spec>s suggested by the server for the\npromisor remotes it accepts.\n\nThis work is part of the \"LOP\" effort documented in:\n\n  Documentation/technical/large-object-promisors.adoc\n\nSee that doc for more information on the broader context.\n\nOverview of the patches\n=======================\n\nPatches 1/9 and 2/9 are the first part of the series and implement the\nnew \"promisor.storeFields\" configuration option. Patch 1/9 is a small\npreparatory refactoring.\n\nPatches from 3/9 to 9/9 implement the `--filter=auto` option:\n\n  - Patches 3/9 and 4/9 are cleanups of \"builtin/clone.c\" and\n    \"builtin/fetch.c\" respectively that make the `filter_options`\n    variable local to cmd_clone() or cmd_fetch().\n\n  - Patch 5/9 is a doc update as `--filter=<filter-spec>` wasn't\n    documented for `git fetch`.\n\n  - Patch 6/9 improves \"list-objects-filter-options.{c,h}\" to\n    support the new 'auto' mode.\n\n  - Patches 7/9 and 8/9 improves \"promisor-remote.{c,h}\" to support\n    the new 'auto' mode.\n\n  - Patch 9/9 make the new 'auto' mode actually work by wiring up\n    everything together.\n\nCI Report\n=========\n\nAll the tests pass, see:\n\nhttps://github.com/chriscool/git/actions/runs/21940309492\n\nChanges since v2\n================\n\nThanks to Patrick Steinhardt, Jean-Noël Avila and Junio Hamano for\nreviewing the previous version!\n\nThe patch series has been rebased on top of current 'master' at\n864f55e190 (The second batch, 2026-02-09) to avoid a small conflict.\n\nIn patch 2/9, new checks have been added to the \"clone with\npromisor.storeFields=partialCloneFilter\" test. We now check that a\nsubsequent fetch can update the configuration.\n\nIn patch 4/9, a small change has been made to the arguments of\n`backfill_tags()` in \"builtin/fetch.c\" to fix a conflict with 'master'.\n\nIn patch 5/9, the commit message has been improved.\n\nIn patch 7/9, `captured_filters` has been renamed `accepted_filters`.\n\nPatch 8/9 is new. It changes the signature of\n`promisor_remote_reply()` and allows this function to not assemble a\nreply string if this is not needed by the caller.\n\nPatch 9/9, has a number of small changes in \"fetch-pack.c\":\n\n  - The call to `promisor_remote_reply()` is simplified a bit as it\n    doesn't require a reply string to be assembled.\n\n  - A comment has been reworded for clarity.\n\n  - The call to `gently_parse_list_objects_filter()` and the check to\n    error out in case it fails have been simplified.\n\nRange diff since v2\n===================\n\n 1:  e19b1518cd =  1:  79255ceba7 promisor-remote: refactor initialising field lists\n 2:  8f20baac17 !  2:  012aa7ef19 promisor-remote: allow a client to store fields\n    @@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with promisor.\n     +\ttest_must_fail git -C client config remote.otherLop.partialCloneFilter >actual &&\n     +\n     +\t# Check that the largest object is still missing on the server\n    -+\tcheck_missing_objects server 1 \"$oid\"\n    ++\tcheck_missing_objects server 1 \"$oid\" &&\n    ++\n    ++\t# Change the configuration on the server and fetch from the client\n    ++\tgit -C server config remote.lop.partialCloneFilter \"blob:limit=7k\" &&\n    ++\tGIT_NO_LAZY_FETCH=0 git -C client fetch \\\n    ++\t\t--filter=\"blob:limit=5k\" ../server 2>err &&\n    ++\n    ++\t# Check that the fetch updated the configuration on the client\n    ++\techo \"blob:limit=7k\" >expected &&\n    ++\tgit -C client config remote.lop.partialCloneFilter >actual &&\n    ++\ttest_cmp expected actual &&\n    ++\n    ++\t# Check that user is notified when the new filter is stored\n    ++\ttest_grep \"Storing new filter from server for remote '\\''lop'\\''\" err &&\n    ++\ttest_grep \"'\\''blob:limit=8k'\\'' -> '\\''blob:limit=7k'\\''\" err\n     +'\n     +\n      test_expect_success \"clone with promisor.advertise set to 'true' but don't delete the client\" '\n 3:  9d53a79600 =  3:  f17a62e73e clone: make filter_options local to cmd_clone()\n 4:  b24907e6dc !  4:  3c6e28dd84 fetch: make filter_options local to cmd_fetch()\n    @@ builtin/fetch.c: static struct transport *prepare_transport(struct remote *remot\n      \t\tset_option(transport, TRANS_OPT_FROM_PROMISOR, \"1\");\n      \t}\n     @@ builtin/fetch.c: static int backfill_tags(struct display_state *display_state,\n    - \t\t\t struct ref_transaction *transaction,\n      \t\t\t struct ref *ref_map,\n      \t\t\t struct fetch_head *fetch_head,\n    --\t\t\t const struct fetch_config *config)\n    -+\t\t\t const struct fetch_config *config,\n    + \t\t\t const struct fetch_config *config,\n    +-\t\t\t struct ref_update_display_info_array *display_array)\n    ++\t\t\t struct ref_update_display_info_array *display_array,\n     +\t\t\t struct list_objects_filter_options *filter_options)\n      {\n      \tint retcode, cannot_reuse;\n    @@ builtin/fetch.c: static int do_fetch(struct transport *transport,\n      \t\t\t * the transaction and don't commit anything.\n      \t\t\t */\n      \t\t\tif (backfill_tags(&display_state, transport, transaction, tags_ref_map,\n    --\t\t\t\t\t  &fetch_head, config))\n    -+\t\t\t\t\t  &fetch_head, config, filter_options))\n    +-\t\t\t\t\t  &fetch_head, config, &display_array))\n    ++\t\t\t\t\t  &fetch_head, config, &display_array, filter_options))\n      \t\t\t\tretcode = 1;\n      \t\t}\n      \n 5:  90fb77360b !  5:  3037d546b2 doc: fetch: document `--filter=<filter-spec>` option\n    @@ Commit message\n         The `--filter=<filter-spec>` option is documented in most commands that\n         support it except `git fetch`.\n     \n    -    Let's fix that and document that option using the same words already\n    -    used to document it for `git clone`.\n    -\n    -    Those words could probably be improved, but they are not wrong, so\n    -    let's just use them for now and leave improving them for future work.\n    +    Let's fix that and document this option. To ensure consistency across\n    +    commands, let's reuse the exact description currently found in\n    +    `git clone`.\n     \n         Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n     \n 6:  b524b24024 =  6:  9ce57b88dc list-objects-filter-options: support 'auto' mode for --filter\n 7:  4ec51ee88f !  7:  37042f7019 promisor-remote: keep advertised filters in memory\n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n      \tstruct store_info *store_info = NULL;\n      \tstruct string_list_item *item;\n      \tbool reload_config = false;\n    -+\tstruct string_list captured_filters = STRING_LIST_INIT_DUP;\n    ++\tstruct string_list accepted_filters = STRING_LIST_INIT_DUP;\n      \n      \tif (!repo_config_get_string_tmp(the_repository, \"promisor.acceptfromserver\", &accept_str)) {\n      \t\tif (!*accept_str || !strcasecmp(\"None\", accept_str))\n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n     +\t\t\t/* Capture advertised filters for accepted remotes */\n     +\t\t\tif (advertised->filter) {\n     +\t\t\t\tstruct string_list_item *i;\n    -+\t\t\t\ti = string_list_append(&captured_filters, advertised->name);\n    ++\t\t\t\ti = string_list_append(&accepted_filters, advertised->name);\n     +\t\t\t\ti->util = xstrdup(advertised->filter);\n     +\t\t\t}\n      \t\t}\n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n      \tif (reload_config)\n      \t\trepo_promisor_remote_reinit(repo);\n     +\n    -+\t/* Apply captured filters to the stable repo state */\n    -+\tfor_each_string_list_item(item, &captured_filters) {\n    ++\t/* Apply accepted remote filters to the stable repo state */\n    ++\tfor_each_string_list_item(item, &accepted_filters) {\n     +\t\tstruct promisor_remote *r = repo_promisor_remote_find(repo, item->string);\n     +\t\tif (r) {\n     +\t\t\tfree(r->advertised_filter);\n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n     +\t\t}\n     +\t}\n     +\n    -+\tstring_list_clear(&captured_filters, 1);\n    ++\tstring_list_clear(&accepted_filters, 1);\n     +\n     +\t/* Mark the remotes as accepted in the repository state */\n     +\tfor (size_t i = 0; i < accepted->nr; i++) {\n -:  ---------- >  8:  dd17069aad promisor-remote: change promisor_remote_reply()'s signature\n 8:  994ecb3317 !  9:  0f9675f477 fetch-pack: wire up and enable auto filter logic\n    @@ fetch-pack.c: static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,\n      \tstruct strvec index_pack_args = STRVEC_INIT;\n     +\tconst char *promisor_remote_config;\n     +\n    -+\tif (server_feature_v2(\"promisor-remote\", &promisor_remote_config)) {\n    -+\t\tchar *remote_name = promisor_remote_reply(promisor_remote_config);\n    -+\t\tfree(remote_name);\n    -+\t}\n    ++\tif (server_feature_v2(\"promisor-remote\", &promisor_remote_config))\n    ++\t\tpromisor_remote_reply(promisor_remote_config, NULL);\n     +\n     +\tif (args->filter_options.choice == LOFC_AUTO) {\n     +\t\tstruct strbuf errbuf = STRBUF_INIT;\n     +\t\tchar *constructed_filter = promisor_remote_construct_filter(r);\n     +\n     +\t\tlist_objects_filter_release(&args->filter_options);\n    -+\t\t/* The result of resolving an 'auto' filter must not be 'auto' */\n    ++\t\t/* Disallow 'auto' as a result of the resolution of this 'auto' filter below */\n     +\t\targs->filter_options.allow_auto_filter = 0;\n     +\n    -+\t\tif (constructed_filter)\n    -+\t\t\tgently_parse_list_objects_filter(&args->filter_options,\n    -+\t\t\t\t\t\t\t constructed_filter,\n    -+\t\t\t\t\t\t\t &errbuf);\n    -+\n    -+\t\tif (errbuf.len > 0)\n    ++\t\tif (constructed_filter &&\n    ++\t\t    gently_parse_list_objects_filter(&args->filter_options,\n    ++\t\t\t\t\t\t     constructed_filter,\n    ++\t\t\t\t\t\t     &errbuf))\n     +\t\t\tdie(_(\"couldn't resolve 'auto' filter '%s': %s\"),\n     +\t\t\t    constructed_filter, errbuf.buf);\n     +\n    @@ fetch-pack.c: static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,\n     \n      ## t/t5710-promisor-remote-capability.sh ##\n     @@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with promisor.storeFields=partialCloneFilter\" '\n    - \tcheck_missing_objects server 1 \"$oid\"\n    + \ttest_grep \"'\\''blob:limit=8k'\\'' -> '\\''blob:limit=7k'\\''\" err\n      '\n      \n     +test_expect_success \"clone and fetch with --filter=auto\" '\n\n\nChristian Couder (9):\n  promisor-remote: refactor initialising field lists\n  promisor-remote: allow a client to store fields\n  clone: make filter_options local to cmd_clone()\n  fetch: make filter_options local to cmd_fetch()\n  doc: fetch: document `--filter=<filter-spec>` option\n  list-objects-filter-options: support 'auto' mode for --filter\n  promisor-remote: keep advertised filters in memory\n  promisor-remote: change promisor_remote_reply()'s signature\n  fetch-pack: wire up and enable auto filter logic\n\n Documentation/config/promisor.adoc           |  33 +++\n Documentation/fetch-options.adoc             |  19 ++\n Documentation/git-clone.adoc                 |  25 +-\n Documentation/gitprotocol-v2.adoc            |  24 +-\n Makefile                                     |   1 +\n builtin/clone.c                              |  18 +-\n builtin/fetch.c                              |  50 ++--\n connect.c                                    |   3 +-\n fetch-pack.c                                 |  24 ++\n list-objects-filter-options.c                |  37 ++-\n list-objects-filter-options.h                |   6 +\n list-objects-filter.c                        |   8 +\n promisor-remote.c                            | 256 +++++++++++++++++--\n promisor-remote.h                            |  17 +-\n t/meson.build                                |   1 +\n t/t5710-promisor-remote-capability.sh        | 123 +++++++++\n t/unit-tests/u-list-objects-filter-options.c |  53 ++++\n transport.c                                  |   1 +\n 18 files changed, 626 insertions(+), 73 deletions(-)\n create mode 100644 t/unit-tests/u-list-objects-filter-options.c\n\n-- \n2.53.0.70.g3d1fd9d397.dirty\n\n"},{"id":"535842","messageId":"20260212100843.883623-2-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260212100843.883623-1-christian.couder@gmail.com","subject":"[PATCH v3 1/9] promisor-remote: refactor initialising field lists","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-12T10:08:32Z","receivedAt":"2026-02-12T10:09:06Z","isPatch":true,"body":"In \"promisor-remote.c\", the fields_sent() and fields_checked()\nfunctions serve similar purposes and contain a small amount of\nduplicated code.\n\nAs we are going to add a similar function in a following commit,\nlet's refactor this common code into a new initialize_fields_list()\nfunction.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 28 ++++++++++++++--------------\n 1 file changed, 14 insertions(+), 14 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 77ebf537e2..5d8151cedb 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -375,18 +375,24 @@ static char *fields_from_config(struct string_list *fields_list, const char *con\n \treturn fields;\n }\n \n+static struct string_list *initialize_fields_list(struct string_list *fields_list, int *initialized,\n+\t\t\t\t\t\t  const char *config_key)\n+{\n+\tif (!*initialized) {\n+\t\tfields_list->cmp = strcasecmp;\n+\t\tfields_from_config(fields_list, config_key);\n+\t\t*initialized = 1;\n+\t}\n+\n+\treturn fields_list;\n+}\n+\n static struct string_list *fields_sent(void)\n {\n \tstatic struct string_list fields_list = STRING_LIST_INIT_NODUP;\n \tstatic int initialized;\n \n-\tif (!initialized) {\n-\t\tfields_list.cmp = strcasecmp;\n-\t\tfields_from_config(&fields_list, \"promisor.sendFields\");\n-\t\tinitialized = 1;\n-\t}\n-\n-\treturn &fields_list;\n+\treturn initialize_fields_list(&fields_list, &initialized, \"promisor.sendFields\");\n }\n \n static struct string_list *fields_checked(void)\n@@ -394,13 +400,7 @@ static struct string_list *fields_checked(void)\n \tstatic struct string_list fields_list = STRING_LIST_INIT_NODUP;\n \tstatic int initialized;\n \n-\tif (!initialized) {\n-\t\tfields_list.cmp = strcasecmp;\n-\t\tfields_from_config(&fields_list, \"promisor.checkFields\");\n-\t\tinitialized = 1;\n-\t}\n-\n-\treturn &fields_list;\n+\treturn initialize_fields_list(&fields_list, &initialized, \"promisor.checkFields\");\n }\n \n /*\n-- \n2.53.0.70.g3d1fd9d397.dirty\n\n"},{"id":"535843","messageId":"20260212100843.883623-3-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260212100843.883623-1-christian.couder@gmail.com","subject":"[PATCH v3 2/9] promisor-remote: allow a client to store fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-12T10:08:33Z","receivedAt":"2026-02-12T10:09:07Z","isPatch":true,"body":"A previous commit allowed a server to pass additional fields through\nthe \"promisor-remote\" protocol capability after the \"name\" and \"url\"\nfields, specifically the \"partialCloneFilter\" and \"token\" fields.\n\nAnother previous commit, c213820c51 (promisor-remote: allow a client\nto check fields, 2025-09-08), has made it possible for a client to\ndecide if it accepts a promisor remote advertised by a server based\non these additional fields.\n\nOften though, it would be interesting for the client to just store in\nits configuration files these additional fields passed by the server,\nso that it can use them when needed.\n\nFor example if a token is necessary to access a promisor remote, that\ntoken could be updated frequently only on the server side and then\npassed to all the clients through the \"promisor-remote\" capability,\navoiding the need to update it on all the clients manually.\n\nStoring the token on the client side makes sure that the token is\navailable when the client needs to access the promisor remotes for a\nlazy fetch.\n\nTo allow this, let's introduce a new \"promisor.storeFields\"\nconfiguration variable.\n\nNote that for a partial clone filter, it's less interesting to have\nit stored on the client. This is because a filter should be used\nright away and we already pass a `--filter=<filter-spec>` option to\n`git clone` when starting a partial clone. Storing the filter could\nperhaps still be interesting for information purposes.\n\nLike \"promisor.checkFields\" and \"promisor.sendFields\", the new\nconfiguration variable should contain a comma or space separated list\nof field names. Only the \"partialCloneFilter\" and \"token\" field names\nare supported for now.\n\nWhen a server advertises a promisor remote, for example \"foo\", along\nwith for example \"token=XXXXX\" to a client, and on the client side\n\"promisor.storeFields\" contains \"token\", then the client will store\nXXXXX for the \"remote.foo.token\" variable in its configuration file\nand reload its configuration so it can immediately use this new\nconfiguration variable.\n\nA message is emitted on stderr to warn users when the config is\nchanged.\n\nNote that even if \"promisor.acceptFromServer\" is set to \"all\", a\npromisor remote has to be already configured on the client side for\nsome of its config to be changed. In any case no new remote is\nconfigured and no new URL is stored.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    |  33 ++++++\n Documentation/gitprotocol-v2.adoc     |  12 ++-\n promisor-remote.c                     | 148 +++++++++++++++++++++++++-\n t/t5710-promisor-remote-capability.sh |  63 +++++++++++\n 4 files changed, 250 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex 93e5e0d9b5..b0fa43b839 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -89,3 +89,36 @@ variable. The fields are checked only if the\n `promisor.acceptFromServer` config variable is not set to \"None\". If\n set to \"None\", this config variable has no effect. See\n linkgit:gitprotocol-v2[5].\n+\n+promisor.storeFields::\n+\tA comma or space separated list of additional remote related\n+\tfield names. If a client accepts an advertised remote, the\n+\tclient will store the values associated with these field names\n+\ttaken from the remote advertisement into its configuration,\n+\tand then reload its remote configuration. Currently,\n+\t\"partialCloneFilter\" and \"token\" are the only supported field\n+\tnames.\n++\n+For example if a server advertises \"partialCloneFilter=blob:limit=20k\"\n+for remote \"foo\", and that remote is accepted, then \"blob:limit=20k\"\n+will be stored for the \"remote.foo.partialCloneFilter\" configuration\n+variable.\n++\n+If the new field value from an advertised remote is the same as the\n+existing field value for that remote on the client side, then no\n+change is made to the client configuration though.\n++\n+When a new value is stored, a message is printed to standard error to\n+let users know about this.\n++\n+Note that for security reasons, if the remote is not already\n+configured on the client side, nothing will be stored for that\n+remote. In any case, no new remote will be created and no URL will be\n+stored.\n++\n+Before storing a partial clone filter, it's parsed to check it's\n+valid. If it's not, a warning is emitted and it's not stored.\n++\n+Before storing a token, a check is performed to ensure it contains no\n+control character. If the check fails, a warning is emitted and it's\n+not stored.\ndiff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\nindex c7db103299..d93dd279ea 100644\n--- a/Documentation/gitprotocol-v2.adoc\n+++ b/Documentation/gitprotocol-v2.adoc\n@@ -826,9 +826,10 @@ are case-sensitive and MUST be transmitted exactly as specified\n above. Clients MUST ignore fields they don't recognize to allow for\n future protocol extensions.\n \n-For now, the client can only use information transmitted through these\n-fields to decide if it accepts the advertised promisor remote. In the\n-future that information might be used for other purposes though.\n+The client can use information transmitted through these fields to\n+decide if it accepts the advertised promisor remote. Also, the client\n+can be configured to store the values of these fields (see\n+\"promisor.storeFields\" in linkgit:git-config[1]).\n \n Field values MUST be urlencoded.\n \n@@ -856,8 +857,9 @@ the server advertised, the client shouldn't advertise the\n On the server side, the \"promisor.advertise\" and \"promisor.sendFields\"\n configuration options can be used to control what it advertises. On\n the client side, the \"promisor.acceptFromServer\" configuration option\n-can be used to control what it accepts. See the documentation of these\n-configuration options for more information.\n+can be used to control what it accepts, and the \"promisor.storeFields\"\n+option, to control what it stores. See the documentation of these\n+configuration options in linkgit:git-config[1] for more information.\n \n Note that in the future it would be nice if the \"promisor-remote\"\n protocol capability could be used by the server, when responding to\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 5d8151cedb..59997dd4c7 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -403,6 +403,14 @@ static struct string_list *fields_checked(void)\n \treturn initialize_fields_list(&fields_list, &initialized, \"promisor.checkFields\");\n }\n \n+static struct string_list *fields_stored(void)\n+{\n+\tstatic struct string_list fields_list = STRING_LIST_INIT_NODUP;\n+\tstatic int initialized;\n+\n+\treturn initialize_fields_list(&fields_list, &initialized, \"promisor.storeFields\");\n+}\n+\n /*\n  * Struct for promisor remotes involved in the \"promisor-remote\"\n  * protocol capability.\n@@ -692,6 +700,132 @@ static struct promisor_info *parse_one_advertised_remote(const char *remote_info\n \treturn info;\n }\n \n+static bool store_one_field(struct repository *repo, const char *remote_name,\n+\t\t\t    const char *field_name, const char *field_key,\n+\t\t\t    const char *advertised, const char *current)\n+{\n+\tif (advertised && (!current || strcmp(current, advertised))) {\n+\t\tchar *key = xstrfmt(\"remote.%s.%s\", remote_name, field_key);\n+\n+\t\tfprintf(stderr, _(\"Storing new %s from server for remote '%s'.\\n\"\n+\t\t\t\t  \"    '%s' -> '%s'\\n\"),\n+\t\t\tfield_name, remote_name,\n+\t\t\tcurrent ? current : \"\",\n+\t\t\tadvertised);\n+\n+\t\trepo_config_set_gently(repo, key, advertised);\n+\t\tfree(key);\n+\n+\t\treturn true;\n+\t}\n+\n+\treturn false;\n+}\n+\n+/* Check that a filter is valid by parsing it */\n+static bool valid_filter(const char *filter, const char *remote_name)\n+{\n+\tstruct list_objects_filter_options filter_opts = LIST_OBJECTS_FILTER_INIT;\n+\tstruct strbuf err = STRBUF_INIT;\n+\tint res = gently_parse_list_objects_filter(&filter_opts, filter, &err);\n+\n+\tif (res)\n+\t\twarning(_(\"invalid filter '%s' for remote '%s' \"\n+\t\t\t  \"will not be stored: %s\"),\n+\t\t\tfilter, remote_name, err.buf);\n+\n+\tlist_objects_filter_release(&filter_opts);\n+\tstrbuf_release(&err);\n+\n+\treturn !res;\n+}\n+\n+/* Check that a token doesn't contain any control character */\n+static bool valid_token(const char *token, const char *remote_name)\n+{\n+\tconst char *c = token;\n+\n+\tfor (; *c; c++)\n+\t\tif (iscntrl(*c)) {\n+\t\t\twarning(_(\"invalid token '%s' for remote '%s' \"\n+\t\t\t\t  \"will not be stored\"),\n+\t\t\t\ttoken, remote_name);\n+\t\t\treturn false;\n+\t\t}\n+\n+\treturn true;\n+}\n+\n+struct store_info {\n+\tstruct repository *repo;\n+\tstruct string_list config_info;\n+\tbool store_filter;\n+\tbool store_token;\n+};\n+\n+static struct store_info *store_info_new(struct repository *repo)\n+{\n+\tstruct string_list *fields_to_store = fields_stored();\n+\tstruct store_info *s = xmalloc(sizeof(*s));\n+\n+\ts->repo = repo;\n+\n+\tstring_list_init_nodup(&s->config_info);\n+\tpromisor_config_info_list(repo, &s->config_info, fields_to_store);\n+\tstring_list_sort(&s->config_info);\n+\n+\ts->store_filter = !!string_list_lookup(fields_to_store, promisor_field_filter);\n+\ts->store_token = !!string_list_lookup(fields_to_store, promisor_field_token);\n+\n+\treturn s;\n+}\n+\n+static void store_info_free(struct store_info *s)\n+{\n+\tif (s) {\n+\t\tpromisor_info_list_clear(&s->config_info);\n+\t\tfree(s);\n+\t}\n+}\n+\n+static bool promisor_store_advertised_fields(struct promisor_info *advertised,\n+\t\t\t\t\t     struct store_info *store_info)\n+{\n+\tstruct promisor_info *p;\n+\tstruct string_list_item *item;\n+\tconst char *remote_name = advertised->name;\n+\tbool reload_config = false;\n+\n+\tif (!(store_info->store_filter || store_info->store_token))\n+\t\treturn false;\n+\n+\t/*\n+\t * Get existing config info for the advertised promisor\n+\t * remote. This ensures the remote is already configured on\n+\t * the client side.\n+\t */\n+\titem = string_list_lookup(&store_info->config_info, remote_name);\n+\n+\tif (!item)\n+\t\treturn false;\n+\n+\tp = item->util;\n+\n+\tif (store_info->store_filter && advertised->filter &&\n+\t    valid_filter(advertised->filter, remote_name))\n+\t\treload_config |= store_one_field(store_info->repo, remote_name,\n+\t\t\t\t\t\t \"filter\", promisor_field_filter,\n+\t\t\t\t\t\t advertised->filter, p->filter);\n+\n+\tif (store_info->store_token && advertised->token &&\n+\t    valid_token(advertised->token, remote_name))\n+\t\treload_config |= store_one_field(store_info->repo, remote_name,\n+\t\t\t\t\t\t \"token\", promisor_field_token,\n+\t\t\t\t\t\t advertised->token, p->token);\n+\n+\treturn reload_config;\n+}\n+\n static void filter_promisor_remote(struct repository *repo,\n \t\t\t\t   struct strvec *accepted,\n \t\t\t\t   const char *info)\n@@ -700,7 +834,9 @@ static void filter_promisor_remote(struct repository *repo,\n \tenum accept_promisor accept = ACCEPT_NONE;\n \tstruct string_list config_info = STRING_LIST_INIT_NODUP;\n \tstruct string_list remote_info = STRING_LIST_INIT_DUP;\n+\tstruct store_info *store_info = NULL;\n \tstruct string_list_item *item;\n+\tbool reload_config = false;\n \n \tif (!repo_config_get_string_tmp(the_repository, \"promisor.acceptfromserver\", &accept_str)) {\n \t\tif (!*accept_str || !strcasecmp(\"None\", accept_str))\n@@ -736,14 +872,24 @@ static void filter_promisor_remote(struct repository *repo,\n \t\t\tstring_list_sort(&config_info);\n \t\t}\n \n-\t\tif (should_accept_remote(accept, advertised, &config_info))\n+\t\tif (should_accept_remote(accept, advertised, &config_info)) {\n+\t\t\tif (!store_info)\n+\t\t\t\tstore_info = store_info_new(repo);\n+\t\t\tif (promisor_store_advertised_fields(advertised, store_info))\n+\t\t\t\treload_config = true;\n+\n \t\t\tstrvec_push(accepted, advertised->name);\n+\t\t}\n \n \t\tpromisor_info_free(advertised);\n \t}\n \n \tpromisor_info_list_clear(&config_info);\n \tstring_list_clear(&remote_info, 0);\n+\tstore_info_free(store_info);\n+\n+\tif (reload_config)\n+\t\trepo_promisor_remote_reinit(repo);\n }\n \n char *promisor_remote_reply(const char *info)\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex 023735d6a8..6ef6431bd7 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -360,6 +360,69 @@ test_expect_success \"clone with promisor.checkFields\" '\n \tcheck_missing_objects server 1 \"$oid\"\n '\n \n+test_expect_success \"clone with promisor.storeFields=partialCloneFilter\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\tgit -C server remote add otherLop \"https://invalid.invalid\"  &&\n+\tgit -C server config remote.otherLop.token \"fooBar\" &&\n+\tgit -C server config remote.otherLop.stuff \"baz\" &&\n+\tgit -C server config remote.otherLop.partialCloneFilter \"blob:limit=10k\" &&\n+\ttest_when_finished \"git -C server remote remove otherLop\" &&\n+\n+\tgit -C server config remote.lop.token \"fooXXX\" &&\n+\tgit -C server config remote.lop.partialCloneFilter \"blob:limit=8k\" &&\n+\n+\ttest_config -C server promisor.sendFields \"partialCloneFilter, token\" &&\n+\ttest_when_finished \"rm trace\" &&\n+\n+\t# Clone from server to create a client\n+\tGIT_TRACE_PACKET=\"$(pwd)/trace\" GIT_NO_LAZY_FETCH=0 git clone \\\n+\t\t-c remote.lop.promisor=true \\\n+\t\t-c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n+\t\t-c remote.lop.url=\"file://$(pwd)/lop\" \\\n+\t\t-c remote.lop.token=\"fooYYY\" \\\n+\t\t-c remote.lop.partialCloneFilter=\"blob:none\" \\\n+\t\t-c promisor.acceptfromserver=All \\\n+\t\t-c promisor.storeFields=partialcloneFilter \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client 2>err &&\n+\n+\t# Check that the filter from the server is stored\n+\techo \"blob:limit=8k\" >expected &&\n+\tgit -C client config remote.lop.partialCloneFilter >actual &&\n+\ttest_cmp expected actual &&\n+\n+\t# Check that user is notified when the filter is stored\n+\ttest_grep \"Storing new filter from server for remote '\\''lop'\\''\" err &&\n+\ttest_grep \"'\\''blob:none'\\'' -> '\\''blob:limit=8k'\\''\" err &&\n+\n+\t# Check that the token from the server is NOT stored\n+\techo \"fooYYY\" >expected &&\n+\tgit -C client config remote.lop.token >actual &&\n+\ttest_cmp expected actual &&\n+\ttest_grep ! \"Storing new token from server\" err &&\n+\n+\t# Check that the filter for an unknown remote is NOT stored\n+\ttest_must_fail git -C client config remote.otherLop.partialCloneFilter >actual &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\" &&\n+\n+\t# Change the configuration on the server and fetch from the client\n+\tgit -C server config remote.lop.partialCloneFilter \"blob:limit=7k\" &&\n+\tGIT_NO_LAZY_FETCH=0 git -C client fetch \\\n+\t\t--filter=\"blob:limit=5k\" ../server 2>err &&\n+\n+\t# Check that the fetch updated the configuration on the client\n+\techo \"blob:limit=7k\" >expected &&\n+\tgit -C client config remote.lop.partialCloneFilter >actual &&\n+\ttest_cmp expected actual &&\n+\n+\t# Check that user is notified when the new filter is stored\n+\ttest_grep \"Storing new filter from server for remote '\\''lop'\\''\" err &&\n+\ttest_grep \"'\\''blob:limit=8k'\\'' -> '\\''blob:limit=7k'\\''\" err\n+'\n+\n test_expect_success \"clone with promisor.advertise set to 'true' but don't delete the client\" '\n \tgit -C server config promisor.advertise true &&\n \n-- \n2.53.0.70.g3d1fd9d397.dirty\n\n"},{"id":"535844","messageId":"20260212100843.883623-4-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260212100843.883623-1-christian.couder@gmail.com","subject":"[PATCH v3 3/9] clone: make filter_options local to cmd_clone()","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-12T10:08:34Z","receivedAt":"2026-02-12T10:09:09Z","isPatch":true,"body":"The `struct list_objects_filter_options filter_options` variable used\nin \"builtin/clone.c\" to store the parsed filters specified by\n`--filter=<filterspec>` is currently a static variable global to the\nfile.\n\nAs we are going to use it more in a following commit, it could become\na bit less easy to understand how it's managed.\n\nTo avoid that, let's make it clear that it's owned by cmd_clone() by\nmoving its definition into that function and making it non-static.\n\nThe only additional change to make this work is to pass it as an\nargument to checkout(). So it's a small quite cheap cleanup anyway.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n builtin/clone.c | 16 +++++++++++-----\n 1 file changed, 11 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex b14a39a687..bb27472020 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -77,7 +77,6 @@ static struct string_list option_required_reference = STRING_LIST_INIT_NODUP;\n static struct string_list option_optional_reference = STRING_LIST_INIT_NODUP;\n static int max_jobs = -1;\n static struct string_list option_recurse_submodules = STRING_LIST_INIT_NODUP;\n-static struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n static int config_filter_submodules = -1;    /* unspecified */\n static int option_remote_submodules;\n \n@@ -634,7 +633,9 @@ static int git_sparse_checkout_init(const char *repo)\n \treturn result;\n }\n \n-static int checkout(int submodule_progress, int filter_submodules,\n+static int checkout(int submodule_progress,\n+\t\t    struct list_objects_filter_options *filter_options,\n+\t\t    int filter_submodules,\n \t\t    enum ref_storage_format ref_storage_format)\n {\n \tstruct object_id oid;\n@@ -723,9 +724,9 @@ static int checkout(int submodule_progress, int filter_submodules,\n \t\t\tstrvec_pushf(&cmd.args, \"--ref-format=%s\",\n \t\t\t\t     ref_storage_format_to_name(ref_storage_format));\n \n-\t\tif (filter_submodules && filter_options.choice)\n+\t\tif (filter_submodules && filter_options->choice)\n \t\t\tstrvec_pushf(&cmd.args, \"--filter=%s\",\n-\t\t\t\t     expand_list_objects_filter_spec(&filter_options));\n+\t\t\t\t     expand_list_objects_filter_spec(filter_options));\n \n \t\tif (option_single_branch >= 0)\n \t\t\tstrvec_push(&cmd.args, option_single_branch ?\n@@ -903,6 +904,7 @@ int cmd_clone(int argc,\n \tenum transport_family family = TRANSPORT_FAMILY_ALL;\n \tstruct string_list option_config = STRING_LIST_INIT_DUP;\n \tint option_dissociate = 0;\n+\tstruct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n \tint option_filter_submodules = -1; /* unspecified */\n \tstruct string_list server_options = STRING_LIST_INIT_NODUP;\n \tconst char *bundle_uri = NULL;\n@@ -1624,9 +1626,13 @@ int cmd_clone(int argc,\n \t\treturn 1;\n \n \tjunk_mode = JUNK_LEAVE_REPO;\n-\terr = checkout(submodule_progress, filter_submodules,\n+\terr = checkout(submodule_progress,\n+\t\t       &filter_options,\n+\t\t       filter_submodules,\n \t\t       ref_storage_format);\n \n+\tlist_objects_filter_release(&filter_options);\n+\n \tstring_list_clear(&option_not, 0);\n \tstring_list_clear(&option_config, 0);\n \tstring_list_clear(&server_options, 0);\n-- \n2.53.0.70.g3d1fd9d397.dirty\n\n"},{"id":"535845","messageId":"20260212100843.883623-5-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260212100843.883623-1-christian.couder@gmail.com","subject":"[PATCH v3 4/9] fetch: make filter_options local to cmd_fetch()","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-12T10:08:35Z","receivedAt":"2026-02-12T10:09:10Z","isPatch":true,"body":"The `struct list_objects_filter_options filter_options` variable used\nin \"builtin/fetch.c\" to store the parsed filters specified by\n`--filter=<filterspec>` is currently a static variable global to the\nfile.\n\nAs we are going to use it more in a following commit, it could become a\nbit less easy to understand how it's managed.\n\nTo avoid that, let's make it clear that it's owned by cmd_fetch() by\nmoving its definition into that function and making it non-static.\n\nThis requires passing a pointer to it through the prepare_transport(),\ndo_fetch(), backfill_tags(), fetch_one_setup_partial(), and fetch_one()\nfunctions, but it's quite straightforward.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n builtin/fetch.c | 48 +++++++++++++++++++++++++++---------------------\n 1 file changed, 27 insertions(+), 21 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex a3bc7e9380..8fbf3557ce 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -97,7 +97,6 @@ static struct strbuf default_rla = STRBUF_INIT;\n static struct transport *gtransport;\n static struct transport *gsecondary;\n static struct refspec refmap = REFSPEC_INIT_FETCH;\n-static struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n static struct string_list server_options = STRING_LIST_INIT_DUP;\n static struct string_list negotiation_tip = STRING_LIST_INIT_NODUP;\n \n@@ -1562,7 +1561,8 @@ static void add_negotiation_tips(struct git_transport_options *smart_options)\n \tsmart_options->negotiation_tips = oids;\n }\n \n-static struct transport *prepare_transport(struct remote *remote, int deepen)\n+static struct transport *prepare_transport(struct remote *remote, int deepen,\n+\t\t\t\t\t   struct list_objects_filter_options *filter_options)\n {\n \tstruct transport *transport;\n \n@@ -1586,9 +1586,9 @@ static struct transport *prepare_transport(struct remote *remote, int deepen)\n \t\tset_option(transport, TRANS_OPT_UPDATE_SHALLOW, \"yes\");\n \tif (refetch)\n \t\tset_option(transport, TRANS_OPT_REFETCH, \"yes\");\n-\tif (filter_options.choice) {\n+\tif (filter_options->choice) {\n \t\tconst char *spec =\n-\t\t\texpand_list_objects_filter_spec(&filter_options);\n+\t\t\texpand_list_objects_filter_spec(filter_options);\n \t\tset_option(transport, TRANS_OPT_LIST_OBJECTS_FILTER, spec);\n \t\tset_option(transport, TRANS_OPT_FROM_PROMISOR, \"1\");\n \t}\n@@ -1607,7 +1607,8 @@ static int backfill_tags(struct display_state *display_state,\n \t\t\t struct ref *ref_map,\n \t\t\t struct fetch_head *fetch_head,\n \t\t\t const struct fetch_config *config,\n-\t\t\t struct ref_update_display_info_array *display_array)\n+\t\t\t struct ref_update_display_info_array *display_array,\n+\t\t\t struct list_objects_filter_options *filter_options)\n {\n \tint retcode, cannot_reuse;\n \n@@ -1621,7 +1622,7 @@ static int backfill_tags(struct display_state *display_state,\n \tcannot_reuse = transport->cannot_reuse ||\n \t\tdeepen_since || deepen_not.nr;\n \tif (cannot_reuse) {\n-\t\tgsecondary = prepare_transport(transport->remote, 0);\n+\t\tgsecondary = prepare_transport(transport->remote, 0, filter_options);\n \t\ttransport = gsecondary;\n \t}\n \n@@ -1834,7 +1835,8 @@ static int commit_ref_transaction(struct ref_transaction **transaction,\n \n static int do_fetch(struct transport *transport,\n \t\t    struct refspec *rs,\n-\t\t    const struct fetch_config *config)\n+\t\t    const struct fetch_config *config,\n+\t\t    struct list_objects_filter_options *filter_options)\n {\n \tstruct ref_transaction *transaction = NULL;\n \tstruct ref *ref_map = NULL;\n@@ -1997,7 +1999,7 @@ static int do_fetch(struct transport *transport,\n \t\t\t * the transaction and don't commit anything.\n \t\t\t */\n \t\t\tif (backfill_tags(&display_state, transport, transaction, tags_ref_map,\n-\t\t\t\t\t  &fetch_head, config, &display_array))\n+\t\t\t\t\t  &fetch_head, config, &display_array, filter_options))\n \t\t\t\tretcode = 1;\n \t\t}\n \n@@ -2339,20 +2341,21 @@ static int fetch_multiple(struct string_list *list, int max_children,\n  * Fetching from the promisor remote should use the given filter-spec\n  * or inherit the default filter-spec from the config.\n  */\n-static inline void fetch_one_setup_partial(struct remote *remote)\n+static inline void fetch_one_setup_partial(struct remote *remote,\n+\t\t\t\t\t   struct list_objects_filter_options *filter_options)\n {\n \t/*\n \t * Explicit --no-filter argument overrides everything, regardless\n \t * of any prior partial clones and fetches.\n \t */\n-\tif (filter_options.no_filter)\n+\tif (filter_options->no_filter)\n \t\treturn;\n \n \t/*\n \t * If no prior partial clone/fetch and the current fetch DID NOT\n \t * request a partial-fetch, do a normal fetch.\n \t */\n-\tif (!repo_has_promisor_remote(the_repository) && !filter_options.choice)\n+\tif (!repo_has_promisor_remote(the_repository) && !filter_options->choice)\n \t\treturn;\n \n \t/*\n@@ -2361,8 +2364,8 @@ static inline void fetch_one_setup_partial(struct remote *remote)\n \t * filter-spec as the default for subsequent fetches to this\n \t * remote if there is currently no default filter-spec.\n \t */\n-\tif (filter_options.choice) {\n-\t\tpartial_clone_register(remote->name, &filter_options);\n+\tif (filter_options->choice) {\n+\t\tpartial_clone_register(remote->name, filter_options);\n \t\treturn;\n \t}\n \n@@ -2371,14 +2374,15 @@ static inline void fetch_one_setup_partial(struct remote *remote)\n \t * explicitly given filter-spec or inherit the filter-spec from\n \t * the config.\n \t */\n-\tif (!filter_options.choice)\n-\t\tpartial_clone_get_default_filter_spec(&filter_options, remote->name);\n+\tif (!filter_options->choice)\n+\t\tpartial_clone_get_default_filter_spec(filter_options, remote->name);\n \treturn;\n }\n \n static int fetch_one(struct remote *remote, int argc, const char **argv,\n \t\t     int prune_tags_ok, int use_stdin_refspecs,\n-\t\t     const struct fetch_config *config)\n+\t\t     const struct fetch_config *config,\n+\t\t     struct list_objects_filter_options *filter_options)\n {\n \tstruct refspec rs = REFSPEC_INIT_FETCH;\n \tint i;\n@@ -2390,7 +2394,7 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,\n \t\tdie(_(\"no remote repository specified; please specify either a URL or a\\n\"\n \t\t      \"remote name from which new revisions should be fetched\"));\n \n-\tgtransport = prepare_transport(remote, 1);\n+\tgtransport = prepare_transport(remote, 1, filter_options);\n \n \tif (prune < 0) {\n \t\t/* no command line request */\n@@ -2445,7 +2449,7 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,\n \tsigchain_push_common(unlock_pack_on_signal);\n \tatexit(unlock_pack_atexit);\n \tsigchain_push(SIGPIPE, SIG_IGN);\n-\texit_code = do_fetch(gtransport, &rs, config);\n+\texit_code = do_fetch(gtransport, &rs, config, filter_options);\n \tsigchain_pop(SIGPIPE);\n \trefspec_clear(&rs);\n \ttransport_disconnect(gtransport);\n@@ -2470,6 +2474,7 @@ int cmd_fetch(int argc,\n \tconst char *submodule_prefix = \"\";\n \tconst char *bundle_uri;\n \tstruct string_list list = STRING_LIST_INIT_DUP;\n+\tstruct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n \tstruct remote *remote = NULL;\n \tint all = -1, multiple = 0;\n \tint result = 0;\n@@ -2735,7 +2740,7 @@ int cmd_fetch(int argc,\n \t\ttrace2_region_enter(\"fetch\", \"negotiate-only\", the_repository);\n \t\tif (!remote)\n \t\t\tdie(_(\"must supply remote when using --negotiate-only\"));\n-\t\tgtransport = prepare_transport(remote, 1);\n+\t\tgtransport = prepare_transport(remote, 1, &filter_options);\n \t\tif (gtransport->smart_options) {\n \t\t\tgtransport->smart_options->acked_commits = &acked_commits;\n \t\t} else {\n@@ -2757,12 +2762,12 @@ int cmd_fetch(int argc,\n \t} else if (remote) {\n \t\tif (filter_options.choice || repo_has_promisor_remote(the_repository)) {\n \t\t\ttrace2_region_enter(\"fetch\", \"setup-partial\", the_repository);\n-\t\t\tfetch_one_setup_partial(remote);\n+\t\t\tfetch_one_setup_partial(remote, &filter_options);\n \t\t\ttrace2_region_leave(\"fetch\", \"setup-partial\", the_repository);\n \t\t}\n \t\ttrace2_region_enter(\"fetch\", \"fetch-one\", the_repository);\n \t\tresult = fetch_one(remote, argc, argv, prune_tags_ok, stdin_refspecs,\n-\t\t\t\t   &config);\n+\t\t\t\t   &config, &filter_options);\n \t\ttrace2_region_leave(\"fetch\", \"fetch-one\", the_repository);\n \t} else {\n \t\tint max_children = max_jobs;\n@@ -2868,5 +2873,6 @@ int cmd_fetch(int argc,\n \n  cleanup:\n \tstring_list_clear(&list, 0);\n+\tlist_objects_filter_release(&filter_options);\n \treturn result;\n }\n-- \n2.53.0.70.g3d1fd9d397.dirty\n\n"},{"id":"535846","messageId":"20260212100843.883623-6-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260212100843.883623-1-christian.couder@gmail.com","subject":"[PATCH v3 5/9] doc: fetch: document `--filter=<filter-spec>` option","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-12T10:08:36Z","receivedAt":"2026-02-12T10:09:11Z","isPatch":true,"body":"The `--filter=<filter-spec>` option is documented in most commands that\nsupport it except `git fetch`.\n\nLet's fix that and document this option. To ensure consistency across\ncommands, let's reuse the exact description currently found in\n`git clone`.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/fetch-options.adoc | 10 ++++++++++\n 1 file changed, 10 insertions(+)\n\ndiff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc\nindex fcba46ee9e..1ef9807d00 100644\n--- a/Documentation/fetch-options.adoc\n+++ b/Documentation/fetch-options.adoc\n@@ -88,6 +88,16 @@ linkgit:git-config[1].\n This is incompatible with `--recurse-submodules=(yes|on-demand)` and takes\n precedence over the `fetch.output` config option.\n \n+`--filter=<filter-spec>`::\n+\tUse the partial clone feature and request that the server sends\n+\ta subset of reachable objects according to a given object filter.\n+\tWhen using `--filter`, the supplied _<filter-spec>_ is used for\n+\tthe partial fetch. For example, `--filter=blob:none` will filter\n+\tout all blobs (file contents) until needed by Git. Also,\n+\t`--filter=blob:limit=<size>` will filter out all blobs of size\n+\tat least _<size>_. For more details on filter specifications, see\n+\tthe `--filter` option in linkgit:git-rev-list[1].\n+\n ifndef::git-pull[]\n `--write-fetch-head`::\n `--no-write-fetch-head`::\n-- \n2.53.0.70.g3d1fd9d397.dirty\n\n"},{"id":"535847","messageId":"20260212100843.883623-7-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260212100843.883623-1-christian.couder@gmail.com","subject":"[PATCH v3 6/9] list-objects-filter-options: support 'auto' mode for --filter","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-12T10:08:37Z","receivedAt":"2026-02-12T10:09:13Z","isPatch":true,"body":"In a following commit, we are going to allow passing \"auto\" as a\n<filterspec> to the `--filter=<filterspec>` option, but only for some\ncommands. Other commands that support the `--filter=<filterspec>`\noption should still die() when 'auto' is passed.\n\nLet's set up the \"list-objects-filter-options.{c,h}\" infrastructure to\nsupport that:\n\n- Add a new `unsigned int allow_auto_filter : 1;` flag to\n  `struct list_objects_filter_options` which specifies if \"auto\" is\n  accepted or not by the current command.\n- Change gently_parse_list_objects_filter() to parse \"auto\" if it's\n  accepted.\n- Make sure we die() if \"auto\" is combined with another filter.\n- Update list_objects_filter_release() to preserve the\n  allow_auto_filter flag, as this function is often called (via\n  opt_parse_list_objects_filter) to reset the struct before parsing a\n  new value.\n\nLet's also update `list-objects-filter.c` to recognize the new\n`LOFC_AUTO` choice. Since \"auto\" must be resolved to a concrete filter\nbefore filtering actually begins, initializing a filter with\n`LOFC_AUTO` is invalid and will trigger a BUG().\n\nNote that ideally combining \"auto\" with \"auto\" could be allowed, but in\npractice, it's probably not worth the added code complexity. And if we\nreally want it, nothing prevents us to allow it in future work.\n\nIf we ever want to give a meaning to combining \"auto\" with a different\nfilter too, nothing prevents us to do that in future work either.\n\nAlso note that the new `allow_auto_filter` flag depends on the command,\nnot user choices, so it should be reset to the command default when\n`struct list_objects_filter_options` instances are reset.\n\nWhile at it, let's add a new \"u-list-objects-filter-options.c\" file for\n`struct list_objects_filter_options` related unit tests. For now it\nonly tests gently_parse_list_objects_filter() though.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Makefile                                     |  1 +\n list-objects-filter-options.c                | 37 ++++++++++++--\n list-objects-filter-options.h                |  6 +++\n list-objects-filter.c                        |  8 +++\n t/meson.build                                |  1 +\n t/unit-tests/u-list-objects-filter-options.c | 53 ++++++++++++++++++++\n 6 files changed, 103 insertions(+), 3 deletions(-)\n create mode 100644 t/unit-tests/u-list-objects-filter-options.c\n\ndiff --git a/Makefile b/Makefile\nindex 4ac44331ea..9e174dd06c 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1518,6 +1518,7 @@ CLAR_TEST_SUITES += u-dir\n CLAR_TEST_SUITES += u-example-decorate\n CLAR_TEST_SUITES += u-hash\n CLAR_TEST_SUITES += u-hashmap\n+CLAR_TEST_SUITES += u-list-objects-filter-options\n CLAR_TEST_SUITES += u-mem-pool\n CLAR_TEST_SUITES += u-oid-array\n CLAR_TEST_SUITES += u-oidmap\ndiff --git a/list-objects-filter-options.c b/list-objects-filter-options.c\nindex 7420bf81fe..ad92cbaa37 100644\n--- a/list-objects-filter-options.c\n+++ b/list-objects-filter-options.c\n@@ -20,6 +20,8 @@ const char *list_object_filter_config_name(enum list_objects_filter_choice c)\n \tcase LOFC_DISABLED:\n \t\t/* we have no name for \"no filter at all\" */\n \t\tbreak;\n+\tcase LOFC_AUTO:\n+\t\treturn \"auto\";\n \tcase LOFC_BLOB_NONE:\n \t\treturn \"blob:none\";\n \tcase LOFC_BLOB_LIMIT:\n@@ -52,7 +54,16 @@ int gently_parse_list_objects_filter(\n \tif (filter_options->choice)\n \t\tBUG(\"filter_options already populated\");\n \n-\tif (!strcmp(arg, \"blob:none\")) {\n+\tif (!strcmp(arg, \"auto\")) {\n+\t\tif (!filter_options->allow_auto_filter) {\n+\t\t\tstrbuf_addstr(errbuf,\n+\t\t\t\t      _(\"'auto' filter not supported by this command\"));\n+\t\t\treturn 1;\n+\t\t}\n+\t\tfilter_options->choice = LOFC_AUTO;\n+\t\treturn 0;\n+\n+\t} else if (!strcmp(arg, \"blob:none\")) {\n \t\tfilter_options->choice = LOFC_BLOB_NONE;\n \t\treturn 0;\n \n@@ -146,10 +157,22 @@ static int parse_combine_subfilter(\n \n \tdecoded = url_percent_decode(subspec->buf);\n \n-\tresult = has_reserved_character(subspec, errbuf) ||\n-\t\tgently_parse_list_objects_filter(\n+\tresult = has_reserved_character(subspec, errbuf);\n+\tif (result)\n+\t\tgoto cleanup;\n+\n+\tresult = gently_parse_list_objects_filter(\n \t\t\t&filter_options->sub[new_index], decoded, errbuf);\n+\tif (result)\n+\t\tgoto cleanup;\n+\n+\tresult = (filter_options->sub[new_index].choice == LOFC_AUTO);\n+\tif (result) {\n+\t\tstrbuf_addstr(errbuf, _(\"an 'auto' filter cannot be combined\"));\n+\t\tgoto cleanup;\n+\t}\n \n+cleanup:\n \tfree(decoded);\n \treturn result;\n }\n@@ -263,6 +286,9 @@ void parse_list_objects_filter(\n \t} else {\n \t\tstruct list_objects_filter_options *sub;\n \n+\t\tif (filter_options->choice == LOFC_AUTO)\n+\t\t\tdie(_(\"an 'auto' filter is incompatible with any other filter\"));\n+\n \t\t/*\n \t\t * Make filter_options an LOFC_COMBINE spec so we can trivially\n \t\t * add subspecs to it.\n@@ -277,6 +303,9 @@ void parse_list_objects_filter(\n \t\tif (gently_parse_list_objects_filter(sub, arg, &errbuf))\n \t\t\tdie(\"%s\", errbuf.buf);\n \n+\t\tif (sub->choice == LOFC_AUTO)\n+\t\t\tdie(_(\"an 'auto' filter is incompatible with any other filter\"));\n+\n \t\tstrbuf_addch(&filter_options->filter_spec, '+');\n \t\tfilter_spec_append_urlencode(filter_options, arg);\n \t}\n@@ -317,6 +346,7 @@ void list_objects_filter_release(\n \tstruct list_objects_filter_options *filter_options)\n {\n \tsize_t sub;\n+\tunsigned int allow_auto_filter = filter_options->allow_auto_filter;\n \n \tif (!filter_options)\n \t\treturn;\n@@ -326,6 +356,7 @@ void list_objects_filter_release(\n \t\tlist_objects_filter_release(&filter_options->sub[sub]);\n \tfree(filter_options->sub);\n \tlist_objects_filter_init(filter_options);\n+\tfilter_options->allow_auto_filter = allow_auto_filter;\n }\n \n void partial_clone_register(\ndiff --git a/list-objects-filter-options.h b/list-objects-filter-options.h\nindex 7b2108b986..77d7bbc846 100644\n--- a/list-objects-filter-options.h\n+++ b/list-objects-filter-options.h\n@@ -18,6 +18,7 @@ enum list_objects_filter_choice {\n \tLOFC_SPARSE_OID,\n \tLOFC_OBJECT_TYPE,\n \tLOFC_COMBINE,\n+\tLOFC_AUTO,\n \tLOFC__COUNT /* must be last */\n };\n \n@@ -50,6 +51,11 @@ struct list_objects_filter_options {\n \t */\n \tunsigned int no_filter : 1;\n \n+\t/*\n+\t * Is LOFC_AUTO a valid option?\n+\t */\n+\tunsigned int allow_auto_filter : 1;\n+\n \t/*\n \t * BEGIN choice-specific parsed values from within the filter-spec. Only\n \t * some values will be defined for any given choice.\ndiff --git a/list-objects-filter.c b/list-objects-filter.c\nindex acd65ebb73..78316e7f90 100644\n--- a/list-objects-filter.c\n+++ b/list-objects-filter.c\n@@ -745,6 +745,13 @@ static void filter_combine__init(\n \tfilter->finalize_omits_fn = filter_combine__finalize_omits;\n }\n \n+static void filter_auto__init(\n+\tstruct list_objects_filter_options *filter_options UNUSED,\n+\tstruct filter *filter UNUSED)\n+{\n+\tBUG(\"LOFC_AUTO should have been resolved before initializing the filter\");\n+}\n+\n typedef void (*filter_init_fn)(\n \tstruct list_objects_filter_options *filter_options,\n \tstruct filter *filter);\n@@ -760,6 +767,7 @@ static filter_init_fn s_filters[] = {\n \tfilter_sparse_oid__init,\n \tfilter_object_type__init,\n \tfilter_combine__init,\n+\tfilter_auto__init,\n };\n \n struct filter *list_objects_filter__init(\ndiff --git a/t/meson.build b/t/meson.build\nindex a04a7a86cf..bec4c72327 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -4,6 +4,7 @@ clar_test_suites = [\n   'unit-tests/u-example-decorate.c',\n   'unit-tests/u-hash.c',\n   'unit-tests/u-hashmap.c',\n+  'unit-tests/u-list-objects-filter-options.c',\n   'unit-tests/u-mem-pool.c',\n   'unit-tests/u-oid-array.c',\n   'unit-tests/u-oidmap.c',\ndiff --git a/t/unit-tests/u-list-objects-filter-options.c b/t/unit-tests/u-list-objects-filter-options.c\nnew file mode 100644\nindex 0000000000..f7d73701b5\n--- /dev/null\n+++ b/t/unit-tests/u-list-objects-filter-options.c\n@@ -0,0 +1,53 @@\n+#include \"unit-test.h\"\n+#include \"list-objects-filter-options.h\"\n+#include \"strbuf.h\"\n+\n+/* Helper to test gently_parse_list_objects_filter() */\n+static void check_gentle_parse(const char *filter_spec,\n+\t\t\t       int expect_success,\n+\t\t\t       int allow_auto,\n+\t\t\t       enum list_objects_filter_choice expected_choice)\n+{\n+\tstruct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n+\tstruct strbuf errbuf = STRBUF_INIT;\n+\tint ret;\n+\n+\tfilter_options.allow_auto_filter = allow_auto;\n+\n+\tret = gently_parse_list_objects_filter(&filter_options, filter_spec, &errbuf);\n+\n+\tif (expect_success) {\n+\t\tcl_assert_equal_i(ret, 0);\n+\t\tcl_assert_equal_i(expected_choice, filter_options.choice);\n+\t\tcl_assert_equal_i(errbuf.len, 0);\n+\t} else {\n+\t\tcl_assert(ret != 0);\n+\t\tcl_assert(errbuf.len > 0);\n+\t}\n+\n+\tstrbuf_release(&errbuf);\n+\tlist_objects_filter_release(&filter_options);\n+}\n+\n+void test_list_objects_filter_options__regular_filters(void)\n+{\n+\tcheck_gentle_parse(\"blob:none\", 1, 0, LOFC_BLOB_NONE);\n+\tcheck_gentle_parse(\"blob:none\", 1, 1, LOFC_BLOB_NONE);\n+\tcheck_gentle_parse(\"blob:limit=5k\", 1, 0, LOFC_BLOB_LIMIT);\n+\tcheck_gentle_parse(\"blob:limit=5k\", 1, 1, LOFC_BLOB_LIMIT);\n+\tcheck_gentle_parse(\"combine:blob:none+tree:0\", 1, 0, LOFC_COMBINE);\n+\tcheck_gentle_parse(\"combine:blob:none+tree:0\", 1, 1, LOFC_COMBINE);\n+}\n+\n+void test_list_objects_filter_options__auto_allowed(void)\n+{\n+\tcheck_gentle_parse(\"auto\", 1, 1, LOFC_AUTO);\n+\tcheck_gentle_parse(\"auto\", 0, 0, 0);\n+}\n+\n+void test_list_objects_filter_options__combine_auto_fails(void)\n+{\n+\tcheck_gentle_parse(\"combine:auto+blob:none\", 0, 1, 0);\n+\tcheck_gentle_parse(\"combine:blob:none+auto\", 0, 1, 0);\n+\tcheck_gentle_parse(\"combine:auto+auto\", 0, 1, 0);\n+}\n-- \n2.53.0.70.g3d1fd9d397.dirty\n\n"},{"id":"535849","messageId":"20260212100843.883623-8-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260212100843.883623-1-christian.couder@gmail.com","subject":"[PATCH v3 7/9] promisor-remote: keep advertised filters in memory","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-12T10:08:38Z","receivedAt":"2026-02-12T10:09:14Z","isPatch":true,"body":"Currently, advertised filters are only kept in memory temporarily\nduring parsing, or persisted to disk if `promisor.storeFields`\ncontains 'partialCloneFilter'.\n\nIn a following commit though, we will add a `--filter=auto` option.\nThis option will enable the client to use the filters that the server\nis suggesting for the promisor remotes the client accepts.\n\nTo use them even if `promisor.storeFields` is not configured, these\nfilters should be stored somewhere for the current session.\n\nLet's add an `advertised_filter` field to `struct promisor_remote`\nfor that purpose.\n\nTo ensure that the filters are available in all cases,\nfilter_promisor_remote() captures them into a temporary list and\napplies them to the `promisor_remote` structs after the potential\nconfiguration reload.\n\nThen the accepted remotes are marked as `accepted` in the repository\nstate. This ensures that subsequent calls to look up accepted remotes\n(like in the filter construction below) actually find them.\n\nIn a following commit, we will add a `--filter=auto` option that will\nenable a client to use the filters suggested by the server for the\npromisor remotes the client accepted.\n\nTo enable the client to construct a filter spec based on these filters,\nlet's also add a `promisor_remote_construct_filter(repo)` function.\n\nThis function:\n\n- iterates over all accepted promisor remotes in the repository,\n- collects the filters advertised for them (using `advertised_filter`\n  added in this commit, and\n- generates a single filter spec for them.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 58 +++++++++++++++++++++++++++++++++++++++++++++++\n promisor-remote.h |  7 ++++++\n 2 files changed, 65 insertions(+)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 59997dd4c7..f3bafb7731 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -193,6 +193,7 @@ void promisor_remote_clear(struct promisor_remote_config *config)\n \twhile (config->promisors) {\n \t\tstruct promisor_remote *r = config->promisors;\n \t\tfree(r->partial_clone_filter);\n+\t\tfree(r->advertised_filter);\n \t\tconfig->promisors = config->promisors->next;\n \t\tfree(r);\n \t}\n@@ -837,6 +838,7 @@ static void filter_promisor_remote(struct repository *repo,\n \tstruct store_info *store_info = NULL;\n \tstruct string_list_item *item;\n \tbool reload_config = false;\n+\tstruct string_list accepted_filters = STRING_LIST_INIT_DUP;\n \n \tif (!repo_config_get_string_tmp(the_repository, \"promisor.acceptfromserver\", &accept_str)) {\n \t\tif (!*accept_str || !strcasecmp(\"None\", accept_str))\n@@ -879,6 +881,13 @@ static void filter_promisor_remote(struct repository *repo,\n \t\t\t\treload_config = true;\n \n \t\t\tstrvec_push(accepted, advertised->name);\n+\n+\t\t\t/* Capture advertised filters for accepted remotes */\n+\t\t\tif (advertised->filter) {\n+\t\t\t\tstruct string_list_item *i;\n+\t\t\t\ti = string_list_append(&accepted_filters, advertised->name);\n+\t\t\t\ti->util = xstrdup(advertised->filter);\n+\t\t\t}\n \t\t}\n \n \t\tpromisor_info_free(advertised);\n@@ -890,6 +899,25 @@ static void filter_promisor_remote(struct repository *repo,\n \n \tif (reload_config)\n \t\trepo_promisor_remote_reinit(repo);\n+\n+\t/* Apply accepted remote filters to the stable repo state */\n+\tfor_each_string_list_item(item, &accepted_filters) {\n+\t\tstruct promisor_remote *r = repo_promisor_remote_find(repo, item->string);\n+\t\tif (r) {\n+\t\t\tfree(r->advertised_filter);\n+\t\t\tr->advertised_filter = item->util;\n+\t\t\titem->util = NULL;\n+\t\t}\n+\t}\n+\n+\tstring_list_clear(&accepted_filters, 1);\n+\n+\t/* Mark the remotes as accepted in the repository state */\n+\tfor (size_t i = 0; i < accepted->nr; i++) {\n+\t\tstruct promisor_remote *r = repo_promisor_remote_find(repo, accepted->v[i]);\n+\t\tif (r)\n+\t\t\tr->accepted = 1;\n+\t}\n }\n \n char *promisor_remote_reply(const char *info)\n@@ -935,3 +963,33 @@ void mark_promisor_remotes_as_accepted(struct repository *r, const char *remotes\n \n \tstring_list_clear(&accepted_remotes, 0);\n }\n+\n+char *promisor_remote_construct_filter(struct repository *repo)\n+{\n+\tstruct promisor_remote *r;\n+\tstruct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n+\tstruct strbuf err = STRBUF_INIT;\n+\tchar *result = NULL;\n+\n+\tpromisor_remote_init(repo);\n+\n+\tfor (r = repo->promisor_remote_config->promisors; r; r = r->next) {\n+\t\tif (r->accepted && r->advertised_filter)\n+\t\t\tif (gently_parse_list_objects_filter(&filter_options,\n+\t\t\t\t\t\t\t     r->advertised_filter,\n+\t\t\t\t\t\t\t     &err)) {\n+\t\t\t\twarning(_(\"promisor remote '%s' advertised invalid filter '%s': %s\"),\n+\t\t\t\t\tr->name, r->advertised_filter, err.buf);\n+\t\t\t\tstrbuf_reset(&err);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\t}\n+\n+\tif (filter_options.choice)\n+\t\tresult = xstrdup(expand_list_objects_filter_spec(&filter_options));\n+\n+\tlist_objects_filter_release(&filter_options);\n+\tstrbuf_release(&err);\n+\n+\treturn result;\n+}\ndiff --git a/promisor-remote.h b/promisor-remote.h\nindex 263d331a55..d227299fd0 100644\n--- a/promisor-remote.h\n+++ b/promisor-remote.h\n@@ -15,6 +15,7 @@ struct object_id;\n struct promisor_remote {\n \tstruct promisor_remote *next;\n \tchar *partial_clone_filter;\n+\tchar *advertised_filter;\n \tunsigned int accepted : 1;\n \tconst char name[FLEX_ARRAY];\n };\n@@ -67,4 +68,10 @@ void mark_promisor_remotes_as_accepted(struct repository *repo, const char *remo\n  */\n int repo_has_accepted_promisor_remote(struct repository *r);\n \n+/*\n+ * Use the filters from the accepted remotes to create a combined\n+ * filter (useful in `--filter=auto` mode).\n+ */\n+char *promisor_remote_construct_filter(struct repository *repo);\n+\n #endif /* PROMISOR_REMOTE_H */\n-- \n2.53.0.70.g3d1fd9d397.dirty\n\n"},{"id":"535848","messageId":"20260212100843.883623-9-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260212100843.883623-1-christian.couder@gmail.com","subject":"[PATCH v3 8/9] promisor-remote: change promisor_remote_reply()'s signature","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-12T10:08:39Z","receivedAt":"2026-02-12T10:09:15Z","isPatch":true,"body":"The `promisor_remote_reply()` function performs two tasks:\n1. It uses filter_promisor_remote() to parse the server's\n   \"promisor-remote\" advertisement and to mark accepted remotes in the\n   repository configuration.\n2. It assembles a reply string containing the accepted remote names to\n   send back to the server.\n\nIn a following commit, the fetch-pack logic will need to trigger the\nside effect (1) to ensure the repository state is correct, but it will\nnot need to send a reply (2).\n\nTo avoid assembling a reply string when it is not needed, let's change\nthe signature of promisor_remote_reply(). It will now return `void` and\naccept a second `char **accepted_out` argument. Only if that argument\nis not NULL will a reply string be assembled and returned back to the\ncaller via that argument.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n connect.c         |  3 ++-\n promisor-remote.c | 24 +++++++++++++-----------\n promisor-remote.h | 10 +++++-----\n 3 files changed, 20 insertions(+), 17 deletions(-)\n\ndiff --git a/connect.c b/connect.c\nindex c6f76e3082..a02583a102 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -505,7 +505,8 @@ static void send_capabilities(int fd_out, struct packet_reader *reader)\n \t\treader->hash_algo = &hash_algos[GIT_HASH_SHA1_LEGACY];\n \t}\n \tif (server_feature_v2(\"promisor-remote\", &promisor_remote_info)) {\n-\t\tchar *reply = promisor_remote_reply(promisor_remote_info);\n+\t\tchar *reply;\n+\t\tpromisor_remote_reply(promisor_remote_info, &reply);\n \t\tif (reply) {\n \t\t\tpacket_write_fmt(fd_out, \"promisor-remote=%s\", reply);\n \t\t\tfree(reply);\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex f3bafb7731..96fa215b06 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -920,25 +920,27 @@ static void filter_promisor_remote(struct repository *repo,\n \t}\n }\n \n-char *promisor_remote_reply(const char *info)\n+void promisor_remote_reply(const char *info, char **accepted_out)\n {\n \tstruct strvec accepted = STRVEC_INIT;\n-\tstruct strbuf reply = STRBUF_INIT;\n \n \tfilter_promisor_remote(the_repository, &accepted, info);\n \n-\tif (!accepted.nr)\n-\t\treturn NULL;\n-\n-\tfor (size_t i = 0; i < accepted.nr; i++) {\n-\t\tif (i)\n-\t\t\tstrbuf_addch(&reply, ';');\n-\t\tstrbuf_addstr_urlencode(&reply, accepted.v[i], allow_unsanitized);\n+\tif (accepted_out) {\n+\t\tif (accepted.nr) {\n+\t\t\tstruct strbuf reply = STRBUF_INIT;\n+\t\t\tfor (size_t i = 0; i < accepted.nr; i++) {\n+\t\t\t\tif (i)\n+\t\t\t\t\tstrbuf_addch(&reply, ';');\n+\t\t\t\tstrbuf_addstr_urlencode(&reply, accepted.v[i], allow_unsanitized);\n+\t\t\t}\n+\t\t\t*accepted_out = strbuf_detach(&reply, NULL);\n+\t\t} else {\n+\t\t\t*accepted_out = NULL;\n+\t\t}\n \t}\n \n \tstrvec_clear(&accepted);\n-\n-\treturn strbuf_detach(&reply, NULL);\n }\n \n void mark_promisor_remotes_as_accepted(struct repository *r, const char *remotes)\ndiff --git a/promisor-remote.h b/promisor-remote.h\nindex d227299fd0..3d4d2de018 100644\n--- a/promisor-remote.h\n+++ b/promisor-remote.h\n@@ -49,12 +49,12 @@ char *promisor_remote_info(struct repository *repo);\n /*\n  * Prepare a reply to a \"promisor-remote\" advertisement from a server.\n  * Check the value of \"promisor.acceptfromserver\" and maybe the\n- * configured promisor remotes, if any, to prepare the reply.\n- * Return value is NULL if no promisor remote from the server\n- * is accepted. Otherwise it contains the names of the accepted promisor\n- * remotes separated by ';'. See gitprotocol-v2(5).\n+ * configured promisor remotes, if any, to prepare the reply. If the\n+ * `accepted_out` argument is not NULL, it is set to either NULL or to\n+ * the names of the accepted promisor remotes separated by ';' if\n+ * any. See gitprotocol-v2(5).\n  */\n-char *promisor_remote_reply(const char *info);\n+void promisor_remote_reply(const char *info, char **accepted_out);\n \n /*\n  * Set the 'accepted' flag for some promisor remotes. Useful on the\n-- \n2.53.0.70.g3d1fd9d397.dirty\n\n"},{"id":"535850","messageId":"20260212100843.883623-10-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260212100843.883623-1-christian.couder@gmail.com","subject":"[PATCH v3 9/9] fetch-pack: wire up and enable auto filter logic","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-12T10:08:40Z","receivedAt":"2026-02-12T10:09:16Z","isPatch":true,"body":"Previous commits have set up an infrastructure for `--filter=auto` to\nautomatically prepare a partial clone filter based on what the server\nadvertised and the client accepted.\n\nUsing that infrastructure, let's now enable the `--filter=auto` option\nin `git clone` and `git fetch` by setting `allow_auto_filter` to 1.\n\nNote that these small changes mean that when `git clone --filter=auto`\nor `git fetch --filter=auto` are used, \"auto\" is automatically saved\nas the partial clone filter for the server on the client. Therefore\nsubsequent calls to `git fetch` on the client will automatically use\nthis \"auto\" mode even without `--filter=auto`.\n\nLet's also set `allow_auto_filter` to 1 in `transport.c`, as the\ntransport layer must be able to accept the \"auto\" filter spec even if\nthe invoking command hasn't fully parsed it yet.\n\nWhen an \"auto\" filter is requested, let's have the \"fetch-pack.c\" code\nin `do_fetch_pack_v2()` compute a filter and send it to the server.\n\nIn `do_fetch_pack_v2()` the logic also needs to check for the\n\"promisor-remote\" capability and call `promisor_remote_reply()` to\nparse advertised remotes and populate the list of those accepted (and\ntheir filters).\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/fetch-options.adoc      | 19 ++++++---\n Documentation/git-clone.adoc          | 25 ++++++++---\n Documentation/gitprotocol-v2.adoc     | 16 ++++---\n builtin/clone.c                       |  2 +\n builtin/fetch.c                       |  2 +\n fetch-pack.c                          | 24 +++++++++++\n t/t5710-promisor-remote-capability.sh | 60 +++++++++++++++++++++++++++\n transport.c                           |  1 +\n 8 files changed, 134 insertions(+), 15 deletions(-)\n\ndiff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc\nindex 1ef9807d00..a0cfb50d89 100644\n--- a/Documentation/fetch-options.adoc\n+++ b/Documentation/fetch-options.adoc\n@@ -92,11 +92,20 @@ precedence over the `fetch.output` config option.\n \tUse the partial clone feature and request that the server sends\n \ta subset of reachable objects according to a given object filter.\n \tWhen using `--filter`, the supplied _<filter-spec>_ is used for\n-\tthe partial fetch. For example, `--filter=blob:none` will filter\n-\tout all blobs (file contents) until needed by Git. Also,\n-\t`--filter=blob:limit=<size>` will filter out all blobs of size\n-\tat least _<size>_. For more details on filter specifications, see\n-\tthe `--filter` option in linkgit:git-rev-list[1].\n+\tthe partial fetch.\n++\n+If `--filter=auto` is used, the filter specification is determined\n+automatically by combining the filter specifications advertised by\n+the server for the promisor remotes that the client accepts (see\n+linkgit:gitprotocol-v2[5] and the `promisor.acceptFromServer`\n+configuration option in linkgit:git-config[1]).\n++\n+For details on all other available filter specifications, see the\n+`--filter=<filter-spec>` option in linkgit:git-rev-list[1].\n++\n+For example, `--filter=blob:none` will filter out all blobs (file\n+contents) until needed by Git. Also, `--filter=blob:limit=<size>` will\n+filter out all blobs of size at least _<size>_.\n \n ifndef::git-pull[]\n `--write-fetch-head`::\ndiff --git a/Documentation/git-clone.adoc b/Documentation/git-clone.adoc\nindex 57cdfb7620..0db2d1e5f0 100644\n--- a/Documentation/git-clone.adoc\n+++ b/Documentation/git-clone.adoc\n@@ -187,11 +187,26 @@ objects from the source repository into a pack in the cloned repository.\n \tUse the partial clone feature and request that the server sends\n \ta subset of reachable objects according to a given object filter.\n \tWhen using `--filter`, the supplied _<filter-spec>_ is used for\n-\tthe partial clone filter. For example, `--filter=blob:none` will\n-\tfilter out all blobs (file contents) until needed by Git. Also,\n-\t`--filter=blob:limit=<size>` will filter out all blobs of size\n-\tat least _<size>_. For more details on filter specifications, see\n-\tthe `--filter` option in linkgit:git-rev-list[1].\n+\tthe partial clone filter.\n++\n+If `--filter=auto` is used the filter specification is determined\n+automatically through the 'promisor-remote' protocol (see\n+linkgit:gitprotocol-v2[5]) by combining the filter specifications\n+advertised by the server for the promisor remotes that the client\n+accepts (see the `promisor.acceptFromServer` configuration option in\n+linkgit:git-config[1]). This allows the server to suggest the optimal\n+filter for the available promisor remotes.\n++\n+As with other filter specifications, the \"auto\" value is persisted in\n+the configuration. This ensures that future fetches will continue to\n+adapt to the server's current recommendation.\n++\n+For details on all other available filter specifications, see the\n+`--filter=<filter-spec>` option in linkgit:git-rev-list[1].\n++\n+For example, `--filter=blob:none` will filter out all blobs (file\n+contents) until needed by Git. Also, `--filter=blob:limit=<size>` will\n+filter out all blobs of size at least _<size>_.\n \n `--also-filter-submodules`::\n \tAlso apply the partial clone filter to any submodules in the repository.\ndiff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\nindex d93dd279ea..f985cb4c47 100644\n--- a/Documentation/gitprotocol-v2.adoc\n+++ b/Documentation/gitprotocol-v2.adoc\n@@ -812,10 +812,15 @@ MUST appear first in each pr-fields, in that order.\n After these mandatory fields, the server MAY advertise the following\n optional fields in any order:\n \n-`partialCloneFilter`:: The filter specification used by the remote.\n+`partialCloneFilter`:: The filter specification for the remote. It\n+corresponds to the \"remote.<name>.partialCloneFilter\" config setting.\n Clients can use this to determine if the remote's filtering strategy\n-is compatible with their needs (e.g., checking if both use \"blob:none\").\n-It corresponds to the \"remote.<name>.partialCloneFilter\" config setting.\n+is compatible with their needs (e.g., checking if both use\n+\"blob:none\"). Additionally they can use this through the\n+`--filter=auto` option in linkgit:git-clone[1]. With that option, the\n+filter specification of the clone will be automatically computed by\n+combining the filter specifications of the promisor remotes the client\n+accepts.\n \n `token`:: An authentication token that clients can use when\n connecting to the remote. It corresponds to the \"remote.<name>.token\"\n@@ -828,8 +833,9 @@ future protocol extensions.\n \n The client can use information transmitted through these fields to\n decide if it accepts the advertised promisor remote. Also, the client\n-can be configured to store the values of these fields (see\n-\"promisor.storeFields\" in linkgit:git-config[1]).\n+can be configured to store the values of these fields or use them\n+to automatically configure the repository (see \"promisor.storeFields\"\n+in linkgit:git-config[1] and `--filter=auto` in linkgit:git-clone[1]).\n \n Field values MUST be urlencoded.\n \ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex bb27472020..45d8fa0eed 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -1001,6 +1001,8 @@ int cmd_clone(int argc,\n \t\tNULL\n \t};\n \n+\tfilter_options.allow_auto_filter = 1;\n+\n \tpacket_trace_identity(\"clone\");\n \n \trepo_config(the_repository, git_clone_config, NULL);\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 8fbf3557ce..573c295241 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -2580,6 +2580,8 @@ int cmd_fetch(int argc,\n \t\tOPT_END()\n \t};\n \n+\tfilter_options.allow_auto_filter = 1;\n+\n \tpacket_trace_identity(\"fetch\");\n \n \t/* Record the command line for the reflog */\ndiff --git a/fetch-pack.c b/fetch-pack.c\nindex 40316c9a34..9f8f980516 100644\n--- a/fetch-pack.c\n+++ b/fetch-pack.c\n@@ -35,6 +35,7 @@\n #include \"sigchain.h\"\n #include \"mergesort.h\"\n #include \"prio-queue.h\"\n+#include \"promisor-remote.h\"\n \n static int transfer_unpack_limit = -1;\n static int fetch_unpack_limit = -1;\n@@ -1661,6 +1662,29 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,\n \tstruct string_list packfile_uris = STRING_LIST_INIT_DUP;\n \tint i;\n \tstruct strvec index_pack_args = STRVEC_INIT;\n+\tconst char *promisor_remote_config;\n+\n+\tif (server_feature_v2(\"promisor-remote\", &promisor_remote_config))\n+\t\tpromisor_remote_reply(promisor_remote_config, NULL);\n+\n+\tif (args->filter_options.choice == LOFC_AUTO) {\n+\t\tstruct strbuf errbuf = STRBUF_INIT;\n+\t\tchar *constructed_filter = promisor_remote_construct_filter(r);\n+\n+\t\tlist_objects_filter_release(&args->filter_options);\n+\t\t/* Disallow 'auto' as a result of the resolution of this 'auto' filter below */\n+\t\targs->filter_options.allow_auto_filter = 0;\n+\n+\t\tif (constructed_filter &&\n+\t\t    gently_parse_list_objects_filter(&args->filter_options,\n+\t\t\t\t\t\t     constructed_filter,\n+\t\t\t\t\t\t     &errbuf))\n+\t\t\tdie(_(\"couldn't resolve 'auto' filter '%s': %s\"),\n+\t\t\t    constructed_filter, errbuf.buf);\n+\n+\t\tfree(constructed_filter);\n+\t\tstrbuf_release(&errbuf);\n+\t}\n \n \tnegotiator = &negotiator_alloc;\n \tif (args->refetch)\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex 6ef6431bd7..532e6f0fea 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -423,6 +423,66 @@ test_expect_success \"clone with promisor.storeFields=partialCloneFilter\" '\n \ttest_grep \"'\\''blob:limit=8k'\\'' -> '\\''blob:limit=7k'\\''\" err\n '\n \n+test_expect_success \"clone and fetch with --filter=auto\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client trace\" &&\n+\n+\tgit -C server config remote.lop.partialCloneFilter \"blob:limit=9500\" &&\n+\ttest_config -C server promisor.sendFields \"partialCloneFilter\" &&\n+\n+\tGIT_TRACE_PACKET=\"$(pwd)/trace\" GIT_NO_LAZY_FETCH=0 git clone \\\n+\t\t-c remote.lop.promisor=true \\\n+\t\t-c remote.lop.url=\"file://$(pwd)/lop\" \\\n+\t\t-c promisor.acceptfromserver=All \\\n+\t\t--no-local --filter=auto server client 2>err &&\n+\n+\ttest_grep \"filter blob:limit=9500\" trace &&\n+\ttest_grep ! \"filter auto\" trace &&\n+\n+\t# Verify \"auto\" is persisted in config\n+\techo auto >expected &&\n+\tgit -C client config remote.origin.partialCloneFilter >actual &&\n+\ttest_cmp expected actual &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\" &&\n+\n+\t# Now change the filter on the server\n+\tgit -C server config remote.lop.partialCloneFilter \"blob:limit=5678\" &&\n+\n+\t# Get a new commit on the server to ensure \"git fetch\" actually runs fetch-pack\n+\ttest_commit -C template new-commit &&\n+\tgit -C template push --all \"$(pwd)/server\" &&\n+\n+\t# Perform a fetch WITH --filter=auto\n+\trm -rf trace &&\n+\tGIT_TRACE_PACKET=\"$(pwd)/trace\" git -C client fetch --filter=auto &&\n+\n+\t# Verify that the new filter was used\n+\ttest_grep \"filter blob:limit=5678\" trace &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\" &&\n+\n+\t# Change the filter on the server again\n+\tgit -C server config remote.lop.partialCloneFilter \"blob:limit=5432\" &&\n+\n+\t# Get yet a new commit on the server to ensure fetch-pack runs\n+\ttest_commit -C template yet-a-new-commit &&\n+\tgit -C template push --all \"$(pwd)/server\" &&\n+\n+\t# Perform a fetch WITHOUT --filter=auto\n+\t# Relies on \"auto\" being persisted in the client config\n+\trm -rf trace &&\n+\tGIT_TRACE_PACKET=\"$(pwd)/trace\" git -C client fetch &&\n+\n+\t# Verify that the new filter was used\n+\ttest_grep \"filter blob:limit=5432\" trace &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\"\n+'\n+\n test_expect_success \"clone with promisor.advertise set to 'true' but don't delete the client\" '\n \tgit -C server config promisor.advertise true &&\n \ndiff --git a/transport.c b/transport.c\nindex c7f06a7382..cde8d83a57 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1219,6 +1219,7 @@ struct transport *transport_get(struct remote *remote, const char *url)\n \t\t */\n \t\tstruct git_transport_data *data = xcalloc(1, sizeof(*data));\n \t\tlist_objects_filter_init(&data->options.filter_options);\n+\t\tdata->options.filter_options.allow_auto_filter = 1;\n \t\tret->data = data;\n \t\tret->vtable = &builtin_smart_vtable;\n \t\tret->smart_options = &(data->options);\n-- \n2.53.0.70.g3d1fd9d397.dirty\n\n"},{"id":"535919","messageId":"aY8KQvozKx70O-aw@pks.im","threadId":"64670","inReplyTo":"20260212100843.883623-9-christian.couder@gmail.com","subject":"Re: [PATCH v3 8/9] promisor-remote: change promisor_remote_reply()'s signature","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-13T11:25:54Z","receivedAt":"2026-02-13T11:26:06Z","isPatch":true,"body":"On Thu, Feb 12, 2026 at 11:08:39AM +0100, Christian Couder wrote:\n> diff --git a/promisor-remote.c b/promisor-remote.c\n> index f3bafb7731..96fa215b06 100644\n> --- a/promisor-remote.c\n> +++ b/promisor-remote.c\n> @@ -920,25 +920,27 @@ static void filter_promisor_remote(struct repository *repo,\n>  \t}\n>  }\n>  \n> -char *promisor_remote_reply(const char *info)\n> +void promisor_remote_reply(const char *info, char **accepted_out)\n>  {\n>  \tstruct strvec accepted = STRVEC_INIT;\n> -\tstruct strbuf reply = STRBUF_INIT;\n>  \n>  \tfilter_promisor_remote(the_repository, &accepted, info);\n>  \n> -\tif (!accepted.nr)\n> -\t\treturn NULL;\n> -\n> -\tfor (size_t i = 0; i < accepted.nr; i++) {\n> -\t\tif (i)\n> -\t\t\tstrbuf_addch(&reply, ';');\n> -\t\tstrbuf_addstr_urlencode(&reply, accepted.v[i], allow_unsanitized);\n> +\tif (accepted_out) {\n> +\t\tif (accepted.nr) {\n> +\t\t\tstruct strbuf reply = STRBUF_INIT;\n> +\t\t\tfor (size_t i = 0; i < accepted.nr; i++) {\n> +\t\t\t\tif (i)\n> +\t\t\t\t\tstrbuf_addch(&reply, ';');\n> +\t\t\t\tstrbuf_addstr_urlencode(&reply, accepted.v[i], allow_unsanitized);\n> +\t\t\t}\n> +\t\t\t*accepted_out = strbuf_detach(&reply, NULL);\n> +\t\t} else {\n> +\t\t\t*accepted_out = NULL;\n> +\t\t}\n>  \t}\n>  \n>  \tstrvec_clear(&accepted);\n> -\n> -\treturn strbuf_detach(&reply, NULL);\n>  }\n\nOkay, makes sense. This directly addresses my comment on v2 that it's\nkind of weird that we do all of this only to discard the result in the\nnext commit.\n\nPatrick\n"},{"id":"535920","messageId":"aY8KTOrCbm7noIVw@pks.im","threadId":"64670","inReplyTo":"20260212100843.883623-10-christian.couder@gmail.com","subject":"Re: [PATCH v3 9/9] fetch-pack: wire up and enable auto filter logic","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-13T11:26:04Z","receivedAt":"2026-02-13T11:26:09Z","isPatch":true,"body":"On Thu, Feb 12, 2026 at 11:08:40AM +0100, Christian Couder wrote:\n> diff --git a/fetch-pack.c b/fetch-pack.c\n> index 40316c9a34..9f8f980516 100644\n> --- a/fetch-pack.c\n> +++ b/fetch-pack.c\n> @@ -1661,6 +1662,29 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,\n>  \tstruct string_list packfile_uris = STRING_LIST_INIT_DUP;\n>  \tint i;\n>  \tstruct strvec index_pack_args = STRVEC_INIT;\n> +\tconst char *promisor_remote_config;\n> +\n> +\tif (server_feature_v2(\"promisor-remote\", &promisor_remote_config))\n> +\t\tpromisor_remote_reply(promisor_remote_config, NULL);\n\nAnd here we now pass a `NULL` pointer so that we don't have to free the\nresult that we didn't want to have in the first place. Good.\n\nPatrick\n"},{"id":"535921","messageId":"aY8KUXRdzniPuiNu@pks.im","threadId":"64670","inReplyTo":"20260212100843.883623-1-christian.couder@gmail.com","subject":"Re: [PATCH v3 0/9] Implement `promisor.storeFields` and `--filter=auto`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-13T11:26:09Z","receivedAt":"2026-02-13T11:26:14Z","isPatch":true,"body":"On Thu, Feb 12, 2026 at 11:08:31AM +0100, Christian Couder wrote:\n> Changes since v2\n> ================\n> \n> Thanks to Patrick Steinhardt, Jean-Noël Avila and Junio Hamano for\n> reviewing the previous version!\n> \n> The patch series has been rebased on top of current 'master' at\n> 864f55e190 (The second batch, 2026-02-09) to avoid a small conflict.\n> \n> In patch 2/9, new checks have been added to the \"clone with\n> promisor.storeFields=partialCloneFilter\" test. We now check that a\n> subsequent fetch can update the configuration.\n> \n> In patch 4/9, a small change has been made to the arguments of\n> `backfill_tags()` in \"builtin/fetch.c\" to fix a conflict with 'master'.\n> \n> In patch 5/9, the commit message has been improved.\n> \n> In patch 7/9, `captured_filters` has been renamed `accepted_filters`.\n> \n> Patch 8/9 is new. It changes the signature of\n> `promisor_remote_reply()` and allows this function to not assemble a\n> reply string if this is not needed by the caller.\n> \n> Patch 9/9, has a number of small changes in \"fetch-pack.c\":\n> \n>   - The call to `promisor_remote_reply()` is simplified a bit as it\n>     doesn't require a reply string to be assembled.\n> \n>   - A comment has been reworded for clarity.\n> \n>   - The call to `gently_parse_list_objects_filter()` and the check to\n>     error out in case it fails have been simplified.\n\nAll of these changes look good to me, thanks!\n\nPatrick\n"},{"id":"535998","messageId":"20260214023509.GA3684377@coredump.intra.peff.net","threadId":"64670","inReplyTo":"20260212100843.883623-7-christian.couder@gmail.com","subject":"Re: [PATCH v3 6/9] list-objects-filter-options: support 'auto' mode for --filter","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-02-14T02:35:09Z","receivedAt":"2026-02-14T02:41:52Z","isPatch":true,"body":"On Thu, Feb 12, 2026 at 11:08:37AM +0100, Christian Couder wrote:\n\n> @@ -317,6 +346,7 @@ void list_objects_filter_release(\n>  \tstruct list_objects_filter_options *filter_options)\n>  {\n>  \tsize_t sub;\n> +\tunsigned int allow_auto_filter = filter_options->allow_auto_filter;\n>  \n>  \tif (!filter_options)\n>  \t\treturn;\n\nThis will segfault if anybody passes in a NULL filter_options, before we\nget to the NULL check in the context.\n\nI don't think anybody does this in practice, but probably we should\neither remove the NULL check, or you should push the assignment of your\nlocal variable down below it.\n\n(Noticed by Coverity).\n\n-Peff\n"},{"id":"536096","messageId":"20260216132317.15894-1-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260212100843.883623-1-christian.couder@gmail.com","subject":"[PATCH v4 0/9] Implement `promisor.storeFields` and `--filter=auto`","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-16T13:23:06Z","receivedAt":"2026-02-16T13:23:36Z","isPatch":true,"body":"Introduction\n============\n\nA previous patch series added the possibility to pass additional\nfields, a \"partialCloneFilter\" and a \"token\" for each advertised\npromisor remote, from a server to a client through the\n\"promisor-remote\" capability.\n\nOn the client side though, it has so far only been possible to use\nthis new information to compare it with local information and then\ndecide if the corresponding advertised promisor remote is accepted or\nnot.\n\nFor the \"token\" it would be useful if it could be stored on the\nclient. For example in a setup where the client uses specialized\nremote helpers which need a token to access the promisor remotes\nadvertised by the server, storing the token would allow the token to\nbe used when the client directly accesses a promisor remote for\nexample to lazy fetch some blobs it now needs.\n\nTo enable such a workflow, where the server can rotate tokens and the\nclient can have updated tokens from the server by simply fetching from\nit, the first part of this series introduces a new\n\"promisor.storeFields\" configuration option on the client side,\nsimilar to the \"promisor.checkFields\" configuration option. When field\nnames, \"token\" or \"partialCloneFilter\", are listed in this new\nconfiguration option, then the values of these field names transmitted\nby the server are stored in the local configuration on the client\nside.\n\nNote that for security reasons, the corresponding remote name and url\nof the advertised promisor remotes must have already been configured\non the client side. No new remote name nor url are configured.\n\nFor the \"partialCloneFilter\" field, simply storing the value is not\nenough to enable dynamic updates. Currently, when a user initiates a\npartial clone with `--filter=<filter-spec>`, that specific\n<filter-spec> is saved in the client's local configuration (e.g.,\nremote.origin.partialCloneFilter). Subsequent fetches then reuse this\nvalue, ignoring suggestions from the server.\n\nTo avoid breaking this mechanism and still be able to use the\n<filter-spec> that the server suggests for the promisor remotes that\nthe client accepts, the second part of this series introduces a new\n`--filter=auto` mode for `git clone` and `git fetch`.\n\nWhen `--filter=auto` is used, then \"auto\" is still saved as the\n<filter-spec> for the server locally on the client, and then when a\nfetch-pack happens, instead of passing just \"auto\", the actual filter\nrequested by the client is computed by combining the <filter-spec>s\nthat the server suggested for the promisor remotes that the client\naccepted. This uses the \"combine\" filter mechanism that already exists\nin \"list-objects-filter-options.{c,h}\".\n\nThis way by just using `--filter=auto` when cloning, a client makes\nsure it will use the <filter-spec>s suggested by the server for the\npromisor remotes it accepts.\n\nThis work is part of the \"LOP\" effort documented in:\n\n  Documentation/technical/large-object-promisors.adoc\n\nSee that doc for more information on the broader context.\n\nOverview of the patches\n=======================\n\nPatches 1/9 and 2/9 are the first part of the series and implement the\nnew \"promisor.storeFields\" configuration option. Patch 1/9 is a small\npreparatory refactoring.\n\nPatches from 3/9 to 9/9 implement the `--filter=auto` option:\n\n  - Patches 3/9 and 4/9 are cleanups of \"builtin/clone.c\" and\n    \"builtin/fetch.c\" respectively that make the `filter_options`\n    variable local to cmd_clone() or cmd_fetch().\n\n  - Patch 5/9 is a doc update as `--filter=<filter-spec>` wasn't\n    documented for `git fetch`.\n\n  - Patch 6/9 improves \"list-objects-filter-options.{c,h}\" to\n    support the new 'auto' mode.\n\n  - Patches 7/9 and 8/9 improves \"promisor-remote.{c,h}\" to support\n    the new 'auto' mode.\n\n  - Patch 9/9 make the new 'auto' mode actually work by wiring up\n    everything together.\n\nCI Report\n=========\n\nAll the tests pass, see:\n\nhttps://github.com/chriscool/git/actions/runs/22059799525\n\nChanges since v3\n================\n\nThanks to Patrick Steinhardt, Jean-Noël Avila, Peff and Junio Hamano\nfor reviewing or commenting on the previous version!\n\nThe only change compared to v3 is in patch 6/9 where in\n\"list-objects-filter-options.c\" the `allow_auto_filter` variable in\n`list_objects_filter_release()` is now initialized after the check to\nreturn if `filter_options` is NULL instead of before that check.\n\nRange diff since v3\n===================\n\n 1:  79255ceba7 =  1:  79255ceba7 promisor-remote: refactor initialising field lists\n 2:  012aa7ef19 =  2:  012aa7ef19 promisor-remote: allow a client to store fields\n 3:  f17a62e73e =  3:  f17a62e73e clone: make filter_options local to cmd_clone()\n 4:  3c6e28dd84 =  4:  3c6e28dd84 fetch: make filter_options local to cmd_fetch()\n 5:  3037d546b2 =  5:  3037d546b2 doc: fetch: document `--filter=<filter-spec>` option\n 6:  9ce57b88dc !  6:  366c93e836 list-objects-filter-options: support 'auto' mode for --filter\n    @@ list-objects-filter-options.c: void list_objects_filter_release(\n        struct list_objects_filter_options *filter_options)\n      {\n        size_t sub;\n    -+  unsigned int allow_auto_filter = filter_options->allow_auto_filter;\n    ++  unsigned int allow_auto_filter;\n      \n        if (!filter_options)\n                return;\n    -@@ list-objects-filter-options.c: void list_objects_filter_release(\n    ++\n    ++  allow_auto_filter = filter_options->allow_auto_filter;\n    +   strbuf_release(&filter_options->filter_spec);\n    +   free(filter_options->sparse_oid_name);\n    +   for (sub = 0; sub < filter_options->sub_nr; sub++)\n                list_objects_filter_release(&filter_options->sub[sub]);\n        free(filter_options->sub);\n        list_objects_filter_init(filter_options);\n 7:  37042f7019 =  7:  2eb3b9cddd promisor-remote: keep advertised filters in memory\n 8:  dd17069aad =  8:  fae3e9089d promisor-remote: change promisor_remote_reply()'s signature\n 9:  0f9675f477 =  9:  4627d513d6 fetch-pack: wire up and enable auto filter logic\n\n\nChristian Couder (9):\n  promisor-remote: refactor initialising field lists\n  promisor-remote: allow a client to store fields\n  clone: make filter_options local to cmd_clone()\n  fetch: make filter_options local to cmd_fetch()\n  doc: fetch: document `--filter=<filter-spec>` option\n  list-objects-filter-options: support 'auto' mode for --filter\n  promisor-remote: keep advertised filters in memory\n  promisor-remote: change promisor_remote_reply()'s signature\n  fetch-pack: wire up and enable auto filter logic\n\n Documentation/config/promisor.adoc           |  33 +++\n Documentation/fetch-options.adoc             |  19 ++\n Documentation/git-clone.adoc                 |  25 +-\n Documentation/gitprotocol-v2.adoc            |  24 +-\n Makefile                                     |   1 +\n builtin/clone.c                              |  18 +-\n builtin/fetch.c                              |  50 ++--\n connect.c                                    |   3 +-\n fetch-pack.c                                 |  24 ++\n list-objects-filter-options.c                |  39 ++-\n list-objects-filter-options.h                |   6 +\n list-objects-filter.c                        |   8 +\n promisor-remote.c                            | 256 +++++++++++++++++--\n promisor-remote.h                            |  17 +-\n t/meson.build                                |   1 +\n t/t5710-promisor-remote-capability.sh        | 123 +++++++++\n t/unit-tests/u-list-objects-filter-options.c |  53 ++++\n transport.c                                  |   1 +\n 18 files changed, 628 insertions(+), 73 deletions(-)\n create mode 100644 t/unit-tests/u-list-objects-filter-options.c\n\n-- \n2.53.0.77.g4627d513d6\n\n"},{"id":"536097","messageId":"20260216132317.15894-2-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260216132317.15894-1-christian.couder@gmail.com","subject":"[PATCH v4 1/9] promisor-remote: refactor initialising field lists","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-16T13:23:07Z","receivedAt":"2026-02-16T13:23:37Z","isPatch":true,"body":"In \"promisor-remote.c\", the fields_sent() and fields_checked()\nfunctions serve similar purposes and contain a small amount of\nduplicated code.\n\nAs we are going to add a similar function in a following commit,\nlet's refactor this common code into a new initialize_fields_list()\nfunction.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 28 ++++++++++++++--------------\n 1 file changed, 14 insertions(+), 14 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 77ebf537e2..5d8151cedb 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -375,18 +375,24 @@ static char *fields_from_config(struct string_list *fields_list, const char *con\n \treturn fields;\n }\n \n+static struct string_list *initialize_fields_list(struct string_list *fields_list, int *initialized,\n+\t\t\t\t\t\t  const char *config_key)\n+{\n+\tif (!*initialized) {\n+\t\tfields_list->cmp = strcasecmp;\n+\t\tfields_from_config(fields_list, config_key);\n+\t\t*initialized = 1;\n+\t}\n+\n+\treturn fields_list;\n+}\n+\n static struct string_list *fields_sent(void)\n {\n \tstatic struct string_list fields_list = STRING_LIST_INIT_NODUP;\n \tstatic int initialized;\n \n-\tif (!initialized) {\n-\t\tfields_list.cmp = strcasecmp;\n-\t\tfields_from_config(&fields_list, \"promisor.sendFields\");\n-\t\tinitialized = 1;\n-\t}\n-\n-\treturn &fields_list;\n+\treturn initialize_fields_list(&fields_list, &initialized, \"promisor.sendFields\");\n }\n \n static struct string_list *fields_checked(void)\n@@ -394,13 +400,7 @@ static struct string_list *fields_checked(void)\n \tstatic struct string_list fields_list = STRING_LIST_INIT_NODUP;\n \tstatic int initialized;\n \n-\tif (!initialized) {\n-\t\tfields_list.cmp = strcasecmp;\n-\t\tfields_from_config(&fields_list, \"promisor.checkFields\");\n-\t\tinitialized = 1;\n-\t}\n-\n-\treturn &fields_list;\n+\treturn initialize_fields_list(&fields_list, &initialized, \"promisor.checkFields\");\n }\n \n /*\n-- \n2.53.0.77.g4627d513d6\n\n"},{"id":"536098","messageId":"20260216132317.15894-3-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260216132317.15894-1-christian.couder@gmail.com","subject":"[PATCH v4 2/9] promisor-remote: allow a client to store fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-16T13:23:08Z","receivedAt":"2026-02-16T13:23:39Z","isPatch":true,"body":"A previous commit allowed a server to pass additional fields through\nthe \"promisor-remote\" protocol capability after the \"name\" and \"url\"\nfields, specifically the \"partialCloneFilter\" and \"token\" fields.\n\nAnother previous commit, c213820c51 (promisor-remote: allow a client\nto check fields, 2025-09-08), has made it possible for a client to\ndecide if it accepts a promisor remote advertised by a server based\non these additional fields.\n\nOften though, it would be interesting for the client to just store in\nits configuration files these additional fields passed by the server,\nso that it can use them when needed.\n\nFor example if a token is necessary to access a promisor remote, that\ntoken could be updated frequently only on the server side and then\npassed to all the clients through the \"promisor-remote\" capability,\navoiding the need to update it on all the clients manually.\n\nStoring the token on the client side makes sure that the token is\navailable when the client needs to access the promisor remotes for a\nlazy fetch.\n\nTo allow this, let's introduce a new \"promisor.storeFields\"\nconfiguration variable.\n\nNote that for a partial clone filter, it's less interesting to have\nit stored on the client. This is because a filter should be used\nright away and we already pass a `--filter=<filter-spec>` option to\n`git clone` when starting a partial clone. Storing the filter could\nperhaps still be interesting for information purposes.\n\nLike \"promisor.checkFields\" and \"promisor.sendFields\", the new\nconfiguration variable should contain a comma or space separated list\nof field names. Only the \"partialCloneFilter\" and \"token\" field names\nare supported for now.\n\nWhen a server advertises a promisor remote, for example \"foo\", along\nwith for example \"token=XXXXX\" to a client, and on the client side\n\"promisor.storeFields\" contains \"token\", then the client will store\nXXXXX for the \"remote.foo.token\" variable in its configuration file\nand reload its configuration so it can immediately use this new\nconfiguration variable.\n\nA message is emitted on stderr to warn users when the config is\nchanged.\n\nNote that even if \"promisor.acceptFromServer\" is set to \"all\", a\npromisor remote has to be already configured on the client side for\nsome of its config to be changed. In any case no new remote is\nconfigured and no new URL is stored.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    |  33 ++++++\n Documentation/gitprotocol-v2.adoc     |  12 ++-\n promisor-remote.c                     | 148 +++++++++++++++++++++++++-\n t/t5710-promisor-remote-capability.sh |  63 +++++++++++\n 4 files changed, 250 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex 93e5e0d9b5..b0fa43b839 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -89,3 +89,36 @@ variable. The fields are checked only if the\n `promisor.acceptFromServer` config variable is not set to \"None\". If\n set to \"None\", this config variable has no effect. See\n linkgit:gitprotocol-v2[5].\n+\n+promisor.storeFields::\n+\tA comma or space separated list of additional remote related\n+\tfield names. If a client accepts an advertised remote, the\n+\tclient will store the values associated with these field names\n+\ttaken from the remote advertisement into its configuration,\n+\tand then reload its remote configuration. Currently,\n+\t\"partialCloneFilter\" and \"token\" are the only supported field\n+\tnames.\n++\n+For example if a server advertises \"partialCloneFilter=blob:limit=20k\"\n+for remote \"foo\", and that remote is accepted, then \"blob:limit=20k\"\n+will be stored for the \"remote.foo.partialCloneFilter\" configuration\n+variable.\n++\n+If the new field value from an advertised remote is the same as the\n+existing field value for that remote on the client side, then no\n+change is made to the client configuration though.\n++\n+When a new value is stored, a message is printed to standard error to\n+let users know about this.\n++\n+Note that for security reasons, if the remote is not already\n+configured on the client side, nothing will be stored for that\n+remote. In any case, no new remote will be created and no URL will be\n+stored.\n++\n+Before storing a partial clone filter, it's parsed to check it's\n+valid. If it's not, a warning is emitted and it's not stored.\n++\n+Before storing a token, a check is performed to ensure it contains no\n+control character. If the check fails, a warning is emitted and it's\n+not stored.\ndiff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\nindex c7db103299..d93dd279ea 100644\n--- a/Documentation/gitprotocol-v2.adoc\n+++ b/Documentation/gitprotocol-v2.adoc\n@@ -826,9 +826,10 @@ are case-sensitive and MUST be transmitted exactly as specified\n above. Clients MUST ignore fields they don't recognize to allow for\n future protocol extensions.\n \n-For now, the client can only use information transmitted through these\n-fields to decide if it accepts the advertised promisor remote. In the\n-future that information might be used for other purposes though.\n+The client can use information transmitted through these fields to\n+decide if it accepts the advertised promisor remote. Also, the client\n+can be configured to store the values of these fields (see\n+\"promisor.storeFields\" in linkgit:git-config[1]).\n \n Field values MUST be urlencoded.\n \n@@ -856,8 +857,9 @@ the server advertised, the client shouldn't advertise the\n On the server side, the \"promisor.advertise\" and \"promisor.sendFields\"\n configuration options can be used to control what it advertises. On\n the client side, the \"promisor.acceptFromServer\" configuration option\n-can be used to control what it accepts. See the documentation of these\n-configuration options for more information.\n+can be used to control what it accepts, and the \"promisor.storeFields\"\n+option, to control what it stores. See the documentation of these\n+configuration options in linkgit:git-config[1] for more information.\n \n Note that in the future it would be nice if the \"promisor-remote\"\n protocol capability could be used by the server, when responding to\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 5d8151cedb..59997dd4c7 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -403,6 +403,14 @@ static struct string_list *fields_checked(void)\n \treturn initialize_fields_list(&fields_list, &initialized, \"promisor.checkFields\");\n }\n \n+static struct string_list *fields_stored(void)\n+{\n+\tstatic struct string_list fields_list = STRING_LIST_INIT_NODUP;\n+\tstatic int initialized;\n+\n+\treturn initialize_fields_list(&fields_list, &initialized, \"promisor.storeFields\");\n+}\n+\n /*\n  * Struct for promisor remotes involved in the \"promisor-remote\"\n  * protocol capability.\n@@ -692,6 +700,132 @@ static struct promisor_info *parse_one_advertised_remote(const char *remote_info\n \treturn info;\n }\n \n+static bool store_one_field(struct repository *repo, const char *remote_name,\n+\t\t\t    const char *field_name, const char *field_key,\n+\t\t\t    const char *advertised, const char *current)\n+{\n+\tif (advertised && (!current || strcmp(current, advertised))) {\n+\t\tchar *key = xstrfmt(\"remote.%s.%s\", remote_name, field_key);\n+\n+\t\tfprintf(stderr, _(\"Storing new %s from server for remote '%s'.\\n\"\n+\t\t\t\t  \"    '%s' -> '%s'\\n\"),\n+\t\t\tfield_name, remote_name,\n+\t\t\tcurrent ? current : \"\",\n+\t\t\tadvertised);\n+\n+\t\trepo_config_set_gently(repo, key, advertised);\n+\t\tfree(key);\n+\n+\t\treturn true;\n+\t}\n+\n+\treturn false;\n+}\n+\n+/* Check that a filter is valid by parsing it */\n+static bool valid_filter(const char *filter, const char *remote_name)\n+{\n+\tstruct list_objects_filter_options filter_opts = LIST_OBJECTS_FILTER_INIT;\n+\tstruct strbuf err = STRBUF_INIT;\n+\tint res = gently_parse_list_objects_filter(&filter_opts, filter, &err);\n+\n+\tif (res)\n+\t\twarning(_(\"invalid filter '%s' for remote '%s' \"\n+\t\t\t  \"will not be stored: %s\"),\n+\t\t\tfilter, remote_name, err.buf);\n+\n+\tlist_objects_filter_release(&filter_opts);\n+\tstrbuf_release(&err);\n+\n+\treturn !res;\n+}\n+\n+/* Check that a token doesn't contain any control character */\n+static bool valid_token(const char *token, const char *remote_name)\n+{\n+\tconst char *c = token;\n+\n+\tfor (; *c; c++)\n+\t\tif (iscntrl(*c)) {\n+\t\t\twarning(_(\"invalid token '%s' for remote '%s' \"\n+\t\t\t\t  \"will not be stored\"),\n+\t\t\t\ttoken, remote_name);\n+\t\t\treturn false;\n+\t\t}\n+\n+\treturn true;\n+}\n+\n+struct store_info {\n+\tstruct repository *repo;\n+\tstruct string_list config_info;\n+\tbool store_filter;\n+\tbool store_token;\n+};\n+\n+static struct store_info *store_info_new(struct repository *repo)\n+{\n+\tstruct string_list *fields_to_store = fields_stored();\n+\tstruct store_info *s = xmalloc(sizeof(*s));\n+\n+\ts->repo = repo;\n+\n+\tstring_list_init_nodup(&s->config_info);\n+\tpromisor_config_info_list(repo, &s->config_info, fields_to_store);\n+\tstring_list_sort(&s->config_info);\n+\n+\ts->store_filter = !!string_list_lookup(fields_to_store, promisor_field_filter);\n+\ts->store_token = !!string_list_lookup(fields_to_store, promisor_field_token);\n+\n+\treturn s;\n+}\n+\n+static void store_info_free(struct store_info *s)\n+{\n+\tif (s) {\n+\t\tpromisor_info_list_clear(&s->config_info);\n+\t\tfree(s);\n+\t}\n+}\n+\n+static bool promisor_store_advertised_fields(struct promisor_info *advertised,\n+\t\t\t\t\t     struct store_info *store_info)\n+{\n+\tstruct promisor_info *p;\n+\tstruct string_list_item *item;\n+\tconst char *remote_name = advertised->name;\n+\tbool reload_config = false;\n+\n+\tif (!(store_info->store_filter || store_info->store_token))\n+\t\treturn false;\n+\n+\t/*\n+\t * Get existing config info for the advertised promisor\n+\t * remote. This ensures the remote is already configured on\n+\t * the client side.\n+\t */\n+\titem = string_list_lookup(&store_info->config_info, remote_name);\n+\n+\tif (!item)\n+\t\treturn false;\n+\n+\tp = item->util;\n+\n+\tif (store_info->store_filter && advertised->filter &&\n+\t    valid_filter(advertised->filter, remote_name))\n+\t\treload_config |= store_one_field(store_info->repo, remote_name,\n+\t\t\t\t\t\t \"filter\", promisor_field_filter,\n+\t\t\t\t\t\t advertised->filter, p->filter);\n+\n+\tif (store_info->store_token && advertised->token &&\n+\t    valid_token(advertised->token, remote_name))\n+\t\treload_config |= store_one_field(store_info->repo, remote_name,\n+\t\t\t\t\t\t \"token\", promisor_field_token,\n+\t\t\t\t\t\t advertised->token, p->token);\n+\n+\treturn reload_config;\n+}\n+\n static void filter_promisor_remote(struct repository *repo,\n \t\t\t\t   struct strvec *accepted,\n \t\t\t\t   const char *info)\n@@ -700,7 +834,9 @@ static void filter_promisor_remote(struct repository *repo,\n \tenum accept_promisor accept = ACCEPT_NONE;\n \tstruct string_list config_info = STRING_LIST_INIT_NODUP;\n \tstruct string_list remote_info = STRING_LIST_INIT_DUP;\n+\tstruct store_info *store_info = NULL;\n \tstruct string_list_item *item;\n+\tbool reload_config = false;\n \n \tif (!repo_config_get_string_tmp(the_repository, \"promisor.acceptfromserver\", &accept_str)) {\n \t\tif (!*accept_str || !strcasecmp(\"None\", accept_str))\n@@ -736,14 +872,24 @@ static void filter_promisor_remote(struct repository *repo,\n \t\t\tstring_list_sort(&config_info);\n \t\t}\n \n-\t\tif (should_accept_remote(accept, advertised, &config_info))\n+\t\tif (should_accept_remote(accept, advertised, &config_info)) {\n+\t\t\tif (!store_info)\n+\t\t\t\tstore_info = store_info_new(repo);\n+\t\t\tif (promisor_store_advertised_fields(advertised, store_info))\n+\t\t\t\treload_config = true;\n+\n \t\t\tstrvec_push(accepted, advertised->name);\n+\t\t}\n \n \t\tpromisor_info_free(advertised);\n \t}\n \n \tpromisor_info_list_clear(&config_info);\n \tstring_list_clear(&remote_info, 0);\n+\tstore_info_free(store_info);\n+\n+\tif (reload_config)\n+\t\trepo_promisor_remote_reinit(repo);\n }\n \n char *promisor_remote_reply(const char *info)\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex 023735d6a8..6ef6431bd7 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -360,6 +360,69 @@ test_expect_success \"clone with promisor.checkFields\" '\n \tcheck_missing_objects server 1 \"$oid\"\n '\n \n+test_expect_success \"clone with promisor.storeFields=partialCloneFilter\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\tgit -C server remote add otherLop \"https://invalid.invalid\"  &&\n+\tgit -C server config remote.otherLop.token \"fooBar\" &&\n+\tgit -C server config remote.otherLop.stuff \"baz\" &&\n+\tgit -C server config remote.otherLop.partialCloneFilter \"blob:limit=10k\" &&\n+\ttest_when_finished \"git -C server remote remove otherLop\" &&\n+\n+\tgit -C server config remote.lop.token \"fooXXX\" &&\n+\tgit -C server config remote.lop.partialCloneFilter \"blob:limit=8k\" &&\n+\n+\ttest_config -C server promisor.sendFields \"partialCloneFilter, token\" &&\n+\ttest_when_finished \"rm trace\" &&\n+\n+\t# Clone from server to create a client\n+\tGIT_TRACE_PACKET=\"$(pwd)/trace\" GIT_NO_LAZY_FETCH=0 git clone \\\n+\t\t-c remote.lop.promisor=true \\\n+\t\t-c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n+\t\t-c remote.lop.url=\"file://$(pwd)/lop\" \\\n+\t\t-c remote.lop.token=\"fooYYY\" \\\n+\t\t-c remote.lop.partialCloneFilter=\"blob:none\" \\\n+\t\t-c promisor.acceptfromserver=All \\\n+\t\t-c promisor.storeFields=partialcloneFilter \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client 2>err &&\n+\n+\t# Check that the filter from the server is stored\n+\techo \"blob:limit=8k\" >expected &&\n+\tgit -C client config remote.lop.partialCloneFilter >actual &&\n+\ttest_cmp expected actual &&\n+\n+\t# Check that user is notified when the filter is stored\n+\ttest_grep \"Storing new filter from server for remote '\\''lop'\\''\" err &&\n+\ttest_grep \"'\\''blob:none'\\'' -> '\\''blob:limit=8k'\\''\" err &&\n+\n+\t# Check that the token from the server is NOT stored\n+\techo \"fooYYY\" >expected &&\n+\tgit -C client config remote.lop.token >actual &&\n+\ttest_cmp expected actual &&\n+\ttest_grep ! \"Storing new token from server\" err &&\n+\n+\t# Check that the filter for an unknown remote is NOT stored\n+\ttest_must_fail git -C client config remote.otherLop.partialCloneFilter >actual &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\" &&\n+\n+\t# Change the configuration on the server and fetch from the client\n+\tgit -C server config remote.lop.partialCloneFilter \"blob:limit=7k\" &&\n+\tGIT_NO_LAZY_FETCH=0 git -C client fetch \\\n+\t\t--filter=\"blob:limit=5k\" ../server 2>err &&\n+\n+\t# Check that the fetch updated the configuration on the client\n+\techo \"blob:limit=7k\" >expected &&\n+\tgit -C client config remote.lop.partialCloneFilter >actual &&\n+\ttest_cmp expected actual &&\n+\n+\t# Check that user is notified when the new filter is stored\n+\ttest_grep \"Storing new filter from server for remote '\\''lop'\\''\" err &&\n+\ttest_grep \"'\\''blob:limit=8k'\\'' -> '\\''blob:limit=7k'\\''\" err\n+'\n+\n test_expect_success \"clone with promisor.advertise set to 'true' but don't delete the client\" '\n \tgit -C server config promisor.advertise true &&\n \n-- \n2.53.0.77.g4627d513d6\n\n"},{"id":"536099","messageId":"20260216132317.15894-4-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260216132317.15894-1-christian.couder@gmail.com","subject":"[PATCH v4 3/9] clone: make filter_options local to cmd_clone()","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-16T13:23:09Z","receivedAt":"2026-02-16T13:23:41Z","isPatch":true,"body":"The `struct list_objects_filter_options filter_options` variable used\nin \"builtin/clone.c\" to store the parsed filters specified by\n`--filter=<filterspec>` is currently a static variable global to the\nfile.\n\nAs we are going to use it more in a following commit, it could become\na bit less easy to understand how it's managed.\n\nTo avoid that, let's make it clear that it's owned by cmd_clone() by\nmoving its definition into that function and making it non-static.\n\nThe only additional change to make this work is to pass it as an\nargument to checkout(). So it's a small quite cheap cleanup anyway.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n builtin/clone.c | 16 +++++++++++-----\n 1 file changed, 11 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex b14a39a687..bb27472020 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -77,7 +77,6 @@ static struct string_list option_required_reference = STRING_LIST_INIT_NODUP;\n static struct string_list option_optional_reference = STRING_LIST_INIT_NODUP;\n static int max_jobs = -1;\n static struct string_list option_recurse_submodules = STRING_LIST_INIT_NODUP;\n-static struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n static int config_filter_submodules = -1;    /* unspecified */\n static int option_remote_submodules;\n \n@@ -634,7 +633,9 @@ static int git_sparse_checkout_init(const char *repo)\n \treturn result;\n }\n \n-static int checkout(int submodule_progress, int filter_submodules,\n+static int checkout(int submodule_progress,\n+\t\t    struct list_objects_filter_options *filter_options,\n+\t\t    int filter_submodules,\n \t\t    enum ref_storage_format ref_storage_format)\n {\n \tstruct object_id oid;\n@@ -723,9 +724,9 @@ static int checkout(int submodule_progress, int filter_submodules,\n \t\t\tstrvec_pushf(&cmd.args, \"--ref-format=%s\",\n \t\t\t\t     ref_storage_format_to_name(ref_storage_format));\n \n-\t\tif (filter_submodules && filter_options.choice)\n+\t\tif (filter_submodules && filter_options->choice)\n \t\t\tstrvec_pushf(&cmd.args, \"--filter=%s\",\n-\t\t\t\t     expand_list_objects_filter_spec(&filter_options));\n+\t\t\t\t     expand_list_objects_filter_spec(filter_options));\n \n \t\tif (option_single_branch >= 0)\n \t\t\tstrvec_push(&cmd.args, option_single_branch ?\n@@ -903,6 +904,7 @@ int cmd_clone(int argc,\n \tenum transport_family family = TRANSPORT_FAMILY_ALL;\n \tstruct string_list option_config = STRING_LIST_INIT_DUP;\n \tint option_dissociate = 0;\n+\tstruct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n \tint option_filter_submodules = -1; /* unspecified */\n \tstruct string_list server_options = STRING_LIST_INIT_NODUP;\n \tconst char *bundle_uri = NULL;\n@@ -1624,9 +1626,13 @@ int cmd_clone(int argc,\n \t\treturn 1;\n \n \tjunk_mode = JUNK_LEAVE_REPO;\n-\terr = checkout(submodule_progress, filter_submodules,\n+\terr = checkout(submodule_progress,\n+\t\t       &filter_options,\n+\t\t       filter_submodules,\n \t\t       ref_storage_format);\n \n+\tlist_objects_filter_release(&filter_options);\n+\n \tstring_list_clear(&option_not, 0);\n \tstring_list_clear(&option_config, 0);\n \tstring_list_clear(&server_options, 0);\n-- \n2.53.0.77.g4627d513d6\n\n"},{"id":"536100","messageId":"20260216132317.15894-5-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260216132317.15894-1-christian.couder@gmail.com","subject":"[PATCH v4 4/9] fetch: make filter_options local to cmd_fetch()","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-16T13:23:10Z","receivedAt":"2026-02-16T13:23:43Z","isPatch":true,"body":"The `struct list_objects_filter_options filter_options` variable used\nin \"builtin/fetch.c\" to store the parsed filters specified by\n`--filter=<filterspec>` is currently a static variable global to the\nfile.\n\nAs we are going to use it more in a following commit, it could become a\nbit less easy to understand how it's managed.\n\nTo avoid that, let's make it clear that it's owned by cmd_fetch() by\nmoving its definition into that function and making it non-static.\n\nThis requires passing a pointer to it through the prepare_transport(),\ndo_fetch(), backfill_tags(), fetch_one_setup_partial(), and fetch_one()\nfunctions, but it's quite straightforward.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n builtin/fetch.c | 48 +++++++++++++++++++++++++++---------------------\n 1 file changed, 27 insertions(+), 21 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex a3bc7e9380..8fbf3557ce 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -97,7 +97,6 @@ static struct strbuf default_rla = STRBUF_INIT;\n static struct transport *gtransport;\n static struct transport *gsecondary;\n static struct refspec refmap = REFSPEC_INIT_FETCH;\n-static struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n static struct string_list server_options = STRING_LIST_INIT_DUP;\n static struct string_list negotiation_tip = STRING_LIST_INIT_NODUP;\n \n@@ -1562,7 +1561,8 @@ static void add_negotiation_tips(struct git_transport_options *smart_options)\n \tsmart_options->negotiation_tips = oids;\n }\n \n-static struct transport *prepare_transport(struct remote *remote, int deepen)\n+static struct transport *prepare_transport(struct remote *remote, int deepen,\n+\t\t\t\t\t   struct list_objects_filter_options *filter_options)\n {\n \tstruct transport *transport;\n \n@@ -1586,9 +1586,9 @@ static struct transport *prepare_transport(struct remote *remote, int deepen)\n \t\tset_option(transport, TRANS_OPT_UPDATE_SHALLOW, \"yes\");\n \tif (refetch)\n \t\tset_option(transport, TRANS_OPT_REFETCH, \"yes\");\n-\tif (filter_options.choice) {\n+\tif (filter_options->choice) {\n \t\tconst char *spec =\n-\t\t\texpand_list_objects_filter_spec(&filter_options);\n+\t\t\texpand_list_objects_filter_spec(filter_options);\n \t\tset_option(transport, TRANS_OPT_LIST_OBJECTS_FILTER, spec);\n \t\tset_option(transport, TRANS_OPT_FROM_PROMISOR, \"1\");\n \t}\n@@ -1607,7 +1607,8 @@ static int backfill_tags(struct display_state *display_state,\n \t\t\t struct ref *ref_map,\n \t\t\t struct fetch_head *fetch_head,\n \t\t\t const struct fetch_config *config,\n-\t\t\t struct ref_update_display_info_array *display_array)\n+\t\t\t struct ref_update_display_info_array *display_array,\n+\t\t\t struct list_objects_filter_options *filter_options)\n {\n \tint retcode, cannot_reuse;\n \n@@ -1621,7 +1622,7 @@ static int backfill_tags(struct display_state *display_state,\n \tcannot_reuse = transport->cannot_reuse ||\n \t\tdeepen_since || deepen_not.nr;\n \tif (cannot_reuse) {\n-\t\tgsecondary = prepare_transport(transport->remote, 0);\n+\t\tgsecondary = prepare_transport(transport->remote, 0, filter_options);\n \t\ttransport = gsecondary;\n \t}\n \n@@ -1834,7 +1835,8 @@ static int commit_ref_transaction(struct ref_transaction **transaction,\n \n static int do_fetch(struct transport *transport,\n \t\t    struct refspec *rs,\n-\t\t    const struct fetch_config *config)\n+\t\t    const struct fetch_config *config,\n+\t\t    struct list_objects_filter_options *filter_options)\n {\n \tstruct ref_transaction *transaction = NULL;\n \tstruct ref *ref_map = NULL;\n@@ -1997,7 +1999,7 @@ static int do_fetch(struct transport *transport,\n \t\t\t * the transaction and don't commit anything.\n \t\t\t */\n \t\t\tif (backfill_tags(&display_state, transport, transaction, tags_ref_map,\n-\t\t\t\t\t  &fetch_head, config, &display_array))\n+\t\t\t\t\t  &fetch_head, config, &display_array, filter_options))\n \t\t\t\tretcode = 1;\n \t\t}\n \n@@ -2339,20 +2341,21 @@ static int fetch_multiple(struct string_list *list, int max_children,\n  * Fetching from the promisor remote should use the given filter-spec\n  * or inherit the default filter-spec from the config.\n  */\n-static inline void fetch_one_setup_partial(struct remote *remote)\n+static inline void fetch_one_setup_partial(struct remote *remote,\n+\t\t\t\t\t   struct list_objects_filter_options *filter_options)\n {\n \t/*\n \t * Explicit --no-filter argument overrides everything, regardless\n \t * of any prior partial clones and fetches.\n \t */\n-\tif (filter_options.no_filter)\n+\tif (filter_options->no_filter)\n \t\treturn;\n \n \t/*\n \t * If no prior partial clone/fetch and the current fetch DID NOT\n \t * request a partial-fetch, do a normal fetch.\n \t */\n-\tif (!repo_has_promisor_remote(the_repository) && !filter_options.choice)\n+\tif (!repo_has_promisor_remote(the_repository) && !filter_options->choice)\n \t\treturn;\n \n \t/*\n@@ -2361,8 +2364,8 @@ static inline void fetch_one_setup_partial(struct remote *remote)\n \t * filter-spec as the default for subsequent fetches to this\n \t * remote if there is currently no default filter-spec.\n \t */\n-\tif (filter_options.choice) {\n-\t\tpartial_clone_register(remote->name, &filter_options);\n+\tif (filter_options->choice) {\n+\t\tpartial_clone_register(remote->name, filter_options);\n \t\treturn;\n \t}\n \n@@ -2371,14 +2374,15 @@ static inline void fetch_one_setup_partial(struct remote *remote)\n \t * explicitly given filter-spec or inherit the filter-spec from\n \t * the config.\n \t */\n-\tif (!filter_options.choice)\n-\t\tpartial_clone_get_default_filter_spec(&filter_options, remote->name);\n+\tif (!filter_options->choice)\n+\t\tpartial_clone_get_default_filter_spec(filter_options, remote->name);\n \treturn;\n }\n \n static int fetch_one(struct remote *remote, int argc, const char **argv,\n \t\t     int prune_tags_ok, int use_stdin_refspecs,\n-\t\t     const struct fetch_config *config)\n+\t\t     const struct fetch_config *config,\n+\t\t     struct list_objects_filter_options *filter_options)\n {\n \tstruct refspec rs = REFSPEC_INIT_FETCH;\n \tint i;\n@@ -2390,7 +2394,7 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,\n \t\tdie(_(\"no remote repository specified; please specify either a URL or a\\n\"\n \t\t      \"remote name from which new revisions should be fetched\"));\n \n-\tgtransport = prepare_transport(remote, 1);\n+\tgtransport = prepare_transport(remote, 1, filter_options);\n \n \tif (prune < 0) {\n \t\t/* no command line request */\n@@ -2445,7 +2449,7 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,\n \tsigchain_push_common(unlock_pack_on_signal);\n \tatexit(unlock_pack_atexit);\n \tsigchain_push(SIGPIPE, SIG_IGN);\n-\texit_code = do_fetch(gtransport, &rs, config);\n+\texit_code = do_fetch(gtransport, &rs, config, filter_options);\n \tsigchain_pop(SIGPIPE);\n \trefspec_clear(&rs);\n \ttransport_disconnect(gtransport);\n@@ -2470,6 +2474,7 @@ int cmd_fetch(int argc,\n \tconst char *submodule_prefix = \"\";\n \tconst char *bundle_uri;\n \tstruct string_list list = STRING_LIST_INIT_DUP;\n+\tstruct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n \tstruct remote *remote = NULL;\n \tint all = -1, multiple = 0;\n \tint result = 0;\n@@ -2735,7 +2740,7 @@ int cmd_fetch(int argc,\n \t\ttrace2_region_enter(\"fetch\", \"negotiate-only\", the_repository);\n \t\tif (!remote)\n \t\t\tdie(_(\"must supply remote when using --negotiate-only\"));\n-\t\tgtransport = prepare_transport(remote, 1);\n+\t\tgtransport = prepare_transport(remote, 1, &filter_options);\n \t\tif (gtransport->smart_options) {\n \t\t\tgtransport->smart_options->acked_commits = &acked_commits;\n \t\t} else {\n@@ -2757,12 +2762,12 @@ int cmd_fetch(int argc,\n \t} else if (remote) {\n \t\tif (filter_options.choice || repo_has_promisor_remote(the_repository)) {\n \t\t\ttrace2_region_enter(\"fetch\", \"setup-partial\", the_repository);\n-\t\t\tfetch_one_setup_partial(remote);\n+\t\t\tfetch_one_setup_partial(remote, &filter_options);\n \t\t\ttrace2_region_leave(\"fetch\", \"setup-partial\", the_repository);\n \t\t}\n \t\ttrace2_region_enter(\"fetch\", \"fetch-one\", the_repository);\n \t\tresult = fetch_one(remote, argc, argv, prune_tags_ok, stdin_refspecs,\n-\t\t\t\t   &config);\n+\t\t\t\t   &config, &filter_options);\n \t\ttrace2_region_leave(\"fetch\", \"fetch-one\", the_repository);\n \t} else {\n \t\tint max_children = max_jobs;\n@@ -2868,5 +2873,6 @@ int cmd_fetch(int argc,\n \n  cleanup:\n \tstring_list_clear(&list, 0);\n+\tlist_objects_filter_release(&filter_options);\n \treturn result;\n }\n-- \n2.53.0.77.g4627d513d6\n\n"},{"id":"536101","messageId":"20260216132317.15894-6-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260216132317.15894-1-christian.couder@gmail.com","subject":"[PATCH v4 5/9] doc: fetch: document `--filter=<filter-spec>` option","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-16T13:23:11Z","receivedAt":"2026-02-16T13:23:44Z","isPatch":true,"body":"The `--filter=<filter-spec>` option is documented in most commands that\nsupport it except `git fetch`.\n\nLet's fix that and document this option. To ensure consistency across\ncommands, let's reuse the exact description currently found in\n`git clone`.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/fetch-options.adoc | 10 ++++++++++\n 1 file changed, 10 insertions(+)\n\ndiff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc\nindex fcba46ee9e..1ef9807d00 100644\n--- a/Documentation/fetch-options.adoc\n+++ b/Documentation/fetch-options.adoc\n@@ -88,6 +88,16 @@ linkgit:git-config[1].\n This is incompatible with `--recurse-submodules=(yes|on-demand)` and takes\n precedence over the `fetch.output` config option.\n \n+`--filter=<filter-spec>`::\n+\tUse the partial clone feature and request that the server sends\n+\ta subset of reachable objects according to a given object filter.\n+\tWhen using `--filter`, the supplied _<filter-spec>_ is used for\n+\tthe partial fetch. For example, `--filter=blob:none` will filter\n+\tout all blobs (file contents) until needed by Git. Also,\n+\t`--filter=blob:limit=<size>` will filter out all blobs of size\n+\tat least _<size>_. For more details on filter specifications, see\n+\tthe `--filter` option in linkgit:git-rev-list[1].\n+\n ifndef::git-pull[]\n `--write-fetch-head`::\n `--no-write-fetch-head`::\n-- \n2.53.0.77.g4627d513d6\n\n"},{"id":"536102","messageId":"20260216132317.15894-7-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260216132317.15894-1-christian.couder@gmail.com","subject":"[PATCH v4 6/9] list-objects-filter-options: support 'auto' mode for --filter","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-16T13:23:12Z","receivedAt":"2026-02-16T13:23:46Z","isPatch":true,"body":"In a following commit, we are going to allow passing \"auto\" as a\n<filterspec> to the `--filter=<filterspec>` option, but only for some\ncommands. Other commands that support the `--filter=<filterspec>`\noption should still die() when 'auto' is passed.\n\nLet's set up the \"list-objects-filter-options.{c,h}\" infrastructure to\nsupport that:\n\n- Add a new `unsigned int allow_auto_filter : 1;` flag to\n  `struct list_objects_filter_options` which specifies if \"auto\" is\n  accepted or not by the current command.\n- Change gently_parse_list_objects_filter() to parse \"auto\" if it's\n  accepted.\n- Make sure we die() if \"auto\" is combined with another filter.\n- Update list_objects_filter_release() to preserve the\n  allow_auto_filter flag, as this function is often called (via\n  opt_parse_list_objects_filter) to reset the struct before parsing a\n  new value.\n\nLet's also update `list-objects-filter.c` to recognize the new\n`LOFC_AUTO` choice. Since \"auto\" must be resolved to a concrete filter\nbefore filtering actually begins, initializing a filter with\n`LOFC_AUTO` is invalid and will trigger a BUG().\n\nNote that ideally combining \"auto\" with \"auto\" could be allowed, but in\npractice, it's probably not worth the added code complexity. And if we\nreally want it, nothing prevents us to allow it in future work.\n\nIf we ever want to give a meaning to combining \"auto\" with a different\nfilter too, nothing prevents us to do that in future work either.\n\nAlso note that the new `allow_auto_filter` flag depends on the command,\nnot user choices, so it should be reset to the command default when\n`struct list_objects_filter_options` instances are reset.\n\nWhile at it, let's add a new \"u-list-objects-filter-options.c\" file for\n`struct list_objects_filter_options` related unit tests. For now it\nonly tests gently_parse_list_objects_filter() though.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Makefile                                     |  1 +\n list-objects-filter-options.c                | 39 ++++++++++++--\n list-objects-filter-options.h                |  6 +++\n list-objects-filter.c                        |  8 +++\n t/meson.build                                |  1 +\n t/unit-tests/u-list-objects-filter-options.c | 53 ++++++++++++++++++++\n 6 files changed, 105 insertions(+), 3 deletions(-)\n create mode 100644 t/unit-tests/u-list-objects-filter-options.c\n\ndiff --git a/Makefile b/Makefile\nindex 4ac44331ea..9e174dd06c 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1518,6 +1518,7 @@ CLAR_TEST_SUITES += u-dir\n CLAR_TEST_SUITES += u-example-decorate\n CLAR_TEST_SUITES += u-hash\n CLAR_TEST_SUITES += u-hashmap\n+CLAR_TEST_SUITES += u-list-objects-filter-options\n CLAR_TEST_SUITES += u-mem-pool\n CLAR_TEST_SUITES += u-oid-array\n CLAR_TEST_SUITES += u-oidmap\ndiff --git a/list-objects-filter-options.c b/list-objects-filter-options.c\nindex 7420bf81fe..7f3e7b8f50 100644\n--- a/list-objects-filter-options.c\n+++ b/list-objects-filter-options.c\n@@ -20,6 +20,8 @@ const char *list_object_filter_config_name(enum list_objects_filter_choice c)\n \tcase LOFC_DISABLED:\n \t\t/* we have no name for \"no filter at all\" */\n \t\tbreak;\n+\tcase LOFC_AUTO:\n+\t\treturn \"auto\";\n \tcase LOFC_BLOB_NONE:\n \t\treturn \"blob:none\";\n \tcase LOFC_BLOB_LIMIT:\n@@ -52,7 +54,16 @@ int gently_parse_list_objects_filter(\n \tif (filter_options->choice)\n \t\tBUG(\"filter_options already populated\");\n \n-\tif (!strcmp(arg, \"blob:none\")) {\n+\tif (!strcmp(arg, \"auto\")) {\n+\t\tif (!filter_options->allow_auto_filter) {\n+\t\t\tstrbuf_addstr(errbuf,\n+\t\t\t\t      _(\"'auto' filter not supported by this command\"));\n+\t\t\treturn 1;\n+\t\t}\n+\t\tfilter_options->choice = LOFC_AUTO;\n+\t\treturn 0;\n+\n+\t} else if (!strcmp(arg, \"blob:none\")) {\n \t\tfilter_options->choice = LOFC_BLOB_NONE;\n \t\treturn 0;\n \n@@ -146,10 +157,22 @@ static int parse_combine_subfilter(\n \n \tdecoded = url_percent_decode(subspec->buf);\n \n-\tresult = has_reserved_character(subspec, errbuf) ||\n-\t\tgently_parse_list_objects_filter(\n+\tresult = has_reserved_character(subspec, errbuf);\n+\tif (result)\n+\t\tgoto cleanup;\n+\n+\tresult = gently_parse_list_objects_filter(\n \t\t\t&filter_options->sub[new_index], decoded, errbuf);\n+\tif (result)\n+\t\tgoto cleanup;\n+\n+\tresult = (filter_options->sub[new_index].choice == LOFC_AUTO);\n+\tif (result) {\n+\t\tstrbuf_addstr(errbuf, _(\"an 'auto' filter cannot be combined\"));\n+\t\tgoto cleanup;\n+\t}\n \n+cleanup:\n \tfree(decoded);\n \treturn result;\n }\n@@ -263,6 +286,9 @@ void parse_list_objects_filter(\n \t} else {\n \t\tstruct list_objects_filter_options *sub;\n \n+\t\tif (filter_options->choice == LOFC_AUTO)\n+\t\t\tdie(_(\"an 'auto' filter is incompatible with any other filter\"));\n+\n \t\t/*\n \t\t * Make filter_options an LOFC_COMBINE spec so we can trivially\n \t\t * add subspecs to it.\n@@ -277,6 +303,9 @@ void parse_list_objects_filter(\n \t\tif (gently_parse_list_objects_filter(sub, arg, &errbuf))\n \t\t\tdie(\"%s\", errbuf.buf);\n \n+\t\tif (sub->choice == LOFC_AUTO)\n+\t\t\tdie(_(\"an 'auto' filter is incompatible with any other filter\"));\n+\n \t\tstrbuf_addch(&filter_options->filter_spec, '+');\n \t\tfilter_spec_append_urlencode(filter_options, arg);\n \t}\n@@ -317,15 +346,19 @@ void list_objects_filter_release(\n \tstruct list_objects_filter_options *filter_options)\n {\n \tsize_t sub;\n+\tunsigned int allow_auto_filter;\n \n \tif (!filter_options)\n \t\treturn;\n+\n+\tallow_auto_filter = filter_options->allow_auto_filter;\n \tstrbuf_release(&filter_options->filter_spec);\n \tfree(filter_options->sparse_oid_name);\n \tfor (sub = 0; sub < filter_options->sub_nr; sub++)\n \t\tlist_objects_filter_release(&filter_options->sub[sub]);\n \tfree(filter_options->sub);\n \tlist_objects_filter_init(filter_options);\n+\tfilter_options->allow_auto_filter = allow_auto_filter;\n }\n \n void partial_clone_register(\ndiff --git a/list-objects-filter-options.h b/list-objects-filter-options.h\nindex 7b2108b986..77d7bbc846 100644\n--- a/list-objects-filter-options.h\n+++ b/list-objects-filter-options.h\n@@ -18,6 +18,7 @@ enum list_objects_filter_choice {\n \tLOFC_SPARSE_OID,\n \tLOFC_OBJECT_TYPE,\n \tLOFC_COMBINE,\n+\tLOFC_AUTO,\n \tLOFC__COUNT /* must be last */\n };\n \n@@ -50,6 +51,11 @@ struct list_objects_filter_options {\n \t */\n \tunsigned int no_filter : 1;\n \n+\t/*\n+\t * Is LOFC_AUTO a valid option?\n+\t */\n+\tunsigned int allow_auto_filter : 1;\n+\n \t/*\n \t * BEGIN choice-specific parsed values from within the filter-spec. Only\n \t * some values will be defined for any given choice.\ndiff --git a/list-objects-filter.c b/list-objects-filter.c\nindex acd65ebb73..78316e7f90 100644\n--- a/list-objects-filter.c\n+++ b/list-objects-filter.c\n@@ -745,6 +745,13 @@ static void filter_combine__init(\n \tfilter->finalize_omits_fn = filter_combine__finalize_omits;\n }\n \n+static void filter_auto__init(\n+\tstruct list_objects_filter_options *filter_options UNUSED,\n+\tstruct filter *filter UNUSED)\n+{\n+\tBUG(\"LOFC_AUTO should have been resolved before initializing the filter\");\n+}\n+\n typedef void (*filter_init_fn)(\n \tstruct list_objects_filter_options *filter_options,\n \tstruct filter *filter);\n@@ -760,6 +767,7 @@ static filter_init_fn s_filters[] = {\n \tfilter_sparse_oid__init,\n \tfilter_object_type__init,\n \tfilter_combine__init,\n+\tfilter_auto__init,\n };\n \n struct filter *list_objects_filter__init(\ndiff --git a/t/meson.build b/t/meson.build\nindex a04a7a86cf..bec4c72327 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -4,6 +4,7 @@ clar_test_suites = [\n   'unit-tests/u-example-decorate.c',\n   'unit-tests/u-hash.c',\n   'unit-tests/u-hashmap.c',\n+  'unit-tests/u-list-objects-filter-options.c',\n   'unit-tests/u-mem-pool.c',\n   'unit-tests/u-oid-array.c',\n   'unit-tests/u-oidmap.c',\ndiff --git a/t/unit-tests/u-list-objects-filter-options.c b/t/unit-tests/u-list-objects-filter-options.c\nnew file mode 100644\nindex 0000000000..f7d73701b5\n--- /dev/null\n+++ b/t/unit-tests/u-list-objects-filter-options.c\n@@ -0,0 +1,53 @@\n+#include \"unit-test.h\"\n+#include \"list-objects-filter-options.h\"\n+#include \"strbuf.h\"\n+\n+/* Helper to test gently_parse_list_objects_filter() */\n+static void check_gentle_parse(const char *filter_spec,\n+\t\t\t       int expect_success,\n+\t\t\t       int allow_auto,\n+\t\t\t       enum list_objects_filter_choice expected_choice)\n+{\n+\tstruct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n+\tstruct strbuf errbuf = STRBUF_INIT;\n+\tint ret;\n+\n+\tfilter_options.allow_auto_filter = allow_auto;\n+\n+\tret = gently_parse_list_objects_filter(&filter_options, filter_spec, &errbuf);\n+\n+\tif (expect_success) {\n+\t\tcl_assert_equal_i(ret, 0);\n+\t\tcl_assert_equal_i(expected_choice, filter_options.choice);\n+\t\tcl_assert_equal_i(errbuf.len, 0);\n+\t} else {\n+\t\tcl_assert(ret != 0);\n+\t\tcl_assert(errbuf.len > 0);\n+\t}\n+\n+\tstrbuf_release(&errbuf);\n+\tlist_objects_filter_release(&filter_options);\n+}\n+\n+void test_list_objects_filter_options__regular_filters(void)\n+{\n+\tcheck_gentle_parse(\"blob:none\", 1, 0, LOFC_BLOB_NONE);\n+\tcheck_gentle_parse(\"blob:none\", 1, 1, LOFC_BLOB_NONE);\n+\tcheck_gentle_parse(\"blob:limit=5k\", 1, 0, LOFC_BLOB_LIMIT);\n+\tcheck_gentle_parse(\"blob:limit=5k\", 1, 1, LOFC_BLOB_LIMIT);\n+\tcheck_gentle_parse(\"combine:blob:none+tree:0\", 1, 0, LOFC_COMBINE);\n+\tcheck_gentle_parse(\"combine:blob:none+tree:0\", 1, 1, LOFC_COMBINE);\n+}\n+\n+void test_list_objects_filter_options__auto_allowed(void)\n+{\n+\tcheck_gentle_parse(\"auto\", 1, 1, LOFC_AUTO);\n+\tcheck_gentle_parse(\"auto\", 0, 0, 0);\n+}\n+\n+void test_list_objects_filter_options__combine_auto_fails(void)\n+{\n+\tcheck_gentle_parse(\"combine:auto+blob:none\", 0, 1, 0);\n+\tcheck_gentle_parse(\"combine:blob:none+auto\", 0, 1, 0);\n+\tcheck_gentle_parse(\"combine:auto+auto\", 0, 1, 0);\n+}\n-- \n2.53.0.77.g4627d513d6\n\n"},{"id":"536103","messageId":"20260216132317.15894-8-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260216132317.15894-1-christian.couder@gmail.com","subject":"[PATCH v4 7/9] promisor-remote: keep advertised filters in memory","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-16T13:23:13Z","receivedAt":"2026-02-16T13:23:47Z","isPatch":true,"body":"Currently, advertised filters are only kept in memory temporarily\nduring parsing, or persisted to disk if `promisor.storeFields`\ncontains 'partialCloneFilter'.\n\nIn a following commit though, we will add a `--filter=auto` option.\nThis option will enable the client to use the filters that the server\nis suggesting for the promisor remotes the client accepts.\n\nTo use them even if `promisor.storeFields` is not configured, these\nfilters should be stored somewhere for the current session.\n\nLet's add an `advertised_filter` field to `struct promisor_remote`\nfor that purpose.\n\nTo ensure that the filters are available in all cases,\nfilter_promisor_remote() captures them into a temporary list and\napplies them to the `promisor_remote` structs after the potential\nconfiguration reload.\n\nThen the accepted remotes are marked as `accepted` in the repository\nstate. This ensures that subsequent calls to look up accepted remotes\n(like in the filter construction below) actually find them.\n\nIn a following commit, we will add a `--filter=auto` option that will\nenable a client to use the filters suggested by the server for the\npromisor remotes the client accepted.\n\nTo enable the client to construct a filter spec based on these filters,\nlet's also add a `promisor_remote_construct_filter(repo)` function.\n\nThis function:\n\n- iterates over all accepted promisor remotes in the repository,\n- collects the filters advertised for them (using `advertised_filter`\n  added in this commit, and\n- generates a single filter spec for them.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 58 +++++++++++++++++++++++++++++++++++++++++++++++\n promisor-remote.h |  7 ++++++\n 2 files changed, 65 insertions(+)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 59997dd4c7..f3bafb7731 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -193,6 +193,7 @@ void promisor_remote_clear(struct promisor_remote_config *config)\n \twhile (config->promisors) {\n \t\tstruct promisor_remote *r = config->promisors;\n \t\tfree(r->partial_clone_filter);\n+\t\tfree(r->advertised_filter);\n \t\tconfig->promisors = config->promisors->next;\n \t\tfree(r);\n \t}\n@@ -837,6 +838,7 @@ static void filter_promisor_remote(struct repository *repo,\n \tstruct store_info *store_info = NULL;\n \tstruct string_list_item *item;\n \tbool reload_config = false;\n+\tstruct string_list accepted_filters = STRING_LIST_INIT_DUP;\n \n \tif (!repo_config_get_string_tmp(the_repository, \"promisor.acceptfromserver\", &accept_str)) {\n \t\tif (!*accept_str || !strcasecmp(\"None\", accept_str))\n@@ -879,6 +881,13 @@ static void filter_promisor_remote(struct repository *repo,\n \t\t\t\treload_config = true;\n \n \t\t\tstrvec_push(accepted, advertised->name);\n+\n+\t\t\t/* Capture advertised filters for accepted remotes */\n+\t\t\tif (advertised->filter) {\n+\t\t\t\tstruct string_list_item *i;\n+\t\t\t\ti = string_list_append(&accepted_filters, advertised->name);\n+\t\t\t\ti->util = xstrdup(advertised->filter);\n+\t\t\t}\n \t\t}\n \n \t\tpromisor_info_free(advertised);\n@@ -890,6 +899,25 @@ static void filter_promisor_remote(struct repository *repo,\n \n \tif (reload_config)\n \t\trepo_promisor_remote_reinit(repo);\n+\n+\t/* Apply accepted remote filters to the stable repo state */\n+\tfor_each_string_list_item(item, &accepted_filters) {\n+\t\tstruct promisor_remote *r = repo_promisor_remote_find(repo, item->string);\n+\t\tif (r) {\n+\t\t\tfree(r->advertised_filter);\n+\t\t\tr->advertised_filter = item->util;\n+\t\t\titem->util = NULL;\n+\t\t}\n+\t}\n+\n+\tstring_list_clear(&accepted_filters, 1);\n+\n+\t/* Mark the remotes as accepted in the repository state */\n+\tfor (size_t i = 0; i < accepted->nr; i++) {\n+\t\tstruct promisor_remote *r = repo_promisor_remote_find(repo, accepted->v[i]);\n+\t\tif (r)\n+\t\t\tr->accepted = 1;\n+\t}\n }\n \n char *promisor_remote_reply(const char *info)\n@@ -935,3 +963,33 @@ void mark_promisor_remotes_as_accepted(struct repository *r, const char *remotes\n \n \tstring_list_clear(&accepted_remotes, 0);\n }\n+\n+char *promisor_remote_construct_filter(struct repository *repo)\n+{\n+\tstruct promisor_remote *r;\n+\tstruct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n+\tstruct strbuf err = STRBUF_INIT;\n+\tchar *result = NULL;\n+\n+\tpromisor_remote_init(repo);\n+\n+\tfor (r = repo->promisor_remote_config->promisors; r; r = r->next) {\n+\t\tif (r->accepted && r->advertised_filter)\n+\t\t\tif (gently_parse_list_objects_filter(&filter_options,\n+\t\t\t\t\t\t\t     r->advertised_filter,\n+\t\t\t\t\t\t\t     &err)) {\n+\t\t\t\twarning(_(\"promisor remote '%s' advertised invalid filter '%s': %s\"),\n+\t\t\t\t\tr->name, r->advertised_filter, err.buf);\n+\t\t\t\tstrbuf_reset(&err);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\t}\n+\n+\tif (filter_options.choice)\n+\t\tresult = xstrdup(expand_list_objects_filter_spec(&filter_options));\n+\n+\tlist_objects_filter_release(&filter_options);\n+\tstrbuf_release(&err);\n+\n+\treturn result;\n+}\ndiff --git a/promisor-remote.h b/promisor-remote.h\nindex 263d331a55..d227299fd0 100644\n--- a/promisor-remote.h\n+++ b/promisor-remote.h\n@@ -15,6 +15,7 @@ struct object_id;\n struct promisor_remote {\n \tstruct promisor_remote *next;\n \tchar *partial_clone_filter;\n+\tchar *advertised_filter;\n \tunsigned int accepted : 1;\n \tconst char name[FLEX_ARRAY];\n };\n@@ -67,4 +68,10 @@ void mark_promisor_remotes_as_accepted(struct repository *repo, const char *remo\n  */\n int repo_has_accepted_promisor_remote(struct repository *r);\n \n+/*\n+ * Use the filters from the accepted remotes to create a combined\n+ * filter (useful in `--filter=auto` mode).\n+ */\n+char *promisor_remote_construct_filter(struct repository *repo);\n+\n #endif /* PROMISOR_REMOTE_H */\n-- \n2.53.0.77.g4627d513d6\n\n"},{"id":"536104","messageId":"20260216132317.15894-9-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260216132317.15894-1-christian.couder@gmail.com","subject":"[PATCH v4 8/9] promisor-remote: change promisor_remote_reply()'s signature","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-16T13:23:14Z","receivedAt":"2026-02-16T13:23:48Z","isPatch":true,"body":"The `promisor_remote_reply()` function performs two tasks:\n1. It uses filter_promisor_remote() to parse the server's\n   \"promisor-remote\" advertisement and to mark accepted remotes in the\n   repository configuration.\n2. It assembles a reply string containing the accepted remote names to\n   send back to the server.\n\nIn a following commit, the fetch-pack logic will need to trigger the\nside effect (1) to ensure the repository state is correct, but it will\nnot need to send a reply (2).\n\nTo avoid assembling a reply string when it is not needed, let's change\nthe signature of promisor_remote_reply(). It will now return `void` and\naccept a second `char **accepted_out` argument. Only if that argument\nis not NULL will a reply string be assembled and returned back to the\ncaller via that argument.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n connect.c         |  3 ++-\n promisor-remote.c | 24 +++++++++++++-----------\n promisor-remote.h | 10 +++++-----\n 3 files changed, 20 insertions(+), 17 deletions(-)\n\ndiff --git a/connect.c b/connect.c\nindex c6f76e3082..a02583a102 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -505,7 +505,8 @@ static void send_capabilities(int fd_out, struct packet_reader *reader)\n \t\treader->hash_algo = &hash_algos[GIT_HASH_SHA1_LEGACY];\n \t}\n \tif (server_feature_v2(\"promisor-remote\", &promisor_remote_info)) {\n-\t\tchar *reply = promisor_remote_reply(promisor_remote_info);\n+\t\tchar *reply;\n+\t\tpromisor_remote_reply(promisor_remote_info, &reply);\n \t\tif (reply) {\n \t\t\tpacket_write_fmt(fd_out, \"promisor-remote=%s\", reply);\n \t\t\tfree(reply);\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex f3bafb7731..96fa215b06 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -920,25 +920,27 @@ static void filter_promisor_remote(struct repository *repo,\n \t}\n }\n \n-char *promisor_remote_reply(const char *info)\n+void promisor_remote_reply(const char *info, char **accepted_out)\n {\n \tstruct strvec accepted = STRVEC_INIT;\n-\tstruct strbuf reply = STRBUF_INIT;\n \n \tfilter_promisor_remote(the_repository, &accepted, info);\n \n-\tif (!accepted.nr)\n-\t\treturn NULL;\n-\n-\tfor (size_t i = 0; i < accepted.nr; i++) {\n-\t\tif (i)\n-\t\t\tstrbuf_addch(&reply, ';');\n-\t\tstrbuf_addstr_urlencode(&reply, accepted.v[i], allow_unsanitized);\n+\tif (accepted_out) {\n+\t\tif (accepted.nr) {\n+\t\t\tstruct strbuf reply = STRBUF_INIT;\n+\t\t\tfor (size_t i = 0; i < accepted.nr; i++) {\n+\t\t\t\tif (i)\n+\t\t\t\t\tstrbuf_addch(&reply, ';');\n+\t\t\t\tstrbuf_addstr_urlencode(&reply, accepted.v[i], allow_unsanitized);\n+\t\t\t}\n+\t\t\t*accepted_out = strbuf_detach(&reply, NULL);\n+\t\t} else {\n+\t\t\t*accepted_out = NULL;\n+\t\t}\n \t}\n \n \tstrvec_clear(&accepted);\n-\n-\treturn strbuf_detach(&reply, NULL);\n }\n \n void mark_promisor_remotes_as_accepted(struct repository *r, const char *remotes)\ndiff --git a/promisor-remote.h b/promisor-remote.h\nindex d227299fd0..3d4d2de018 100644\n--- a/promisor-remote.h\n+++ b/promisor-remote.h\n@@ -49,12 +49,12 @@ char *promisor_remote_info(struct repository *repo);\n /*\n  * Prepare a reply to a \"promisor-remote\" advertisement from a server.\n  * Check the value of \"promisor.acceptfromserver\" and maybe the\n- * configured promisor remotes, if any, to prepare the reply.\n- * Return value is NULL if no promisor remote from the server\n- * is accepted. Otherwise it contains the names of the accepted promisor\n- * remotes separated by ';'. See gitprotocol-v2(5).\n+ * configured promisor remotes, if any, to prepare the reply. If the\n+ * `accepted_out` argument is not NULL, it is set to either NULL or to\n+ * the names of the accepted promisor remotes separated by ';' if\n+ * any. See gitprotocol-v2(5).\n  */\n-char *promisor_remote_reply(const char *info);\n+void promisor_remote_reply(const char *info, char **accepted_out);\n \n /*\n  * Set the 'accepted' flag for some promisor remotes. Useful on the\n-- \n2.53.0.77.g4627d513d6\n\n"},{"id":"536105","messageId":"20260216132317.15894-10-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260216132317.15894-1-christian.couder@gmail.com","subject":"[PATCH v4 9/9] fetch-pack: wire up and enable auto filter logic","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-16T13:23:15Z","receivedAt":"2026-02-16T13:23:51Z","isPatch":true,"body":"Previous commits have set up an infrastructure for `--filter=auto` to\nautomatically prepare a partial clone filter based on what the server\nadvertised and the client accepted.\n\nUsing that infrastructure, let's now enable the `--filter=auto` option\nin `git clone` and `git fetch` by setting `allow_auto_filter` to 1.\n\nNote that these small changes mean that when `git clone --filter=auto`\nor `git fetch --filter=auto` are used, \"auto\" is automatically saved\nas the partial clone filter for the server on the client. Therefore\nsubsequent calls to `git fetch` on the client will automatically use\nthis \"auto\" mode even without `--filter=auto`.\n\nLet's also set `allow_auto_filter` to 1 in `transport.c`, as the\ntransport layer must be able to accept the \"auto\" filter spec even if\nthe invoking command hasn't fully parsed it yet.\n\nWhen an \"auto\" filter is requested, let's have the \"fetch-pack.c\" code\nin `do_fetch_pack_v2()` compute a filter and send it to the server.\n\nIn `do_fetch_pack_v2()` the logic also needs to check for the\n\"promisor-remote\" capability and call `promisor_remote_reply()` to\nparse advertised remotes and populate the list of those accepted (and\ntheir filters).\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/fetch-options.adoc      | 19 ++++++---\n Documentation/git-clone.adoc          | 25 ++++++++---\n Documentation/gitprotocol-v2.adoc     | 16 ++++---\n builtin/clone.c                       |  2 +\n builtin/fetch.c                       |  2 +\n fetch-pack.c                          | 24 +++++++++++\n t/t5710-promisor-remote-capability.sh | 60 +++++++++++++++++++++++++++\n transport.c                           |  1 +\n 8 files changed, 134 insertions(+), 15 deletions(-)\n\ndiff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc\nindex 1ef9807d00..a0cfb50d89 100644\n--- a/Documentation/fetch-options.adoc\n+++ b/Documentation/fetch-options.adoc\n@@ -92,11 +92,20 @@ precedence over the `fetch.output` config option.\n \tUse the partial clone feature and request that the server sends\n \ta subset of reachable objects according to a given object filter.\n \tWhen using `--filter`, the supplied _<filter-spec>_ is used for\n-\tthe partial fetch. For example, `--filter=blob:none` will filter\n-\tout all blobs (file contents) until needed by Git. Also,\n-\t`--filter=blob:limit=<size>` will filter out all blobs of size\n-\tat least _<size>_. For more details on filter specifications, see\n-\tthe `--filter` option in linkgit:git-rev-list[1].\n+\tthe partial fetch.\n++\n+If `--filter=auto` is used, the filter specification is determined\n+automatically by combining the filter specifications advertised by\n+the server for the promisor remotes that the client accepts (see\n+linkgit:gitprotocol-v2[5] and the `promisor.acceptFromServer`\n+configuration option in linkgit:git-config[1]).\n++\n+For details on all other available filter specifications, see the\n+`--filter=<filter-spec>` option in linkgit:git-rev-list[1].\n++\n+For example, `--filter=blob:none` will filter out all blobs (file\n+contents) until needed by Git. Also, `--filter=blob:limit=<size>` will\n+filter out all blobs of size at least _<size>_.\n \n ifndef::git-pull[]\n `--write-fetch-head`::\ndiff --git a/Documentation/git-clone.adoc b/Documentation/git-clone.adoc\nindex 57cdfb7620..0db2d1e5f0 100644\n--- a/Documentation/git-clone.adoc\n+++ b/Documentation/git-clone.adoc\n@@ -187,11 +187,26 @@ objects from the source repository into a pack in the cloned repository.\n \tUse the partial clone feature and request that the server sends\n \ta subset of reachable objects according to a given object filter.\n \tWhen using `--filter`, the supplied _<filter-spec>_ is used for\n-\tthe partial clone filter. For example, `--filter=blob:none` will\n-\tfilter out all blobs (file contents) until needed by Git. Also,\n-\t`--filter=blob:limit=<size>` will filter out all blobs of size\n-\tat least _<size>_. For more details on filter specifications, see\n-\tthe `--filter` option in linkgit:git-rev-list[1].\n+\tthe partial clone filter.\n++\n+If `--filter=auto` is used the filter specification is determined\n+automatically through the 'promisor-remote' protocol (see\n+linkgit:gitprotocol-v2[5]) by combining the filter specifications\n+advertised by the server for the promisor remotes that the client\n+accepts (see the `promisor.acceptFromServer` configuration option in\n+linkgit:git-config[1]). This allows the server to suggest the optimal\n+filter for the available promisor remotes.\n++\n+As with other filter specifications, the \"auto\" value is persisted in\n+the configuration. This ensures that future fetches will continue to\n+adapt to the server's current recommendation.\n++\n+For details on all other available filter specifications, see the\n+`--filter=<filter-spec>` option in linkgit:git-rev-list[1].\n++\n+For example, `--filter=blob:none` will filter out all blobs (file\n+contents) until needed by Git. Also, `--filter=blob:limit=<size>` will\n+filter out all blobs of size at least _<size>_.\n \n `--also-filter-submodules`::\n \tAlso apply the partial clone filter to any submodules in the repository.\ndiff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\nindex d93dd279ea..f985cb4c47 100644\n--- a/Documentation/gitprotocol-v2.adoc\n+++ b/Documentation/gitprotocol-v2.adoc\n@@ -812,10 +812,15 @@ MUST appear first in each pr-fields, in that order.\n After these mandatory fields, the server MAY advertise the following\n optional fields in any order:\n \n-`partialCloneFilter`:: The filter specification used by the remote.\n+`partialCloneFilter`:: The filter specification for the remote. It\n+corresponds to the \"remote.<name>.partialCloneFilter\" config setting.\n Clients can use this to determine if the remote's filtering strategy\n-is compatible with their needs (e.g., checking if both use \"blob:none\").\n-It corresponds to the \"remote.<name>.partialCloneFilter\" config setting.\n+is compatible with their needs (e.g., checking if both use\n+\"blob:none\"). Additionally they can use this through the\n+`--filter=auto` option in linkgit:git-clone[1]. With that option, the\n+filter specification of the clone will be automatically computed by\n+combining the filter specifications of the promisor remotes the client\n+accepts.\n \n `token`:: An authentication token that clients can use when\n connecting to the remote. It corresponds to the \"remote.<name>.token\"\n@@ -828,8 +833,9 @@ future protocol extensions.\n \n The client can use information transmitted through these fields to\n decide if it accepts the advertised promisor remote. Also, the client\n-can be configured to store the values of these fields (see\n-\"promisor.storeFields\" in linkgit:git-config[1]).\n+can be configured to store the values of these fields or use them\n+to automatically configure the repository (see \"promisor.storeFields\"\n+in linkgit:git-config[1] and `--filter=auto` in linkgit:git-clone[1]).\n \n Field values MUST be urlencoded.\n \ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex bb27472020..45d8fa0eed 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -1001,6 +1001,8 @@ int cmd_clone(int argc,\n \t\tNULL\n \t};\n \n+\tfilter_options.allow_auto_filter = 1;\n+\n \tpacket_trace_identity(\"clone\");\n \n \trepo_config(the_repository, git_clone_config, NULL);\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 8fbf3557ce..573c295241 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -2580,6 +2580,8 @@ int cmd_fetch(int argc,\n \t\tOPT_END()\n \t};\n \n+\tfilter_options.allow_auto_filter = 1;\n+\n \tpacket_trace_identity(\"fetch\");\n \n \t/* Record the command line for the reflog */\ndiff --git a/fetch-pack.c b/fetch-pack.c\nindex 40316c9a34..9f8f980516 100644\n--- a/fetch-pack.c\n+++ b/fetch-pack.c\n@@ -35,6 +35,7 @@\n #include \"sigchain.h\"\n #include \"mergesort.h\"\n #include \"prio-queue.h\"\n+#include \"promisor-remote.h\"\n \n static int transfer_unpack_limit = -1;\n static int fetch_unpack_limit = -1;\n@@ -1661,6 +1662,29 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,\n \tstruct string_list packfile_uris = STRING_LIST_INIT_DUP;\n \tint i;\n \tstruct strvec index_pack_args = STRVEC_INIT;\n+\tconst char *promisor_remote_config;\n+\n+\tif (server_feature_v2(\"promisor-remote\", &promisor_remote_config))\n+\t\tpromisor_remote_reply(promisor_remote_config, NULL);\n+\n+\tif (args->filter_options.choice == LOFC_AUTO) {\n+\t\tstruct strbuf errbuf = STRBUF_INIT;\n+\t\tchar *constructed_filter = promisor_remote_construct_filter(r);\n+\n+\t\tlist_objects_filter_release(&args->filter_options);\n+\t\t/* Disallow 'auto' as a result of the resolution of this 'auto' filter below */\n+\t\targs->filter_options.allow_auto_filter = 0;\n+\n+\t\tif (constructed_filter &&\n+\t\t    gently_parse_list_objects_filter(&args->filter_options,\n+\t\t\t\t\t\t     constructed_filter,\n+\t\t\t\t\t\t     &errbuf))\n+\t\t\tdie(_(\"couldn't resolve 'auto' filter '%s': %s\"),\n+\t\t\t    constructed_filter, errbuf.buf);\n+\n+\t\tfree(constructed_filter);\n+\t\tstrbuf_release(&errbuf);\n+\t}\n \n \tnegotiator = &negotiator_alloc;\n \tif (args->refetch)\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex 6ef6431bd7..532e6f0fea 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -423,6 +423,66 @@ test_expect_success \"clone with promisor.storeFields=partialCloneFilter\" '\n \ttest_grep \"'\\''blob:limit=8k'\\'' -> '\\''blob:limit=7k'\\''\" err\n '\n \n+test_expect_success \"clone and fetch with --filter=auto\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client trace\" &&\n+\n+\tgit -C server config remote.lop.partialCloneFilter \"blob:limit=9500\" &&\n+\ttest_config -C server promisor.sendFields \"partialCloneFilter\" &&\n+\n+\tGIT_TRACE_PACKET=\"$(pwd)/trace\" GIT_NO_LAZY_FETCH=0 git clone \\\n+\t\t-c remote.lop.promisor=true \\\n+\t\t-c remote.lop.url=\"file://$(pwd)/lop\" \\\n+\t\t-c promisor.acceptfromserver=All \\\n+\t\t--no-local --filter=auto server client 2>err &&\n+\n+\ttest_grep \"filter blob:limit=9500\" trace &&\n+\ttest_grep ! \"filter auto\" trace &&\n+\n+\t# Verify \"auto\" is persisted in config\n+\techo auto >expected &&\n+\tgit -C client config remote.origin.partialCloneFilter >actual &&\n+\ttest_cmp expected actual &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\" &&\n+\n+\t# Now change the filter on the server\n+\tgit -C server config remote.lop.partialCloneFilter \"blob:limit=5678\" &&\n+\n+\t# Get a new commit on the server to ensure \"git fetch\" actually runs fetch-pack\n+\ttest_commit -C template new-commit &&\n+\tgit -C template push --all \"$(pwd)/server\" &&\n+\n+\t# Perform a fetch WITH --filter=auto\n+\trm -rf trace &&\n+\tGIT_TRACE_PACKET=\"$(pwd)/trace\" git -C client fetch --filter=auto &&\n+\n+\t# Verify that the new filter was used\n+\ttest_grep \"filter blob:limit=5678\" trace &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\" &&\n+\n+\t# Change the filter on the server again\n+\tgit -C server config remote.lop.partialCloneFilter \"blob:limit=5432\" &&\n+\n+\t# Get yet a new commit on the server to ensure fetch-pack runs\n+\ttest_commit -C template yet-a-new-commit &&\n+\tgit -C template push --all \"$(pwd)/server\" &&\n+\n+\t# Perform a fetch WITHOUT --filter=auto\n+\t# Relies on \"auto\" being persisted in the client config\n+\trm -rf trace &&\n+\tGIT_TRACE_PACKET=\"$(pwd)/trace\" git -C client fetch &&\n+\n+\t# Verify that the new filter was used\n+\ttest_grep \"filter blob:limit=5432\" trace &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\"\n+'\n+\n test_expect_success \"clone with promisor.advertise set to 'true' but don't delete the client\" '\n \tgit -C server config promisor.advertise true &&\n \ndiff --git a/transport.c b/transport.c\nindex c7f06a7382..cde8d83a57 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1219,6 +1219,7 @@ struct transport *transport_get(struct remote *remote, const char *url)\n \t\t */\n \t\tstruct git_transport_data *data = xcalloc(1, sizeof(*data));\n \t\tlist_objects_filter_init(&data->options.filter_options);\n+\t\tdata->options.filter_options.allow_auto_filter = 1;\n \t\tret->data = data;\n \t\tret->vtable = &builtin_smart_vtable;\n \t\tret->smart_options = &(data->options);\n-- \n2.53.0.77.g4627d513d6\n\n"},{"id":"536106","messageId":"CAP8UFD3B6ZVdp_YbjEJigiRKE5Y=i8svu5AOcUCvJMxuZQuH6w@mail.gmail.com","threadId":"64670","inReplyTo":"20260214023509.GA3684377@coredump.intra.peff.net","subject":"Re: [PATCH v3 6/9] list-objects-filter-options: support 'auto' mode for --filter","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-16T13:26:04Z","receivedAt":"2026-02-16T13:26:17Z","isPatch":true,"body":"On Sat, Feb 14, 2026 at 3:35 AM Jeff King <peff@peff.net> wrote:\n>\n> On Thu, Feb 12, 2026 at 11:08:37AM +0100, Christian Couder wrote:\n>\n> > @@ -317,6 +346,7 @@ void list_objects_filter_release(\n> >       struct list_objects_filter_options *filter_options)\n> >  {\n> >       size_t sub;\n> > +     unsigned int allow_auto_filter = filter_options->allow_auto_filter;\n> >\n> >       if (!filter_options)\n> >               return;\n>\n> This will segfault if anybody passes in a NULL filter_options, before we\n> get to the NULL check in the context.\n>\n> I don't think anybody does this in practice, but probably we should\n> either remove the NULL check, or you should push the assignment of your\n> local variable down below it.\n\nThanks Peff, I have moved the assignment of the local variable below\nthe NULL check.\n\nA v4 with this single change compared to v3 has just been sent.\n"},{"id":"542371","messageId":"20260427124108.3524129-1-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20251223111113.47473-1-christian.couder@gmail.com","subject":"[PATCH v2 0/8] Auto-configure advertised remotes via URL allowlist","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-04-27T12:41:00Z","receivedAt":"2026-04-27T12:41:29Z","isPatch":true,"body":"Currently, the \"promisor-remote\" protocol capability allows a server\nto advertise promisor remotes (and their tokens/filters), but the\nclient's `promisor.acceptFromServer` mechanism requires these remotes\nto already exist in the config.\n\nThis is a significant burden for users and administrators who have to\npre-configure remotes.\n\nThis patch series improves on this by introducing a new\n`promisor.acceptFromServerUrl` config option, which provides an\nadditive, URL-based security allowlist.\n\nMultiple `promisor.acceptFromServerUrl` config options can be provided\nin different config files. Each one should contain a URL glob pattern\nwhich can optionally be prefixed with a remote name in the\n\"[<name>=]<pattern>\" format.\n\nThe goal is for something like a simple:\n\n  git config set --global promisor.acceptFromServerUrl \"https://my-org.com/*\"\n\nto be all that is needed for internal work in many organizations. \n\nWith this new config option:\n\n - The server can update fields (like tokens) for known remotes,\n   provided their URL matches the allowlist, even if\n   `acceptFromServer` is set to `None`.\n\n - Unknown remotes advertised by the server can be automatically\n   configured on the client if their URL matches the allowlist.\n\n - If there is no `<name>` prefix before the glob pattern matched, the\n   auto-configured remote is named using the\n   \"promisor-auto-<sanitized-url>\" format. So the same auto-configured\n   remote config entry will be reused for the same URL.\n\n - If a `<name>` prefix is provided, it will be used for the\n   auto-configured remote config entry.\n\n - If the chosen name (auto-generated or prefixed) already exists but\n   points to a different URL, overwriting the existing config is\n   prevented by appending a numeric suffix (e.g., -1, -2) to the name\n   and auto-configuring using that name.\n\n - The server's originally advertised name is always saved in the\n   `remote.<name>.advertisedAs` config variable of the auto-configured\n   remote for tracing and debugging.\n\nSecurity considerations:\n\n - Advertised URLs and glob patterns are routed through\n   url_normalize() / url_normalize_pattern() before matching, to\n   prevent percent-encoding, case variation, or path-traversal (..)\n   bypasses.\n\n - URL matching is done component by component: scheme and port\n   must match exactly (no wildcards), the host is matched with\n   WM_PATHNAME so a '*' cannot cross the '/' boundary into the\n   path, and the path is matched without WM_PATHNAME so '*' can\n   still span multi-level paths.\n\n - Auto-generated remote names are sanitized (non-alphanumeric\n   characters are replaced with '-', runs of '-' are collapsed)\n   and prefixed with 'promisor-auto-'. User-supplied names (from\n   the 'name=<pattern>' syntax) are validated with\n   valid_remote_name(). Together, these prevent a server from\n   maliciously overwriting standard remotes (like 'origin').\n\n - If the auto-generated or user-supplied name collides with an\n   existing remote configured to a different URL, a numeric\n   suffix ('-1', '-2', ...) is appended, up to a bounded limit,\n   so a server cannot hijack an existing remote by name.\n\n - Known remotes are still subject to URL consistency checks:\n   even if an advertised URL matches the allowlist, it is only\n   accepted for a known remote if it matches the URL already\n   configured locally for that remote.\n\n - The documentation explains in detail how to write secure glob\n   patterns in `promisor.acceptFromServerUrl`, and highlights the\n   risks of overly broad patterns on shared hosting platforms.\n\nHigh level description of the patches\n=====================================\n\n - Patch 1/8 is new. It is a very small preparatory patch that\n   simplifies some tests a bit.\n\n - Patches 2/8 and 3/8 expose and adapt a url_normalize_pattern()\n   helper function in the urlmatch API.\n\n - Patch 4/8 adapts `struct promisor_info` by adding a new\n   `local_name` member to it to prepare for the next patches.\n\n - Patches 5/8 to 7/8 implement the core feature. They introduce the\n   parsing machinery, add the additive allowlist for known remotes\n   (with url_normalize() security), and finally implement the\n   auto-creation and collision resolution for unknown remotes.\n\n - Patch 8/8 cleans up and modernizes the existing\n   `promisor.acceptFromServer` documentation.\n\nChanges compared to v1\n======================\n\nThanks to Patrick and Junio for reviewing the previous versions of\nthis series and of the preparatory series.\n\n - A lot of preparatory patches have been moved to a preparatory series\n   that has already been merged. See:\n\n   https://lore.kernel.org/git/20260407115243.358642-1-christian.couder@gmail.com/\n\n   This is why this v2 contains only 8 patches compared to 16 patches\n   in v1.\n\n - Everywhere in this series \"whitelist\" as been replaced with\n   \"allowlist\".\n\n - In the tests added in this series, the new $TRASH_DIRECTORY_URL and\n   $ENCODED_TRASH_DIRECTORY_URL introduced by the preparatory series\n   are used instead of the previous $PWD_URL and $ENCODED_PWD_URL.\n\n - Patch 1/8 (\"t5710: simplify 'mkdir X' followed by 'git -C X init'\")\n   is new.\n\n - Patch 3/8 (\"urlmatch: add url_normalize_pattern() helper\") replaces\n   patch 3/16 (\"urlmatch: add url_is_valid_pattern() helper\") because\n   in subsequent patches we now normalize patterns to validate them\n   and match them component by component against URLs.\n\n - In patch 5/8, previously 13/16, (\"promisor-remote: introduce\n   promisor.acceptFromServerUrl\"):\n\n   - We add a `struct url_info pattern_info;` to `struct allowed_url`,\n     so we can validate patterns using url_normalize_pattern() and, in\n     a subsequent patch, match URLs component by component. This\n     requires a new allowed_url_free() function that is passed to\n     string_list_clear_func() to clear the `struct allowed_url`\n     instances.\n\n   - We don't use a `static struct string_list` to store the URL\n     patterns we accept. Instead we load them from the config into a\n     `struct string_list` passed as argument. The function doing this\n     is renamed accordingly from accept_from_server_url() to\n     load_accept_from_server_url().\n\n   - A \"clone with invalid promisor.acceptFromServerUrl\" test is moved\n     from patch 15/16 to this patch as it's more relevant in this\n     patch (where we validate the content of the\n     `promisor.acceptFromServerUrl` environment variable).\n\n - In patch 6/8, previously 14/16, (\"promisor-remote: trust known\n   remotes matching acceptFromServerUrl\"):\n\n   - In the commit message, an example, which shows how the new\n     \"acceptFromServerUrl\" config option can be useful, is added.\n\n   - The matching of URLs advertised by the server to URLs patterns\n     from the config, is now performed component by component. This is\n     reflected in the commit message, the documentation and the\n     code. This ensures a `*` in the host pattern cannot cross into\n     the path.\n\n   - In the code, we add a new match_one_url() function to perform the\n     matching.\n\n - In patch 7/8, previously 15/16 (\"promisor-remote: auto-configure\n   unknown remotes\"):\n\n   - In the doc, the unclear \"considered trusted by the client\" is\n     clarified using \"a client is allowed to act on\" and subsequent\n     explanations. In general the doc is also improved a bit. \n\n   - In the tests, parsing the \"remote.<name>.advertisedAs\" config\n     option is now more careful about the possibility that more than\n     one such options exist.\n\n   - The test that was moved to patch 5/8 is still enhanced a bit in\n     this commit by checking that no \"remote.<name>.advertisedAs\"\n     config option has been added.\n\nCI tests\n========\n\nThey all pass, see:\n\nhttps://github.com/chriscool/git/actions/runs/24992478331\n\nRange diff since v1\n===================\n\n 1:  b2894eb33a <  -:  ---------- promisor-remote: try accepted remotes before others in get_direct()\n -:  ---------- >  1:  44e9a16455 t5710: simplify 'mkdir X' followed by 'git -C X init'\n 2:  a3206a6ae9 =  2:  42f174910c urlmatch: change 'allow_globs' arg to bool\n 3:  51bbf65c52 <  -:  ---------- urlmatch: add url_is_valid_pattern() helper\n 4:  f367beef72 <  -:  ---------- promisor-remote: clarify that a remote is ignored\n 5:  1faf74cb3f <  -:  ---------- promisor-remote: refactor has_control_char()\n 6:  40cf0af639 <  -:  ---------- promisor-remote: refactor accept_from_server()\n 7:  b75dca8037 <  -:  ---------- promisor-remote: keep accepted promisor_info structs alive\n 8:  f5e55dc407 <  -:  ---------- promisor-remote: remove the 'accepted' strvec\n -:  ---------- >  3:  8088374458 urlmatch: add url_normalize_pattern() helper\n 9:  63c1db30de !  4:  6bfda89a79 promisor-remote: add 'local_name' to 'struct promisor_info'\n    @@ Commit message\n         In a following commit, we will store promisor remote information under\n         a remote name different than the one the server advertised.\n     \n    -    To prepare for this change, let's add a new 'char* local_name' member\n    +    To prepare for this change, let's add a new 'char *local_name' member\n         to 'struct promisor_info', and let's update the related functions.\n     \n         While at it, let's also add a small promisor_info_internal_name()\n    @@ Commit message\n     \n      ## promisor-remote.c ##\n     @@ promisor-remote.c: static struct string_list *fields_stored(void)\n    - \n    - /*\n       * Struct for promisor remotes involved in the \"promisor-remote\"\n    -- * protocol capability.\n    -+ * protocol capability:\n    +  * protocol capability.\n       *\n     - * Except for \"name\", each <member> in this struct and its <value>\n     - * should correspond (either on the client side or on the server side)\n     - * to a \"remote.<name>.<member>\" config variable set to <value> where\n     - * \"<name>\" is a promisor remote name.\n    -+ * - \"name\" is the name the server advertised.\n    -+ * - \"local_name\" is the name we use locally (may be auto-generated).\n    -+ *\n     + * Except for \"name\" and \"local_name\", each <member> in this struct\n     + * and its <value> should correspond (either on the client side or on\n     + * the server side) to a \"remote.<name>.<member>\" config variable set\n     + * to <value> where \"<name>\" is a promisor remote name.\n       */\n      struct promisor_info {\n    - \tconst char *name;\n    -+\tconst char *local_name;\n    +-\tconst char *name;\n    ++\tconst char *name;\t/* name the server advertised */\n    ++\tconst char *local_name;\t/* name used locally (may be auto-generated) */\n      \tconst char *url;\n      \tconst char *filter;\n      \tconst char *token;\n10:  e9b8a64ab8 <  -:  ---------- promisor-remote: pass config entry to all_fields_match() directly\n11:  2e1260190a <  -:  ---------- promisor-remote: refactor should_accept_remote() control flow\n12:  b33f06173a <  -:  ---------- t5710: use proper file:// URIs for absolute paths\n13:  681b03e248 !  5:  fefa17e6dd promisor-remote: introduce promisor.acceptFromServerUrl\n    @@ promisor-remote.c: static bool has_control_char(const char *s)\n     +struct allowed_url {\n     +\tchar *remote_name;\n     +\tchar *url_pattern;\n    ++\tstruct url_info pattern_info;\n     +};\n     +\n    ++static void allowed_url_free(void *util, const char *str UNUSED)\n    ++{\n    ++\tstruct allowed_url *allowed = util;\n    ++\n    ++\tif (!allowed)\n    ++\t\treturn;\n    ++\n    ++\t/* Depending on prefix, free either remote_name or url_pattern */\n    ++\tfree(allowed->remote_name ? allowed->remote_name : allowed->url_pattern);\n    ++\tfree(allowed->pattern_info.url);\n    ++\tfree(allowed);\n    ++}\n    ++\n     +static struct allowed_url *valid_accept_url(const char *url)\n     +{\n     +\tchar *dup, *p;\n    @@ promisor-remote.c: static bool has_control_char(const char *s)\n     +\t\tp = dup;\n     +\t}\n     +\n    -+\tif (has_control_char(p) || !url_is_valid_pattern(p)) {\n    ++\tif (has_control_char(p)) {\n     +\t\twarning(_(\"invalid url pattern '%s' \"\n     +\t\t\t  \"in '%s' from promisor.acceptFromServerUrl config\"), p, url);\n     +\t\tfree(dup);\n    @@ promisor-remote.c: static bool has_control_char(const char *s)\n     +\tallowed = xmalloc(sizeof(*allowed));\n     +\tallowed->remote_name = (p == dup) ? NULL : dup;\n     +\tallowed->url_pattern = p;\n    ++\tallowed->pattern_info.url = url_normalize_pattern(p, &allowed->pattern_info);\n    ++\tif (!allowed->pattern_info.url) {\n    ++\t\twarning(_(\"invalid url pattern '%s' \"\n    ++\t\t\t  \"in '%s' from promisor.acceptFromServerUrl config\"), p, url);\n    ++\t\tfree(dup);\n    ++\t\tfree(allowed);\n    ++\t\treturn NULL;\n    ++\t}\n     +\n     +\treturn allowed;\n     +}\n     +\n    -+static struct string_list *accept_from_server_url(struct repository *repo)\n    ++static void load_accept_from_server_url(struct repository *repo,\n    ++\t\t\t\t\tstruct string_list *accept_urls)\n     +{\n    -+\tstatic struct string_list accept_urls = STRING_LIST_INIT_DUP;\n    -+\tstatic int initialized;\n     +\tconst struct string_list *config_urls;\n     +\n    -+\tif (initialized)\n    -+\t\treturn &accept_urls;\n    -+\n    -+\tinitialized = 1;\n    -+\n     +\tif (!repo_config_get_string_multi(repo, \"promisor.acceptfromserverurl\", &config_urls)) {\n     +\t\tstruct string_list_item *item;\n     +\n    @@ promisor-remote.c: static bool has_control_char(const char *s)\n     +\t\t\tstruct allowed_url *allowed = valid_accept_url(item->string);\n     +\t\t\tif (allowed) {\n     +\t\t\t\tstruct string_list_item *new;\n    -+\t\t\t\tnew = string_list_append(&accept_urls, item->string);\n    ++\t\t\t\tnew = string_list_append(accept_urls, item->string);\n     +\t\t\t\tnew->util = allowed;\n     +\t\t\t}\n     +\t\t}\n     +\t}\n    -+\n    -+\treturn &accept_urls;\n     +}\n     +\n      static int should_accept_remote(enum accept_promisor accept,\n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n      \tstruct string_list_item *item;\n      \tbool reload_config = false;\n      \tenum accept_promisor accept = accept_from_server(repo);\n    -+\t/* Pre-load and validate the acceptFromServerUrl config */\n    -+\t(void)accept_from_server_url(repo);\n    ++\tstruct string_list accept_urls = STRING_LIST_INIT_DUP;\n    ++\n    ++\t/* Load and validate the acceptFromServerUrl config */\n    ++\tload_accept_from_server_url(repo, &accept_urls);\n      \n      \tif (accept == ACCEPT_NONE)\n      \t\treturn;\n    +@@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n    + \t\t}\n    + \t}\n    + \n    ++\tstring_list_clear_func(&accept_urls, allowed_url_free);\n    + \tpromisor_info_list_clear(&config_info);\n    + \tstring_list_clear(&remote_info, 0);\n    + \tstore_info_free(store_info);\n    +\n    + ## t/t5710-promisor-remote-capability.sh ##\n    +@@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'KnownUrl' and empty url, so not advertised\" '\n    + \tcheck_missing_objects server 1 \"$oid\"\n    + '\n    + \n    ++test_expect_success \"clone with invalid promisor.acceptFromServerUrl\" '\n    ++\tgit -C server config promisor.advertise true &&\n    ++\ttest_when_finished \"rm -rf client\" &&\n    ++\n    ++\t# As \"bad name\" contains a space, which is not a valid remote name,\n    ++\t# the pattern should be rejected with a warning and no remote created.\n    ++\tGIT_NO_LAZY_FETCH=0 git clone \\\n    ++\t\t-c promisor.acceptfromserver=None \\\n    ++\t\t-c \"promisor.acceptFromServerUrl=bad name=https://example.com/*\" \\\n    ++\t\t--no-local --filter=\"blob:limit=5k\" server client 2>err &&\n    ++\n    ++\t# Check that a warning was emitted\n    ++\ttest_grep \"invalid remote name '\\''bad name'\\''\" err &&\n    ++\n    ++\t# Check that the largest object is not missing on the server\n    ++\tcheck_missing_objects server 0 \"\" &&\n    ++\n    ++\t# Reinitialize server so that the largest object is missing again\n    ++\tinitialize_server 1 \"$oid\"\n    ++'\n    ++\n    + test_expect_success \"clone with promisor.sendFields\" '\n    + \tgit -C server config promisor.advertise true &&\n    + \ttest_when_finished \"rm -rf client\" &&\n14:  8c04e48d66 !  6:  2f238d0a7a promisor-remote: trust known remotes matching acceptFromServerUrl\n    @@ Commit message\n     \n         To enable such targeted updates for trusted URLs, let's use the URL\n         patterns from `promisor.acceptFromServerUrl` as an additional URL\n    -    based whitelist.\n    +    based allowlist.\n     \n         Concretely, let's check the advertised URLs against the URL glob\n         patterns by introducing a new small helper function called\n         url_matches_accept_list(), which iterates over the glob patterns and\n         returns the first matching allowed_url entry (or NULL).\n     \n    -    (Before matching, the advertised URL is passed through url_normalize()\n    -    so that case variations in the scheme/host, percent-encoding tricks,\n    -    and \"..\" path segments cannot bypass the whitelist.)\n    +    The URL matching is done component by component: scheme and port are\n    +    compared exactly, the host is matched with wildmatch() using the\n    +    WM_PATHNAME flag (so '*' cannot cross the '/' boundary into the path),\n    +    and the path is matched with wildmatch() without WM_PATHNAME (so '*'\n    +    can still match multi-level paths). Before matching, the advertised\n    +    URL is passed through url_normalize() so that case variations in the\n    +    scheme/host, percent-encoding tricks, and \"..\" path segments cannot\n    +    bypass the allowlist.\n     \n         Let's then use this helper at the tail of should_accept_remote() so\n         that, when `accept == ACCEPT_NONE`, a known remote whose URL matches\n    -    the whitelist is still accepted.\n    +    the allowlist is still accepted.\n     \n         To prepare for this new logic, let's also:\n     \n    @@ Commit message\n            and relax its early return so that the function is entered when\n            `accept_urls` has entries even if `accept == ACCEPT_NONE`.\n     \n    +    With this, many organizations may only need something like:\n    +\n    +      git config set --global \\\n    +              promisor.acceptFromServerUrl \"https://my-org.com/*\"\n    +\n    +    to accept only their own remotes. And if they need to accept additional\n    +    remotes in some specific repos, they can also set:\n    +\n    +      git config set promisor.acceptFromServer knownUrl\n    +\n    +    and configure the additional remote manually only in the repos where\n    +    they are needed.\n    +\n         Let's then properly document `promisor.acceptFromServerUrl` in\n    -    \"promisor.adoc\" as an additive security whitelist for known remotes,\n    -    including the URL normalization behavior, and let's mention it in\n    -    \"gitprotocol-v2.adoc\".\n    +    \"promisor.adoc\" as an additive security allowlist for known remotes,\n    +    including the URL normalization behavior and the component-wise\n    +    matching, and let's mention it in \"gitprotocol-v2.adoc\".\n     \n         Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n     \n    @@ Documentation/config/promisor.adoc: promisor.acceptFromServer::\n      \tcomparisons are case sensitive. See linkgit:gitprotocol-v2[5].\n      \n     +promisor.acceptFromServerUrl::\n    -+\tA glob pattern to specify which URLs advertised by a server\n    -+\tare considered trusted by the client. This option acts as an\n    -+\tadditive security whitelist that works in conjunction with\n    -+\t`promisor.acceptFromServer`.\n    ++\tA glob pattern to specify which server-advertised URLs a\n    ++\tclient is allowed to act on. When a URL matches, the client\n    ++\twill accept the advertised remote as a promisor remote and may\n    ++\tautomatically accept field updates (such as authentication\n    ++\ttokens) from the server, even if `promisor.acceptFromServer`\n    ++\tis set to `none` (the default).\n     ++\n     +This option can appear multiple times in config files. An advertised\n     +URL will be accepted if it matches _ANY_ glob pattern specified by\n     +this option in _ANY_ config file read by Git.\n     ++\n    -+Be _VERY_ careful with these glob patterns, as it can be a big\n    -+security hole to allow any advertised remote to be auto-configured!\n    ++Be _VERY_ careful with these patterns: `*` matches any sequence of\n    ++characters within the 'host' and 'path' parts of a URL (but cannot\n    ++cross part boundaries). An overly broad pattern is a major security\n    ++risk, as a matching URL allows a server to update fields (such as\n    ++authentication tokens) on known remotes without further confirmation.\n     +To minimize security risks, follow these guidelines:\n     ++\n     +1. Start with a secure protocol scheme, like `https://` or `ssh://`.\n    @@ Documentation/config/promisor.adoc: promisor.acceptFromServer::\n     +   your specific organization or namespace (e.g.,\n     +   `https://gitlab.com/your-org/*`).\n     ++\n    -+3. Don't use globs (`*`) in the domain name. For example\n    -+   `https://cdn.example.com/*` is much safer than\n    -+   `https://*.example.com/*`, because the latter matches\n    -+   `https://evil-hacker.net/fake.example.com/repo`.\n    ++3. Never use globs at the end of domain names. For example,\n    ++   `https://cdn.your-org.com/*` might be safe, but\n    ++   `https://cdn.your-org.com*/*` is a major security risk because\n    ++   the latter matches `https://cdn.your-org.com.hacker.net/repo`.\n     ++\n    -+4. Make sure to have a `/` at the end of the domain name (or the end\n    -+   of specific directories). For example `https://cdn.example.com/*`\n    -+   is much safer than `https://cdn.example.com*`, because the latter\n    -+   matches `https://cdn.example.com.hacker.net/repo`.\n    ++4. Be careful using globs at the beginning of domain names. While the\n    ++   code ensures a `*` in the host cannot cross into the path, a\n    ++   pattern like `https://*.example.com/*` will still match any\n    ++   subdomain. This is extremely dangerous on shared hosting platforms\n    ++   (e.g., `https://*.github.io/*` trusts every user's site on the\n    ++   entire platform).\n     ++\n    -+Before matching, the advertised URL is normalized: the scheme and\n    -+host are lowercased, percent-encoded characters are decoded where\n    -+possible, and path segments like `..` are resolved.  Glob patterns\n    -+are matched against this normalized URL as-is, so patterns should\n    -+be written in normalized form (e.g., lowercase scheme and host).\n    ++Before matching, both the advertised URL and the pattern are\n    ++normalized: the scheme and host are lowercased, percent-encoded\n    ++characters are decoded where possible, and path segments like `..`\n    ++are resolved. The port must also match exactly (e.g.,\n    ++`https://example.com:8080/*` will not match a URL advertised on\n    ++port 9999).\n     ++\n    -+Even if `promisor.acceptFromServer` is set to `None` (the default),\n    -+Git will still accept field updates (like tokens) for known remotes,\n    -+provided their URLs match a pattern in\n    -+`promisor.acceptFromServerUrl`. See linkgit:gitprotocol-v2[5] for\n    -+details on the protocol.\n    ++For the security implications of accepting a promisor remote, see the\n    ++documentation of `promisor.acceptFromServer`. For details on the\n    ++protocol, see linkgit:gitprotocol-v2[5].\n     +\n      promisor.checkFields::\n      \tA comma or space separated list of additional remote related\n    @@ promisor-remote.c\n      \n      struct promisor_remote_config {\n      \tstruct promisor_remote *promisors;\n    -@@ promisor-remote.c: static struct string_list *accept_from_server_url(struct repository *repo)\n    - \treturn &accept_urls;\n    +@@ promisor-remote.c: static void load_accept_from_server_url(struct repository *repo,\n    + \t}\n      }\n      \n    ++static bool match_one_url(const struct url_info *pi, const struct url_info *ui)\n    ++{\n    ++\tconst char *pat = pi->url;\n    ++\tconst char *url = ui->url;\n    ++\tchar *p_str, *u_str;\n    ++\tbool res;\n    ++\n    ++\t/*\n    ++\t * Schemes must match exactly. They are case-folded by\n    ++\t * url_normalize(), so strncmp() suffices.\n    ++\t */\n    ++\tif (pi->scheme_len != ui->scheme_len || strncmp(pat, url, pi->scheme_len))\n    ++\t\treturn false;\n    ++\n    ++\t/*\n    ++\t * Ports must match exactly. url_normalize() strips default\n    ++\t * ports (like 443 for https), so length and content\n    ++\t * comparisons are sufficient.\n    ++\t */\n    ++\tif (pi->port_len != ui->port_len ||\n    ++\t    strncmp(pat + pi->port_off, url + ui->port_off, pi->port_len))\n    ++\t\treturn false;\n    ++\n    ++\t/*\n    ++\t * Match host and path separately to prevent a '*' in the host\n    ++\t * portion of the pattern from matching across the '/'\n    ++\t * boundary into the path. Use WM_PATHNAME for the host so '*'\n    ++\t * cannot cross '/' there, and 0 for the path so '*' can still\n    ++\t * match multi-level paths.\n    ++\t */\n    ++\n    ++\tp_str = xstrndup(pat + pi->host_off, pi->host_len);\n    ++\tu_str = xstrndup(url + ui->host_off, ui->host_len);\n    ++\tres = !wildmatch(p_str, u_str, WM_PATHNAME);\n    ++\tfree(p_str);\n    ++\tfree(u_str);\n    ++\n    ++\tif (!res)\n    ++\t\treturn false;\n    ++\n    ++\tp_str = xstrndup(pat + pi->path_off, pi->path_len);\n    ++\tu_str = xstrndup(url + ui->path_off, ui->path_len);\n    ++\tres = !wildmatch(p_str, u_str, 0);\n    ++\tfree(p_str);\n    ++\tfree(u_str);\n    ++\n    ++\treturn res;\n    ++}\n    ++\n     +static struct allowed_url *url_matches_accept_list(\n     +\t\tstruct string_list *accept_urls, const char *url)\n     +{\n     +\tstruct string_list_item *item;\n    -+\tchar *normalized = url_normalize(url, NULL);\n    ++\tstruct url_info url_info;\n    ++\n    ++\turl_info.url = url_normalize(url, &url_info);\n     +\n    -+\tif (!normalized)\n    ++\tif (!url_info.url)\n     +\t\treturn NULL;\n     +\n     +\tfor_each_string_list_item(item, accept_urls) {\n     +\t\tstruct allowed_url *allowed = item->util;\n     +\n    -+\t\tif (!wildmatch(allowed->url_pattern, normalized, 0)) {\n    -+\t\t\tfree(normalized);\n    ++\t\tif (match_one_url(&allowed->pattern_info, &url_info)) {\n    ++\t\t\tfree(url_info.url);\n     +\t\t\treturn allowed;\n     +\t\t}\n     +\t}\n     +\n    -+\tfree(normalized);\n    ++\tfree(url_info.url);\n     +\treturn NULL;\n     +}\n     +\n    @@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n     +\t/*\n     +\t * Even if accept == ACCEPT_NONE, we MUST trust this known\n     +\t * remote to update its token or other such fields if its URL\n    -+\t * matches the acceptFromServerUrl whitelist!\n    ++\t * matches the acceptFromServerUrl allowlist!\n     +\t */\n     +\tif (url_matches_accept_list(accept_urls, remote_url))\n     +\t\treturn all_fields_match(advertised, config_info, p);\n    @@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n      \n      static int skip_field_name_prefix(const char *elem, const char *field_name, const char **value)\n     @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n    - \tstruct string_list_item *item;\n    - \tbool reload_config = false;\n    - \tenum accept_promisor accept = accept_from_server(repo);\n    --\t/* Pre-load and validate the acceptFromServerUrl config */\n    --\t(void)accept_from_server_url(repo);\n    -+\tstruct string_list *accept_urls = accept_from_server_url(repo);\n    + \t/* Load and validate the acceptFromServerUrl config */\n    + \tload_accept_from_server_url(repo, &accept_urls);\n      \n     -\tif (accept == ACCEPT_NONE)\n    -+\tif (accept == ACCEPT_NONE && !accept_urls->nr)\n    ++\tif (accept == ACCEPT_NONE && !accept_urls.nr)\n      \t\treturn;\n      \n      \t/* Parse remote info received */\n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n      \t\t}\n      \n     -\t\tif (should_accept_remote(accept, advertised, &config_info)) {\n    -+\t\tif (should_accept_remote(accept, advertised, accept_urls, &config_info)) {\n    ++\t\tif (should_accept_remote(accept, advertised, &accept_urls, &config_info)) {\n      \t\t\tif (!store_info)\n      \t\t\t\tstore_info = store_info_new(repo);\n      \t\t\tif (promisor_store_advertised_fields(advertised, store_info))\n    @@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'KnownUrl\n      \tcheck_missing_objects server 1 \"$oid\"\n      '\n      \n    -+test_expect_success \"clone with 'None' but URL whitelisted\" '\n    ++test_expect_success \"clone with 'None' but URL allowlisted\" '\n     +\tgit -C server config promisor.advertise true &&\n     +\ttest_when_finished \"rm -rf client\" &&\n     +\n     +\tGIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \\\n     +\t\t-c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n    -+\t\t-c remote.lop.url=\"$PWD_URL/lop\" \\\n    ++\t\t-c remote.lop.url=\"$TRASH_DIRECTORY_URL/lop\" \\\n     +\t\t-c promisor.acceptfromserver=None \\\n    -+\t\t-c promisor.acceptFromServerUrl=\"$ENCODED_PWD_URL/*\" \\\n    ++\t\t-c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n     +\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n     +\n     +\t# Check that the largest object is still missing on the server\n     +\tcheck_missing_objects server 1 \"$oid\"\n     +'\n     +\n    -+test_expect_success \"clone with 'None' but URL not in whitelist\" '\n    ++test_expect_success \"clone with 'None' but URL not in allowlist\" '\n     +\tgit -C server config promisor.advertise true &&\n     +\ttest_when_finished \"rm -rf client\" &&\n     +\n     +\tGIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \\\n     +\t\t-c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n    -+\t\t-c remote.lop.url=\"$PWD_URL/lop\" \\\n    ++\t\t-c remote.lop.url=\"$TRASH_DIRECTORY_URL/lop\" \\\n     +\t\t-c promisor.acceptfromserver=None \\\n     +\t\t-c promisor.acceptFromServerUrl=\"https://example.com/*\" \\\n     +\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n    @@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'KnownUrl\n     +\tinitialize_server 1 \"$oid\"\n     +'\n     +\n    -+test_expect_success \"clone with 'None' but URL whitelisted in one pattern out of two\" '\n    ++test_expect_success \"clone with 'None' but URL allowlisted in one pattern out of two\" '\n     +\tgit -C server config promisor.advertise true &&\n     +\ttest_when_finished \"rm -rf client\" &&\n     +\n     +\tGIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \\\n     +\t\t-c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n    -+\t\t-c remote.lop.url=\"$PWD_URL/lop\" \\\n    ++\t\t-c remote.lop.url=\"$TRASH_DIRECTORY_URL/lop\" \\\n     +\t\t-c promisor.acceptfromserver=None \\\n     +\t\t-c promisor.acceptFromServerUrl=\"https://example.com/*\" \\\n    -+\t\t-c promisor.acceptFromServerUrl=\"$ENCODED_PWD_URL/*\" \\\n    ++\t\t-c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n     +\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n     +\n     +\t# Check that the largest object is still missing on the server\n     +\tcheck_missing_objects server 1 \"$oid\"\n     +'\n     +\n    -+test_expect_success \"clone with 'None', URL whitelisted, but client has different URL\" '\n    ++test_expect_success \"clone with 'None', URL allowlisted, but client has different URL\" '\n     +\tgit -C server config promisor.advertise true &&\n     +\ttest_when_finished \"rm -rf client\" &&\n     +\n     +\t# The client configures \"lop\" with a different URL (serverTwo) than\n     +\t# what the server advertises (lop). Even though the advertised URL\n    -+\t# matches the whitelist, the remote is rejected because the\n    ++\t# matches the allowlist, the remote is rejected because the\n     +\t# configured URL does not match the advertised one.\n     +\tGIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \\\n     +\t\t-c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n    -+\t\t-c remote.lop.url=\"$PWD_URL/serverTwo\" \\\n    ++\t\t-c remote.lop.url=\"$TRASH_DIRECTORY_URL/serverTwo\" \\\n     +\t\t-c promisor.acceptfromserver=None \\\n    -+\t\t-c promisor.acceptFromServerUrl=\"$ENCODED_PWD_URL/*\" \\\n    ++\t\t-c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n     +\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n     +\n     +\t# Check that the largest object is not missing on the server\n    @@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'KnownUrl\n     +\tinitialize_server 1 \"$oid\"\n     +'\n     +\n    - test_expect_success \"clone with promisor.sendFields\" '\n    + test_expect_success \"clone with invalid promisor.acceptFromServerUrl\" '\n      \tgit -C server config promisor.advertise true &&\n      \ttest_when_finished \"rm -rf client\" &&\n    -@@ t/t5710-promisor-remote-capability.sh: test_expect_success \"subsequent fetch from a client when promisor.advertise is f\n    - \tcheck_missing_objects server 1 \"$oid\"\n    - '\n    - \n    -+\n    -+\n    - test_done\n15:  314150a860 !  7:  a077f33df4 promisor-remote: auto-configure unknown remotes\n    @@ Commit message\n         promisor-remote: auto-configure unknown remotes\n     \n         Previous commits have introduced the `promisor.acceptFromServerUrl`\n    -    config variable to whitelist some URLs advertised by a server through\n    +    config variable to allowlist some URLs advertised by a server through\n         the \"promisor-remote\" protocol capability.\n     \n         However the new `promisor.acceptFromServerUrl` mechanism, like the old\n    @@ Commit message\n     \n      ## Documentation/config/promisor.adoc ##\n     @@ Documentation/config/promisor.adoc: promisor.acceptFromServer::\n    - \n      promisor.acceptFromServerUrl::\n    - \tA glob pattern to specify which URLs advertised by a server\n    --\tare considered trusted by the client. This option acts as an\n    --\tadditive security whitelist that works in conjunction with\n    --\t`promisor.acceptFromServer`.\n    -+\tare allowed to be auto-configured (created and persisted) on\n    -+\tthe client side. Unlike `promisor.acceptFromServer`, which\n    -+\tonly accepts already configured remotes, a match against this\n    -+\toption instructs Git to write a new `[remote \"<name>\"]`\n    -+\tsection to the client's configuration.\n    + \tA glob pattern to specify which server-advertised URLs a\n    + \tclient is allowed to act on. When a URL matches, the client\n    +-\twill accept the advertised remote as a promisor remote and may\n    ++\twill accept the advertised remote as a promisor remote, may\n    ++\tautomatically create a new remote configuration for it and may\n    + \tautomatically accept field updates (such as authentication\n    + \ttokens) from the server, even if `promisor.acceptFromServer`\n    + \tis set to `none` (the default).\n    +@@ Documentation/config/promisor.adoc: this option in _ANY_ config file read by Git.\n    + Be _VERY_ careful with these patterns: `*` matches any sequence of\n    + characters within the 'host' and 'path' parts of a URL (but cannot\n    + cross part boundaries). An overly broad pattern is a major security\n    +-risk, as a matching URL allows a server to update fields (such as\n    +-authentication tokens) on known remotes without further confirmation.\n    +-To minimize security risks, follow these guidelines:\n    ++risk, as a matching URL allows a server to auto-configure new remotes\n    ++and to update fields (such as authentication tokens) on known remotes\n    ++without further confirmation. To minimize security risks, follow these\n    ++guidelines:\n    + +\n    + 1. Start with a secure protocol scheme, like `https://` or `ssh://`.\n      +\n    - This option can appear multiple times in config files. An advertised\n    - URL will be accepted if it matches _ANY_ glob pattern specified by\n    -@@ Documentation/config/promisor.adoc: possible, and path segments like `..` are resolved.  Glob patterns\n    - are matched against this normalized URL as-is, so patterns should\n    - be written in normalized form (e.g., lowercase scheme and host).\n    +@@ Documentation/config/promisor.adoc: are resolved. The port must also match exactly (e.g.,\n    + `https://example.com:8080/*` will not match a URL advertised on\n    + port 9999).\n      +\n    --Even if `promisor.acceptFromServer` is set to `None` (the default),\n    --Git will still accept field updates (like tokens) for known remotes,\n    --provided their URLs match a pattern in\n    --`promisor.acceptFromServerUrl`. See linkgit:gitprotocol-v2[5] for\n    --details on the protocol.\n     +The glob pattern can optionally be prefixed with a remote name and an\n     +equals sign (e.g., `cdn=https://cdn.example.com/*`). If such a prefix\n     +is provided, accepted remotes will be saved under that name. If no\n     +such prefix is provided, a safe remote name will be automatically\n     +generated by sanitizing the URL and prefixing it with\n    -+`promisor-auto-`. If a remote with the chosen name already exists but\n    -+points to a different URL, Git will append a numeric suffix (e.g.,\n    -+`-1`, `-2`) to the name to prevent overwriting existing\n    -+configurations. You should make sure that this doesn't happen often\n    -+though, as remotes will be rejected if the numeric suffix increases\n    -+too much. In all cases, the original name advertised by the server is\n    -+recorded in the `remote.<name>.advertisedAs` configuration variable\n    -+for tracing and debugging purposes.\n    ++`promisor-auto-`.\n     ++\n    -+Note that this option acts as an additive security whitelist. It works\n    -+in conjunction with `promisor.acceptFromServer` (see the documentation\n    -+of that option for the implications of accepting a promisor\n    -+remote). Even if `promisor.acceptFromServer` is set to `None` (the\n    -+default), Git will still automatically configure new remotes, and\n    -+accept field updates (like tokens) for known remotes, provided their\n    -+URLs match a pattern in `promisor.acceptFromServerUrl`. See\n    -+linkgit:gitprotocol-v2[5] for details on the protocol.\n    - \n    - promisor.checkFields::\n    - \tA comma or space separated list of additional remote related\n    ++If a remote with the chosen name already exists but points to a\n    ++different URL, Git will append a numeric suffix (e.g., `-1`, `-2`) to\n    ++the name to prevent overwriting existing configurations. You should\n    ++make sure that this doesn't happen often though, as remotes will be\n    ++rejected if the numeric suffix increases too much. In all cases, the\n    ++original name advertised by the server is recorded in the\n    ++`remote.<name>.advertisedAs` configuration variable for tracing and\n    ++debugging purposes.\n    +++\n    + For the security implications of accepting a promisor remote, see the\n    + documentation of `promisor.acceptFromServer`. For details on the\n    + protocol, see linkgit:gitprotocol-v2[5].\n     \n      ## Documentation/config/remote.adoc ##\n     @@ Documentation/config/remote.adoc: remote.<name>.promisor::\n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n      \t\t\tstring_list_sort(&config_info);\n      \t\t}\n      \n    --\t\tif (should_accept_remote(accept, advertised, accept_urls, &config_info)) {\n    -+\t\tif (should_accept_remote(repo, accept, advertised, accept_urls,\n    +-\t\tif (should_accept_remote(accept, advertised, &accept_urls, &config_info)) {\n    ++\t\tif (should_accept_remote(repo, accept, advertised, &accept_urls,\n     +\t\t\t\t\t &config_info, &reload_config)) {\n      \t\t\tif (!store_info)\n      \t\t\t\tstore_info = store_info_new(repo);\n      \t\t\tif (promisor_store_advertised_fields(advertised, store_info))\n     \n      ## t/t5710-promisor-remote-capability.sh ##\n    -@@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'None', URL whitelisted, but client has differen\n    +@@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'None', URL allowlisted, but client has differen\n      \tinitialize_server 1 \"$oid\"\n      '\n      \n    -+test_expect_success \"clone with URL whitelisted and no remote already configured\" '\n    ++test_expect_success \"clone with URL allowlisted and no remote already configured\" '\n     +\tgit -C server config promisor.advertise true &&\n     +\ttest_when_finished \"rm -rf client\" &&\n    ++\ttest_when_finished \"rm -f full_names\" &&\n     +\n     +\tGIT_NO_LAZY_FETCH=0 git clone \\\n     +\t\t-c promisor.acceptfromserver=None \\\n    -+\t\t-c promisor.acceptFromServerUrl=\"$ENCODED_PWD_URL/*\" \\\n    ++\t\t-c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n     +\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n     +\n    -+\t# Check that a remote has been auto-created with the right fields.\n    -+\t# The remote is identified by \"remote.<name>.advertisedAs\" == \"lop\".\n    -+\tFULL_NAME=$(git -C client config --name-only --get-regexp \"remote\\..*\\.advertisedas\" \"^lop$\") &&\n    -+\tREMOTE_NAME=$(echo \"$FULL_NAME\" | sed \"s/remote\\.\\(.*\\)\\.advertisedas/\\1/\") &&\n    ++\t# Check that exactly one remote has been auto-created, identified\n    ++\t# by \"remote.<name>.advertisedAs\" == \"lop\".\n    ++\tgit -C client config get --all --show-names --regexp \\\n    ++\t\t\"remote\\..*\\.advertisedas\" >full_names &&\n    ++\ttest_line_count = 1 full_names &&\n    ++\tREMOTE_NAME=$(sed \"s/^remote\\.\\(.*\\)\\.advertisedas .*$/\\1/\" full_names) &&\n     +\n     +\t# Check \".url\" and \".promisor\" values\n    -+\tprintf \"%s\\n\" \"$PWD_URL/lop\" \"true\" >expect &&\n    ++\tprintf \"%s\\n\" \"$TRASH_DIRECTORY_URL/lop\" \"true\" >expect &&\n     +\tgit -C client config \"remote.$REMOTE_NAME.url\" >actual &&\n     +\tgit -C client config \"remote.$REMOTE_NAME.promisor\" >>actual &&\n     +\ttest_cmp expect actual &&\n    @@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'None', U\n     +\tcheck_missing_objects server 1 \"$oid\"\n     +'\n     +\n    -+test_expect_success \"clone with named URL whitelisted and no pre-configured remote\" '\n    ++test_expect_success \"clone with named URL allowlisted and no pre-configured remote\" '\n     +\tgit -C server config promisor.advertise true &&\n     +\ttest_when_finished \"rm -rf client\" &&\n     +\n     +\tGIT_NO_LAZY_FETCH=0 git clone \\\n     +\t\t-c promisor.acceptfromserver=None \\\n    -+\t\t-c promisor.acceptFromServerUrl=\"cdn=$ENCODED_PWD_URL/*\" \\\n    ++\t\t-c promisor.acceptFromServerUrl=\"cdn=$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n     +\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n     +\n     +\t# Check that a remote has been auto-created with the right \"cdn\" name and fields.\n    -+\tprintf \"%s\\n\" \"$PWD_URL/lop\" \"true\" \"lop\" >expect &&\n    ++\tprintf \"%s\\n\" \"$TRASH_DIRECTORY_URL/lop\" \"true\" \"lop\" >expect &&\n     +\tgit -C client config \"remote.cdn.url\" >actual &&\n     +\tgit -C client config \"remote.cdn.promisor\" >>actual &&\n     +\tgit -C client config \"remote.cdn.advertisedAs\" >>actual &&\n    @@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'None', U\n     +\tcheck_missing_objects server 1 \"$oid\"\n     +'\n     +\n    -+test_expect_success \"clone with URL whitelisted but colliding name\" '\n    ++test_expect_success \"clone with URL allowlisted but colliding name\" '\n     +\tgit -C server config promisor.advertise true &&\n     +\ttest_when_finished \"rm -rf client\" &&\n     +\n    @@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'None', U\n     +\t\t-c remote.cdn.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n     +\t\t-c remote.cdn.url=\"https://example.com/cdn\" \\\n     +\t\t-c promisor.acceptfromserver=None \\\n    -+\t\t-c promisor.acceptFromServerUrl=\"cdn=$ENCODED_PWD_URL/*\" \\\n    ++\t\t-c promisor.acceptFromServerUrl=\"cdn=$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n     +\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n     +\n     +\t# Check that a remote has been auto-created with the right \"cdn-1\" name and fields.\n    -+\tprintf \"%s\\n\" \"$PWD_URL/lop\" \"true\" \"lop\" >expect &&\n    ++\tprintf \"%s\\n\" \"$TRASH_DIRECTORY_URL/lop\" \"true\" \"lop\" >expect &&\n     +\tgit -C client config \"remote.cdn-1.url\" >actual &&\n     +\tgit -C client config \"remote.cdn-1.promisor\" >>actual &&\n     +\tgit -C client config \"remote.cdn-1.advertisedAs\" >>actual &&\n    @@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'None', U\n     +\tcheck_missing_objects server 1 \"$oid\"\n     +'\n     +\n    -+test_expect_success \"clone with URL whitelisted and reusable remote\" '\n    ++test_expect_success \"clone with URL allowlisted and reusable remote\" '\n     +\tgit -C server config promisor.advertise true &&\n     +\ttest_when_finished \"rm -rf client\" &&\n     +\n     +\tGIT_NO_LAZY_FETCH=0 git clone \\\n     +\t\t-c remote.cdn.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n    -+\t\t-c remote.cdn.url=\"$PWD_URL/lop\" \\\n    ++\t\t-c remote.cdn.url=\"$TRASH_DIRECTORY_URL/lop\" \\\n     +\t\t-c promisor.acceptfromserver=None \\\n    -+\t\t-c promisor.acceptFromServerUrl=\"cdn=$ENCODED_PWD_URL/*\" \\\n    ++\t\t-c promisor.acceptFromServerUrl=\"cdn=$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n     +\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n     +\n     +\t# Check that the existing \"cdn\" remote has been properly updated.\n    -+\tprintf \"%s\\n\" \"$PWD_URL/lop\" \"true\" \"lop\" \"+refs/heads/*:refs/remotes/lop/*\" >expect &&\n    ++\tprintf \"%s\\n\" \"$TRASH_DIRECTORY_URL/lop\" \"true\" \"lop\" \"+refs/heads/*:refs/remotes/lop/*\" >expect &&\n     +\tgit -C client config \"remote.cdn.url\" >actual &&\n     +\tgit -C client config \"remote.cdn.promisor\" >>actual &&\n     +\tgit -C client config \"remote.cdn.advertisedAs\" >>actual &&\n    @@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'None', U\n     +\tcheck_missing_objects server 1 \"$oid\"\n     +'\n     +\n    -+test_expect_success \"clone with invalid promisor.acceptFromServerUrl\" '\n    -+\tgit -C server config promisor.advertise true &&\n    -+\ttest_when_finished \"rm -rf client\" &&\n    -+\n    -+\t# As \"bad name\" contains a space, which is not a valid remote name,\n    -+\t# the pattern should be rejected with a warning and no remote created.\n    -+\tGIT_NO_LAZY_FETCH=0 git clone \\\n    -+\t\t-c promisor.acceptfromserver=None \\\n    -+\t\t-c \"promisor.acceptFromServerUrl=bad name=https://example.com/*\" \\\n    -+\t\t--no-local --filter=\"blob:limit=5k\" server client 2>err &&\n    -+\n    -+\t# Check that a warning was emitted\n    -+\ttest_grep \"invalid remote name '\\''bad name'\\''\" err &&\n    -+\n    -+\t# Check that no remote was auto-created\n    -+\ttest_must_fail git -C client config --get-regexp \"remote\\..*\\.advertisedas\" &&\n    -+\n    -+\t# Check that the largest object is not missing on the server\n    -+\tcheck_missing_objects server 0 \"\" &&\n    -+\n    -+\t# Reinitialize server so that the largest object is missing again\n    -+\tinitialize_server 1 \"$oid\"\n    -+'\n    -+\n    - test_expect_success \"clone with promisor.sendFields\" '\n    + test_expect_success \"clone with invalid promisor.acceptFromServerUrl\" '\n      \tgit -C server config promisor.advertise true &&\n      \ttest_when_finished \"rm -rf client\" &&\n    +@@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with invalid promisor.acceptFromServerUrl\" '\n    + \t# Check that a warning was emitted\n    + \ttest_grep \"invalid remote name '\\''bad name'\\''\" err &&\n    + \n    ++\t# Check that no remote was auto-created\n    ++\ttest_must_fail git -C client config get --regexp \"remote\\..*\\.advertisedas\" &&\n    ++\n    + \t# Check that the largest object is not missing on the server\n    + \tcheck_missing_objects server 0 \"\" &&\n    + \n16:  20f70b52bb !  8:  b68b9497aa doc: promisor: improve acceptFromServer entry\n    @@ Documentation/config/promisor.adoc: variable is set to \"true\", and the \"name\" an\n     +for protocol details.\n      \n      promisor.acceptFromServerUrl::\n    - \tA glob pattern to specify which URLs advertised by a server\n    + \tA glob pattern to specify which server-advertised URLs a\n\n\nChristian Couder (8):\n  t5710: simplify 'mkdir X' followed by 'git -C X init'\n  urlmatch: change 'allow_globs' arg to bool\n  urlmatch: add url_normalize_pattern() helper\n  promisor-remote: add 'local_name' to 'struct promisor_info'\n  promisor-remote: introduce promisor.acceptFromServerUrl\n  promisor-remote: trust known remotes matching acceptFromServerUrl\n  promisor-remote: auto-configure unknown remotes\n  doc: promisor: improve acceptFromServer entry\n\n Documentation/config/promisor.adoc    | 123 ++++++--\n Documentation/config/remote.adoc      |   9 +\n Documentation/gitprotocol-v2.adoc     |   9 +-\n promisor-remote.c                     | 410 ++++++++++++++++++++++++--\n t/t5710-promisor-remote-capability.sh | 202 ++++++++++++-\n urlmatch.c                            |  11 +-\n urlmatch.h                            |  12 +\n 7 files changed, 730 insertions(+), 46 deletions(-)\n\n-- \n2.54.0.19.gb68b9497aa\n\n"},{"id":"542369","messageId":"20260427124108.3524129-2-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260427124108.3524129-1-christian.couder@gmail.com","subject":"[PATCH v2 1/8] t5710: simplify 'mkdir X' followed by 'git -C X init'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-04-27T12:41:01Z","receivedAt":"2026-04-27T12:41:30Z","isPatch":true,"body":"It's simpler and more efficient to just use `git init client` instead\nof `mkdir client && git -C client init`.\n\nSo let's replace the latter with the former.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n t/t5710-promisor-remote-capability.sh | 6 ++----\n 1 file changed, 2 insertions(+), 4 deletions(-)\n\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex b404ad9f0a..bf1cc54605 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -177,8 +177,7 @@ test_expect_success \"init + fetch with promisor.advertise set to 'true'\" '\n \tgit -C server config promisor.advertise true &&\n \ttest_when_finished \"rm -rf client\" &&\n \n-\tmkdir client &&\n-\tgit -C client init &&\n+\tgit init client &&\n \tgit -C client config remote.lop.promisor true &&\n \tgit -C client config remote.lop.fetch \"+refs/heads/*:refs/remotes/lop/*\" &&\n \tgit -C client config remote.lop.url \"$TRASH_DIRECTORY_URL/lop\" &&\n@@ -231,8 +230,7 @@ test_expect_success \"init + fetch two promisors but only one advertised\" '\n \t# Create a promisor that will be configured but not be used\n \tgit init --bare unused_lop &&\n \n-\tmkdir client &&\n-\tgit -C client init &&\n+\tgit init client &&\n \tgit -C client config remote.unused_lop.promisor true &&\n \tgit -C client config remote.unused_lop.fetch \"+refs/heads/*:refs/remotes/unused_lop/*\" &&\n \tgit -C client config remote.unused_lop.url \"$TRASH_DIRECTORY_URL/unused_lop\" &&\n-- \n2.54.0.19.gb68b9497aa\n\n"},{"id":"542370","messageId":"20260427124108.3524129-3-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260427124108.3524129-1-christian.couder@gmail.com","subject":"[PATCH v2 2/8] urlmatch: change 'allow_globs' arg to bool","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-04-27T12:41:02Z","receivedAt":"2026-04-27T12:41:30Z","isPatch":true,"body":"The last argument of url_normalize_1() is `char allow_globs` but it is\nused as a boolean, not as a char.\n\nLet's convert it to a `bool`, and while at it convert the two calls to\nurl_normalize_1() so they pass 'true' or 'false' instead of '1' or '0'.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n urlmatch.c | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/urlmatch.c b/urlmatch.c\nindex eea8300489..989bc7eb8b 100644\n--- a/urlmatch.c\n+++ b/urlmatch.c\n@@ -111,7 +111,7 @@ static int match_host(const struct url_info *url_info,\n \treturn (!url_len && !pat_len);\n }\n \n-static char *url_normalize_1(const char *url, struct url_info *out_info, char allow_globs)\n+static char *url_normalize_1(const char *url, struct url_info *out_info, bool allow_globs)\n {\n \t/*\n \t * Normalize NUL-terminated url using the following rules:\n@@ -437,7 +437,7 @@ static char *url_normalize_1(const char *url, struct url_info *out_info, char al\n \n char *url_normalize(const char *url, struct url_info *out_info)\n {\n-\treturn url_normalize_1(url, out_info, 0);\n+\treturn url_normalize_1(url, out_info, false);\n }\n \n static size_t url_match_prefix(const char *url,\n@@ -577,7 +577,7 @@ int urlmatch_config_entry(const char *var, const char *value,\n \t\tstruct url_info norm_info;\n \n \t\tconfig_url = xmemdupz(key, dot - key);\n-\t\tnorm_url = url_normalize_1(config_url, &norm_info, 1);\n+\t\tnorm_url = url_normalize_1(config_url, &norm_info, true);\n \t\tif (norm_url)\n \t\t\tretval = match_urls(url, &norm_info, &matched);\n \t\telse if (collect->fallback_match_fn)\n-- \n2.54.0.19.gb68b9497aa\n\n"},{"id":"542372","messageId":"20260427124108.3524129-4-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260427124108.3524129-1-christian.couder@gmail.com","subject":"[PATCH v2 3/8] urlmatch: add url_normalize_pattern() helper","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-04-27T12:41:03Z","receivedAt":"2026-04-27T12:41:32Z","isPatch":true,"body":"In a following commit, we will need to normalize a URL glob pattern\n(which may contain '*' in the host portion) and extract its component\noffsets (host, path, etc.) for separate matching. Let's export a\ndedicated helper function url_normalize_pattern() for that purpose.\n\nIt works like url_normalize(), but passes allow_globs=true to the\ninternal url_normalize_1(), so that '*' characters in the host are\naccepted rather than rejected.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n urlmatch.c |  5 +++++\n urlmatch.h | 12 ++++++++++++\n 2 files changed, 17 insertions(+)\n\ndiff --git a/urlmatch.c b/urlmatch.c\nindex 989bc7eb8b..7e734e2660 100644\n--- a/urlmatch.c\n+++ b/urlmatch.c\n@@ -440,6 +440,11 @@ char *url_normalize(const char *url, struct url_info *out_info)\n \treturn url_normalize_1(url, out_info, false);\n }\n \n+char *url_normalize_pattern(const char *url, struct url_info *out_info)\n+{\n+\treturn url_normalize_1(url, out_info, true);\n+}\n+\n static size_t url_match_prefix(const char *url,\n \t\t\t       const char *url_prefix,\n \t\t\t       size_t url_prefix_len)\ndiff --git a/urlmatch.h b/urlmatch.h\nindex 5ba85cea13..32c5067f9b 100644\n--- a/urlmatch.h\n+++ b/urlmatch.h\n@@ -36,6 +36,18 @@ struct url_info {\n \n char *url_normalize(const char *, struct url_info *);\n \n+/*\n+ * Like url_normalize(), but also allows '*' glob characters in the host\n+ * portion. Use this when normalizing URL patterns from user configuration.\n+ *\n+ * Note that '*' is a valid path character per RFC 3986 (as a sub-delim),\n+ * so glob patterns using '*' in the path are also accepted.\n+ *\n+ * Returns a newly allocated normalized string and fills out_info if\n+ * non-NULL, or NULL if the pattern is invalid.\n+ */\n+char *url_normalize_pattern(const char *url, struct url_info *out_info);\n+\n struct urlmatch_item {\n \tsize_t hostmatch_len;\n \tsize_t pathmatch_len;\n-- \n2.54.0.19.gb68b9497aa\n\n"},{"id":"542374","messageId":"20260427124108.3524129-5-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260427124108.3524129-1-christian.couder@gmail.com","subject":"[PATCH v2 4/8] promisor-remote: add 'local_name' to 'struct promisor_info'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-04-27T12:41:04Z","receivedAt":"2026-04-27T12:41:35Z","isPatch":true,"body":"In a following commit, we will store promisor remote information under\na remote name different than the one the server advertised.\n\nTo prepare for this change, let's add a new 'char *local_name' member\nto 'struct promisor_info', and let's update the related functions.\n\nWhile at it, let's also add a small promisor_info_internal_name()\nhelper that returns `local_name` when set, `name` otherwise, and let's\nuse this small helper in promisor_store_advertised_fields() and in the\npost-loop of filter_promisor_remote() so that lookups against the local\nrepo configuration use the right name.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 22 +++++++++++++++-------\n 1 file changed, 15 insertions(+), 7 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 38fa050542..7699e259eb 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -434,13 +434,14 @@ static struct string_list *fields_stored(void)\n  * Struct for promisor remotes involved in the \"promisor-remote\"\n  * protocol capability.\n  *\n- * Except for \"name\", each <member> in this struct and its <value>\n- * should correspond (either on the client side or on the server side)\n- * to a \"remote.<name>.<member>\" config variable set to <value> where\n- * \"<name>\" is a promisor remote name.\n+ * Except for \"name\" and \"local_name\", each <member> in this struct\n+ * and its <value> should correspond (either on the client side or on\n+ * the server side) to a \"remote.<name>.<member>\" config variable set\n+ * to <value> where \"<name>\" is a promisor remote name.\n  */\n struct promisor_info {\n-\tconst char *name;\n+\tconst char *name;\t/* name the server advertised */\n+\tconst char *local_name;\t/* name used locally (may be auto-generated) */\n \tconst char *url;\n \tconst char *filter;\n \tconst char *token;\n@@ -449,6 +450,7 @@ struct promisor_info {\n static void promisor_info_free(struct promisor_info *p)\n {\n \tfree((char *)p->name);\n+\tfree((char *)p->local_name);\n \tfree((char *)p->url);\n \tfree((char *)p->filter);\n \tfree((char *)p->token);\n@@ -462,6 +464,11 @@ static void promisor_info_list_clear(struct string_list *list)\n \tstring_list_clear(list, 0);\n }\n \n+static const char *promisor_info_internal_name(struct promisor_info *p)\n+{\n+\treturn p->local_name ? p->local_name : p->name;\n+}\n+\n static void set_one_field(struct promisor_info *p,\n \t\t\t  const char *field, const char *value)\n {\n@@ -829,7 +836,7 @@ static bool promisor_store_advertised_fields(struct promisor_info *advertised,\n {\n \tstruct promisor_info *p;\n \tstruct string_list_item *item;\n-\tconst char *remote_name = advertised->name;\n+\tconst char *remote_name = promisor_info_internal_name(advertised);\n \tbool reload_config = false;\n \n \tif (!(store_info->store_filter || store_info->store_token))\n@@ -937,7 +944,8 @@ static void filter_promisor_remote(struct repository *repo,\n \t/* Apply accepted remotes to the stable repo state */\n \tfor_each_string_list_item(item, accepted_remotes) {\n \t\tstruct promisor_info *info = item->util;\n-\t\tstruct promisor_remote *r = repo_promisor_remote_find(repo, info->name);\n+\t\tconst char *local = promisor_info_internal_name(info);\n+\t\tstruct promisor_remote *r = repo_promisor_remote_find(repo, local);\n \n \t\tif (r) {\n \t\t\tr->accepted = 1;\n-- \n2.54.0.19.gb68b9497aa\n\n"},{"id":"542373","messageId":"20260427124108.3524129-6-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260427124108.3524129-1-christian.couder@gmail.com","subject":"[PATCH v2 5/8] promisor-remote: introduce promisor.acceptFromServerUrl","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-04-27T12:41:05Z","receivedAt":"2026-04-27T12:41:36Z","isPatch":true,"body":"The \"promisor-remote\" protocol capability allows servers to advertise\npromisor remotes, but doesn't allow these remotes to be automatically\nconfigured on the client.\n\nLet's introduce a new `promisor.acceptFromServerUrl` config variable\nwhich contains a glob pattern, so that advertised remotes with a URL\nmatching that pattern will be automatically configured.\n\nThe glob pattern can optionally be prefixed with a remote name which\nwill be used as the name of the new local remote.\n\nFor now though, let's only introduce the functions to read and validate\nthe glob patterns and the optional prefixes.\n\nChecking if the URLs of the advertised remotes match the glob patterns\nand taking the appropriate action is left for a following commit.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c                     | 90 +++++++++++++++++++++++++++\n t/t5710-promisor-remote-capability.sh | 21 +++++++\n 2 files changed, 111 insertions(+)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 7699e259eb..3f3924f587 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -12,6 +12,7 @@\n #include \"packfile.h\"\n #include \"environment.h\"\n #include \"url.h\"\n+#include \"urlmatch.h\"\n #include \"version.h\"\n \n struct promisor_remote_config {\n@@ -657,6 +658,90 @@ static bool has_control_char(const char *s)\n \treturn false;\n }\n \n+struct allowed_url {\n+\tchar *remote_name;\n+\tchar *url_pattern;\n+\tstruct url_info pattern_info;\n+};\n+\n+static void allowed_url_free(void *util, const char *str UNUSED)\n+{\n+\tstruct allowed_url *allowed = util;\n+\n+\tif (!allowed)\n+\t\treturn;\n+\n+\t/* Depending on prefix, free either remote_name or url_pattern */\n+\tfree(allowed->remote_name ? allowed->remote_name : allowed->url_pattern);\n+\tfree(allowed->pattern_info.url);\n+\tfree(allowed);\n+}\n+\n+static struct allowed_url *valid_accept_url(const char *url)\n+{\n+\tchar *dup, *p;\n+\tstruct allowed_url *allowed;\n+\n+\tif (!url)\n+\t\treturn NULL;\n+\n+\tdup = xstrdup(url);\n+\tp = strchr(dup, '=');\n+\tif (p) {\n+\t\t*p = '\\0';\n+\t\tif (!valid_remote_name(dup)) {\n+\t\t\twarning(_(\"invalid remote name '%s' before '=' sign \"\n+\t\t\t\t  \"in '%s' from promisor.acceptFromServerUrl config\"),\n+\t\t\t\tdup, url);\n+\t\t\tfree(dup);\n+\t\t\treturn NULL;\n+\t\t}\n+\t\tp++;\n+\t} else {\n+\t\tp = dup;\n+\t}\n+\n+\tif (has_control_char(p)) {\n+\t\twarning(_(\"invalid url pattern '%s' \"\n+\t\t\t  \"in '%s' from promisor.acceptFromServerUrl config\"), p, url);\n+\t\tfree(dup);\n+\t\treturn NULL;\n+\t}\n+\n+\tallowed = xmalloc(sizeof(*allowed));\n+\tallowed->remote_name = (p == dup) ? NULL : dup;\n+\tallowed->url_pattern = p;\n+\tallowed->pattern_info.url = url_normalize_pattern(p, &allowed->pattern_info);\n+\tif (!allowed->pattern_info.url) {\n+\t\twarning(_(\"invalid url pattern '%s' \"\n+\t\t\t  \"in '%s' from promisor.acceptFromServerUrl config\"), p, url);\n+\t\tfree(dup);\n+\t\tfree(allowed);\n+\t\treturn NULL;\n+\t}\n+\n+\treturn allowed;\n+}\n+\n+static void load_accept_from_server_url(struct repository *repo,\n+\t\t\t\t\tstruct string_list *accept_urls)\n+{\n+\tconst struct string_list *config_urls;\n+\n+\tif (!repo_config_get_string_multi(repo, \"promisor.acceptfromserverurl\", &config_urls)) {\n+\t\tstruct string_list_item *item;\n+\n+\t\tfor_each_string_list_item(item, config_urls) {\n+\t\t\tstruct allowed_url *allowed = valid_accept_url(item->string);\n+\t\t\tif (allowed) {\n+\t\t\t\tstruct string_list_item *new;\n+\t\t\t\tnew = string_list_append(accept_urls, item->string);\n+\t\t\t\tnew->util = allowed;\n+\t\t\t}\n+\t\t}\n+\t}\n+}\n+\n static int should_accept_remote(enum accept_promisor accept,\n \t\t\t\tstruct promisor_info *advertised,\n \t\t\t\tstruct string_list *config_info)\n@@ -901,6 +986,10 @@ static void filter_promisor_remote(struct repository *repo,\n \tstruct string_list_item *item;\n \tbool reload_config = false;\n \tenum accept_promisor accept = accept_from_server(repo);\n+\tstruct string_list accept_urls = STRING_LIST_INIT_DUP;\n+\n+\t/* Load and validate the acceptFromServerUrl config */\n+\tload_accept_from_server_url(repo, &accept_urls);\n \n \tif (accept == ACCEPT_NONE)\n \t\treturn;\n@@ -934,6 +1023,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\t}\n \t}\n \n+\tstring_list_clear_func(&accept_urls, allowed_url_free);\n \tpromisor_info_list_clear(&config_info);\n \tstring_list_clear(&remote_info, 0);\n \tstore_info_free(store_info);\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex bf1cc54605..3b39505380 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -387,6 +387,27 @@ test_expect_success \"clone with 'KnownUrl' and empty url, so not advertised\" '\n \tcheck_missing_objects server 1 \"$oid\"\n '\n \n+test_expect_success \"clone with invalid promisor.acceptFromServerUrl\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\t# As \"bad name\" contains a space, which is not a valid remote name,\n+\t# the pattern should be rejected with a warning and no remote created.\n+\tGIT_NO_LAZY_FETCH=0 git clone \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c \"promisor.acceptFromServerUrl=bad name=https://example.com/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client 2>err &&\n+\n+\t# Check that a warning was emitted\n+\ttest_grep \"invalid remote name '\\''bad name'\\''\" err &&\n+\n+\t# Check that the largest object is not missing on the server\n+\tcheck_missing_objects server 0 \"\" &&\n+\n+\t# Reinitialize server so that the largest object is missing again\n+\tinitialize_server 1 \"$oid\"\n+'\n+\n test_expect_success \"clone with promisor.sendFields\" '\n \tgit -C server config promisor.advertise true &&\n \ttest_when_finished \"rm -rf client\" &&\n-- \n2.54.0.19.gb68b9497aa\n\n"},{"id":"542375","messageId":"20260427124108.3524129-7-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260427124108.3524129-1-christian.couder@gmail.com","subject":"[PATCH v2 6/8] promisor-remote: trust known remotes matching acceptFromServerUrl","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-04-27T12:41:06Z","receivedAt":"2026-04-27T12:41:37Z","isPatch":true,"body":"A previous commit introduced the `promisor.acceptFromServerUrl` config\nvariable along with the machinery to parse and validate the URL glob\npatterns and optional remote name prefixes it contains. However, these\nURL patterns are not yet tied into the client's acceptance logic.\n\nWhen a promisor remote is already configured locally, its fields (like\nauthentication tokens) may occasionally need to be refreshed by the\nserver. If `promisor.acceptFromServer` is set to the secure default\n(\"None\"), these updates are rejected, potentially causing future\nfetches to fail.\n\nTo enable such targeted updates for trusted URLs, let's use the URL\npatterns from `promisor.acceptFromServerUrl` as an additional URL\nbased allowlist.\n\nConcretely, let's check the advertised URLs against the URL glob\npatterns by introducing a new small helper function called\nurl_matches_accept_list(), which iterates over the glob patterns and\nreturns the first matching allowed_url entry (or NULL).\n\nThe URL matching is done component by component: scheme and port are\ncompared exactly, the host is matched with wildmatch() using the\nWM_PATHNAME flag (so '*' cannot cross the '/' boundary into the path),\nand the path is matched with wildmatch() without WM_PATHNAME (so '*'\ncan still match multi-level paths). Before matching, the advertised\nURL is passed through url_normalize() so that case variations in the\nscheme/host, percent-encoding tricks, and \"..\" path segments cannot\nbypass the allowlist.\n\nLet's then use this helper at the tail of should_accept_remote() so\nthat, when `accept == ACCEPT_NONE`, a known remote whose URL matches\nthe allowlist is still accepted.\n\nTo prepare for this new logic, let's also:\n\n - Add an 'accept_urls' parameter to should_accept_remote().\n\n - Replace the BUG() guard in the ACCEPT_KNOWN_URL case with an\n   explicit 'if (accept == ACCEPT_KNOWN_URL) return' and a new\n   BUG() guard in the ACCEPT_NONE case, so url_matches_accept_list()\n   is only called in the ACCEPT_NONE case.\n\n - Call accept_from_server_url() from filter_promisor_remote()\n   and relax its early return so that the function is entered when\n   `accept_urls` has entries even if `accept == ACCEPT_NONE`.\n\nWith this, many organizations may only need something like:\n\n  git config set --global \\\n          promisor.acceptFromServerUrl \"https://my-org.com/*\"\n\nto accept only their own remotes. And if they need to accept additional\nremotes in some specific repos, they can also set:\n\n  git config set promisor.acceptFromServer knownUrl\n\nand configure the additional remote manually only in the repos where\nthey are needed.\n\nLet's then properly document `promisor.acceptFromServerUrl` in\n\"promisor.adoc\" as an additive security allowlist for known remotes,\nincluding the URL normalization behavior and the component-wise\nmatching, and let's mention it in \"gitprotocol-v2.adoc\".\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    | 52 ++++++++++++++\n Documentation/gitprotocol-v2.adoc     |  9 +--\n promisor-remote.c                     | 98 +++++++++++++++++++++++++--\n t/t5710-promisor-remote-capability.sh | 71 +++++++++++++++++++\n 4 files changed, 220 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex b0fa43b839..efc066c3f2 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -51,6 +51,58 @@ promisor.acceptFromServer::\n \tto \"fetch\" and \"clone\" requests from the client. Name and URL\n \tcomparisons are case sensitive. See linkgit:gitprotocol-v2[5].\n \n+promisor.acceptFromServerUrl::\n+\tA glob pattern to specify which server-advertised URLs a\n+\tclient is allowed to act on. When a URL matches, the client\n+\twill accept the advertised remote as a promisor remote and may\n+\tautomatically accept field updates (such as authentication\n+\ttokens) from the server, even if `promisor.acceptFromServer`\n+\tis set to `none` (the default).\n++\n+This option can appear multiple times in config files. An advertised\n+URL will be accepted if it matches _ANY_ glob pattern specified by\n+this option in _ANY_ config file read by Git.\n++\n+Be _VERY_ careful with these patterns: `*` matches any sequence of\n+characters within the 'host' and 'path' parts of a URL (but cannot\n+cross part boundaries). An overly broad pattern is a major security\n+risk, as a matching URL allows a server to update fields (such as\n+authentication tokens) on known remotes without further confirmation.\n+To minimize security risks, follow these guidelines:\n++\n+1. Start with a secure protocol scheme, like `https://` or `ssh://`.\n++\n+2. Only allow domain names or paths where you control and trust _ALL_\n+   the content. Be especially careful with shared hosting platforms\n+   like `github.com` or `gitlab.com`. A broad pattern like\n+   `https://gitlab.com/*` is dangerous because it trusts every\n+   repository on the entire platform. Always restrict such patterns to\n+   your specific organization or namespace (e.g.,\n+   `https://gitlab.com/your-org/*`).\n++\n+3. Never use globs at the end of domain names. For example,\n+   `https://cdn.your-org.com/*` might be safe, but\n+   `https://cdn.your-org.com*/*` is a major security risk because\n+   the latter matches `https://cdn.your-org.com.hacker.net/repo`.\n++\n+4. Be careful using globs at the beginning of domain names. While the\n+   code ensures a `*` in the host cannot cross into the path, a\n+   pattern like `https://*.example.com/*` will still match any\n+   subdomain. This is extremely dangerous on shared hosting platforms\n+   (e.g., `https://*.github.io/*` trusts every user's site on the\n+   entire platform).\n++\n+Before matching, both the advertised URL and the pattern are\n+normalized: the scheme and host are lowercased, percent-encoded\n+characters are decoded where possible, and path segments like `..`\n+are resolved. The port must also match exactly (e.g.,\n+`https://example.com:8080/*` will not match a URL advertised on\n+port 9999).\n++\n+For the security implications of accepting a promisor remote, see the\n+documentation of `promisor.acceptFromServer`. For details on the\n+protocol, see linkgit:gitprotocol-v2[5].\n+\n promisor.checkFields::\n \tA comma or space separated list of additional remote related\n \tfield names. A client checks if the values of these fields\ndiff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\nindex befa697d21..2beb70595f 100644\n--- a/Documentation/gitprotocol-v2.adoc\n+++ b/Documentation/gitprotocol-v2.adoc\n@@ -866,10 +866,11 @@ the server advertised, the client shouldn't advertise the\n \n On the server side, the \"promisor.advertise\" and \"promisor.sendFields\"\n configuration options can be used to control what it advertises. On\n-the client side, the \"promisor.acceptFromServer\" configuration option\n-can be used to control what it accepts, and the \"promisor.storeFields\"\n-option, to control what it stores. See the documentation of these\n-configuration options in linkgit:git-config[1] for more information.\n+the client side, the \"promisor.acceptFromServer\" and\n+\"promisor.acceptFromServerUrl\" configuration options can be used to\n+control what it accepts, and the \"promisor.storeFields\" option, to\n+control what it stores. See the documentation of these configuration\n+options in linkgit:git-config[1] for more information.\n \n Note that in the future it would be nice if the \"promisor-remote\"\n protocol capability could be used by the server, when responding to\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 3f3924f587..72d5b94bf7 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -14,6 +14,7 @@\n #include \"url.h\"\n #include \"urlmatch.h\"\n #include \"version.h\"\n+#include \"wildmatch.h\"\n \n struct promisor_remote_config {\n \tstruct promisor_remote *promisors;\n@@ -742,8 +743,82 @@ static void load_accept_from_server_url(struct repository *repo,\n \t}\n }\n \n+static bool match_one_url(const struct url_info *pi, const struct url_info *ui)\n+{\n+\tconst char *pat = pi->url;\n+\tconst char *url = ui->url;\n+\tchar *p_str, *u_str;\n+\tbool res;\n+\n+\t/*\n+\t * Schemes must match exactly. They are case-folded by\n+\t * url_normalize(), so strncmp() suffices.\n+\t */\n+\tif (pi->scheme_len != ui->scheme_len || strncmp(pat, url, pi->scheme_len))\n+\t\treturn false;\n+\n+\t/*\n+\t * Ports must match exactly. url_normalize() strips default\n+\t * ports (like 443 for https), so length and content\n+\t * comparisons are sufficient.\n+\t */\n+\tif (pi->port_len != ui->port_len ||\n+\t    strncmp(pat + pi->port_off, url + ui->port_off, pi->port_len))\n+\t\treturn false;\n+\n+\t/*\n+\t * Match host and path separately to prevent a '*' in the host\n+\t * portion of the pattern from matching across the '/'\n+\t * boundary into the path. Use WM_PATHNAME for the host so '*'\n+\t * cannot cross '/' there, and 0 for the path so '*' can still\n+\t * match multi-level paths.\n+\t */\n+\n+\tp_str = xstrndup(pat + pi->host_off, pi->host_len);\n+\tu_str = xstrndup(url + ui->host_off, ui->host_len);\n+\tres = !wildmatch(p_str, u_str, WM_PATHNAME);\n+\tfree(p_str);\n+\tfree(u_str);\n+\n+\tif (!res)\n+\t\treturn false;\n+\n+\tp_str = xstrndup(pat + pi->path_off, pi->path_len);\n+\tu_str = xstrndup(url + ui->path_off, ui->path_len);\n+\tres = !wildmatch(p_str, u_str, 0);\n+\tfree(p_str);\n+\tfree(u_str);\n+\n+\treturn res;\n+}\n+\n+static struct allowed_url *url_matches_accept_list(\n+\t\tstruct string_list *accept_urls, const char *url)\n+{\n+\tstruct string_list_item *item;\n+\tstruct url_info url_info;\n+\n+\turl_info.url = url_normalize(url, &url_info);\n+\n+\tif (!url_info.url)\n+\t\treturn NULL;\n+\n+\tfor_each_string_list_item(item, accept_urls) {\n+\t\tstruct allowed_url *allowed = item->util;\n+\n+\t\tif (match_one_url(&allowed->pattern_info, &url_info)) {\n+\t\t\tfree(url_info.url);\n+\t\t\treturn allowed;\n+\t\t}\n+\t}\n+\n+\tfree(url_info.url);\n+\treturn NULL;\n+}\n+\n static int should_accept_remote(enum accept_promisor accept,\n \t\t\t\tstruct promisor_info *advertised,\n+\t\t\t\tstruct string_list *accept_urls,\n \t\t\t\tstruct string_list *config_info)\n {\n \tstruct promisor_info *p;\n@@ -771,9 +846,6 @@ static int should_accept_remote(enum accept_promisor accept,\n \tif (accept == ACCEPT_KNOWN_NAME)\n \t\treturn all_fields_match(advertised, config_info, p);\n \n-\tif (accept != ACCEPT_KNOWN_URL)\n-\t\tBUG(\"Unhandled 'enum accept_promisor' value '%d'\", accept);\n-\n \tif (strcmp(p->url, remote_url)) {\n \t\twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s', \"\n \t\t\t  \"ignoring this remote\"),\n@@ -781,7 +853,21 @@ static int should_accept_remote(enum accept_promisor accept,\n \t\treturn 0;\n \t}\n \n-\treturn all_fields_match(advertised, config_info, p);\n+\tif (accept == ACCEPT_KNOWN_URL)\n+\t\treturn all_fields_match(advertised, config_info, p);\n+\n+\tif (accept != ACCEPT_NONE)\n+\t\tBUG(\"Unhandled 'enum accept_promisor' value '%d'\", accept);\n+\n+\t/*\n+\t * Even if accept == ACCEPT_NONE, we MUST trust this known\n+\t * remote to update its token or other such fields if its URL\n+\t * matches the acceptFromServerUrl allowlist!\n+\t */\n+\tif (url_matches_accept_list(accept_urls, remote_url))\n+\t\treturn all_fields_match(advertised, config_info, p);\n+\n+\treturn 0;\n }\n \n static int skip_field_name_prefix(const char *elem, const char *field_name, const char **value)\n@@ -991,7 +1077,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t/* Load and validate the acceptFromServerUrl config */\n \tload_accept_from_server_url(repo, &accept_urls);\n \n-\tif (accept == ACCEPT_NONE)\n+\tif (accept == ACCEPT_NONE && !accept_urls.nr)\n \t\treturn;\n \n \t/* Parse remote info received */\n@@ -1011,7 +1097,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\t\tstring_list_sort(&config_info);\n \t\t}\n \n-\t\tif (should_accept_remote(accept, advertised, &config_info)) {\n+\t\tif (should_accept_remote(accept, advertised, &accept_urls, &config_info)) {\n \t\t\tif (!store_info)\n \t\t\t\tstore_info = store_info_new(repo);\n \t\t\tif (promisor_store_advertised_fields(advertised, store_info))\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex 3b39505380..0659b2ac15 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -387,6 +387,77 @@ test_expect_success \"clone with 'KnownUrl' and empty url, so not advertised\" '\n \tcheck_missing_objects server 1 \"$oid\"\n '\n \n+test_expect_success \"clone with 'None' but URL allowlisted\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\tGIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \\\n+\t\t-c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n+\t\t-c remote.lop.url=\"$TRASH_DIRECTORY_URL/lop\" \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\"\n+'\n+\n+test_expect_success \"clone with 'None' but URL not in allowlist\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\tGIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \\\n+\t\t-c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n+\t\t-c remote.lop.url=\"$TRASH_DIRECTORY_URL/lop\" \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c promisor.acceptFromServerUrl=\"https://example.com/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that the largest object is not missing on the server\n+\tcheck_missing_objects server 0 \"\" &&\n+\n+\t# Reinitialize server so that the largest object is missing again\n+\tinitialize_server 1 \"$oid\"\n+'\n+\n+test_expect_success \"clone with 'None' but URL allowlisted in one pattern out of two\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\tGIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \\\n+\t\t-c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n+\t\t-c remote.lop.url=\"$TRASH_DIRECTORY_URL/lop\" \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c promisor.acceptFromServerUrl=\"https://example.com/*\" \\\n+\t\t-c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\"\n+'\n+\n+test_expect_success \"clone with 'None', URL allowlisted, but client has different URL\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\t# The client configures \"lop\" with a different URL (serverTwo) than\n+\t# what the server advertises (lop). Even though the advertised URL\n+\t# matches the allowlist, the remote is rejected because the\n+\t# configured URL does not match the advertised one.\n+\tGIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \\\n+\t\t-c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n+\t\t-c remote.lop.url=\"$TRASH_DIRECTORY_URL/serverTwo\" \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that the largest object is not missing on the server\n+\tcheck_missing_objects server 0 \"\" &&\n+\n+\t# Reinitialize server so that the largest object is missing again\n+\tinitialize_server 1 \"$oid\"\n+'\n+\n test_expect_success \"clone with invalid promisor.acceptFromServerUrl\" '\n \tgit -C server config promisor.advertise true &&\n \ttest_when_finished \"rm -rf client\" &&\n-- \n2.54.0.19.gb68b9497aa\n\n"},{"id":"542376","messageId":"20260427124108.3524129-8-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260427124108.3524129-1-christian.couder@gmail.com","subject":"[PATCH v2 7/8] promisor-remote: auto-configure unknown remotes","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-04-27T12:41:07Z","receivedAt":"2026-04-27T12:41:39Z","isPatch":true,"body":"Previous commits have introduced the `promisor.acceptFromServerUrl`\nconfig variable to allowlist some URLs advertised by a server through\nthe \"promisor-remote\" protocol capability.\n\nHowever the new `promisor.acceptFromServerUrl` mechanism, like the old\n`promisor.acceptFromServer` mechanism, still requires a remote to\nalready exist in the client's local configuration before it can be\naccepted. This places a significant manual burden on users to\npre-configure these remotes, and creates friction for administrators\nwho have to troubleshoot or manually provision these setups for their\nteams.\n\nTo eliminate this burden, let's automatically create a new `[remote]`\nsection in the client's config when a server advertises an unknown\nremote whose URL matches a `promisor.acceptFromServerUrl` glob pattern.\n\nConcretely, let's add four helpers:\n\n - sanitize_remote_name(): turn an arbitrary URL-derived string into a\n   valid remote name by replacing non-alphanumeric characters,\n   collapsing runs of '-', and prepending \"promisor-auto-\".\n\n - promisor_remote_name_from_url(): normalize the URL and extract\n   host+port+path to build a human-readable base name, then pass it\n   through sanitize_remote_name().\n\n - configure_auto_promisor_remote(): write the remote.*.url,\n   remote.*.promisor and remote.*.advertisedAs keys to the repo\n   config.\n\n - handle_matching_allowed_url(): pick the final name (user-supplied\n   alias or auto-generated), handle collisions by appending \"-1\",\n   \"-2\", etc., then call configure_auto_promisor_remote().\n\nLet's also add should_accept_new_remote_url() which reuses the\nurl_matches_accept_list() helper introduced in a previous commit to\nfind a matching pattern, then delegates to handle_matching_allowed_url()\nto create the remote.\n\nAnd then let's call should_accept_new_remote_url() from the '!item'\n(unknown remote) branch of should_accept_remote(), setting\n`reload_config` so that the newly-written config is picked up.\n\nFinally let's document all that by:\n\n - expanding the `promisor.acceptFromServerUrl` entry to describe\n   auto-creation, the optional \"name=\" prefix syntax, the\n   \"promisor-auto-*\" generation rules, and numeric-suffix collision\n   handling, and by\n - adding a \"remote.<name>.advertisedAs\" entry to \"remote.adoc\".\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    |  26 +++-\n Documentation/config/remote.adoc      |   9 ++\n promisor-remote.c                     | 202 +++++++++++++++++++++++++-\n t/t5710-promisor-remote-capability.sh | 104 +++++++++++++\n 4 files changed, 332 insertions(+), 9 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex efc066c3f2..ae1686a6e0 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -54,7 +54,8 @@ promisor.acceptFromServer::\n promisor.acceptFromServerUrl::\n \tA glob pattern to specify which server-advertised URLs a\n \tclient is allowed to act on. When a URL matches, the client\n-\twill accept the advertised remote as a promisor remote and may\n+\twill accept the advertised remote as a promisor remote, may\n+\tautomatically create a new remote configuration for it and may\n \tautomatically accept field updates (such as authentication\n \ttokens) from the server, even if `promisor.acceptFromServer`\n \tis set to `none` (the default).\n@@ -66,9 +67,10 @@ this option in _ANY_ config file read by Git.\n Be _VERY_ careful with these patterns: `*` matches any sequence of\n characters within the 'host' and 'path' parts of a URL (but cannot\n cross part boundaries). An overly broad pattern is a major security\n-risk, as a matching URL allows a server to update fields (such as\n-authentication tokens) on known remotes without further confirmation.\n-To minimize security risks, follow these guidelines:\n+risk, as a matching URL allows a server to auto-configure new remotes\n+and to update fields (such as authentication tokens) on known remotes\n+without further confirmation. To minimize security risks, follow these\n+guidelines:\n +\n 1. Start with a secure protocol scheme, like `https://` or `ssh://`.\n +\n@@ -99,6 +101,22 @@ are resolved. The port must also match exactly (e.g.,\n `https://example.com:8080/*` will not match a URL advertised on\n port 9999).\n +\n+The glob pattern can optionally be prefixed with a remote name and an\n+equals sign (e.g., `cdn=https://cdn.example.com/*`). If such a prefix\n+is provided, accepted remotes will be saved under that name. If no\n+such prefix is provided, a safe remote name will be automatically\n+generated by sanitizing the URL and prefixing it with\n+`promisor-auto-`.\n++\n+If a remote with the chosen name already exists but points to a\n+different URL, Git will append a numeric suffix (e.g., `-1`, `-2`) to\n+the name to prevent overwriting existing configurations. You should\n+make sure that this doesn't happen often though, as remotes will be\n+rejected if the numeric suffix increases too much. In all cases, the\n+original name advertised by the server is recorded in the\n+`remote.<name>.advertisedAs` configuration variable for tracing and\n+debugging purposes.\n++\n For the security implications of accepting a promisor remote, see the\n documentation of `promisor.acceptFromServer`. For details on the\n protocol, see linkgit:gitprotocol-v2[5].\ndiff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc\nindex 91e46f66f5..6e2bbdf457 100644\n--- a/Documentation/config/remote.adoc\n+++ b/Documentation/config/remote.adoc\n@@ -91,6 +91,15 @@ remote.<name>.promisor::\n \tWhen set to true, this remote will be used to fetch promisor\n \tobjects.\n \n+remote.<name>.advertisedAs::\n+\tWhen a promisor remote is automatically configured using\n+\tinformation advertised by a server through the\n+\t`promisor-remote` protocol capability (see\n+\t`promisor.acceptFromServerUrl`), the server's originally\n+\tadvertised name is saved in this variable. This is for\n+\tinformation, tracing and debugging purposes. Users should not\n+\ttypically modify or create such configuration entries.\n+\n remote.<name>.partialclonefilter::\n \tThe filter that will be applied when fetching from this\tpromisor remote.\n \tChanging or clearing this value will only affect fetches for new commits.\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 72d5b94bf7..8c8a798fdb 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -816,10 +816,197 @@ static struct allowed_url *url_matches_accept_list(\n \treturn NULL;\n }\n \n-static int should_accept_remote(enum accept_promisor accept,\n+/*\n+ * Sanitize the buffer to make it a valid remote name coming from the\n+ * server by:\n+ *\n+ * - replacing any non alphanumeric character with a '-'\n+ * - stripping any leading '-',\n+ * - condensing multiple '-' into one,\n+ * - prepending \"promisor-auto-\",\n+ * - validating the result.\n+ */\n+static int sanitize_remote_name(struct strbuf *buf, const char *url)\n+{\n+\tchar prev = '-';\n+\tfor (size_t i = 0; i < buf->len; ) {\n+\t\tif (!isalnum(buf->buf[i]))\n+\t\t\tbuf->buf[i] = '-';\n+\t\tif (prev == '-' && buf->buf[i] == '-') {\n+\t\t\tstrbuf_remove(buf, i, 1);\n+\t\t} else {\n+\t\t\tprev = buf->buf[i];\n+\t\t\ti++;\n+\t\t}\n+\t}\n+\n+\tstrbuf_strip_suffix(buf, \"-\");\n+\n+\tif (!buf->len) {\n+\t\twarning(_(\"couldn't generate a valid remote name from \"\n+\t\t\t  \"advertised url '%s', ignoring this remote\"), url);\n+\t\treturn -1;\n+\t}\n+\n+\tstrbuf_insertstr(buf, 0, \"promisor-auto-\");\n+\n+\tif (!valid_remote_name(buf->buf)) {\n+\t\twarning(_(\"generated remote name '%s' from advertised url '%s' \"\n+\t\t\t  \"is invalid, ignoring this remote\"), buf->buf, url);\n+\t\treturn -1;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static char *promisor_remote_name_from_url(const char *url)\n+{\n+\tstruct url_info url_info = { 0 };\n+\tchar *normalized = url_normalize(url, &url_info);\n+\tstruct strbuf buf = STRBUF_INIT;\n+\n+\tif (!normalized) {\n+\t\twarning(_(\"couldn't normalize advertised url '%s', \"\n+\t\t\t  \"ignoring this remote\"), url);\n+\t\treturn NULL;\n+\t}\n+\n+\tif (url_info.host_len) {\n+\t\tstrbuf_add(&buf, normalized + url_info.host_off, url_info.host_len);\n+\t\tstrbuf_addch(&buf, '-');\n+\t}\n+\n+\tif (url_info.port_len) {\n+\t\tstrbuf_add(&buf, normalized + url_info.port_off, url_info.port_len);\n+\t\tstrbuf_addch(&buf, '-');\n+\t}\n+\n+\tif (url_info.path_len) {\n+\t\tstrbuf_add(&buf, normalized + url_info.path_off, url_info.path_len);\n+\t\tstrbuf_trim_trailing_dir_sep(&buf);\n+\t\tstrbuf_strip_suffix(&buf, \".git\");\n+\t}\n+\n+\tfree(normalized);\n+\n+\tif (sanitize_remote_name(&buf, url)) {\n+\t\tstrbuf_release(&buf);\n+\t\treturn NULL;\n+\t}\n+\n+\treturn strbuf_detach(&buf, NULL);\n+}\n+\n+static void configure_auto_promisor_remote(struct repository *repo,\n+\t\t\t\t\t   const char *name,\n+\t\t\t\t\t   const char *url,\n+\t\t\t\t\t   const char *advertised_as,\n+\t\t\t\t\t   bool reuse)\n+{\n+\tchar *key;\n+\n+\tif (!reuse) {\n+\t\tfprintf(stderr, _(\"Auto-creating promisor remote '%s' for URL '%s'\\n\"),\n+\t\t\tname, url);\n+\n+\t\tkey = xstrfmt(\"remote.%s.url\", name);\n+\t\trepo_config_set_gently(repo, key, url);\n+\t\tfree(key);\n+\t}\n+\n+\t/* NB: when reusing, this promotes an existing non-promisor remote */\n+\tkey = xstrfmt(\"remote.%s.promisor\", name);\n+\trepo_config_set_gently(repo, key, \"true\");\n+\tfree(key);\n+\n+\tif (advertised_as) {\n+\t\tkey = xstrfmt(\"remote.%s.advertisedAs\", name);\n+\t\trepo_config_set_gently(repo, key, advertised_as);\n+\t\tfree(key);\n+\t}\n+}\n+\n+#define MAX_REMOTES_WITH_SIMILAR_NAMES 20\n+\n+/* Return the allocated local name, or NULL on failure */\n+static char *handle_matching_allowed_url(struct repository *repo,\n+\t\t\t\t\t char *allowed_name,\n+\t\t\t\t\t const char *remote_url,\n+\t\t\t\t\t const char *remote_name)\n+{\n+\tchar *name;\n+\tchar *basename = allowed_name ?\n+\t\txstrdup(allowed_name) :\n+\t\tpromisor_remote_name_from_url(remote_url);\n+\tint i = 0;\n+\tbool reuse = false;\n+\n+\tif (!basename)\n+\t\treturn NULL;\n+\n+\tname = xstrdup(basename);\n+\n+\twhile (i < MAX_REMOTES_WITH_SIMILAR_NAMES) {\n+\t\tchar *url_key = xstrfmt(\"remote.%s.url\", name);\n+\t\tconst char *existing_url;\n+\t\tint exists = !repo_config_get_string_tmp(repo, url_key, &existing_url);\n+\n+\t\tfree(url_key);\n+\n+\t\tif (!exists)\n+\t\t\tbreak; /* Free to use */\n+\n+\t\tif (!strcmp(existing_url, remote_url)) {\n+\t\t\treuse = true;\n+\t\t\tbreak; /* Same URL, so safe to reuse */\n+\t\t}\n+\n+\t\ti++;\n+\t\tfree(name);\n+\t\tname = xstrfmt(\"%s-%d\", basename, i);\n+\t}\n+\n+\tif (i < MAX_REMOTES_WITH_SIMILAR_NAMES) {\n+\t\tconfigure_auto_promisor_remote(repo, name,\n+\t\t\t\t\t       remote_url, remote_name,\n+\t\t\t\t\t       reuse);\n+\t} else {\n+\t\twarning(_(\"too many remotes accepted with name like '%s-X', \"\n+\t\t\t  \"ignoring this remote\"), basename);\n+\t\tFREE_AND_NULL(name);\n+\t}\n+\n+\tfree(basename);\n+\treturn name;\n+}\n+\n+static int should_accept_new_remote_url(struct repository *repo,\n+\t\t\t\t\tstruct string_list *accept_urls,\n+\t\t\t\t\tstruct promisor_info *advertised)\n+{\n+\tstruct allowed_url *allowed = url_matches_accept_list(accept_urls,\n+\t\t\t\t\t\t\t     advertised->url);\n+\tif (allowed) {\n+\t\tchar *name = handle_matching_allowed_url(repo,\n+\t\t\t\t\t\t\t allowed->remote_name,\n+\t\t\t\t\t\t\t advertised->url,\n+\t\t\t\t\t\t\t advertised->name);\n+\t\tif (name) {\n+\t\t\tfree((char *)advertised->local_name);\n+\t\t\tadvertised->local_name = name;\n+\t\t\treturn 1;\n+\t\t}\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static int should_accept_remote(struct repository *repo,\n+\t\t\t\tenum accept_promisor accept,\n \t\t\t\tstruct promisor_info *advertised,\n \t\t\t\tstruct string_list *accept_urls,\n-\t\t\t\tstruct string_list *config_info)\n+\t\t\t\tstruct string_list *config_info,\n+\t\t\t\tbool *reload_config)\n {\n \tstruct promisor_info *p;\n \tstruct string_list_item *item;\n@@ -837,9 +1024,13 @@ static int should_accept_remote(enum accept_promisor accept,\n \t/* Get config info for that promisor remote */\n \titem = string_list_lookup(config_info, remote_name);\n \n-\tif (!item)\n+\tif (!item) {\n \t\t/* We don't know about that remote */\n-\t\treturn 0;\n+\t\tint res = should_accept_new_remote_url(repo, accept_urls, advertised);\n+\t\tif (res)\n+\t\t\t*reload_config = true;\n+\t\treturn res;\n+\t}\n \n \tp = item->util;\n \n@@ -1097,7 +1288,8 @@ static void filter_promisor_remote(struct repository *repo,\n \t\t\tstring_list_sort(&config_info);\n \t\t}\n \n-\t\tif (should_accept_remote(accept, advertised, &accept_urls, &config_info)) {\n+\t\tif (should_accept_remote(repo, accept, advertised, &accept_urls,\n+\t\t\t\t\t &config_info, &reload_config)) {\n \t\t\tif (!store_info)\n \t\t\t\tstore_info = store_info_new(repo);\n \t\t\tif (promisor_store_advertised_fields(advertised, store_info))\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex 0659b2ac15..549acff23f 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -458,6 +458,107 @@ test_expect_success \"clone with 'None', URL allowlisted, but client has differen\n \tinitialize_server 1 \"$oid\"\n '\n \n+test_expect_success \"clone with URL allowlisted and no remote already configured\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\ttest_when_finished \"rm -f full_names\" &&\n+\n+\tGIT_NO_LAZY_FETCH=0 git clone \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that exactly one remote has been auto-created, identified\n+\t# by \"remote.<name>.advertisedAs\" == \"lop\".\n+\tgit -C client config get --all --show-names --regexp \\\n+\t\t\"remote\\..*\\.advertisedas\" >full_names &&\n+\ttest_line_count = 1 full_names &&\n+\tREMOTE_NAME=$(sed \"s/^remote\\.\\(.*\\)\\.advertisedas .*$/\\1/\" full_names) &&\n+\n+\t# Check \".url\" and \".promisor\" values\n+\tprintf \"%s\\n\" \"$TRASH_DIRECTORY_URL/lop\" \"true\" >expect &&\n+\tgit -C client config \"remote.$REMOTE_NAME.url\" >actual &&\n+\tgit -C client config \"remote.$REMOTE_NAME.promisor\" >>actual &&\n+\ttest_cmp expect actual &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\"\n+'\n+\n+test_expect_success \"clone with named URL allowlisted and no pre-configured remote\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\tGIT_NO_LAZY_FETCH=0 git clone \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c promisor.acceptFromServerUrl=\"cdn=$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that a remote has been auto-created with the right \"cdn\" name and fields.\n+\tprintf \"%s\\n\" \"$TRASH_DIRECTORY_URL/lop\" \"true\" \"lop\" >expect &&\n+\tgit -C client config \"remote.cdn.url\" >actual &&\n+\tgit -C client config \"remote.cdn.promisor\" >>actual &&\n+\tgit -C client config \"remote.cdn.advertisedAs\" >>actual &&\n+\ttest_cmp expect actual &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\"\n+'\n+\n+test_expect_success \"clone with URL allowlisted but colliding name\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\tGIT_NO_LAZY_FETCH=0 git clone -c remote.cdn.promisor=true \\\n+\t\t-c remote.cdn.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n+\t\t-c remote.cdn.url=\"https://example.com/cdn\" \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c promisor.acceptFromServerUrl=\"cdn=$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that a remote has been auto-created with the right \"cdn-1\" name and fields.\n+\tprintf \"%s\\n\" \"$TRASH_DIRECTORY_URL/lop\" \"true\" \"lop\" >expect &&\n+\tgit -C client config \"remote.cdn-1.url\" >actual &&\n+\tgit -C client config \"remote.cdn-1.promisor\" >>actual &&\n+\tgit -C client config \"remote.cdn-1.advertisedAs\" >>actual &&\n+\ttest_cmp expect actual &&\n+\n+\t# Check that the original \"cdn\" remote was not overwritten.\n+\tprintf \"%s\\n\" \"https://example.com/cdn\" \"true\" >expect &&\n+\tgit -C client config \"remote.cdn.url\" >actual &&\n+\tgit -C client config \"remote.cdn.promisor\" >>actual &&\n+\ttest_cmp expect actual &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\"\n+'\n+\n+test_expect_success \"clone with URL allowlisted and reusable remote\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\tGIT_NO_LAZY_FETCH=0 git clone \\\n+\t\t-c remote.cdn.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n+\t\t-c remote.cdn.url=\"$TRASH_DIRECTORY_URL/lop\" \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c promisor.acceptFromServerUrl=\"cdn=$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that the existing \"cdn\" remote has been properly updated.\n+\tprintf \"%s\\n\" \"$TRASH_DIRECTORY_URL/lop\" \"true\" \"lop\" \"+refs/heads/*:refs/remotes/lop/*\" >expect &&\n+\tgit -C client config \"remote.cdn.url\" >actual &&\n+\tgit -C client config \"remote.cdn.promisor\" >>actual &&\n+\tgit -C client config \"remote.cdn.advertisedAs\" >>actual &&\n+\tgit -C client config \"remote.cdn.fetch\" >>actual &&\n+\ttest_cmp expect actual &&\n+\n+\t# Check that no new \"cdn-1\" remote has been created.\n+\ttest_must_fail git -C client config \"remote.cdn-1.url\" &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\"\n+'\n+\n test_expect_success \"clone with invalid promisor.acceptFromServerUrl\" '\n \tgit -C server config promisor.advertise true &&\n \ttest_when_finished \"rm -rf client\" &&\n@@ -472,6 +573,9 @@ test_expect_success \"clone with invalid promisor.acceptFromServerUrl\" '\n \t# Check that a warning was emitted\n \ttest_grep \"invalid remote name '\\''bad name'\\''\" err &&\n \n+\t# Check that no remote was auto-created\n+\ttest_must_fail git -C client config get --regexp \"remote\\..*\\.advertisedas\" &&\n+\n \t# Check that the largest object is not missing on the server\n \tcheck_missing_objects server 0 \"\" &&\n \n-- \n2.54.0.19.gb68b9497aa\n\n"},{"id":"542377","messageId":"20260427124108.3524129-9-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260427124108.3524129-1-christian.couder@gmail.com","subject":"[PATCH v2 8/8] doc: promisor: improve acceptFromServer entry","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-04-27T12:41:08Z","receivedAt":"2026-04-27T12:41:40Z","isPatch":true,"body":"The entry for the `promisor.acceptFromServer` in\n\"Documentation/config/promisor.adoc\" has a number of issues:\n\n- it's not clear if new remotes and URLs can be created,\n- it looks like a big block of text,\n- it's not easy to see all the options,\n- it's not easy to see which option is the default one,\n- for \"knownName\", it says \"advertised by the client\" instead of\n  \"advertised by the server\",\n- it doesn't refer to the new related `acceptFromServerUrl`\n  option.\n\nLet's address all these issues by rewording large parts of it\nand using bullet points for the different options.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc | 53 ++++++++++++++++++++----------\n 1 file changed, 35 insertions(+), 18 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex ae1686a6e0..095c1693ac 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -32,24 +32,41 @@ variable is set to \"true\", and the \"name\" and \"url\" fields are always\n advertised regardless of this setting.\n \n promisor.acceptFromServer::\n-\tIf set to \"all\", a client will accept all the promisor remotes\n-\ta server might advertise using the \"promisor-remote\"\n-\tcapability. If set to \"knownName\" the client will accept\n-\tpromisor remotes which are already configured on the client\n-\tand have the same name as those advertised by the client. This\n-\tis not very secure, but could be used in a corporate setup\n-\twhere servers and clients are trusted to not switch name and\n-\tURLs. If set to \"knownUrl\", the client will accept promisor\n-\tremotes which have both the same name and the same URL\n-\tconfigured on the client as the name and URL advertised by the\n-\tserver. This is more secure than \"all\" or \"knownName\", so it\n-\tshould be used if possible instead of those options. Default\n-\tis \"none\", which means no promisor remote advertised by a\n-\tserver will be accepted. By accepting a promisor remote, the\n-\tclient agrees that the server might omit objects that are\n-\tlazily fetchable from this promisor remote from its responses\n-\tto \"fetch\" and \"clone\" requests from the client. Name and URL\n-\tcomparisons are case sensitive. See linkgit:gitprotocol-v2[5].\n+\tControls which promisor remotes advertised by a server (using the\n+\t\"promisor-remote\" protocol capability) a client will accept. By\n+\taccepting a promisor remote, the client agrees that the server\n+\tmight omit objects that are lazily fetchable from this promisor\n+\tremote from its responses to \"fetch\" and \"clone\" requests.\n++\n+Note that this option does not cause new remotes to be automatically\n+created in the client's configuration. It only allows remotes which\n+are somehow already configured to be trusted for the current\n+operation, or their fields to be updated (if `promisor.storeFields` is\n+set and the remote already exists locally). To allow Git to\n+automatically create and persist new remotes from server\n+advertisements, use `promisor.acceptFromServerUrl`.\n++\n+The available options are:\n++\n+* `none` (default): No promisor remote advertised by a server will be\n+  accepted.\n++\n+* `knownUrl`: The client will accept promisor remotes that are already\n+  configured on the client and have both the same name and the same URL\n+  as advertised by the server. This is more secure than `all` or\n+  `knownName`, and should be used if possible instead of those options.\n++\n+* `knownName`: The client will accept promisor remotes that are already\n+  configured on the client and have the same name as those advertised\n+  by the server. This is not very secure, but could be used in a corporate\n+  setup where servers and clients are trusted to not switch names and URLs.\n++\n+* `all`: The client will accept all the promisor remotes a server might\n+  advertise. This is the least secure option and should only be used in\n+  fully trusted environments.\n++\n+Name and URL comparisons are case-sensitive. See linkgit:gitprotocol-v2[5]\n+for protocol details.\n \n promisor.acceptFromServerUrl::\n \tA glob pattern to specify which server-advertised URLs a\n-- \n2.54.0.19.gb68b9497aa\n\n"},{"id":"542384","messageId":"CAP8UFD2b+AHD0rfR9PxBcANAGg823LRk0S6zWCJWKMVWEGHYjQ@mail.gmail.com","threadId":"64670","inReplyTo":"20260427124108.3524129-1-christian.couder@gmail.com","subject":"Re: [PATCH v2 0/8] Auto-configure advertised remotes via URL allowlist","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-04-27T13:00:49Z","receivedAt":"2026-04-27T13:01:04Z","isPatch":true,"body":"Sorry, it looks like I sent this series in reply to:\n\nhttps://lore.kernel.org/git/20251223111113.47473-1-christian.couder@gmail.com/\n\ninstead of:\n\nhttps://lore.kernel.org/git/20260323080520.887550-1-christian.couder@gmail.com/\n\nI will try to do better next time.\n\nAlso I forgot to say that this series is based on a merge of 'master'\n@ v2.54.0 and 'cc/promisor-auto-config-url' (which is in 'next' but is\nmarked with \"Will merge to master\" in the last \"What's cooking...\"\nemail).\n\nOn Mon, Apr 27, 2026 at 2:41 PM Christian Couder\n<christian.couder@gmail.com> wrote:\n>\n> Currently, the \"promisor-remote\" protocol capability allows a server\n> to advertise promisor remotes (and their tokens/filters), but the\n> client's `promisor.acceptFromServer` mechanism requires these remotes\n> to already exist in the config.\n>\n> This is a significant burden for users and administrators who have to\n> pre-configure remotes.\n>\n> This patch series improves on this by introducing a new\n> `promisor.acceptFromServerUrl` config option, which provides an\n> additive, URL-based security allowlist.\n>\n> Multiple `promisor.acceptFromServerUrl` config options can be provided\n> in different config files. Each one should contain a URL glob pattern\n> which can optionally be prefixed with a remote name in the\n> \"[<name>=]<pattern>\" format.\n>\n> The goal is for something like a simple:\n>\n>   git config set --global promisor.acceptFromServerUrl \"https://my-org.com/*\"\n>\n> to be all that is needed for internal work in many organizations.\n>\n> With this new config option:\n>\n>  - The server can update fields (like tokens) for known remotes,\n>    provided their URL matches the allowlist, even if\n>    `acceptFromServer` is set to `None`.\n>\n>  - Unknown remotes advertised by the server can be automatically\n>    configured on the client if their URL matches the allowlist.\n>\n>  - If there is no `<name>` prefix before the glob pattern matched, the\n>    auto-configured remote is named using the\n>    \"promisor-auto-<sanitized-url>\" format. So the same auto-configured\n>    remote config entry will be reused for the same URL.\n>\n>  - If a `<name>` prefix is provided, it will be used for the\n>    auto-configured remote config entry.\n>\n>  - If the chosen name (auto-generated or prefixed) already exists but\n>    points to a different URL, overwriting the existing config is\n>    prevented by appending a numeric suffix (e.g., -1, -2) to the name\n>    and auto-configuring using that name.\n>\n>  - The server's originally advertised name is always saved in the\n>    `remote.<name>.advertisedAs` config variable of the auto-configured\n>    remote for tracing and debugging.\n>\n> Security considerations:\n>\n>  - Advertised URLs and glob patterns are routed through\n>    url_normalize() / url_normalize_pattern() before matching, to\n>    prevent percent-encoding, case variation, or path-traversal (..)\n>    bypasses.\n>\n>  - URL matching is done component by component: scheme and port\n>    must match exactly (no wildcards), the host is matched with\n>    WM_PATHNAME so a '*' cannot cross the '/' boundary into the\n>    path, and the path is matched without WM_PATHNAME so '*' can\n>    still span multi-level paths.\n>\n>  - Auto-generated remote names are sanitized (non-alphanumeric\n>    characters are replaced with '-', runs of '-' are collapsed)\n>    and prefixed with 'promisor-auto-'. User-supplied names (from\n>    the 'name=<pattern>' syntax) are validated with\n>    valid_remote_name(). Together, these prevent a server from\n>    maliciously overwriting standard remotes (like 'origin').\n>\n>  - If the auto-generated or user-supplied name collides with an\n>    existing remote configured to a different URL, a numeric\n>    suffix ('-1', '-2', ...) is appended, up to a bounded limit,\n>    so a server cannot hijack an existing remote by name.\n>\n>  - Known remotes are still subject to URL consistency checks:\n>    even if an advertised URL matches the allowlist, it is only\n>    accepted for a known remote if it matches the URL already\n>    configured locally for that remote.\n>\n>  - The documentation explains in detail how to write secure glob\n>    patterns in `promisor.acceptFromServerUrl`, and highlights the\n>    risks of overly broad patterns on shared hosting platforms.\n>\n> High level description of the patches\n> =====================================\n>\n>  - Patch 1/8 is new. It is a very small preparatory patch that\n>    simplifies some tests a bit.\n>\n>  - Patches 2/8 and 3/8 expose and adapt a url_normalize_pattern()\n>    helper function in the urlmatch API.\n>\n>  - Patch 4/8 adapts `struct promisor_info` by adding a new\n>    `local_name` member to it to prepare for the next patches.\n>\n>  - Patches 5/8 to 7/8 implement the core feature. They introduce the\n>    parsing machinery, add the additive allowlist for known remotes\n>    (with url_normalize() security), and finally implement the\n>    auto-creation and collision resolution for unknown remotes.\n>\n>  - Patch 8/8 cleans up and modernizes the existing\n>    `promisor.acceptFromServer` documentation.\n>\n> Changes compared to v1\n> ======================\n>\n> Thanks to Patrick and Junio for reviewing the previous versions of\n> this series and of the preparatory series.\n>\n>  - A lot of preparatory patches have been moved to a preparatory series\n>    that has already been merged. See:\n>\n>    https://lore.kernel.org/git/20260407115243.358642-1-christian.couder@gmail.com/\n>\n>    This is why this v2 contains only 8 patches compared to 16 patches\n>    in v1.\n>\n>  - Everywhere in this series \"whitelist\" as been replaced with\n>    \"allowlist\".\n>\n>  - In the tests added in this series, the new $TRASH_DIRECTORY_URL and\n>    $ENCODED_TRASH_DIRECTORY_URL introduced by the preparatory series\n>    are used instead of the previous $PWD_URL and $ENCODED_PWD_URL.\n>\n>  - Patch 1/8 (\"t5710: simplify 'mkdir X' followed by 'git -C X init'\")\n>    is new.\n>\n>  - Patch 3/8 (\"urlmatch: add url_normalize_pattern() helper\") replaces\n>    patch 3/16 (\"urlmatch: add url_is_valid_pattern() helper\") because\n>    in subsequent patches we now normalize patterns to validate them\n>    and match them component by component against URLs.\n>\n>  - In patch 5/8, previously 13/16, (\"promisor-remote: introduce\n>    promisor.acceptFromServerUrl\"):\n>\n>    - We add a `struct url_info pattern_info;` to `struct allowed_url`,\n>      so we can validate patterns using url_normalize_pattern() and, in\n>      a subsequent patch, match URLs component by component. This\n>      requires a new allowed_url_free() function that is passed to\n>      string_list_clear_func() to clear the `struct allowed_url`\n>      instances.\n>\n>    - We don't use a `static struct string_list` to store the URL\n>      patterns we accept. Instead we load them from the config into a\n>      `struct string_list` passed as argument. The function doing this\n>      is renamed accordingly from accept_from_server_url() to\n>      load_accept_from_server_url().\n>\n>    - A \"clone with invalid promisor.acceptFromServerUrl\" test is moved\n>      from patch 15/16 to this patch as it's more relevant in this\n>      patch (where we validate the content of the\n>      `promisor.acceptFromServerUrl` environment variable).\n>\n>  - In patch 6/8, previously 14/16, (\"promisor-remote: trust known\n>    remotes matching acceptFromServerUrl\"):\n>\n>    - In the commit message, an example, which shows how the new\n>      \"acceptFromServerUrl\" config option can be useful, is added.\n>\n>    - The matching of URLs advertised by the server to URLs patterns\n>      from the config, is now performed component by component. This is\n>      reflected in the commit message, the documentation and the\n>      code. This ensures a `*` in the host pattern cannot cross into\n>      the path.\n>\n>    - In the code, we add a new match_one_url() function to perform the\n>      matching.\n>\n>  - In patch 7/8, previously 15/16 (\"promisor-remote: auto-configure\n>    unknown remotes\"):\n>\n>    - In the doc, the unclear \"considered trusted by the client\" is\n>      clarified using \"a client is allowed to act on\" and subsequent\n>      explanations. In general the doc is also improved a bit.\n>\n>    - In the tests, parsing the \"remote.<name>.advertisedAs\" config\n>      option is now more careful about the possibility that more than\n>      one such options exist.\n>\n>    - The test that was moved to patch 5/8 is still enhanced a bit in\n>      this commit by checking that no \"remote.<name>.advertisedAs\"\n>      config option has been added.\n>\n> CI tests\n> ========\n>\n> They all pass, see:\n>\n> https://github.com/chriscool/git/actions/runs/24992478331\n>\n> Range diff since v1\n> ===================\n>\n>  1:  b2894eb33a <  -:  ---------- promisor-remote: try accepted remotes before others in get_direct()\n>  -:  ---------- >  1:  44e9a16455 t5710: simplify 'mkdir X' followed by 'git -C X init'\n>  2:  a3206a6ae9 =  2:  42f174910c urlmatch: change 'allow_globs' arg to bool\n>  3:  51bbf65c52 <  -:  ---------- urlmatch: add url_is_valid_pattern() helper\n>  4:  f367beef72 <  -:  ---------- promisor-remote: clarify that a remote is ignored\n>  5:  1faf74cb3f <  -:  ---------- promisor-remote: refactor has_control_char()\n>  6:  40cf0af639 <  -:  ---------- promisor-remote: refactor accept_from_server()\n>  7:  b75dca8037 <  -:  ---------- promisor-remote: keep accepted promisor_info structs alive\n>  8:  f5e55dc407 <  -:  ---------- promisor-remote: remove the 'accepted' strvec\n>  -:  ---------- >  3:  8088374458 urlmatch: add url_normalize_pattern() helper\n>  9:  63c1db30de !  4:  6bfda89a79 promisor-remote: add 'local_name' to 'struct promisor_info'\n>     @@ Commit message\n>          In a following commit, we will store promisor remote information under\n>          a remote name different than the one the server advertised.\n>\n>     -    To prepare for this change, let's add a new 'char* local_name' member\n>     +    To prepare for this change, let's add a new 'char *local_name' member\n>          to 'struct promisor_info', and let's update the related functions.\n>\n>          While at it, let's also add a small promisor_info_internal_name()\n>     @@ Commit message\n>\n>       ## promisor-remote.c ##\n>      @@ promisor-remote.c: static struct string_list *fields_stored(void)\n>     -\n>     - /*\n>        * Struct for promisor remotes involved in the \"promisor-remote\"\n>     -- * protocol capability.\n>     -+ * protocol capability:\n>     +  * protocol capability.\n>        *\n>      - * Except for \"name\", each <member> in this struct and its <value>\n>      - * should correspond (either on the client side or on the server side)\n>      - * to a \"remote.<name>.<member>\" config variable set to <value> where\n>      - * \"<name>\" is a promisor remote name.\n>     -+ * - \"name\" is the name the server advertised.\n>     -+ * - \"local_name\" is the name we use locally (may be auto-generated).\n>     -+ *\n>      + * Except for \"name\" and \"local_name\", each <member> in this struct\n>      + * and its <value> should correspond (either on the client side or on\n>      + * the server side) to a \"remote.<name>.<member>\" config variable set\n>      + * to <value> where \"<name>\" is a promisor remote name.\n>        */\n>       struct promisor_info {\n>     -   const char *name;\n>     -+  const char *local_name;\n>     +-  const char *name;\n>     ++  const char *name;       /* name the server advertised */\n>     ++  const char *local_name; /* name used locally (may be auto-generated) */\n>         const char *url;\n>         const char *filter;\n>         const char *token;\n> 10:  e9b8a64ab8 <  -:  ---------- promisor-remote: pass config entry to all_fields_match() directly\n> 11:  2e1260190a <  -:  ---------- promisor-remote: refactor should_accept_remote() control flow\n> 12:  b33f06173a <  -:  ---------- t5710: use proper file:// URIs for absolute paths\n> 13:  681b03e248 !  5:  fefa17e6dd promisor-remote: introduce promisor.acceptFromServerUrl\n>     @@ promisor-remote.c: static bool has_control_char(const char *s)\n>      +struct allowed_url {\n>      +  char *remote_name;\n>      +  char *url_pattern;\n>     ++  struct url_info pattern_info;\n>      +};\n>      +\n>     ++static void allowed_url_free(void *util, const char *str UNUSED)\n>     ++{\n>     ++  struct allowed_url *allowed = util;\n>     ++\n>     ++  if (!allowed)\n>     ++          return;\n>     ++\n>     ++  /* Depending on prefix, free either remote_name or url_pattern */\n>     ++  free(allowed->remote_name ? allowed->remote_name : allowed->url_pattern);\n>     ++  free(allowed->pattern_info.url);\n>     ++  free(allowed);\n>     ++}\n>     ++\n>      +static struct allowed_url *valid_accept_url(const char *url)\n>      +{\n>      +  char *dup, *p;\n>     @@ promisor-remote.c: static bool has_control_char(const char *s)\n>      +          p = dup;\n>      +  }\n>      +\n>     -+  if (has_control_char(p) || !url_is_valid_pattern(p)) {\n>     ++  if (has_control_char(p)) {\n>      +          warning(_(\"invalid url pattern '%s' \"\n>      +                    \"in '%s' from promisor.acceptFromServerUrl config\"), p, url);\n>      +          free(dup);\n>     @@ promisor-remote.c: static bool has_control_char(const char *s)\n>      +  allowed = xmalloc(sizeof(*allowed));\n>      +  allowed->remote_name = (p == dup) ? NULL : dup;\n>      +  allowed->url_pattern = p;\n>     ++  allowed->pattern_info.url = url_normalize_pattern(p, &allowed->pattern_info);\n>     ++  if (!allowed->pattern_info.url) {\n>     ++          warning(_(\"invalid url pattern '%s' \"\n>     ++                    \"in '%s' from promisor.acceptFromServerUrl config\"), p, url);\n>     ++          free(dup);\n>     ++          free(allowed);\n>     ++          return NULL;\n>     ++  }\n>      +\n>      +  return allowed;\n>      +}\n>      +\n>     -+static struct string_list *accept_from_server_url(struct repository *repo)\n>     ++static void load_accept_from_server_url(struct repository *repo,\n>     ++                                  struct string_list *accept_urls)\n>      +{\n>     -+  static struct string_list accept_urls = STRING_LIST_INIT_DUP;\n>     -+  static int initialized;\n>      +  const struct string_list *config_urls;\n>      +\n>     -+  if (initialized)\n>     -+          return &accept_urls;\n>     -+\n>     -+  initialized = 1;\n>     -+\n>      +  if (!repo_config_get_string_multi(repo, \"promisor.acceptfromserverurl\", &config_urls)) {\n>      +          struct string_list_item *item;\n>      +\n>     @@ promisor-remote.c: static bool has_control_char(const char *s)\n>      +                  struct allowed_url *allowed = valid_accept_url(item->string);\n>      +                  if (allowed) {\n>      +                          struct string_list_item *new;\n>     -+                          new = string_list_append(&accept_urls, item->string);\n>     ++                          new = string_list_append(accept_urls, item->string);\n>      +                          new->util = allowed;\n>      +                  }\n>      +          }\n>      +  }\n>     -+\n>     -+  return &accept_urls;\n>      +}\n>      +\n>       static int should_accept_remote(enum accept_promisor accept,\n>     @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n>         struct string_list_item *item;\n>         bool reload_config = false;\n>         enum accept_promisor accept = accept_from_server(repo);\n>     -+  /* Pre-load and validate the acceptFromServerUrl config */\n>     -+  (void)accept_from_server_url(repo);\n>     ++  struct string_list accept_urls = STRING_LIST_INIT_DUP;\n>     ++\n>     ++  /* Load and validate the acceptFromServerUrl config */\n>     ++  load_accept_from_server_url(repo, &accept_urls);\n>\n>         if (accept == ACCEPT_NONE)\n>                 return;\n>     +@@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n>     +           }\n>     +   }\n>     +\n>     ++  string_list_clear_func(&accept_urls, allowed_url_free);\n>     +   promisor_info_list_clear(&config_info);\n>     +   string_list_clear(&remote_info, 0);\n>     +   store_info_free(store_info);\n>     +\n>     + ## t/t5710-promisor-remote-capability.sh ##\n>     +@@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'KnownUrl' and empty url, so not advertised\" '\n>     +   check_missing_objects server 1 \"$oid\"\n>     + '\n>     +\n>     ++test_expect_success \"clone with invalid promisor.acceptFromServerUrl\" '\n>     ++  git -C server config promisor.advertise true &&\n>     ++  test_when_finished \"rm -rf client\" &&\n>     ++\n>     ++  # As \"bad name\" contains a space, which is not a valid remote name,\n>     ++  # the pattern should be rejected with a warning and no remote created.\n>     ++  GIT_NO_LAZY_FETCH=0 git clone \\\n>     ++          -c promisor.acceptfromserver=None \\\n>     ++          -c \"promisor.acceptFromServerUrl=bad name=https://example.com/*\" \\\n>     ++          --no-local --filter=\"blob:limit=5k\" server client 2>err &&\n>     ++\n>     ++  # Check that a warning was emitted\n>     ++  test_grep \"invalid remote name '\\''bad name'\\''\" err &&\n>     ++\n>     ++  # Check that the largest object is not missing on the server\n>     ++  check_missing_objects server 0 \"\" &&\n>     ++\n>     ++  # Reinitialize server so that the largest object is missing again\n>     ++  initialize_server 1 \"$oid\"\n>     ++'\n>     ++\n>     + test_expect_success \"clone with promisor.sendFields\" '\n>     +   git -C server config promisor.advertise true &&\n>     +   test_when_finished \"rm -rf client\" &&\n> 14:  8c04e48d66 !  6:  2f238d0a7a promisor-remote: trust known remotes matching acceptFromServerUrl\n>     @@ Commit message\n>\n>          To enable such targeted updates for trusted URLs, let's use the URL\n>          patterns from `promisor.acceptFromServerUrl` as an additional URL\n>     -    based whitelist.\n>     +    based allowlist.\n>\n>          Concretely, let's check the advertised URLs against the URL glob\n>          patterns by introducing a new small helper function called\n>          url_matches_accept_list(), which iterates over the glob patterns and\n>          returns the first matching allowed_url entry (or NULL).\n>\n>     -    (Before matching, the advertised URL is passed through url_normalize()\n>     -    so that case variations in the scheme/host, percent-encoding tricks,\n>     -    and \"..\" path segments cannot bypass the whitelist.)\n>     +    The URL matching is done component by component: scheme and port are\n>     +    compared exactly, the host is matched with wildmatch() using the\n>     +    WM_PATHNAME flag (so '*' cannot cross the '/' boundary into the path),\n>     +    and the path is matched with wildmatch() without WM_PATHNAME (so '*'\n>     +    can still match multi-level paths). Before matching, the advertised\n>     +    URL is passed through url_normalize() so that case variations in the\n>     +    scheme/host, percent-encoding tricks, and \"..\" path segments cannot\n>     +    bypass the allowlist.\n>\n>          Let's then use this helper at the tail of should_accept_remote() so\n>          that, when `accept == ACCEPT_NONE`, a known remote whose URL matches\n>     -    the whitelist is still accepted.\n>     +    the allowlist is still accepted.\n>\n>          To prepare for this new logic, let's also:\n>\n>     @@ Commit message\n>             and relax its early return so that the function is entered when\n>             `accept_urls` has entries even if `accept == ACCEPT_NONE`.\n>\n>     +    With this, many organizations may only need something like:\n>     +\n>     +      git config set --global \\\n>     +              promisor.acceptFromServerUrl \"https://my-org.com/*\"\n>     +\n>     +    to accept only their own remotes. And if they need to accept additional\n>     +    remotes in some specific repos, they can also set:\n>     +\n>     +      git config set promisor.acceptFromServer knownUrl\n>     +\n>     +    and configure the additional remote manually only in the repos where\n>     +    they are needed.\n>     +\n>          Let's then properly document `promisor.acceptFromServerUrl` in\n>     -    \"promisor.adoc\" as an additive security whitelist for known remotes,\n>     -    including the URL normalization behavior, and let's mention it in\n>     -    \"gitprotocol-v2.adoc\".\n>     +    \"promisor.adoc\" as an additive security allowlist for known remotes,\n>     +    including the URL normalization behavior and the component-wise\n>     +    matching, and let's mention it in \"gitprotocol-v2.adoc\".\n>\n>          Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n>\n>     @@ Documentation/config/promisor.adoc: promisor.acceptFromServer::\n>         comparisons are case sensitive. See linkgit:gitprotocol-v2[5].\n>\n>      +promisor.acceptFromServerUrl::\n>     -+  A glob pattern to specify which URLs advertised by a server\n>     -+  are considered trusted by the client. This option acts as an\n>     -+  additive security whitelist that works in conjunction with\n>     -+  `promisor.acceptFromServer`.\n>     ++  A glob pattern to specify which server-advertised URLs a\n>     ++  client is allowed to act on. When a URL matches, the client\n>     ++  will accept the advertised remote as a promisor remote and may\n>     ++  automatically accept field updates (such as authentication\n>     ++  tokens) from the server, even if `promisor.acceptFromServer`\n>     ++  is set to `none` (the default).\n>      ++\n>      +This option can appear multiple times in config files. An advertised\n>      +URL will be accepted if it matches _ANY_ glob pattern specified by\n>      +this option in _ANY_ config file read by Git.\n>      ++\n>     -+Be _VERY_ careful with these glob patterns, as it can be a big\n>     -+security hole to allow any advertised remote to be auto-configured!\n>     ++Be _VERY_ careful with these patterns: `*` matches any sequence of\n>     ++characters within the 'host' and 'path' parts of a URL (but cannot\n>     ++cross part boundaries). An overly broad pattern is a major security\n>     ++risk, as a matching URL allows a server to update fields (such as\n>     ++authentication tokens) on known remotes without further confirmation.\n>      +To minimize security risks, follow these guidelines:\n>      ++\n>      +1. Start with a secure protocol scheme, like `https://` or `ssh://`.\n>     @@ Documentation/config/promisor.adoc: promisor.acceptFromServer::\n>      +   your specific organization or namespace (e.g.,\n>      +   `https://gitlab.com/your-org/*`).\n>      ++\n>     -+3. Don't use globs (`*`) in the domain name. For example\n>     -+   `https://cdn.example.com/*` is much safer than\n>     -+   `https://*.example.com/*`, because the latter matches\n>     -+   `https://evil-hacker.net/fake.example.com/repo`.\n>     ++3. Never use globs at the end of domain names. For example,\n>     ++   `https://cdn.your-org.com/*` might be safe, but\n>     ++   `https://cdn.your-org.com*/*` is a major security risk because\n>     ++   the latter matches `https://cdn.your-org.com.hacker.net/repo`.\n>      ++\n>     -+4. Make sure to have a `/` at the end of the domain name (or the end\n>     -+   of specific directories). For example `https://cdn.example.com/*`\n>     -+   is much safer than `https://cdn.example.com*`, because the latter\n>     -+   matches `https://cdn.example.com.hacker.net/repo`.\n>     ++4. Be careful using globs at the beginning of domain names. While the\n>     ++   code ensures a `*` in the host cannot cross into the path, a\n>     ++   pattern like `https://*.example.com/*` will still match any\n>     ++   subdomain. This is extremely dangerous on shared hosting platforms\n>     ++   (e.g., `https://*.github.io/*` trusts every user's site on the\n>     ++   entire platform).\n>      ++\n>     -+Before matching, the advertised URL is normalized: the scheme and\n>     -+host are lowercased, percent-encoded characters are decoded where\n>     -+possible, and path segments like `..` are resolved.  Glob patterns\n>     -+are matched against this normalized URL as-is, so patterns should\n>     -+be written in normalized form (e.g., lowercase scheme and host).\n>     ++Before matching, both the advertised URL and the pattern are\n>     ++normalized: the scheme and host are lowercased, percent-encoded\n>     ++characters are decoded where possible, and path segments like `..`\n>     ++are resolved. The port must also match exactly (e.g.,\n>     ++`https://example.com:8080/*` will not match a URL advertised on\n>     ++port 9999).\n>      ++\n>     -+Even if `promisor.acceptFromServer` is set to `None` (the default),\n>     -+Git will still accept field updates (like tokens) for known remotes,\n>     -+provided their URLs match a pattern in\n>     -+`promisor.acceptFromServerUrl`. See linkgit:gitprotocol-v2[5] for\n>     -+details on the protocol.\n>     ++For the security implications of accepting a promisor remote, see the\n>     ++documentation of `promisor.acceptFromServer`. For details on the\n>     ++protocol, see linkgit:gitprotocol-v2[5].\n>      +\n>       promisor.checkFields::\n>         A comma or space separated list of additional remote related\n>     @@ promisor-remote.c\n>\n>       struct promisor_remote_config {\n>         struct promisor_remote *promisors;\n>     -@@ promisor-remote.c: static struct string_list *accept_from_server_url(struct repository *repo)\n>     -   return &accept_urls;\n>     +@@ promisor-remote.c: static void load_accept_from_server_url(struct repository *repo,\n>     +   }\n>       }\n>\n>     ++static bool match_one_url(const struct url_info *pi, const struct url_info *ui)\n>     ++{\n>     ++  const char *pat = pi->url;\n>     ++  const char *url = ui->url;\n>     ++  char *p_str, *u_str;\n>     ++  bool res;\n>     ++\n>     ++  /*\n>     ++   * Schemes must match exactly. They are case-folded by\n>     ++   * url_normalize(), so strncmp() suffices.\n>     ++   */\n>     ++  if (pi->scheme_len != ui->scheme_len || strncmp(pat, url, pi->scheme_len))\n>     ++          return false;\n>     ++\n>     ++  /*\n>     ++   * Ports must match exactly. url_normalize() strips default\n>     ++   * ports (like 443 for https), so length and content\n>     ++   * comparisons are sufficient.\n>     ++   */\n>     ++  if (pi->port_len != ui->port_len ||\n>     ++      strncmp(pat + pi->port_off, url + ui->port_off, pi->port_len))\n>     ++          return false;\n>     ++\n>     ++  /*\n>     ++   * Match host and path separately to prevent a '*' in the host\n>     ++   * portion of the pattern from matching across the '/'\n>     ++   * boundary into the path. Use WM_PATHNAME for the host so '*'\n>     ++   * cannot cross '/' there, and 0 for the path so '*' can still\n>     ++   * match multi-level paths.\n>     ++   */\n>     ++\n>     ++  p_str = xstrndup(pat + pi->host_off, pi->host_len);\n>     ++  u_str = xstrndup(url + ui->host_off, ui->host_len);\n>     ++  res = !wildmatch(p_str, u_str, WM_PATHNAME);\n>     ++  free(p_str);\n>     ++  free(u_str);\n>     ++\n>     ++  if (!res)\n>     ++          return false;\n>     ++\n>     ++  p_str = xstrndup(pat + pi->path_off, pi->path_len);\n>     ++  u_str = xstrndup(url + ui->path_off, ui->path_len);\n>     ++  res = !wildmatch(p_str, u_str, 0);\n>     ++  free(p_str);\n>     ++  free(u_str);\n>     ++\n>     ++  return res;\n>     ++}\n>     ++\n>      +static struct allowed_url *url_matches_accept_list(\n>      +          struct string_list *accept_urls, const char *url)\n>      +{\n>      +  struct string_list_item *item;\n>     -+  char *normalized = url_normalize(url, NULL);\n>     ++  struct url_info url_info;\n>     ++\n>     ++  url_info.url = url_normalize(url, &url_info);\n>      +\n>     -+  if (!normalized)\n>     ++  if (!url_info.url)\n>      +          return NULL;\n>      +\n>      +  for_each_string_list_item(item, accept_urls) {\n>      +          struct allowed_url *allowed = item->util;\n>      +\n>     -+          if (!wildmatch(allowed->url_pattern, normalized, 0)) {\n>     -+                  free(normalized);\n>     ++          if (match_one_url(&allowed->pattern_info, &url_info)) {\n>     ++                  free(url_info.url);\n>      +                  return allowed;\n>      +          }\n>      +  }\n>      +\n>     -+  free(normalized);\n>     ++  free(url_info.url);\n>      +  return NULL;\n>      +}\n>      +\n>     @@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n>      +  /*\n>      +   * Even if accept == ACCEPT_NONE, we MUST trust this known\n>      +   * remote to update its token or other such fields if its URL\n>     -+   * matches the acceptFromServerUrl whitelist!\n>     ++   * matches the acceptFromServerUrl allowlist!\n>      +   */\n>      +  if (url_matches_accept_list(accept_urls, remote_url))\n>      +          return all_fields_match(advertised, config_info, p);\n>     @@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n>\n>       static int skip_field_name_prefix(const char *elem, const char *field_name, const char **value)\n>      @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n>     -   struct string_list_item *item;\n>     -   bool reload_config = false;\n>     -   enum accept_promisor accept = accept_from_server(repo);\n>     --  /* Pre-load and validate the acceptFromServerUrl config */\n>     --  (void)accept_from_server_url(repo);\n>     -+  struct string_list *accept_urls = accept_from_server_url(repo);\n>     +   /* Load and validate the acceptFromServerUrl config */\n>     +   load_accept_from_server_url(repo, &accept_urls);\n>\n>      -  if (accept == ACCEPT_NONE)\n>     -+  if (accept == ACCEPT_NONE && !accept_urls->nr)\n>     ++  if (accept == ACCEPT_NONE && !accept_urls.nr)\n>                 return;\n>\n>         /* Parse remote info received */\n>     @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n>                 }\n>\n>      -          if (should_accept_remote(accept, advertised, &config_info)) {\n>     -+          if (should_accept_remote(accept, advertised, accept_urls, &config_info)) {\n>     ++          if (should_accept_remote(accept, advertised, &accept_urls, &config_info)) {\n>                         if (!store_info)\n>                                 store_info = store_info_new(repo);\n>                         if (promisor_store_advertised_fields(advertised, store_info))\n>     @@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'KnownUrl\n>         check_missing_objects server 1 \"$oid\"\n>       '\n>\n>     -+test_expect_success \"clone with 'None' but URL whitelisted\" '\n>     ++test_expect_success \"clone with 'None' but URL allowlisted\" '\n>      +  git -C server config promisor.advertise true &&\n>      +  test_when_finished \"rm -rf client\" &&\n>      +\n>      +  GIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \\\n>      +          -c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n>     -+          -c remote.lop.url=\"$PWD_URL/lop\" \\\n>     ++          -c remote.lop.url=\"$TRASH_DIRECTORY_URL/lop\" \\\n>      +          -c promisor.acceptfromserver=None \\\n>     -+          -c promisor.acceptFromServerUrl=\"$ENCODED_PWD_URL/*\" \\\n>     ++          -c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n>      +          --no-local --filter=\"blob:limit=5k\" server client &&\n>      +\n>      +  # Check that the largest object is still missing on the server\n>      +  check_missing_objects server 1 \"$oid\"\n>      +'\n>      +\n>     -+test_expect_success \"clone with 'None' but URL not in whitelist\" '\n>     ++test_expect_success \"clone with 'None' but URL not in allowlist\" '\n>      +  git -C server config promisor.advertise true &&\n>      +  test_when_finished \"rm -rf client\" &&\n>      +\n>      +  GIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \\\n>      +          -c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n>     -+          -c remote.lop.url=\"$PWD_URL/lop\" \\\n>     ++          -c remote.lop.url=\"$TRASH_DIRECTORY_URL/lop\" \\\n>      +          -c promisor.acceptfromserver=None \\\n>      +          -c promisor.acceptFromServerUrl=\"https://example.com/*\" \\\n>      +          --no-local --filter=\"blob:limit=5k\" server client &&\n>     @@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'KnownUrl\n>      +  initialize_server 1 \"$oid\"\n>      +'\n>      +\n>     -+test_expect_success \"clone with 'None' but URL whitelisted in one pattern out of two\" '\n>     ++test_expect_success \"clone with 'None' but URL allowlisted in one pattern out of two\" '\n>      +  git -C server config promisor.advertise true &&\n>      +  test_when_finished \"rm -rf client\" &&\n>      +\n>      +  GIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \\\n>      +          -c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n>     -+          -c remote.lop.url=\"$PWD_URL/lop\" \\\n>     ++          -c remote.lop.url=\"$TRASH_DIRECTORY_URL/lop\" \\\n>      +          -c promisor.acceptfromserver=None \\\n>      +          -c promisor.acceptFromServerUrl=\"https://example.com/*\" \\\n>     -+          -c promisor.acceptFromServerUrl=\"$ENCODED_PWD_URL/*\" \\\n>     ++          -c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n>      +          --no-local --filter=\"blob:limit=5k\" server client &&\n>      +\n>      +  # Check that the largest object is still missing on the server\n>      +  check_missing_objects server 1 \"$oid\"\n>      +'\n>      +\n>     -+test_expect_success \"clone with 'None', URL whitelisted, but client has different URL\" '\n>     ++test_expect_success \"clone with 'None', URL allowlisted, but client has different URL\" '\n>      +  git -C server config promisor.advertise true &&\n>      +  test_when_finished \"rm -rf client\" &&\n>      +\n>      +  # The client configures \"lop\" with a different URL (serverTwo) than\n>      +  # what the server advertises (lop). Even though the advertised URL\n>     -+  # matches the whitelist, the remote is rejected because the\n>     ++  # matches the allowlist, the remote is rejected because the\n>      +  # configured URL does not match the advertised one.\n>      +  GIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \\\n>      +          -c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n>     -+          -c remote.lop.url=\"$PWD_URL/serverTwo\" \\\n>     ++          -c remote.lop.url=\"$TRASH_DIRECTORY_URL/serverTwo\" \\\n>      +          -c promisor.acceptfromserver=None \\\n>     -+          -c promisor.acceptFromServerUrl=\"$ENCODED_PWD_URL/*\" \\\n>     ++          -c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n>      +          --no-local --filter=\"blob:limit=5k\" server client &&\n>      +\n>      +  # Check that the largest object is not missing on the server\n>     @@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'KnownUrl\n>      +  initialize_server 1 \"$oid\"\n>      +'\n>      +\n>     - test_expect_success \"clone with promisor.sendFields\" '\n>     + test_expect_success \"clone with invalid promisor.acceptFromServerUrl\" '\n>         git -C server config promisor.advertise true &&\n>         test_when_finished \"rm -rf client\" &&\n>     -@@ t/t5710-promisor-remote-capability.sh: test_expect_success \"subsequent fetch from a client when promisor.advertise is f\n>     -   check_missing_objects server 1 \"$oid\"\n>     - '\n>     -\n>     -+\n>     -+\n>     - test_done\n> 15:  314150a860 !  7:  a077f33df4 promisor-remote: auto-configure unknown remotes\n>     @@ Commit message\n>          promisor-remote: auto-configure unknown remotes\n>\n>          Previous commits have introduced the `promisor.acceptFromServerUrl`\n>     -    config variable to whitelist some URLs advertised by a server through\n>     +    config variable to allowlist some URLs advertised by a server through\n>          the \"promisor-remote\" protocol capability.\n>\n>          However the new `promisor.acceptFromServerUrl` mechanism, like the old\n>     @@ Commit message\n>\n>       ## Documentation/config/promisor.adoc ##\n>      @@ Documentation/config/promisor.adoc: promisor.acceptFromServer::\n>     -\n>       promisor.acceptFromServerUrl::\n>     -   A glob pattern to specify which URLs advertised by a server\n>     --  are considered trusted by the client. This option acts as an\n>     --  additive security whitelist that works in conjunction with\n>     --  `promisor.acceptFromServer`.\n>     -+  are allowed to be auto-configured (created and persisted) on\n>     -+  the client side. Unlike `promisor.acceptFromServer`, which\n>     -+  only accepts already configured remotes, a match against this\n>     -+  option instructs Git to write a new `[remote \"<name>\"]`\n>     -+  section to the client's configuration.\n>     +   A glob pattern to specify which server-advertised URLs a\n>     +   client is allowed to act on. When a URL matches, the client\n>     +-  will accept the advertised remote as a promisor remote and may\n>     ++  will accept the advertised remote as a promisor remote, may\n>     ++  automatically create a new remote configuration for it and may\n>     +   automatically accept field updates (such as authentication\n>     +   tokens) from the server, even if `promisor.acceptFromServer`\n>     +   is set to `none` (the default).\n>     +@@ Documentation/config/promisor.adoc: this option in _ANY_ config file read by Git.\n>     + Be _VERY_ careful with these patterns: `*` matches any sequence of\n>     + characters within the 'host' and 'path' parts of a URL (but cannot\n>     + cross part boundaries). An overly broad pattern is a major security\n>     +-risk, as a matching URL allows a server to update fields (such as\n>     +-authentication tokens) on known remotes without further confirmation.\n>     +-To minimize security risks, follow these guidelines:\n>     ++risk, as a matching URL allows a server to auto-configure new remotes\n>     ++and to update fields (such as authentication tokens) on known remotes\n>     ++without further confirmation. To minimize security risks, follow these\n>     ++guidelines:\n>     + +\n>     + 1. Start with a secure protocol scheme, like `https://` or `ssh://`.\n>       +\n>     - This option can appear multiple times in config files. An advertised\n>     - URL will be accepted if it matches _ANY_ glob pattern specified by\n>     -@@ Documentation/config/promisor.adoc: possible, and path segments like `..` are resolved.  Glob patterns\n>     - are matched against this normalized URL as-is, so patterns should\n>     - be written in normalized form (e.g., lowercase scheme and host).\n>     +@@ Documentation/config/promisor.adoc: are resolved. The port must also match exactly (e.g.,\n>     + `https://example.com:8080/*` will not match a URL advertised on\n>     + port 9999).\n>       +\n>     --Even if `promisor.acceptFromServer` is set to `None` (the default),\n>     --Git will still accept field updates (like tokens) for known remotes,\n>     --provided their URLs match a pattern in\n>     --`promisor.acceptFromServerUrl`. See linkgit:gitprotocol-v2[5] for\n>     --details on the protocol.\n>      +The glob pattern can optionally be prefixed with a remote name and an\n>      +equals sign (e.g., `cdn=https://cdn.example.com/*`). If such a prefix\n>      +is provided, accepted remotes will be saved under that name. If no\n>      +such prefix is provided, a safe remote name will be automatically\n>      +generated by sanitizing the URL and prefixing it with\n>     -+`promisor-auto-`. If a remote with the chosen name already exists but\n>     -+points to a different URL, Git will append a numeric suffix (e.g.,\n>     -+`-1`, `-2`) to the name to prevent overwriting existing\n>     -+configurations. You should make sure that this doesn't happen often\n>     -+though, as remotes will be rejected if the numeric suffix increases\n>     -+too much. In all cases, the original name advertised by the server is\n>     -+recorded in the `remote.<name>.advertisedAs` configuration variable\n>     -+for tracing and debugging purposes.\n>     ++`promisor-auto-`.\n>      ++\n>     -+Note that this option acts as an additive security whitelist. It works\n>     -+in conjunction with `promisor.acceptFromServer` (see the documentation\n>     -+of that option for the implications of accepting a promisor\n>     -+remote). Even if `promisor.acceptFromServer` is set to `None` (the\n>     -+default), Git will still automatically configure new remotes, and\n>     -+accept field updates (like tokens) for known remotes, provided their\n>     -+URLs match a pattern in `promisor.acceptFromServerUrl`. See\n>     -+linkgit:gitprotocol-v2[5] for details on the protocol.\n>     -\n>     - promisor.checkFields::\n>     -   A comma or space separated list of additional remote related\n>     ++If a remote with the chosen name already exists but points to a\n>     ++different URL, Git will append a numeric suffix (e.g., `-1`, `-2`) to\n>     ++the name to prevent overwriting existing configurations. You should\n>     ++make sure that this doesn't happen often though, as remotes will be\n>     ++rejected if the numeric suffix increases too much. In all cases, the\n>     ++original name advertised by the server is recorded in the\n>     ++`remote.<name>.advertisedAs` configuration variable for tracing and\n>     ++debugging purposes.\n>     +++\n>     + For the security implications of accepting a promisor remote, see the\n>     + documentation of `promisor.acceptFromServer`. For details on the\n>     + protocol, see linkgit:gitprotocol-v2[5].\n>\n>       ## Documentation/config/remote.adoc ##\n>      @@ Documentation/config/remote.adoc: remote.<name>.promisor::\n>     @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n>                         string_list_sort(&config_info);\n>                 }\n>\n>     --          if (should_accept_remote(accept, advertised, accept_urls, &config_info)) {\n>     -+          if (should_accept_remote(repo, accept, advertised, accept_urls,\n>     +-          if (should_accept_remote(accept, advertised, &accept_urls, &config_info)) {\n>     ++          if (should_accept_remote(repo, accept, advertised, &accept_urls,\n>      +                                   &config_info, &reload_config)) {\n>                         if (!store_info)\n>                                 store_info = store_info_new(repo);\n>                         if (promisor_store_advertised_fields(advertised, store_info))\n>\n>       ## t/t5710-promisor-remote-capability.sh ##\n>     -@@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'None', URL whitelisted, but client has differen\n>     +@@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'None', URL allowlisted, but client has differen\n>         initialize_server 1 \"$oid\"\n>       '\n>\n>     -+test_expect_success \"clone with URL whitelisted and no remote already configured\" '\n>     ++test_expect_success \"clone with URL allowlisted and no remote already configured\" '\n>      +  git -C server config promisor.advertise true &&\n>      +  test_when_finished \"rm -rf client\" &&\n>     ++  test_when_finished \"rm -f full_names\" &&\n>      +\n>      +  GIT_NO_LAZY_FETCH=0 git clone \\\n>      +          -c promisor.acceptfromserver=None \\\n>     -+          -c promisor.acceptFromServerUrl=\"$ENCODED_PWD_URL/*\" \\\n>     ++          -c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n>      +          --no-local --filter=\"blob:limit=5k\" server client &&\n>      +\n>     -+  # Check that a remote has been auto-created with the right fields.\n>     -+  # The remote is identified by \"remote.<name>.advertisedAs\" == \"lop\".\n>     -+  FULL_NAME=$(git -C client config --name-only --get-regexp \"remote\\..*\\.advertisedas\" \"^lop$\") &&\n>     -+  REMOTE_NAME=$(echo \"$FULL_NAME\" | sed \"s/remote\\.\\(.*\\)\\.advertisedas/\\1/\") &&\n>     ++  # Check that exactly one remote has been auto-created, identified\n>     ++  # by \"remote.<name>.advertisedAs\" == \"lop\".\n>     ++  git -C client config get --all --show-names --regexp \\\n>     ++          \"remote\\..*\\.advertisedas\" >full_names &&\n>     ++  test_line_count = 1 full_names &&\n>     ++  REMOTE_NAME=$(sed \"s/^remote\\.\\(.*\\)\\.advertisedas .*$/\\1/\" full_names) &&\n>      +\n>      +  # Check \".url\" and \".promisor\" values\n>     -+  printf \"%s\\n\" \"$PWD_URL/lop\" \"true\" >expect &&\n>     ++  printf \"%s\\n\" \"$TRASH_DIRECTORY_URL/lop\" \"true\" >expect &&\n>      +  git -C client config \"remote.$REMOTE_NAME.url\" >actual &&\n>      +  git -C client config \"remote.$REMOTE_NAME.promisor\" >>actual &&\n>      +  test_cmp expect actual &&\n>     @@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'None', U\n>      +  check_missing_objects server 1 \"$oid\"\n>      +'\n>      +\n>     -+test_expect_success \"clone with named URL whitelisted and no pre-configured remote\" '\n>     ++test_expect_success \"clone with named URL allowlisted and no pre-configured remote\" '\n>      +  git -C server config promisor.advertise true &&\n>      +  test_when_finished \"rm -rf client\" &&\n>      +\n>      +  GIT_NO_LAZY_FETCH=0 git clone \\\n>      +          -c promisor.acceptfromserver=None \\\n>     -+          -c promisor.acceptFromServerUrl=\"cdn=$ENCODED_PWD_URL/*\" \\\n>     ++          -c promisor.acceptFromServerUrl=\"cdn=$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n>      +          --no-local --filter=\"blob:limit=5k\" server client &&\n>      +\n>      +  # Check that a remote has been auto-created with the right \"cdn\" name and fields.\n>     -+  printf \"%s\\n\" \"$PWD_URL/lop\" \"true\" \"lop\" >expect &&\n>     ++  printf \"%s\\n\" \"$TRASH_DIRECTORY_URL/lop\" \"true\" \"lop\" >expect &&\n>      +  git -C client config \"remote.cdn.url\" >actual &&\n>      +  git -C client config \"remote.cdn.promisor\" >>actual &&\n>      +  git -C client config \"remote.cdn.advertisedAs\" >>actual &&\n>     @@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'None', U\n>      +  check_missing_objects server 1 \"$oid\"\n>      +'\n>      +\n>     -+test_expect_success \"clone with URL whitelisted but colliding name\" '\n>     ++test_expect_success \"clone with URL allowlisted but colliding name\" '\n>      +  git -C server config promisor.advertise true &&\n>      +  test_when_finished \"rm -rf client\" &&\n>      +\n>     @@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'None', U\n>      +          -c remote.cdn.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n>      +          -c remote.cdn.url=\"https://example.com/cdn\" \\\n>      +          -c promisor.acceptfromserver=None \\\n>     -+          -c promisor.acceptFromServerUrl=\"cdn=$ENCODED_PWD_URL/*\" \\\n>     ++          -c promisor.acceptFromServerUrl=\"cdn=$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n>      +          --no-local --filter=\"blob:limit=5k\" server client &&\n>      +\n>      +  # Check that a remote has been auto-created with the right \"cdn-1\" name and fields.\n>     -+  printf \"%s\\n\" \"$PWD_URL/lop\" \"true\" \"lop\" >expect &&\n>     ++  printf \"%s\\n\" \"$TRASH_DIRECTORY_URL/lop\" \"true\" \"lop\" >expect &&\n>      +  git -C client config \"remote.cdn-1.url\" >actual &&\n>      +  git -C client config \"remote.cdn-1.promisor\" >>actual &&\n>      +  git -C client config \"remote.cdn-1.advertisedAs\" >>actual &&\n>     @@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'None', U\n>      +  check_missing_objects server 1 \"$oid\"\n>      +'\n>      +\n>     -+test_expect_success \"clone with URL whitelisted and reusable remote\" '\n>     ++test_expect_success \"clone with URL allowlisted and reusable remote\" '\n>      +  git -C server config promisor.advertise true &&\n>      +  test_when_finished \"rm -rf client\" &&\n>      +\n>      +  GIT_NO_LAZY_FETCH=0 git clone \\\n>      +          -c remote.cdn.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n>     -+          -c remote.cdn.url=\"$PWD_URL/lop\" \\\n>     ++          -c remote.cdn.url=\"$TRASH_DIRECTORY_URL/lop\" \\\n>      +          -c promisor.acceptfromserver=None \\\n>     -+          -c promisor.acceptFromServerUrl=\"cdn=$ENCODED_PWD_URL/*\" \\\n>     ++          -c promisor.acceptFromServerUrl=\"cdn=$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n>      +          --no-local --filter=\"blob:limit=5k\" server client &&\n>      +\n>      +  # Check that the existing \"cdn\" remote has been properly updated.\n>     -+  printf \"%s\\n\" \"$PWD_URL/lop\" \"true\" \"lop\" \"+refs/heads/*:refs/remotes/lop/*\" >expect &&\n>     ++  printf \"%s\\n\" \"$TRASH_DIRECTORY_URL/lop\" \"true\" \"lop\" \"+refs/heads/*:refs/remotes/lop/*\" >expect &&\n>      +  git -C client config \"remote.cdn.url\" >actual &&\n>      +  git -C client config \"remote.cdn.promisor\" >>actual &&\n>      +  git -C client config \"remote.cdn.advertisedAs\" >>actual &&\n>     @@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'None', U\n>      +  check_missing_objects server 1 \"$oid\"\n>      +'\n>      +\n>     -+test_expect_success \"clone with invalid promisor.acceptFromServerUrl\" '\n>     -+  git -C server config promisor.advertise true &&\n>     -+  test_when_finished \"rm -rf client\" &&\n>     -+\n>     -+  # As \"bad name\" contains a space, which is not a valid remote name,\n>     -+  # the pattern should be rejected with a warning and no remote created.\n>     -+  GIT_NO_LAZY_FETCH=0 git clone \\\n>     -+          -c promisor.acceptfromserver=None \\\n>     -+          -c \"promisor.acceptFromServerUrl=bad name=https://example.com/*\" \\\n>     -+          --no-local --filter=\"blob:limit=5k\" server client 2>err &&\n>     -+\n>     -+  # Check that a warning was emitted\n>     -+  test_grep \"invalid remote name '\\''bad name'\\''\" err &&\n>     -+\n>     -+  # Check that no remote was auto-created\n>     -+  test_must_fail git -C client config --get-regexp \"remote\\..*\\.advertisedas\" &&\n>     -+\n>     -+  # Check that the largest object is not missing on the server\n>     -+  check_missing_objects server 0 \"\" &&\n>     -+\n>     -+  # Reinitialize server so that the largest object is missing again\n>     -+  initialize_server 1 \"$oid\"\n>     -+'\n>     -+\n>     - test_expect_success \"clone with promisor.sendFields\" '\n>     + test_expect_success \"clone with invalid promisor.acceptFromServerUrl\" '\n>         git -C server config promisor.advertise true &&\n>         test_when_finished \"rm -rf client\" &&\n>     +@@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with invalid promisor.acceptFromServerUrl\" '\n>     +   # Check that a warning was emitted\n>     +   test_grep \"invalid remote name '\\''bad name'\\''\" err &&\n>     +\n>     ++  # Check that no remote was auto-created\n>     ++  test_must_fail git -C client config get --regexp \"remote\\..*\\.advertisedas\" &&\n>     ++\n>     +   # Check that the largest object is not missing on the server\n>     +   check_missing_objects server 0 \"\" &&\n>     +\n> 16:  20f70b52bb !  8:  b68b9497aa doc: promisor: improve acceptFromServer entry\n>     @@ Documentation/config/promisor.adoc: variable is set to \"true\", and the \"name\" an\n>      +for protocol details.\n>\n>       promisor.acceptFromServerUrl::\n>     -   A glob pattern to specify which URLs advertised by a server\n>     +   A glob pattern to specify which server-advertised URLs a\n>\n>\n> Christian Couder (8):\n>   t5710: simplify 'mkdir X' followed by 'git -C X init'\n>   urlmatch: change 'allow_globs' arg to bool\n>   urlmatch: add url_normalize_pattern() helper\n>   promisor-remote: add 'local_name' to 'struct promisor_info'\n>   promisor-remote: introduce promisor.acceptFromServerUrl\n>   promisor-remote: trust known remotes matching acceptFromServerUrl\n>   promisor-remote: auto-configure unknown remotes\n>   doc: promisor: improve acceptFromServer entry\n>\n>  Documentation/config/promisor.adoc    | 123 ++++++--\n>  Documentation/config/remote.adoc      |   9 +\n>  Documentation/gitprotocol-v2.adoc     |   9 +-\n>  promisor-remote.c                     | 410 ++++++++++++++++++++++++--\n>  t/t5710-promisor-remote-capability.sh | 202 ++++++++++++-\n>  urlmatch.c                            |  11 +-\n>  urlmatch.h                            |  12 +\n>  7 files changed, 730 insertions(+), 46 deletions(-)\n>\n> --\n> 2.54.0.19.gb68b9497aa\n>\n"},{"id":"542660","messageId":"87bjevs8gg.fsf@toon--20250203-5JQV3.mail-host-address-is-not-set","threadId":"64670","inReplyTo":"20260427124108.3524129-5-christian.couder@gmail.com","subject":"Re: [PATCH v2 4/8] promisor-remote: add 'local_name' to 'struct promisor_info'","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-05-04T11:46:55Z","receivedAt":"2026-05-04T11:47:04Z","isPatch":true,"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> In a following commit, we will store promisor remote information under\n> a remote name different than the one the server advertised.\n>\n> To prepare for this change, let's add a new 'char *local_name' member\n> to 'struct promisor_info', and let's update the related functions.\n>\n> While at it, let's also add a small promisor_info_internal_name()\n> helper that returns `local_name` when set, `name` otherwise, and let's\n> use this small helper in promisor_store_advertised_fields() and in the\n> post-loop of filter_promisor_remote() so that lookups against the local\n> repo configuration use the right name.\n\nIt seems the `local_name` doesn't get filled in yet, so because\npromisor_info_internal_name() falls back to `name` there is no\nfunctional change in this commit. Okay.\n\n-- \nCheers,\nToon\n"},{"id":"542906","messageId":"875x4yoys5.fsf@toon--20250203-5JQV3.mail-host-address-is-not-set","threadId":"64670","inReplyTo":"20260427124108.3524129-7-christian.couder@gmail.com","subject":"Re: [PATCH v2 6/8] promisor-remote: trust known remotes matching acceptFromServerUrl","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-05-08T12:45:30Z","receivedAt":"2026-05-08T12:45:37Z","isPatch":true,"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> A previous commit introduced the `promisor.acceptFromServerUrl` config\n> variable along with the machinery to parse and validate the URL glob\n> patterns and optional remote name prefixes it contains. However, these\n> URL patterns are not yet tied into the client's acceptance logic.\n>\n> When a promisor remote is already configured locally, its fields (like\n> authentication tokens) may occasionally need to be refreshed by the\n> server. If `promisor.acceptFromServer` is set to the secure default\n> (\"None\"), these updates are rejected, potentially causing future\n> fetches to fail.\n>\n> To enable such targeted updates for trusted URLs, let's use the URL\n> patterns from `promisor.acceptFromServerUrl` as an additional URL\n> based allowlist.\n>\n> Concretely, let's check the advertised URLs against the URL glob\n> patterns by introducing a new small helper function called\n> url_matches_accept_list(), which iterates over the glob patterns and\n> returns the first matching allowed_url entry (or NULL).\n>\n> The URL matching is done component by component: scheme and port are\n> compared exactly, the host is matched with wildmatch() using the\n> WM_PATHNAME flag (so '*' cannot cross the '/' boundary into the path),\n> and the path is matched with wildmatch() without WM_PATHNAME (so '*'\n> can still match multi-level paths). Before matching, the advertised\n> URL is passed through url_normalize() so that case variations in the\n> scheme/host, percent-encoding tricks, and \"..\" path segments cannot\n> bypass the allowlist.\n>\n> Let's then use this helper at the tail of should_accept_remote() so\n> that, when `accept == ACCEPT_NONE`, a known remote whose URL matches\n> the allowlist is still accepted.\n>\n> To prepare for this new logic, let's also:\n>\n>  - Add an 'accept_urls' parameter to should_accept_remote().\n>\n>  - Replace the BUG() guard in the ACCEPT_KNOWN_URL case with an\n>    explicit 'if (accept == ACCEPT_KNOWN_URL) return' and a new\n>    BUG() guard in the ACCEPT_NONE case, so url_matches_accept_list()\n>    is only called in the ACCEPT_NONE case.\n>\n>  - Call accept_from_server_url() from filter_promisor_remote()\n>    and relax its early return so that the function is entered when\n>    `accept_urls` has entries even if `accept == ACCEPT_NONE`.\n>\n> With this, many organizations may only need something like:\n>\n>   git config set --global \\\n>           promisor.acceptFromServerUrl \"https://my-org.com/*\"\n>\n> to accept only their own remotes. And if they need to accept additional\n> remotes in some specific repos, they can also set:\n>\n>   git config set promisor.acceptFromServer knownUrl\n>\n> and configure the additional remote manually only in the repos where\n> they are needed.\n>\n> Let's then properly document `promisor.acceptFromServerUrl` in\n> \"promisor.adoc\" as an additive security allowlist for known remotes,\n> including the URL normalization behavior and the component-wise\n> matching, and let's mention it in \"gitprotocol-v2.adoc\".\n>\n> Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n> ---\n>  Documentation/config/promisor.adoc    | 52 ++++++++++++++\n>  Documentation/gitprotocol-v2.adoc     |  9 +--\n>  promisor-remote.c                     | 98 +++++++++++++++++++++++++--\n>  t/t5710-promisor-remote-capability.sh | 71 +++++++++++++++++++\n>  4 files changed, 220 insertions(+), 10 deletions(-)\n>\n> diff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\n> index b0fa43b839..efc066c3f2 100644\n> --- a/Documentation/config/promisor.adoc\n> +++ b/Documentation/config/promisor.adoc\n> @@ -51,6 +51,58 @@ promisor.acceptFromServer::\n>  \tto \"fetch\" and \"clone\" requests from the client. Name and URL\n>  \tcomparisons are case sensitive. See linkgit:gitprotocol-v2[5].\n>  \n> +promisor.acceptFromServerUrl::\n> +\tA glob pattern to specify which server-advertised URLs a\n> +\tclient is allowed to act on. When a URL matches, the client\n> +\twill accept the advertised remote as a promisor remote and may\n> +\tautomatically accept field updates (such as authentication\n> +\ttokens) from the server, even if `promisor.acceptFromServer`\n> +\tis set to `none` (the default).\n> ++\n> +This option can appear multiple times in config files. An advertised\n> +URL will be accepted if it matches _ANY_ glob pattern specified by\n> +this option in _ANY_ config file read by Git.\n> ++\n> +Be _VERY_ careful with these patterns: `*` matches any sequence of\n> +characters within the 'host' and 'path' parts of a URL (but cannot\n> +cross part boundaries). An overly broad pattern is a major security\n> +risk, as a matching URL allows a server to update fields (such as\n> +authentication tokens) on known remotes without further confirmation.\n> +To minimize security risks, follow these guidelines:\n> ++\n> +1. Start with a secure protocol scheme, like `https://` or `ssh://`.\n> ++\n> +2. Only allow domain names or paths where you control and trust _ALL_\n> +   the content. Be especially careful with shared hosting platforms\n> +   like `github.com` or `gitlab.com`. A broad pattern like\n> +   `https://gitlab.com/*` is dangerous because it trusts every\n> +   repository on the entire platform. Always restrict such patterns to\n> +   your specific organization or namespace (e.g.,\n> +   `https://gitlab.com/your-org/*`).\n> ++\n> +3. Never use globs at the end of domain names. For example,\n> +   `https://cdn.your-org.com/*` might be safe, but\n> +   `https://cdn.your-org.com*/*` is a major security risk because\n> +   the latter matches `https://cdn.your-org.com.hacker.net/repo`.\n> ++\n> +4. Be careful using globs at the beginning of domain names. While the\n> +   code ensures a `*` in the host cannot cross into the path, a\n> +   pattern like `https://*.example.com/*` will still match any\n> +   subdomain. This is extremely dangerous on shared hosting platforms\n> +   (e.g., `https://*.github.io/*` trusts every user's site on the\n> +   entire platform).\n> ++\n> +Before matching, both the advertised URL and the pattern are\n> +normalized: the scheme and host are lowercased, percent-encoded\n> +characters are decoded where possible, and path segments like `..`\n> +are resolved. The port must also match exactly (e.g.,\n> +`https://example.com:8080/*` will not match a URL advertised on\n> +port 9999).\n> ++\n> +For the security implications of accepting a promisor remote, see the\n> +documentation of `promisor.acceptFromServer`. For details on the\n> +protocol, see linkgit:gitprotocol-v2[5].\n> +\n>  promisor.checkFields::\n>  \tA comma or space separated list of additional remote related\n>  \tfield names. A client checks if the values of these fields\n> diff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\n> index befa697d21..2beb70595f 100644\n> --- a/Documentation/gitprotocol-v2.adoc\n> +++ b/Documentation/gitprotocol-v2.adoc\n> @@ -866,10 +866,11 @@ the server advertised, the client shouldn't advertise the\n>  \n>  On the server side, the \"promisor.advertise\" and \"promisor.sendFields\"\n>  configuration options can be used to control what it advertises. On\n> -the client side, the \"promisor.acceptFromServer\" configuration option\n> -can be used to control what it accepts, and the \"promisor.storeFields\"\n> -option, to control what it stores. See the documentation of these\n> -configuration options in linkgit:git-config[1] for more information.\n> +the client side, the \"promisor.acceptFromServer\" and\n> +\"promisor.acceptFromServerUrl\" configuration options can be used to\n> +control what it accepts, and the \"promisor.storeFields\" option, to\n> +control what it stores. See the documentation of these configuration\n> +options in linkgit:git-config[1] for more information.\n>  \n>  Note that in the future it would be nice if the \"promisor-remote\"\n>  protocol capability could be used by the server, when responding to\n> diff --git a/promisor-remote.c b/promisor-remote.c\n> index 3f3924f587..72d5b94bf7 100644\n> --- a/promisor-remote.c\n> +++ b/promisor-remote.c\n> @@ -14,6 +14,7 @@\n>  #include \"url.h\"\n>  #include \"urlmatch.h\"\n>  #include \"version.h\"\n> +#include \"wildmatch.h\"\n>  \n>  struct promisor_remote_config {\n>  \tstruct promisor_remote *promisors;\n> @@ -742,8 +743,82 @@ static void load_accept_from_server_url(struct repository *repo,\n>  \t}\n>  }\n>  \n> +static bool match_one_url(const struct url_info *pi, const struct url_info *ui)\n> +{\n> +\tconst char *pat = pi->url;\n> +\tconst char *url = ui->url;\n> +\tchar *p_str, *u_str;\n> +\tbool res;\n> +\n> +\t/*\n> +\t * Schemes must match exactly. They are case-folded by\n> +\t * url_normalize(), so strncmp() suffices.\n> +\t */\n> +\tif (pi->scheme_len != ui->scheme_len || strncmp(pat, url, pi->scheme_len))\n> +\t\treturn false;\n> +\n> +\t/*\n> +\t * Ports must match exactly. url_normalize() strips default\n> +\t * ports (like 443 for https), so length and content\n> +\t * comparisons are sufficient.\n> +\t */\n> +\tif (pi->port_len != ui->port_len ||\n> +\t    strncmp(pat + pi->port_off, url + ui->port_off, pi->port_len))\n> +\t\treturn false;\n> +\n> +\t/*\n> +\t * Match host and path separately to prevent a '*' in the host\n> +\t * portion of the pattern from matching across the '/'\n> +\t * boundary into the path. Use WM_PATHNAME for the host so '*'\n> +\t * cannot cross '/' there, and 0 for the path so '*' can still\n> +\t * match multi-level paths.\n> +\t */\n\nDo we actually need WM_PATHNAME, because we only xstrndup() the host\npart anyway?\n\n> +\n> +\tp_str = xstrndup(pat + pi->host_off, pi->host_len);\n> +\tu_str = xstrndup(url + ui->host_off, ui->host_len);\n> +\tres = !wildmatch(p_str, u_str, WM_PATHNAME);\n> +\tfree(p_str);\n> +\tfree(u_str);\n> +\n> +\tif (!res)\n> +\t\treturn false;\n> +\n> +\tp_str = xstrndup(pat + pi->path_off, pi->path_len);\n> +\tu_str = xstrndup(url + ui->path_off, ui->path_len);\n> +\tres = !wildmatch(p_str, u_str, 0);\n> +\tfree(p_str);\n> +\tfree(u_str);\n\nIs it correct we intentionally do not compare the user and pass (at\n`user_off` and `passwd_off`)? I assume so, because this allows the\nserver to update those?\n\n> +\n> +\treturn res;\n> +}\n> +\n> +static struct allowed_url *url_matches_accept_list(\n> +\t\tstruct string_list *accept_urls, const char *url)\n> +{\n> +\tstruct string_list_item *item;\n> +\tstruct url_info url_info;\n> +\n> +\turl_info.url = url_normalize(url, &url_info);\n> +\n> +\tif (!url_info.url)\n> +\t\treturn NULL;\n> +\n> +\tfor_each_string_list_item(item, accept_urls) {\n> +\t\tstruct allowed_url *allowed = item->util;\n> +\n> +\t\tif (match_one_url(&allowed->pattern_info, &url_info)) {\n> +\t\t\tfree(url_info.url);\n> +\t\t\treturn allowed;\n> +\t\t}\n> +\t}\n> +\n> +\tfree(url_info.url);\n> +\treturn NULL;\n> +}\n> +\n>  static int should_accept_remote(enum accept_promisor accept,\n>  \t\t\t\tstruct promisor_info *advertised,\n> +\t\t\t\tstruct string_list *accept_urls,\n>  \t\t\t\tstruct string_list *config_info)\n>  {\n>  \tstruct promisor_info *p;\n> @@ -771,9 +846,6 @@ static int should_accept_remote(enum accept_promisor accept,\n>  \tif (accept == ACCEPT_KNOWN_NAME)\n>  \t\treturn all_fields_match(advertised, config_info, p);\n>  \n> -\tif (accept != ACCEPT_KNOWN_URL)\n> -\t\tBUG(\"Unhandled 'enum accept_promisor' value '%d'\", accept);\n> -\n>  \tif (strcmp(p->url, remote_url)) {\n>  \t\twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s', \"\n>  \t\t\t  \"ignoring this remote\"),\n> @@ -781,7 +853,21 @@ static int should_accept_remote(enum accept_promisor accept,\n>  \t\treturn 0;\n>  \t}\n>  \n> -\treturn all_fields_match(advertised, config_info, p);\n> +\tif (accept == ACCEPT_KNOWN_URL)\n> +\t\treturn all_fields_match(advertised, config_info, p);\n> +\n> +\tif (accept != ACCEPT_NONE)\n> +\t\tBUG(\"Unhandled 'enum accept_promisor' value '%d'\", accept);\n> +\n> +\t/*\n> +\t * Even if accept == ACCEPT_NONE, we MUST trust this known\n> +\t * remote to update its token or other such fields if its URL\n> +\t * matches the acceptFromServerUrl allowlist!\n> +\t */\n> +\tif (url_matches_accept_list(accept_urls, remote_url))\n> +\t\treturn all_fields_match(advertised, config_info, p);\n\nI should verify in the following patches, but it seems to me only when\npromisor.AcceptFromServer is set to None it will store the advertised\nservers to the local .git/config, or not?\n\n> +\n> +\treturn 0;\n>  }\n>  \n>  static int skip_field_name_prefix(const char *elem, const char *field_name, const char **value)\n> @@ -991,7 +1077,7 @@ static void filter_promisor_remote(struct repository *repo,\n>  \t/* Load and validate the acceptFromServerUrl config */\n>  \tload_accept_from_server_url(repo, &accept_urls);\n>  \n> -\tif (accept == ACCEPT_NONE)\n> +\tif (accept == ACCEPT_NONE && !accept_urls.nr)\n>  \t\treturn;\n>  \n>  \t/* Parse remote info received */\n> @@ -1011,7 +1097,7 @@ static void filter_promisor_remote(struct repository *repo,\n>  \t\t\tstring_list_sort(&config_info);\n>  \t\t}\n>  \n> -\t\tif (should_accept_remote(accept, advertised, &config_info)) {\n> +\t\tif (should_accept_remote(accept, advertised, &accept_urls, &config_info)) {\n>  \t\t\tif (!store_info)\n>  \t\t\t\tstore_info = store_info_new(repo);\n>  \t\t\tif (promisor_store_advertised_fields(advertised, store_info))\n> diff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\n> index 3b39505380..0659b2ac15 100755\n> --- a/t/t5710-promisor-remote-capability.sh\n> +++ b/t/t5710-promisor-remote-capability.sh\n> @@ -387,6 +387,77 @@ test_expect_success \"clone with 'KnownUrl' and empty url, so not advertised\" '\n>  \tcheck_missing_objects server 1 \"$oid\"\n>  '\n>  \n> +test_expect_success \"clone with 'None' but URL allowlisted\" '\n> +\tgit -C server config promisor.advertise true &&\n> +\ttest_when_finished \"rm -rf client\" &&\n> +\n> +\tGIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \\\n> +\t\t-c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n> +\t\t-c remote.lop.url=\"$TRASH_DIRECTORY_URL/lop\" \\\n> +\t\t-c promisor.acceptfromserver=None \\\n> +\t\t-c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n> +\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n> +\n> +\t# Check that the largest object is still missing on the server\n> +\tcheck_missing_objects server 1 \"$oid\"\n> +'\n\nWhy do some tests end with `initialize_server 1 \"$oid\"` and this one\nnot? Isn't it weird tests prepare for the next test?\n\n> +\n> +test_expect_success \"clone with 'None' but URL not in allowlist\" '\n> +\tgit -C server config promisor.advertise true &&\n> +\ttest_when_finished \"rm -rf client\" &&\n> +\n> +\tGIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \\\n> +\t\t-c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n> +\t\t-c remote.lop.url=\"$TRASH_DIRECTORY_URL/lop\" \\\n> +\t\t-c promisor.acceptfromserver=None \\\n> +\t\t-c promisor.acceptFromServerUrl=\"https://example.com/*\" \\\n> +\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n> +\n> +\t# Check that the largest object is not missing on the server\n> +\tcheck_missing_objects server 0 \"\" &&\n> +\n> +\t# Reinitialize server so that the largest object is missing again\n> +\tinitialize_server 1 \"$oid\"\n> +'\n> +\n> +test_expect_success \"clone with 'None' but URL allowlisted in one pattern out of two\" '\n> +\tgit -C server config promisor.advertise true &&\n> +\ttest_when_finished \"rm -rf client\" &&\n> +\n> +\tGIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \\\n> +\t\t-c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n> +\t\t-c remote.lop.url=\"$TRASH_DIRECTORY_URL/lop\" \\\n> +\t\t-c promisor.acceptfromserver=None \\\n> +\t\t-c promisor.acceptFromServerUrl=\"https://example.com/*\" \\\n> +\t\t-c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n> +\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n> +\n> +\t# Check that the largest object is still missing on the server\n> +\tcheck_missing_objects server 1 \"$oid\"\n> +'\n> +\n> +test_expect_success \"clone with 'None', URL allowlisted, but client has different URL\" '\n> +\tgit -C server config promisor.advertise true &&\n> +\ttest_when_finished \"rm -rf client\" &&\n> +\n> +\t# The client configures \"lop\" with a different URL (serverTwo) than\n> +\t# what the server advertises (lop). Even though the advertised URL\n> +\t# matches the allowlist, the remote is rejected because the\n> +\t# configured URL does not match the advertised one.\n> +\tGIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \\\n> +\t\t-c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n> +\t\t-c remote.lop.url=\"$TRASH_DIRECTORY_URL/serverTwo\" \\\n> +\t\t-c promisor.acceptfromserver=None \\\n> +\t\t-c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n> +\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n> +\n> +\t# Check that the largest object is not missing on the server\n> +\tcheck_missing_objects server 0 \"\" &&\n> +\n> +\t# Reinitialize server so that the largest object is missing again\n> +\tinitialize_server 1 \"$oid\"\n> +'\n> +\n>  test_expect_success \"clone with invalid promisor.acceptFromServerUrl\" '\n>  \tgit -C server config promisor.advertise true &&\n>  \ttest_when_finished \"rm -rf client\" &&\n> -- \n> 2.54.0.19.gb68b9497aa\n>\n>\n\n-- \nCheers,\nToon\n"},{"id":"543053","messageId":"87v7cunlid.fsf@toon--20250203-5JQV3.mail-host-address-is-not-set","threadId":"64670","inReplyTo":"20260427124108.3524129-8-christian.couder@gmail.com","subject":"Re: [PATCH v2 7/8] promisor-remote: auto-configure unknown remotes","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-05-11T13:06:34Z","receivedAt":"2026-05-11T13:06:44Z","isPatch":true,"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> Previous commits have introduced the `promisor.acceptFromServerUrl`\n> config variable to allowlist some URLs advertised by a server through\n> the \"promisor-remote\" protocol capability.\n>\n> However the new `promisor.acceptFromServerUrl` mechanism, like the old\n> `promisor.acceptFromServer` mechanism, still requires a remote to\n> already exist in the client's local configuration before it can be\n> accepted. This places a significant manual burden on users to\n> pre-configure these remotes, and creates friction for administrators\n> who have to troubleshoot or manually provision these setups for their\n> teams.\n>\n> To eliminate this burden, let's automatically create a new `[remote]`\n> section in the client's config when a server advertises an unknown\n> remote whose URL matches a `promisor.acceptFromServerUrl` glob pattern.\n>\n> Concretely, let's add four helpers:\n>\n>  - sanitize_remote_name(): turn an arbitrary URL-derived string into a\n>    valid remote name by replacing non-alphanumeric characters,\n>    collapsing runs of '-', and prepending \"promisor-auto-\".\n>\n>  - promisor_remote_name_from_url(): normalize the URL and extract\n>    host+port+path to build a human-readable base name, then pass it\n>    through sanitize_remote_name().\n>\n>  - configure_auto_promisor_remote(): write the remote.*.url,\n>    remote.*.promisor and remote.*.advertisedAs keys to the repo\n>    config.\n>\n>  - handle_matching_allowed_url(): pick the final name (user-supplied\n>    alias or auto-generated), handle collisions by appending \"-1\",\n>    \"-2\", etc., then call configure_auto_promisor_remote().\n>\n> Let's also add should_accept_new_remote_url() which reuses the\n> url_matches_accept_list() helper introduced in a previous commit to\n> find a matching pattern, then delegates to handle_matching_allowed_url()\n> to create the remote.\n>\n> And then let's call should_accept_new_remote_url() from the '!item'\n> (unknown remote) branch of should_accept_remote(), setting\n> `reload_config` so that the newly-written config is picked up.\n>\n> Finally let's document all that by:\n>\n>  - expanding the `promisor.acceptFromServerUrl` entry to describe\n>    auto-creation, the optional \"name=\" prefix syntax, the\n>    \"promisor-auto-*\" generation rules, and numeric-suffix collision\n>    handling, and by\n>  - adding a \"remote.<name>.advertisedAs\" entry to \"remote.adoc\".\n>\n> Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n> ---\n>  Documentation/config/promisor.adoc    |  26 +++-\n>  Documentation/config/remote.adoc      |   9 ++\n>  promisor-remote.c                     | 202 +++++++++++++++++++++++++-\n>  t/t5710-promisor-remote-capability.sh | 104 +++++++++++++\n>  4 files changed, 332 insertions(+), 9 deletions(-)\n>\n> diff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\n> index efc066c3f2..ae1686a6e0 100644\n> --- a/Documentation/config/promisor.adoc\n> +++ b/Documentation/config/promisor.adoc\n> @@ -54,7 +54,8 @@ promisor.acceptFromServer::\n>  promisor.acceptFromServerUrl::\n>  \tA glob pattern to specify which server-advertised URLs a\n>  \tclient is allowed to act on. When a URL matches, the client\n> -\twill accept the advertised remote as a promisor remote and may\n> +\twill accept the advertised remote as a promisor remote, may\n> +\tautomatically create a new remote configuration for it and may\n>  \tautomatically accept field updates (such as authentication\n>  \ttokens) from the server, even if `promisor.acceptFromServer`\n>  \tis set to `none` (the default).\n> @@ -66,9 +67,10 @@ this option in _ANY_ config file read by Git.\n>  Be _VERY_ careful with these patterns: `*` matches any sequence of\n>  characters within the 'host' and 'path' parts of a URL (but cannot\n>  cross part boundaries). An overly broad pattern is a major security\n> -risk, as a matching URL allows a server to update fields (such as\n> -authentication tokens) on known remotes without further confirmation.\n> -To minimize security risks, follow these guidelines:\n> +risk, as a matching URL allows a server to auto-configure new remotes\n> +and to update fields (such as authentication tokens) on known remotes\n> +without further confirmation. To minimize security risks, follow these\n> +guidelines:\n>  +\n>  1. Start with a secure protocol scheme, like `https://` or `ssh://`.\n>  +\n> @@ -99,6 +101,22 @@ are resolved. The port must also match exactly (e.g.,\n>  `https://example.com:8080/*` will not match a URL advertised on\n>  port 9999).\n>  +\n> +The glob pattern can optionally be prefixed with a remote name and an\n> +equals sign (e.g., `cdn=https://cdn.example.com/*`). If such a prefix\n> +is provided, accepted remotes will be saved under that name. If no\n> +such prefix is provided, a safe remote name will be automatically\n> +generated by sanitizing the URL and prefixing it with\n> +`promisor-auto-`.\n> ++\n> +If a remote with the chosen name already exists but points to a\n> +different URL, Git will append a numeric suffix (e.g., `-1`, `-2`) to\n> +the name to prevent overwriting existing configurations. You should\n> +make sure that this doesn't happen often though, as remotes will be\n> +rejected if the numeric suffix increases too much. In all cases, the\n> +original name advertised by the server is recorded in the\n> +`remote.<name>.advertisedAs` configuration variable for tracing and\n> +debugging purposes.\n> ++\n>  For the security implications of accepting a promisor remote, see the\n>  documentation of `promisor.acceptFromServer`. For details on the\n>  protocol, see linkgit:gitprotocol-v2[5].\n> diff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc\n> index 91e46f66f5..6e2bbdf457 100644\n> --- a/Documentation/config/remote.adoc\n> +++ b/Documentation/config/remote.adoc\n> @@ -91,6 +91,15 @@ remote.<name>.promisor::\n>  \tWhen set to true, this remote will be used to fetch promisor\n>  \tobjects.\n>  \n> +remote.<name>.advertisedAs::\n> +\tWhen a promisor remote is automatically configured using\n> +\tinformation advertised by a server through the\n> +\t`promisor-remote` protocol capability (see\n> +\t`promisor.acceptFromServerUrl`), the server's originally\n> +\tadvertised name is saved in this variable. This is for\n> +\tinformation, tracing and debugging purposes. Users should not\n> +\ttypically modify or create such configuration entries.\n> +\n>  remote.<name>.partialclonefilter::\n>  \tThe filter that will be applied when fetching from this\tpromisor remote.\n>  \tChanging or clearing this value will only affect fetches for new commits.\n> diff --git a/promisor-remote.c b/promisor-remote.c\n> index 72d5b94bf7..8c8a798fdb 100644\n> --- a/promisor-remote.c\n> +++ b/promisor-remote.c\n> @@ -816,10 +816,197 @@ static struct allowed_url *url_matches_accept_list(\n>  \treturn NULL;\n>  }\n>  \n> -static int should_accept_remote(enum accept_promisor accept,\n> +/*\n> + * Sanitize the buffer to make it a valid remote name coming from the\n> + * server by:\n> + *\n> + * - replacing any non alphanumeric character with a '-'\n> + * - stripping any leading '-',\n> + * - condensing multiple '-' into one,\n> + * - prepending \"promisor-auto-\",\n> + * - validating the result.\n> + */\n> +static int sanitize_remote_name(struct strbuf *buf, const char *url)\n> +{\n> +\tchar prev = '-';\n> +\tfor (size_t i = 0; i < buf->len; ) {\n> +\t\tif (!isalnum(buf->buf[i]))\n> +\t\t\tbuf->buf[i] = '-';\n> +\t\tif (prev == '-' && buf->buf[i] == '-') {\n> +\t\t\tstrbuf_remove(buf, i, 1);\n> +\t\t} else {\n> +\t\t\tprev = buf->buf[i];\n> +\t\t\ti++;\n> +\t\t}\n> +\t}\n> +\n> +\tstrbuf_strip_suffix(buf, \"-\");\n> +\n> +\tif (!buf->len) {\n> +\t\twarning(_(\"couldn't generate a valid remote name from \"\n> +\t\t\t  \"advertised url '%s', ignoring this remote\"), url);\n> +\t\treturn -1;\n> +\t}\n> +\n> +\tstrbuf_insertstr(buf, 0, \"promisor-auto-\");\n> +\n> +\tif (!valid_remote_name(buf->buf)) {\n> +\t\twarning(_(\"generated remote name '%s' from advertised url '%s' \"\n> +\t\t\t  \"is invalid, ignoring this remote\"), buf->buf, url);\n> +\t\treturn -1;\n> +\t}\n> +\n> +\treturn 0;\n> +}\n> +\n> +static char *promisor_remote_name_from_url(const char *url)\n> +{\n> +\tstruct url_info url_info = { 0 };\n> +\tchar *normalized = url_normalize(url, &url_info);\n> +\tstruct strbuf buf = STRBUF_INIT;\n> +\n> +\tif (!normalized) {\n> +\t\twarning(_(\"couldn't normalize advertised url '%s', \"\n> +\t\t\t  \"ignoring this remote\"), url);\n> +\t\treturn NULL;\n> +\t}\n> +\n> +\tif (url_info.host_len) {\n> +\t\tstrbuf_add(&buf, normalized + url_info.host_off, url_info.host_len);\n> +\t\tstrbuf_addch(&buf, '-');\n> +\t}\n> +\n> +\tif (url_info.port_len) {\n> +\t\tstrbuf_add(&buf, normalized + url_info.port_off, url_info.port_len);\n> +\t\tstrbuf_addch(&buf, '-');\n\nIf the url doesn't have a path, this could lead to the name being\n`example-com-8443`. But we have a MAX_REMOTES_WITH_SIMILAR_NAMES at 20,\nwould this be an issue for a second remote without configured name?\n\nAs far as I can tell from handle_matching_allowed_url(), it's no issue,\nbecause the numeric `-%d` suffix is added and we never atoi() the number\nfrom existing remotes in the config.\n\n> +\t}\n> +\n> +\tif (url_info.path_len) {\n> +\t\tstrbuf_add(&buf, normalized + url_info.path_off, url_info.path_len);\n> +\t\tstrbuf_trim_trailing_dir_sep(&buf);\n> +\t\tstrbuf_strip_suffix(&buf, \".git\");\n> +\t}\n> +\n> +\tfree(normalized);\n> +\n> +\tif (sanitize_remote_name(&buf, url)) {\n> +\t\tstrbuf_release(&buf);\n> +\t\treturn NULL;\n> +\t}\n> +\n> +\treturn strbuf_detach(&buf, NULL);\n> +}\n> +\n> +static void configure_auto_promisor_remote(struct repository *repo,\n> +\t\t\t\t\t   const char *name,\n> +\t\t\t\t\t   const char *url,\n> +\t\t\t\t\t   const char *advertised_as,\n> +\t\t\t\t\t   bool reuse)\n> +{\n> +\tchar *key;\n> +\n> +\tif (!reuse) {\n> +\t\tfprintf(stderr, _(\"Auto-creating promisor remote '%s' for URL '%s'\\n\"),\n> +\t\t\tname, url);\n> +\n> +\t\tkey = xstrfmt(\"remote.%s.url\", name);\n> +\t\trepo_config_set_gently(repo, key, url);\n> +\t\tfree(key);\n> +\t}\n> +\n> +\t/* NB: when reusing, this promotes an existing non-promisor remote */\n> +\tkey = xstrfmt(\"remote.%s.promisor\", name);\n> +\trepo_config_set_gently(repo, key, \"true\");\n> +\tfree(key);\n> +\n> +\tif (advertised_as) {\n> +\t\tkey = xstrfmt(\"remote.%s.advertisedAs\", name);\n> +\t\trepo_config_set_gently(repo, key, advertised_as);\n> +\t\tfree(key);\n> +\t}\n> +}\n> +\n> +#define MAX_REMOTES_WITH_SIMILAR_NAMES 20\n> +\n> +/* Return the allocated local name, or NULL on failure */\n> +static char *handle_matching_allowed_url(struct repository *repo,\n> +\t\t\t\t\t char *allowed_name,\n> +\t\t\t\t\t const char *remote_url,\n> +\t\t\t\t\t const char *remote_name)\n> +{\n> +\tchar *name;\n> +\tchar *basename = allowed_name ?\n> +\t\txstrdup(allowed_name) :\n> +\t\tpromisor_remote_name_from_url(remote_url);\n> +\tint i = 0;\n> +\tbool reuse = false;\n> +\n> +\tif (!basename)\n> +\t\treturn NULL;\n> +\n> +\tname = xstrdup(basename);\n> +\n> +\twhile (i < MAX_REMOTES_WITH_SIMILAR_NAMES) {\n> +\t\tchar *url_key = xstrfmt(\"remote.%s.url\", name);\n> +\t\tconst char *existing_url;\n> +\t\tint exists = !repo_config_get_string_tmp(repo, url_key, &existing_url);\n> +\n> +\t\tfree(url_key);\n> +\n> +\t\tif (!exists)\n> +\t\t\tbreak; /* Free to use */\n> +\n> +\t\tif (!strcmp(existing_url, remote_url)) {\n> +\t\t\treuse = true;\n> +\t\t\tbreak; /* Same URL, so safe to reuse */\n> +\t\t}\n> +\n> +\t\ti++;\n> +\t\tfree(name);\n> +\t\tname = xstrfmt(\"%s-%d\", basename, i);\n> +\t}\n> +\n> +\tif (i < MAX_REMOTES_WITH_SIMILAR_NAMES) {\n> +\t\tconfigure_auto_promisor_remote(repo, name,\n> +\t\t\t\t\t       remote_url, remote_name,\n> +\t\t\t\t\t       reuse);\n> +\t} else {\n> +\t\twarning(_(\"too many remotes accepted with name like '%s-X', \"\n> +\t\t\t  \"ignoring this remote\"), basename);\n> +\t\tFREE_AND_NULL(name);\n> +\t}\n> +\n> +\tfree(basename);\n> +\treturn name;\n> +}\n> +\n> +static int should_accept_new_remote_url(struct repository *repo,\n> +\t\t\t\t\tstruct string_list *accept_urls,\n> +\t\t\t\t\tstruct promisor_info *advertised)\n> +{\n> +\tstruct allowed_url *allowed = url_matches_accept_list(accept_urls,\n> +\t\t\t\t\t\t\t     advertised->url);\n> +\tif (allowed) {\n> +\t\tchar *name = handle_matching_allowed_url(repo,\n> +\t\t\t\t\t\t\t allowed->remote_name,\n> +\t\t\t\t\t\t\t advertised->url,\n> +\t\t\t\t\t\t\t advertised->name);\n> +\t\tif (name) {\n> +\t\t\tfree((char *)advertised->local_name);\n> +\t\t\tadvertised->local_name = name;\n> +\t\t\treturn 1;\n> +\t\t}\n> +\t}\n> +\n> +\treturn 0;\n> +}\n> +\n> +static int should_accept_remote(struct repository *repo,\n> +\t\t\t\tenum accept_promisor accept,\n>  \t\t\t\tstruct promisor_info *advertised,\n>  \t\t\t\tstruct string_list *accept_urls,\n> -\t\t\t\tstruct string_list *config_info)\n> +\t\t\t\tstruct string_list *config_info,\n> +\t\t\t\tbool *reload_config)\n>  {\n>  \tstruct promisor_info *p;\n>  \tstruct string_list_item *item;\n> @@ -837,9 +1024,13 @@ static int should_accept_remote(enum accept_promisor accept,\n>  \t/* Get config info for that promisor remote */\n>  \titem = string_list_lookup(config_info, remote_name);\n>  \n> -\tif (!item)\n> +\tif (!item) {\n>  \t\t/* We don't know about that remote */\n> -\t\treturn 0;\n> +\t\tint res = should_accept_new_remote_url(repo, accept_urls, advertised);\n> +\t\tif (res)\n> +\t\t\t*reload_config = true;\n> +\t\treturn res;\n> +\t}\n>  \n>  \tp = item->util;\n>  \n> @@ -1097,7 +1288,8 @@ static void filter_promisor_remote(struct repository *repo,\n>  \t\t\tstring_list_sort(&config_info);\n>  \t\t}\n>  \n> -\t\tif (should_accept_remote(accept, advertised, &accept_urls, &config_info)) {\n> +\t\tif (should_accept_remote(repo, accept, advertised, &accept_urls,\n> +\t\t\t\t\t &config_info, &reload_config)) {\n>  \t\t\tif (!store_info)\n>  \t\t\t\tstore_info = store_info_new(repo);\n>  \t\t\tif (promisor_store_advertised_fields(advertised, store_info))\n> diff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\n> index 0659b2ac15..549acff23f 100755\n> --- a/t/t5710-promisor-remote-capability.sh\n> +++ b/t/t5710-promisor-remote-capability.sh\n> @@ -458,6 +458,107 @@ test_expect_success \"clone with 'None', URL allowlisted, but client has differen\n>  \tinitialize_server 1 \"$oid\"\n>  '\n>  \n> +test_expect_success \"clone with URL allowlisted and no remote already configured\" '\n> +\tgit -C server config promisor.advertise true &&\n> +\ttest_when_finished \"rm -rf client\" &&\n> +\ttest_when_finished \"rm -f full_names\" &&\n> +\n> +\tGIT_NO_LAZY_FETCH=0 git clone \\\n> +\t\t-c promisor.acceptfromserver=None \\\n> +\t\t-c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n> +\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n\nSo promisor.acceptFromServerUrl only works if promisor.acceptFromServer\nis \"none\"? I mean which one should precedence? If\npromisor.acceptFromServer is set to \"all\", the promisor remote is\naccepted by the client, but not saved to the config. Is that\nintentional? Should we document that?\n\n> +\t# Check that exactly one remote has been auto-created, identified\n> +\t# by \"remote.<name>.advertisedAs\" == \"lop\".\n> +\tgit -C client config get --all --show-names --regexp \\\n> +\t\t\"remote\\..*\\.advertisedas\" >full_names &&\n> +\ttest_line_count = 1 full_names &&\n> +\tREMOTE_NAME=$(sed \"s/^remote\\.\\(.*\\)\\.advertisedas .*$/\\1/\" full_names) &&\n> +\n> +\t# Check \".url\" and \".promisor\" values\n> +\tprintf \"%s\\n\" \"$TRASH_DIRECTORY_URL/lop\" \"true\" >expect &&\n> +\tgit -C client config \"remote.$REMOTE_NAME.url\" >actual &&\n> +\tgit -C client config \"remote.$REMOTE_NAME.promisor\" >>actual &&\n> +\ttest_cmp expect actual &&\n> +\n> +\t# Check that the largest object is still missing on the server\n> +\tcheck_missing_objects server 1 \"$oid\"\n> +'\n> +\n> +test_expect_success \"clone with named URL allowlisted and no pre-configured remote\" '\n> +\tgit -C server config promisor.advertise true &&\n> +\ttest_when_finished \"rm -rf client\" &&\n> +\n> +\tGIT_NO_LAZY_FETCH=0 git clone \\\n> +\t\t-c promisor.acceptfromserver=None \\\n> +\t\t-c promisor.acceptFromServerUrl=\"cdn=$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n> +\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n> +\n> +\t# Check that a remote has been auto-created with the right \"cdn\" name and fields.\n> +\tprintf \"%s\\n\" \"$TRASH_DIRECTORY_URL/lop\" \"true\" \"lop\" >expect &&\n> +\tgit -C client config \"remote.cdn.url\" >actual &&\n> +\tgit -C client config \"remote.cdn.promisor\" >>actual &&\n> +\tgit -C client config \"remote.cdn.advertisedAs\" >>actual &&\n> +\ttest_cmp expect actual &&\n> +\n> +\t# Check that the largest object is still missing on the server\n> +\tcheck_missing_objects server 1 \"$oid\"\n> +'\n> +\n> +test_expect_success \"clone with URL allowlisted but colliding name\" '\n> +\tgit -C server config promisor.advertise true &&\n> +\ttest_when_finished \"rm -rf client\" &&\n> +\n> +\tGIT_NO_LAZY_FETCH=0 git clone -c remote.cdn.promisor=true \\\n> +\t\t-c remote.cdn.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n> +\t\t-c remote.cdn.url=\"https://example.com/cdn\" \\\n> +\t\t-c promisor.acceptfromserver=None \\\n> +\t\t-c promisor.acceptFromServerUrl=\"cdn=$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n> +\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n> +\n> +\t# Check that a remote has been auto-created with the right \"cdn-1\" name and fields.\n> +\tprintf \"%s\\n\" \"$TRASH_DIRECTORY_URL/lop\" \"true\" \"lop\" >expect &&\n> +\tgit -C client config \"remote.cdn-1.url\" >actual &&\n> +\tgit -C client config \"remote.cdn-1.promisor\" >>actual &&\n> +\tgit -C client config \"remote.cdn-1.advertisedAs\" >>actual &&\n> +\ttest_cmp expect actual &&\n> +\n> +\t# Check that the original \"cdn\" remote was not overwritten.\n> +\tprintf \"%s\\n\" \"https://example.com/cdn\" \"true\" >expect &&\n> +\tgit -C client config \"remote.cdn.url\" >actual &&\n> +\tgit -C client config \"remote.cdn.promisor\" >>actual &&\n> +\ttest_cmp expect actual &&\n> +\n> +\t# Check that the largest object is still missing on the server\n> +\tcheck_missing_objects server 1 \"$oid\"\n> +'\n> +\n> +test_expect_success \"clone with URL allowlisted and reusable remote\" '\n> +\tgit -C server config promisor.advertise true &&\n> +\ttest_when_finished \"rm -rf client\" &&\n> +\n> +\tGIT_NO_LAZY_FETCH=0 git clone \\\n> +\t\t-c remote.cdn.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n> +\t\t-c remote.cdn.url=\"$TRASH_DIRECTORY_URL/lop\" \\\n> +\t\t-c promisor.acceptfromserver=None \\\n> +\t\t-c promisor.acceptFromServerUrl=\"cdn=$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n> +\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n> +\n> +\t# Check that the existing \"cdn\" remote has been properly updated.\n> +\tprintf \"%s\\n\" \"$TRASH_DIRECTORY_URL/lop\" \"true\" \"lop\" \"+refs/heads/*:refs/remotes/lop/*\" >expect &&\n> +\tgit -C client config \"remote.cdn.url\" >actual &&\n> +\tgit -C client config \"remote.cdn.promisor\" >>actual &&\n> +\tgit -C client config \"remote.cdn.advertisedAs\" >>actual &&\n> +\tgit -C client config \"remote.cdn.fetch\" >>actual &&\n> +\ttest_cmp expect actual &&\n> +\n> +\t# Check that no new \"cdn-1\" remote has been created.\n> +\ttest_must_fail git -C client config \"remote.cdn-1.url\" &&\n> +\n> +\t# Check that the largest object is still missing on the server\n> +\tcheck_missing_objects server 1 \"$oid\"\n> +'\n> +\n>  test_expect_success \"clone with invalid promisor.acceptFromServerUrl\" '\n>  \tgit -C server config promisor.advertise true &&\n>  \ttest_when_finished \"rm -rf client\" &&\n> @@ -472,6 +573,9 @@ test_expect_success \"clone with invalid promisor.acceptFromServerUrl\" '\n>  \t# Check that a warning was emitted\n>  \ttest_grep \"invalid remote name '\\''bad name'\\''\" err &&\n>  \n> +\t# Check that no remote was auto-created\n> +\ttest_must_fail git -C client config get --regexp \"remote\\..*\\.advertisedas\" &&\n> +\n>  \t# Check that the largest object is not missing on the server\n>  \tcheck_missing_objects server 0 \"\" &&\n>  \n> -- \n> 2.54.0.19.gb68b9497aa\n>\n>\n\n-- \nCheers,\nToon\n"},{"id":"543054","messageId":"87qzninlb4.fsf@toon--20250203-5JQV3.mail-host-address-is-not-set","threadId":"64670","inReplyTo":"20260427124108.3524129-7-christian.couder@gmail.com","subject":"Re: [PATCH v2 6/8] promisor-remote: trust known remotes matching acceptFromServerUrl","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-05-11T13:10:55Z","receivedAt":"2026-05-11T13:11:01Z","isPatch":true,"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> +static bool match_one_url(const struct url_info *pi, const struct url_info *ui)\n> +{\n> +\tconst char *pat = pi->url;\n> +\tconst char *url = ui->url;\n> +\tchar *p_str, *u_str;\n> +\tbool res;\n> +\n> +\t/*\n> +\t * Schemes must match exactly. They are case-folded by\n> +\t * url_normalize(), so strncmp() suffices.\n> +\t */\n> +\tif (pi->scheme_len != ui->scheme_len || strncmp(pat, url, pi->scheme_len))\n> +\t\treturn false;\n> +\n> +\t/*\n> +\t * Ports must match exactly. url_normalize() strips default\n> +\t * ports (like 443 for https), so length and content\n> +\t * comparisons are sufficient.\n> +\t */\n> +\tif (pi->port_len != ui->port_len ||\n> +\t    strncmp(pat + pi->port_off, url + ui->port_off, pi->port_len))\n> +\t\treturn false;\n> +\n> +\t/*\n> +\t * Match host and path separately to prevent a '*' in the host\n> +\t * portion of the pattern from matching across the '/'\n> +\t * boundary into the path. Use WM_PATHNAME for the host so '*'\n> +\t * cannot cross '/' there, and 0 for the path so '*' can still\n> +\t * match multi-level paths.\n> +\t */\n> +\n> +\tp_str = xstrndup(pat + pi->host_off, pi->host_len);\n> +\tu_str = xstrndup(url + ui->host_off, ui->host_len);\n> +\tres = !wildmatch(p_str, u_str, WM_PATHNAME);\n> +\tfree(p_str);\n> +\tfree(u_str);\n> +\n> +\tif (!res)\n\nI feel it's a bit confusing your negating the result from wildmatch()\nto negate it here again? Maybe keep using the int return value, or\nrename the variable to 'matches' ?\n\n> +\t\treturn false;\n> +\n> +\tp_str = xstrndup(pat + pi->path_off, pi->path_len);\n> +\tu_str = xstrndup(url + ui->path_off, ui->path_len);\n> +\tres = !wildmatch(p_str, u_str, 0);\n> +\tfree(p_str);\n> +\tfree(u_str);\n> +\n> +\treturn res;\n> +}\n\n-- \nCheers,\nToon\n"},{"id":"543642","messageId":"CAP8UFD0Mbt8JtGW4fyyf4mzZB5t4Bk2LE2y45OJ08mUw-O6EYA@mail.gmail.com","threadId":"64670","inReplyTo":"875x4yoys5.fsf@toon--20250203-5JQV3.mail-host-address-is-not-set","subject":"Re: [PATCH v2 6/8] promisor-remote: trust known remotes matching acceptFromServerUrl","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-05-19T15:24:52Z","receivedAt":"2026-05-19T15:25:05Z","isPatch":true,"body":"On Fri, May 8, 2026 at 3:45 PM Toon Claes <toon@iotcl.com> wrote:\n>\n> Christian Couder <christian.couder@gmail.com> writes:\n\n> > +static bool match_one_url(const struct url_info *pi, const struct url_info *ui)\n> > +{\n> > +     const char *pat = pi->url;\n> > +     const char *url = ui->url;\n> > +     char *p_str, *u_str;\n> > +     bool res;\n> > +\n> > +     /*\n> > +      * Schemes must match exactly. They are case-folded by\n> > +      * url_normalize(), so strncmp() suffices.\n> > +      */\n> > +     if (pi->scheme_len != ui->scheme_len || strncmp(pat, url, pi->scheme_len))\n> > +             return false;\n> > +\n> > +     /*\n> > +      * Ports must match exactly. url_normalize() strips default\n> > +      * ports (like 443 for https), so length and content\n> > +      * comparisons are sufficient.\n> > +      */\n> > +     if (pi->port_len != ui->port_len ||\n> > +         strncmp(pat + pi->port_off, url + ui->port_off, pi->port_len))\n> > +             return false;\n> > +\n> > +     /*\n> > +      * Match host and path separately to prevent a '*' in the host\n> > +      * portion of the pattern from matching across the '/'\n> > +      * boundary into the path. Use WM_PATHNAME for the host so '*'\n> > +      * cannot cross '/' there, and 0 for the path so '*' can still\n> > +      * match multi-level paths.\n> > +      */\n>\n> Do we actually need WM_PATHNAME, because we only xstrndup() the host\n> part anyway?\n\nYeah, it's not really needed.\n\nOn one hand it doesn't hurt either, and it conveys the intent, which\nis that no / boundary should be crossed.\n\nBut on the other hand I agree it could be confusing and it's simpler\nto just remove it, so I have removed it in the v3 I will send very\nsoon.\n\n> > +\n> > +     p_str = xstrndup(pat + pi->host_off, pi->host_len);\n> > +     u_str = xstrndup(url + ui->host_off, ui->host_len);\n> > +     res = !wildmatch(p_str, u_str, WM_PATHNAME);\n> > +     free(p_str);\n> > +     free(u_str);\n> > +\n> > +     if (!res)\n> > +             return false;\n> > +\n> > +     p_str = xstrndup(pat + pi->path_off, pi->path_len);\n> > +     u_str = xstrndup(url + ui->path_off, ui->path_len);\n> > +     res = !wildmatch(p_str, u_str, 0);\n> > +     free(p_str);\n> > +     free(u_str);\n>\n> Is it correct we intentionally do not compare the user and pass (at\n> `user_off` and `passwd_off`)? I assume so, because this allows the\n> server to update those?\n\nYes, we ignore them intentionally. Using the existing `token` field\nshould be prefered, but maybe some need a user and password part of\nthe URL.\n\nAnyway I have documented that in v3.\n\n> >  static int should_accept_remote(enum accept_promisor accept,\n> >                               struct promisor_info *advertised,\n> > +                             struct string_list *accept_urls,\n> >                               struct string_list *config_info)\n> >  {\n> >       struct promisor_info *p;\n> > @@ -771,9 +846,6 @@ static int should_accept_remote(enum accept_promisor accept,\n> >       if (accept == ACCEPT_KNOWN_NAME)\n> >               return all_fields_match(advertised, config_info, p);\n> >\n> > -     if (accept != ACCEPT_KNOWN_URL)\n> > -             BUG(\"Unhandled 'enum accept_promisor' value '%d'\", accept);\n> > -\n> >       if (strcmp(p->url, remote_url)) {\n> >               warning(_(\"known remote named '%s' but with URL '%s' instead of '%s', \"\n> >                         \"ignoring this remote\"),\n> > @@ -781,7 +853,21 @@ static int should_accept_remote(enum accept_promisor accept,\n> >               return 0;\n> >       }\n> >\n> > -     return all_fields_match(advertised, config_info, p);\n> > +     if (accept == ACCEPT_KNOWN_URL)\n> > +             return all_fields_match(advertised, config_info, p);\n> > +\n> > +     if (accept != ACCEPT_NONE)\n> > +             BUG(\"Unhandled 'enum accept_promisor' value '%d'\", accept);\n> > +\n> > +     /*\n> > +      * Even if accept == ACCEPT_NONE, we MUST trust this known\n> > +      * remote to update its token or other such fields if its URL\n> > +      * matches the acceptFromServerUrl allowlist!\n> > +      */\n> > +     if (url_matches_accept_list(accept_urls, remote_url))\n> > +             return all_fields_match(advertised, config_info, p);\n>\n> I should verify in the following patches, but it seems to me only when\n> promisor.AcceptFromServer is set to None it will store the advertised\n> servers to the local .git/config, or not?\n\nRight, it's better to check if the URL is in the allowlist as soon as\nwe can. So in the v3 I have moved as much as possible the\n`promisor.acceptFromServerUrl` related checks before the other checks.\n\nThe idea is that `promisor.acceptFromServerUrl` takes precedence over\n`promisor.acceptFromServer`, so having the\n`promisor.acceptFromServerUrl` checks first makes sense.\n\nNote that we should still not accept an advertised remote with an URL\nthat matches a pattern in `promisor.acceptFromServerUrl` if a remote\nwith the same name but a different URL exist on the client, unless the\nuser has explicitly set `promisor.AcceptFromServer` to either 'All' or\n'knownName'.\n\nIn the v3 I have also added some documentation to be explicit about this.\n\n> > +test_expect_success \"clone with 'None' but URL allowlisted\" '\n> > +     git -C server config promisor.advertise true &&\n> > +     test_when_finished \"rm -rf client\" &&\n> > +\n> > +     GIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \\\n> > +             -c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n> > +             -c remote.lop.url=\"$TRASH_DIRECTORY_URL/lop\" \\\n> > +             -c promisor.acceptfromserver=None \\\n> > +             -c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n> > +             --no-local --filter=\"blob:limit=5k\" server client &&\n> > +\n> > +     # Check that the largest object is still missing on the server\n> > +     check_missing_objects server 1 \"$oid\"\n> > +'\n>\n> Why do some tests end with `initialize_server 1 \"$oid\"` and this one\n> not? Isn't it weird tests prepare for the next test?\n\nIt's more cleaning up after themselves than preparing for the next test.\n\nInitializing the server with `initialize_server 1 \"$oid\"` is only\nneeded when some large objects end up on the server where they should\nnot be.\n\nIf we wanted to be sure that the state of the server is always clean\nfor the next test, then we should initialize the server at the end of\nevery test, using something like:\n\n  test_when_finished \"initialize_server 1 \\\"$oid\\\"\"\n\njust in case something went wrong and a large file was transferred to\nthe server. But I think that's quite expensive for not much gain.\n\nIt's also clearer for readers to have `initialize_server 1 \"$oid\"`\nonly where we think it's really needed.\n\nSo in the end I prefer to leave this as-is for now. We can still\naddress this later in a separate series for all the tests in\n\"t5710-promisor-remote-capability.sh\" if we really think it's worth\naddressing.\n\nThanks.\n"},{"id":"543643","messageId":"CAP8UFD1VxxRzTG9ea8H7U032Ef76RFg_KgvO-w8zw5mUEdZSaA@mail.gmail.com","threadId":"64670","inReplyTo":"87qzninlb4.fsf@toon--20250203-5JQV3.mail-host-address-is-not-set","subject":"Re: [PATCH v2 6/8] promisor-remote: trust known remotes matching acceptFromServerUrl","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-05-19T15:25:03Z","receivedAt":"2026-05-19T15:25:16Z","isPatch":true,"body":"On Mon, May 11, 2026 at 3:11 PM Toon Claes <toon@iotcl.com> wrote:\n>\n> Christian Couder <christian.couder@gmail.com> writes:\n>\n> > +static bool match_one_url(const struct url_info *pi, const struct url_info *ui)\n> > +{\n> > +     const char *pat = pi->url;\n> > +     const char *url = ui->url;\n> > +     char *p_str, *u_str;\n> > +     bool res;\n> > +\n> > +     /*\n> > +      * Schemes must match exactly. They are case-folded by\n> > +      * url_normalize(), so strncmp() suffices.\n> > +      */\n> > +     if (pi->scheme_len != ui->scheme_len || strncmp(pat, url, pi->scheme_len))\n> > +             return false;\n> > +\n> > +     /*\n> > +      * Ports must match exactly. url_normalize() strips default\n> > +      * ports (like 443 for https), so length and content\n> > +      * comparisons are sufficient.\n> > +      */\n> > +     if (pi->port_len != ui->port_len ||\n> > +         strncmp(pat + pi->port_off, url + ui->port_off, pi->port_len))\n> > +             return false;\n> > +\n> > +     /*\n> > +      * Match host and path separately to prevent a '*' in the host\n> > +      * portion of the pattern from matching across the '/'\n> > +      * boundary into the path. Use WM_PATHNAME for the host so '*'\n> > +      * cannot cross '/' there, and 0 for the path so '*' can still\n> > +      * match multi-level paths.\n> > +      */\n> > +\n> > +     p_str = xstrndup(pat + pi->host_off, pi->host_len);\n> > +     u_str = xstrndup(url + ui->host_off, ui->host_len);\n> > +     res = !wildmatch(p_str, u_str, WM_PATHNAME);\n> > +     free(p_str);\n> > +     free(u_str);\n> > +\n> > +     if (!res)\n>\n> I feel it's a bit confusing your negating the result from wildmatch()\n> to negate it here again? Maybe keep using the int return value, or\n> rename the variable to 'matches' ?\n\nI have simplified this in the v3.\n\nThanks.\n"},{"id":"543644","messageId":"CAP8UFD3fT3RE=CkgS5rUW6TisV9dCN9GQeM0nSswxQw-b75QuA@mail.gmail.com","threadId":"64670","inReplyTo":"87v7cunlid.fsf@toon--20250203-5JQV3.mail-host-address-is-not-set","subject":"Re: [PATCH v2 7/8] promisor-remote: auto-configure unknown remotes","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-05-19T15:25:25Z","receivedAt":"2026-05-19T15:25:38Z","isPatch":true,"body":"On Mon, May 11, 2026 at 3:06 PM Toon Claes <toon@iotcl.com> wrote:\n\n> > +static char *promisor_remote_name_from_url(const char *url)\n> > +{\n> > +     struct url_info url_info = { 0 };\n> > +     char *normalized = url_normalize(url, &url_info);\n> > +     struct strbuf buf = STRBUF_INIT;\n> > +\n> > +     if (!normalized) {\n> > +             warning(_(\"couldn't normalize advertised url '%s', \"\n> > +                       \"ignoring this remote\"), url);\n> > +             return NULL;\n> > +     }\n> > +\n> > +     if (url_info.host_len) {\n> > +             strbuf_add(&buf, normalized + url_info.host_off, url_info.host_len);\n> > +             strbuf_addch(&buf, '-');\n> > +     }\n> > +\n> > +     if (url_info.port_len) {\n> > +             strbuf_add(&buf, normalized + url_info.port_off, url_info.port_len);\n> > +             strbuf_addch(&buf, '-');\n>\n> If the url doesn't have a path, this could lead to the name being\n> `example-com-8443`. But we have a MAX_REMOTES_WITH_SIMILAR_NAMES at 20,\n> would this be an issue for a second remote without configured name?\n>\n> As far as I can tell from handle_matching_allowed_url(), it's no issue,\n> because the numeric `-%d` suffix is added and we never atoi() the number\n> from existing remotes in the config.\n\nRight.\n\n[...]\n\n> > +test_expect_success \"clone with URL allowlisted and no remote already configured\" '\n> > +     git -C server config promisor.advertise true &&\n> > +     test_when_finished \"rm -rf client\" &&\n> > +     test_when_finished \"rm -f full_names\" &&\n> > +\n> > +     GIT_NO_LAZY_FETCH=0 git clone \\\n> > +             -c promisor.acceptfromserver=None \\\n> > +             -c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n> > +             --no-local --filter=\"blob:limit=5k\" server client &&\n>\n> So promisor.acceptFromServerUrl only works if promisor.acceptFromServer\n> is \"none\"? I mean which one should precedence? If\n> promisor.acceptFromServer is set to \"all\", the promisor remote is\n> accepted by the client, but not saved to the config. Is that\n> intentional? Should we document that?\n\nYeah, it was buggy in v2 for some values of\n`promisor.acceptfromserver`, and things were not properly documented.\nBut I think it's correct and much clearer now in v3 as discussed in my\nreply to your comments on the previous patch.\n\nThanks.\n"},{"id":"543645","messageId":"20260519153808.494105-1-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260427124108.3524129-1-christian.couder@gmail.com","subject":"[PATCH v3 0/8] Auto-configure advertised remotes via URL allowlist","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-05-19T15:38:00Z","receivedAt":"2026-05-19T15:38:27Z","isPatch":true,"body":"Currently, the \"promisor-remote\" protocol capability allows a server\nto advertise promisor remotes (and their tokens/filters), but the\nclient's `promisor.acceptFromServer` mechanism requires these remotes\nto already exist in the config.\n\nThis is a significant burden for users and administrators who have to\npre-configure remotes.\n\nThis patch series improves on this by introducing a new\n`promisor.acceptFromServerUrl` config option, which provides an\nadditive, URL-based security allowlist.\n\nMultiple `promisor.acceptFromServerUrl` config options can be provided\nin different config files. Each one should contain a URL glob pattern\nwhich can optionally be prefixed with a remote name in the\n\"[<name>=]<pattern>\" format.\n\nThe goal is for something like a simple:\n\n  git config set --global promisor.acceptFromServerUrl \"https://my-org.com/*\"\n\nto be all that is needed for internal work in many organizations. \n\nWith this new config option:\n\n - The server can update fields (like tokens) for known remotes,\n   provided their URL matches the allowlist, even if\n   `acceptFromServer` is set to `None`.\n\n - Unknown remotes advertised by the server can be automatically\n   configured on the client if their URL matches the allowlist.\n\n - If there is no `<name>` prefix before the glob pattern matched, the\n   auto-configured remote is named using the\n   \"promisor-auto-<sanitized-url>\" format. So the same auto-configured\n   remote config entry will be reused for the same URL.\n\n - If a `<name>` prefix is provided, it will be used for the\n   auto-configured remote config entry.\n\n - If the chosen name (auto-generated or prefixed) already exists but\n   points to a different URL, overwriting the existing config is\n   prevented by appending a numeric suffix (e.g., -1, -2) to the name\n   and auto-configuring using that name.\n\n - The server's originally advertised name is always saved in the\n   `remote.<name>.advertisedAs` config variable of the auto-configured\n   remote for tracing and debugging.\n\nSecurity considerations:\n\n - Advertised URLs and glob patterns are routed through\n   url_normalize() / url_normalize_pattern() before matching, to\n   prevent percent-encoding, case variation, or path-traversal (..)\n   bypasses.\n\n - URL matching is done component by component: scheme and port\n   must match exactly (no wildcards), the host is matched with\n   WM_PATHNAME so a '*' cannot cross the '/' boundary into the\n   path, and the path is matched without WM_PATHNAME so '*' can\n   still span multi-level paths.\n\n - Auto-generated remote names are sanitized (non-alphanumeric\n   characters are replaced with '-', runs of '-' are collapsed)\n   and prefixed with 'promisor-auto-'. User-supplied names (from\n   the 'name=<pattern>' syntax) are validated with\n   valid_remote_name(). Together, these prevent a server from\n   maliciously overwriting standard remotes (like 'origin').\n\n - If the auto-generated or user-supplied name collides with an\n   existing remote configured to a different URL, a numeric\n   suffix ('-1', '-2', ...) is appended, up to a bounded limit,\n   so a server cannot hijack an existing remote by name.\n\n - Known remotes are still subject to URL consistency checks:\n   even if an advertised URL matches the allowlist, it is only\n   accepted for a known remote if it matches the URL already\n   configured locally for that remote.\n\n - The documentation explains in detail how to write secure glob\n   patterns in `promisor.acceptFromServerUrl`, and highlights the\n   risks of overly broad patterns on shared hosting platforms.\n\nHigh level description of the patches\n=====================================\n\n - Patch 1/8 is a very small preparatory patch that simplifies some\n   tests a bit.\n\n - Patches 2/8 and 3/8 expose and adapt a url_normalize_pattern()\n   helper function in the urlmatch API.\n\n - Patch 4/8 adapts `struct promisor_info` by adding a new\n   `local_name` member to it to prepare for the next patches.\n\n - Patches 5/8 to 7/8 implement the core feature. They introduce the\n   parsing machinery, add the additive allowlist for known remotes\n   (with url_normalize() security), and finally implement the\n   auto-creation and collision resolution for unknown remotes.\n\n - Patch 8/8 cleans up and modernizes the existing\n   `promisor.acceptFromServer` documentation.\n\nChanges compared to v2\n======================\n\nThanks to Toon, Patrick and Junio for reviewing the previous versions\nof this series and of the preparatory series.\n\nThis series has been rebased on top of master now that the preparatory\nseries has been merged in a19de4d24a (Merge branch\n'cc/promisor-auto-config-url', 2026-05-11).\n\nOnly the following patches changed:\n\n - Patch 6/8 (promisor-remote: trust known remotes matching acceptFromServerUrl)\n\n   - The WM_PATHNAME flag is not used anymore when calling wildmatch().\n\n   - The match_one_url() function has been refactored using a new\n     match_pattern_url() helper function. There is no double negation\n     anymore.\n\n   - The call to url_matches_accept_list() in should_accept_remote()\n     has been moved up to make sure `promisor.acceptFromServerUrl`\n     takes precedence over `promisor.acceptFromServer`.\n\n   - It's documented that the username and password components of the\n     URL are intentionally ignored during matching.\n\n   - The documentation now clarifies how\n     `promisor.acceptFromServerUrl` interacts with\n     `promisor.acceptFromServer`.\n\n - Patch 7/8 (promisor-remote: auto-configure unknown remotes)\n\n   - The call to should_accept_new_remote_url() is now before the\n     `accept == ACCEPT_ALL` check.\n\n   - The documentation continues to clarify how\n     `promisor.acceptFromServerUrl` interacts with\n     `promisor.acceptFromServer`.\n\nCI tests\n========\n\nThey all pass, see:\n\nhttps://github.com/chriscool/git/actions/runs/26103407562\n\nRange diff since v2\n===================\n\n1:  44e9a16455 = 1:  ab231c0896 t5710: simplify 'mkdir X' followed by 'git -C X init'\n2:  42f174910c = 2:  b3e66f329f urlmatch: change 'allow_globs' arg to bool\n3:  8088374458 = 3:  813d748dd6 urlmatch: add url_normalize_pattern() helper\n4:  6bfda89a79 = 4:  e92863bee8 promisor-remote: add 'local_name' to 'struct promisor_info'\n5:  fefa17e6dd = 5:  7e1b106404 promisor-remote: introduce promisor.acceptFromServerUrl\n6:  2f238d0a7a ! 6:  f00eed4bf2 promisor-remote: trust known remotes matching acceptFromServerUrl\n    @@ Commit message\n         returns the first matching allowed_url entry (or NULL).\n     \n         The URL matching is done component by component: scheme and port are\n    -    compared exactly, the host is matched with wildmatch() using the\n    -    WM_PATHNAME flag (so '*' cannot cross the '/' boundary into the path),\n    -    and the path is matched with wildmatch() without WM_PATHNAME (so '*'\n    -    can still match multi-level paths). Before matching, the advertised\n    -    URL is passed through url_normalize() so that case variations in the\n    -    scheme/host, percent-encoding tricks, and \"..\" path segments cannot\n    -    bypass the allowlist.\n    +    compared exactly, the host and path are matched with wildmatch().\n    +    Before matching, the advertised URL is passed through url_normalize()\n    +    so that case variations in the scheme/host, percent-encoding tricks,\n    +    and \"..\" path segments cannot bypass the allowlist.\n     \n    -    Let's then use this helper at the tail of should_accept_remote() so\n    -    that, when `accept == ACCEPT_NONE`, a known remote whose URL matches\n    -    the allowlist is still accepted.\n    +    The username and password components of the URL are intentionally\n    +    ignored during matching to allow servers to rotate them, though using\n    +    the 'token' field of the capability is preferred over embedding\n    +    credentials in the URL.\n    +\n    +    Let's then use this helper in should_accept_remote() so that, a known\n    +    remote whose URL matches the allowlist is accepted.\n     \n         To prepare for this new logic, let's also:\n     \n    @@ Commit message\n     \n          - Replace the BUG() guard in the ACCEPT_KNOWN_URL case with an\n            explicit 'if (accept == ACCEPT_KNOWN_URL) return' and a new\n    -       BUG() guard in the ACCEPT_NONE case, so url_matches_accept_list()\n    -       is only called in the ACCEPT_NONE case.\n    +       BUG() guard in the ACCEPT_NONE case.\n     \n          - Call accept_from_server_url() from filter_promisor_remote()\n            and relax its early return so that the function is entered when\n    @@ Commit message\n         including the URL normalization behavior and the component-wise\n         matching, and let's mention it in \"gitprotocol-v2.adoc\".\n     \n    +    Also let's clarify in the documentation how\n    +    `promisor.acceptFromServerUrl` interacts with\n    +    `promisor.acceptFromServer`:\n    +\n    +     - Precedence: when both options are set,\n    +       `promisor.acceptFromServerUrl` is consulted first. If a matching\n    +       pattern leads to acceptance, the remote is accepted regardless of\n    +       `promisor.acceptFromServer`. Otherwise the decision is left to\n    +       `promisor.acceptFromServer`.\n    +\n    +     - URL-mismatch guard: even when the advertised URL matches the\n    +       allowlist, an already-existing client-side remote whose configured\n    +       URL differs from the advertised one is not accepted through\n    +       `promisor.acceptFromServerUrl`. `promisor.acceptFromServer=all` and\n    +       `=knownName` keep their pre-existing, looser semantics.\n    +\n    +    The precedence paragraph is intentionally scoped here to known remotes\n    +    only (field updates). A following commit that introduces auto-creation\n    +    of unknown remotes will extend it to cover that case as well.\n    +\n         Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n     \n      ## Documentation/config/promisor.adoc ##\n    @@ Documentation/config/promisor.adoc: promisor.acceptFromServer::\n     +URL will be accepted if it matches _ANY_ glob pattern specified by\n     +this option in _ANY_ config file read by Git.\n     ++\n    ++When both `promisor.acceptFromServer` and `promisor.acceptFromServerUrl`\n    ++are set, `promisor.acceptFromServerUrl` is consulted first and takes\n    ++precedence: if a matching pattern leads to acceptance (by accepting\n    ++field updates for a known remote whose URL matches both the local\n    ++configuration and the allowlist), the advertised remote is accepted\n    ++regardless of the `promisor.acceptFromServer` setting. If no pattern\n    ++in `promisor.acceptFromServerUrl` triggers acceptance, the decision\n    ++is left to `promisor.acceptFromServer`.\n    +++\n    ++Note however that, even when an advertised URL matches a pattern in\n    ++`promisor.acceptFromServerUrl`, an already-existing remote on the\n    ++client whose name matches the advertised name but whose configured URL\n    ++differs from the advertised one will _NOT_ be accepted through\n    ++`promisor.acceptFromServerUrl`. This prevents a server from silently\n    ++re-pointing an existing client-side remote at a different URL. (Such a\n    ++remote may still be accepted through `promisor.acceptFromServer=all`\n    ++or `=knownName`, which have their own, looser semantics; see the\n    ++documentation of that option.)\n    +++\n     +Be _VERY_ careful with these patterns: `*` matches any sequence of\n     +characters within the 'host' and 'path' parts of a URL (but cannot\n     +cross part boundaries). An overly broad pattern is a major security\n    @@ Documentation/config/promisor.adoc: promisor.acceptFromServer::\n     +characters are decoded where possible, and path segments like `..`\n     +are resolved. The port must also match exactly (e.g.,\n     +`https://example.com:8080/*` will not match a URL advertised on\n    -+port 9999).\n    ++port 9999). The username and password components of the URL are\n    ++ignored during matching. Note that embedding credentials in URLs is\n    ++discouraged. Passing authentication tokens via the `token` field of\n    ++the `promisor-remote` capability is strongly preferred.\n     ++\n     +For the security implications of accepting a promisor remote, see the\n     +documentation of `promisor.acceptFromServer`. For details on the\n    @@ promisor-remote.c: static void load_accept_from_server_url(struct repository *re\n      \t}\n      }\n      \n    ++static bool match_pattern_url(const char *pat, size_t pat_len,\n    ++\t\t\t      const char *url, size_t url_len)\n    ++{\n    ++\tchar *p_str = xstrndup(pat, pat_len);\n    ++\tchar *u_str = xstrndup(url, url_len);\n    ++\tbool res = !wildmatch(p_str, u_str, 0);\n    ++\n    ++\tfree(p_str);\n    ++\tfree(u_str);\n    ++\n    ++\treturn res;\n    ++}\n    ++\n     +static bool match_one_url(const struct url_info *pi, const struct url_info *ui)\n     +{\n     +\tconst char *pat = pi->url;\n     +\tconst char *url = ui->url;\n    -+\tchar *p_str, *u_str;\n    -+\tbool res;\n     +\n     +\t/*\n     +\t * Schemes must match exactly. They are case-folded by\n    @@ promisor-remote.c: static void load_accept_from_server_url(struct repository *re\n     +\t/*\n     +\t * Match host and path separately to prevent a '*' in the host\n     +\t * portion of the pattern from matching across the '/'\n    -+\t * boundary into the path. Use WM_PATHNAME for the host so '*'\n    -+\t * cannot cross '/' there, and 0 for the path so '*' can still\n    -+\t * match multi-level paths.\n    ++\t * boundary into the path.\n     +\t */\n     +\n    -+\tp_str = xstrndup(pat + pi->host_off, pi->host_len);\n    -+\tu_str = xstrndup(url + ui->host_off, ui->host_len);\n    -+\tres = !wildmatch(p_str, u_str, WM_PATHNAME);\n    -+\tfree(p_str);\n    -+\tfree(u_str);\n    -+\n    -+\tif (!res)\n    -+\t\treturn false;\n    -+\n    -+\tp_str = xstrndup(pat + pi->path_off, pi->path_len);\n    -+\tu_str = xstrndup(url + ui->path_off, ui->path_len);\n    -+\tres = !wildmatch(p_str, u_str, 0);\n    -+\tfree(p_str);\n    -+\tfree(u_str);\n    -+\n    -+\treturn res;\n    ++\treturn match_pattern_url(pat + pi->host_off, pi->host_len,\n    ++\t\t\t\t url + ui->host_off, ui->host_len) &&\n    ++\t\tmatch_pattern_url(pat + pi->path_off, pi->path_len,\n    ++\t\t\t\t  url + ui->path_off, ui->path_len);\n     +}\n     +\n     +static struct allowed_url *url_matches_accept_list(\n    @@ promisor-remote.c: static void load_accept_from_server_url(struct repository *re\n      {\n      \tstruct promisor_info *p;\n     @@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n    - \tif (accept == ACCEPT_KNOWN_NAME)\n    + \t\t    \"this remote should have been rejected earlier\",\n    + \t\t    remote_name);\n    + \n    +-\tif (accept == ACCEPT_ALL)\n    +-\t\treturn all_fields_match(advertised, config_info, NULL);\n    +-\n    + \t/* Get config info for that promisor remote */\n    + \titem = string_list_lookup(config_info, remote_name);\n    + \n    +-\tif (!item)\n    ++\tif (!item) {\n    + \t\t/* We don't know about that remote */\n    ++\t\tif (accept == ACCEPT_ALL)\n    ++\t\t\treturn all_fields_match(advertised, config_info, NULL);\n    + \t\treturn 0;\n    ++\t}\n    + \n    + \tp = item->util;\n    + \n    +-\tif (accept == ACCEPT_KNOWN_NAME)\n    ++\t/* Known remote in the allowlist? */\n    ++\tif (!strcmp(p->url, remote_url) && url_matches_accept_list(accept_urls, remote_url))\n      \t\treturn all_fields_match(advertised, config_info, p);\n      \n     -\tif (accept != ACCEPT_KNOWN_URL)\n     -\t\tBUG(\"Unhandled 'enum accept_promisor' value '%d'\", accept);\n    --\n    ++\tif (accept == ACCEPT_ALL)\n    ++\t\treturn all_fields_match(advertised, config_info, NULL);\n    ++\n    ++\tif (accept == ACCEPT_KNOWN_NAME)\n    ++\t\treturn all_fields_match(advertised, config_info, p);\n    + \n      \tif (strcmp(p->url, remote_url)) {\n      \t\twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s', \"\n    - \t\t\t  \"ignoring this remote\"),\n     @@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n      \t\treturn 0;\n      \t}\n    @@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n     +\tif (accept != ACCEPT_NONE)\n     +\t\tBUG(\"Unhandled 'enum accept_promisor' value '%d'\", accept);\n     +\n    -+\t/*\n    -+\t * Even if accept == ACCEPT_NONE, we MUST trust this known\n    -+\t * remote to update its token or other such fields if its URL\n    -+\t * matches the acceptFromServerUrl allowlist!\n    -+\t */\n    -+\tif (url_matches_accept_list(accept_urls, remote_url))\n    -+\t\treturn all_fields_match(advertised, config_info, p);\n    -+\n     +\treturn 0;\n      }\n      \n7:  a077f33df4 ! 7:  af06fb31db promisor-remote: auto-configure unknown remotes\n    @@ Commit message\n            handling, and by\n          - adding a \"remote.<name>.advertisedAs\" entry to \"remote.adoc\".\n     \n    +    Also let's extend the precedence paragraph added by a previous commit\n    +    to mention this new acceptance path: until now, the only way for\n    +    `promisor.acceptFromServerUrl` to trigger acceptance was to allow\n    +    field updates for a known remote. With this commit, it can also trigger\n    +    auto-creation of a previously-unknown remote whose advertised URL\n    +    matches the allowlist.\n    +\n         Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n     \n      ## Documentation/config/promisor.adoc ##\n    @@ Documentation/config/promisor.adoc: promisor.acceptFromServer::\n      \ttokens) from the server, even if `promisor.acceptFromServer`\n      \tis set to `none` (the default).\n     @@ Documentation/config/promisor.adoc: this option in _ANY_ config file read by Git.\n    + +\n    + When both `promisor.acceptFromServer` and `promisor.acceptFromServerUrl`\n    + are set, `promisor.acceptFromServerUrl` is consulted first and takes\n    +-precedence: if a matching pattern leads to acceptance (by accepting\n    +-field updates for a known remote whose URL matches both the local\n    +-configuration and the allowlist), the advertised remote is accepted\n    +-regardless of the `promisor.acceptFromServer` setting. If no pattern\n    +-in `promisor.acceptFromServerUrl` triggers acceptance, the decision\n    +-is left to `promisor.acceptFromServer`.\n    ++precedence: if a matching pattern leads to acceptance (either by\n    ++auto-configuring an unknown remote or by accepting field updates for\n    ++a known remote whose URL matches both the local configuration and the\n    ++allowlist), the advertised remote is accepted regardless of the\n    ++`promisor.acceptFromServer` setting. If no pattern in\n    ++`promisor.acceptFromServerUrl` triggers acceptance, the decision is\n    ++left to `promisor.acceptFromServer`.\n    + +\n    + Note however that, even when an advertised URL matches a pattern in\n    + `promisor.acceptFromServerUrl`, an already-existing remote on the\n    +@@ Documentation/config/promisor.adoc: documentation of that option.)\n      Be _VERY_ careful with these patterns: `*` matches any sequence of\n      characters within the 'host' and 'path' parts of a URL (but cannot\n      cross part boundaries). An overly broad pattern is a major security\n    @@ Documentation/config/promisor.adoc: this option in _ANY_ config file read by Git\n      +\n      1. Start with a secure protocol scheme, like `https://` or `ssh://`.\n      +\n    -@@ Documentation/config/promisor.adoc: are resolved. The port must also match exactly (e.g.,\n    - `https://example.com:8080/*` will not match a URL advertised on\n    - port 9999).\n    +@@ Documentation/config/promisor.adoc: ignored during matching. Note that embedding credentials in URLs is\n    + discouraged. Passing authentication tokens via the `token` field of\n    + the `promisor-remote` capability is strongly preferred.\n      +\n     +The glob pattern can optionally be prefixed with a remote name and an\n     +equals sign (e.g., `cdn=https://cdn.example.com/*`). If such a prefix\n    @@ promisor-remote.c: static struct allowed_url *url_matches_accept_list(\n      \tstruct promisor_info *p;\n      \tstruct string_list_item *item;\n     @@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n    - \t/* Get config info for that promisor remote */\n    - \titem = string_list_lookup(config_info, remote_name);\n      \n    --\tif (!item)\n    -+\tif (!item) {\n    + \tif (!item) {\n      \t\t/* We don't know about that remote */\n    --\t\treturn 0;\n    ++\n     +\t\tint res = should_accept_new_remote_url(repo, accept_urls, advertised);\n    -+\t\tif (res)\n    ++\t\tif (res) {\n     +\t\t\t*reload_config = true;\n    -+\t\treturn res;\n    -+\t}\n    - \n    - \tp = item->util;\n    - \n    ++\t\t\treturn res;\n    ++\t\t}\n    ++\n    + \t\tif (accept == ACCEPT_ALL)\n    + \t\t\treturn all_fields_match(advertised, config_info, NULL);\n    + \t\treturn 0;\n     @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n      \t\t\tstring_list_sort(&config_info);\n      \t\t}\n8:  b68b9497aa = 8:  92075d88d8 doc: promisor: improve acceptFromServer entry\n\n\nChristian Couder (8):\n  t5710: simplify 'mkdir X' followed by 'git -C X init'\n  urlmatch: change 'allow_globs' arg to bool\n  urlmatch: add url_normalize_pattern() helper\n  promisor-remote: add 'local_name' to 'struct promisor_info'\n  promisor-remote: introduce promisor.acceptFromServerUrl\n  promisor-remote: trust known remotes matching acceptFromServerUrl\n  promisor-remote: auto-configure unknown remotes\n  doc: promisor: improve acceptFromServer entry\n\n Documentation/config/promisor.adoc    | 146 +++++++--\n Documentation/config/remote.adoc      |   9 +\n Documentation/gitprotocol-v2.adoc     |   9 +-\n promisor-remote.c                     | 413 ++++++++++++++++++++++++--\n t/t5710-promisor-remote-capability.sh | 202 ++++++++++++-\n urlmatch.c                            |  11 +-\n urlmatch.h                            |  12 +\n 7 files changed, 754 insertions(+), 48 deletions(-)\n\n-- \n2.54.0.134.gbbe8e27878.dirty\n\n"},{"id":"543646","messageId":"20260519153808.494105-2-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260519153808.494105-1-christian.couder@gmail.com","subject":"[PATCH v3 1/8] t5710: simplify 'mkdir X' followed by 'git -C X init'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-05-19T15:38:01Z","receivedAt":"2026-05-19T15:38:28Z","isPatch":true,"body":"It's simpler and more efficient to just use `git init client` instead\nof `mkdir client && git -C client init`.\n\nSo let's replace the latter with the former.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n t/t5710-promisor-remote-capability.sh | 6 ++----\n 1 file changed, 2 insertions(+), 4 deletions(-)\n\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex b404ad9f0a..bf1cc54605 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -177,8 +177,7 @@ test_expect_success \"init + fetch with promisor.advertise set to 'true'\" '\n \tgit -C server config promisor.advertise true &&\n \ttest_when_finished \"rm -rf client\" &&\n \n-\tmkdir client &&\n-\tgit -C client init &&\n+\tgit init client &&\n \tgit -C client config remote.lop.promisor true &&\n \tgit -C client config remote.lop.fetch \"+refs/heads/*:refs/remotes/lop/*\" &&\n \tgit -C client config remote.lop.url \"$TRASH_DIRECTORY_URL/lop\" &&\n@@ -231,8 +230,7 @@ test_expect_success \"init + fetch two promisors but only one advertised\" '\n \t# Create a promisor that will be configured but not be used\n \tgit init --bare unused_lop &&\n \n-\tmkdir client &&\n-\tgit -C client init &&\n+\tgit init client &&\n \tgit -C client config remote.unused_lop.promisor true &&\n \tgit -C client config remote.unused_lop.fetch \"+refs/heads/*:refs/remotes/unused_lop/*\" &&\n \tgit -C client config remote.unused_lop.url \"$TRASH_DIRECTORY_URL/unused_lop\" &&\n-- \n2.54.0.134.gbbe8e27878.dirty\n\n"},{"id":"543647","messageId":"20260519153808.494105-3-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260519153808.494105-1-christian.couder@gmail.com","subject":"[PATCH v3 2/8] urlmatch: change 'allow_globs' arg to bool","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-05-19T15:38:02Z","receivedAt":"2026-05-19T15:38:31Z","isPatch":true,"body":"The last argument of url_normalize_1() is `char allow_globs` but it is\nused as a boolean, not as a char.\n\nLet's convert it to a `bool`, and while at it convert the two calls to\nurl_normalize_1() so they pass 'true' or 'false' instead of '1' or '0'.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n urlmatch.c | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/urlmatch.c b/urlmatch.c\nindex eea8300489..989bc7eb8b 100644\n--- a/urlmatch.c\n+++ b/urlmatch.c\n@@ -111,7 +111,7 @@ static int match_host(const struct url_info *url_info,\n \treturn (!url_len && !pat_len);\n }\n \n-static char *url_normalize_1(const char *url, struct url_info *out_info, char allow_globs)\n+static char *url_normalize_1(const char *url, struct url_info *out_info, bool allow_globs)\n {\n \t/*\n \t * Normalize NUL-terminated url using the following rules:\n@@ -437,7 +437,7 @@ static char *url_normalize_1(const char *url, struct url_info *out_info, char al\n \n char *url_normalize(const char *url, struct url_info *out_info)\n {\n-\treturn url_normalize_1(url, out_info, 0);\n+\treturn url_normalize_1(url, out_info, false);\n }\n \n static size_t url_match_prefix(const char *url,\n@@ -577,7 +577,7 @@ int urlmatch_config_entry(const char *var, const char *value,\n \t\tstruct url_info norm_info;\n \n \t\tconfig_url = xmemdupz(key, dot - key);\n-\t\tnorm_url = url_normalize_1(config_url, &norm_info, 1);\n+\t\tnorm_url = url_normalize_1(config_url, &norm_info, true);\n \t\tif (norm_url)\n \t\t\tretval = match_urls(url, &norm_info, &matched);\n \t\telse if (collect->fallback_match_fn)\n-- \n2.54.0.134.gbbe8e27878.dirty\n\n"},{"id":"543648","messageId":"20260519153808.494105-4-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260519153808.494105-1-christian.couder@gmail.com","subject":"[PATCH v3 3/8] urlmatch: add url_normalize_pattern() helper","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-05-19T15:38:03Z","receivedAt":"2026-05-19T15:38:33Z","isPatch":true,"body":"In a following commit, we will need to normalize a URL glob pattern\n(which may contain '*' in the host portion) and extract its component\noffsets (host, path, etc.) for separate matching. Let's export a\ndedicated helper function url_normalize_pattern() for that purpose.\n\nIt works like url_normalize(), but passes allow_globs=true to the\ninternal url_normalize_1(), so that '*' characters in the host are\naccepted rather than rejected.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n urlmatch.c |  5 +++++\n urlmatch.h | 12 ++++++++++++\n 2 files changed, 17 insertions(+)\n\ndiff --git a/urlmatch.c b/urlmatch.c\nindex 989bc7eb8b..7e734e2660 100644\n--- a/urlmatch.c\n+++ b/urlmatch.c\n@@ -440,6 +440,11 @@ char *url_normalize(const char *url, struct url_info *out_info)\n \treturn url_normalize_1(url, out_info, false);\n }\n \n+char *url_normalize_pattern(const char *url, struct url_info *out_info)\n+{\n+\treturn url_normalize_1(url, out_info, true);\n+}\n+\n static size_t url_match_prefix(const char *url,\n \t\t\t       const char *url_prefix,\n \t\t\t       size_t url_prefix_len)\ndiff --git a/urlmatch.h b/urlmatch.h\nindex 5ba85cea13..32c5067f9b 100644\n--- a/urlmatch.h\n+++ b/urlmatch.h\n@@ -36,6 +36,18 @@ struct url_info {\n \n char *url_normalize(const char *, struct url_info *);\n \n+/*\n+ * Like url_normalize(), but also allows '*' glob characters in the host\n+ * portion. Use this when normalizing URL patterns from user configuration.\n+ *\n+ * Note that '*' is a valid path character per RFC 3986 (as a sub-delim),\n+ * so glob patterns using '*' in the path are also accepted.\n+ *\n+ * Returns a newly allocated normalized string and fills out_info if\n+ * non-NULL, or NULL if the pattern is invalid.\n+ */\n+char *url_normalize_pattern(const char *url, struct url_info *out_info);\n+\n struct urlmatch_item {\n \tsize_t hostmatch_len;\n \tsize_t pathmatch_len;\n-- \n2.54.0.134.gbbe8e27878.dirty\n\n"},{"id":"543649","messageId":"20260519153808.494105-5-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260519153808.494105-1-christian.couder@gmail.com","subject":"[PATCH v3 4/8] promisor-remote: add 'local_name' to 'struct promisor_info'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-05-19T15:38:04Z","receivedAt":"2026-05-19T15:38:34Z","isPatch":true,"body":"In a following commit, we will store promisor remote information under\na remote name different than the one the server advertised.\n\nTo prepare for this change, let's add a new 'char *local_name' member\nto 'struct promisor_info', and let's update the related functions.\n\nWhile at it, let's also add a small promisor_info_internal_name()\nhelper that returns `local_name` when set, `name` otherwise, and let's\nuse this small helper in promisor_store_advertised_fields() and in the\npost-loop of filter_promisor_remote() so that lookups against the local\nrepo configuration use the right name.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 22 +++++++++++++++-------\n 1 file changed, 15 insertions(+), 7 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 38fa050542..7699e259eb 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -434,13 +434,14 @@ static struct string_list *fields_stored(void)\n  * Struct for promisor remotes involved in the \"promisor-remote\"\n  * protocol capability.\n  *\n- * Except for \"name\", each <member> in this struct and its <value>\n- * should correspond (either on the client side or on the server side)\n- * to a \"remote.<name>.<member>\" config variable set to <value> where\n- * \"<name>\" is a promisor remote name.\n+ * Except for \"name\" and \"local_name\", each <member> in this struct\n+ * and its <value> should correspond (either on the client side or on\n+ * the server side) to a \"remote.<name>.<member>\" config variable set\n+ * to <value> where \"<name>\" is a promisor remote name.\n  */\n struct promisor_info {\n-\tconst char *name;\n+\tconst char *name;\t/* name the server advertised */\n+\tconst char *local_name;\t/* name used locally (may be auto-generated) */\n \tconst char *url;\n \tconst char *filter;\n \tconst char *token;\n@@ -449,6 +450,7 @@ struct promisor_info {\n static void promisor_info_free(struct promisor_info *p)\n {\n \tfree((char *)p->name);\n+\tfree((char *)p->local_name);\n \tfree((char *)p->url);\n \tfree((char *)p->filter);\n \tfree((char *)p->token);\n@@ -462,6 +464,11 @@ static void promisor_info_list_clear(struct string_list *list)\n \tstring_list_clear(list, 0);\n }\n \n+static const char *promisor_info_internal_name(struct promisor_info *p)\n+{\n+\treturn p->local_name ? p->local_name : p->name;\n+}\n+\n static void set_one_field(struct promisor_info *p,\n \t\t\t  const char *field, const char *value)\n {\n@@ -829,7 +836,7 @@ static bool promisor_store_advertised_fields(struct promisor_info *advertised,\n {\n \tstruct promisor_info *p;\n \tstruct string_list_item *item;\n-\tconst char *remote_name = advertised->name;\n+\tconst char *remote_name = promisor_info_internal_name(advertised);\n \tbool reload_config = false;\n \n \tif (!(store_info->store_filter || store_info->store_token))\n@@ -937,7 +944,8 @@ static void filter_promisor_remote(struct repository *repo,\n \t/* Apply accepted remotes to the stable repo state */\n \tfor_each_string_list_item(item, accepted_remotes) {\n \t\tstruct promisor_info *info = item->util;\n-\t\tstruct promisor_remote *r = repo_promisor_remote_find(repo, info->name);\n+\t\tconst char *local = promisor_info_internal_name(info);\n+\t\tstruct promisor_remote *r = repo_promisor_remote_find(repo, local);\n \n \t\tif (r) {\n \t\t\tr->accepted = 1;\n-- \n2.54.0.134.gbbe8e27878.dirty\n\n"},{"id":"543650","messageId":"20260519153808.494105-6-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260519153808.494105-1-christian.couder@gmail.com","subject":"[PATCH v3 5/8] promisor-remote: introduce promisor.acceptFromServerUrl","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-05-19T15:38:05Z","receivedAt":"2026-05-19T15:38:36Z","isPatch":true,"body":"The \"promisor-remote\" protocol capability allows servers to advertise\npromisor remotes, but doesn't allow these remotes to be automatically\nconfigured on the client.\n\nLet's introduce a new `promisor.acceptFromServerUrl` config variable\nwhich contains a glob pattern, so that advertised remotes with a URL\nmatching that pattern will be automatically configured.\n\nThe glob pattern can optionally be prefixed with a remote name which\nwill be used as the name of the new local remote.\n\nFor now though, let's only introduce the functions to read and validate\nthe glob patterns and the optional prefixes.\n\nChecking if the URLs of the advertised remotes match the glob patterns\nand taking the appropriate action is left for a following commit.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c                     | 90 +++++++++++++++++++++++++++\n t/t5710-promisor-remote-capability.sh | 21 +++++++\n 2 files changed, 111 insertions(+)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 7699e259eb..3f3924f587 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -12,6 +12,7 @@\n #include \"packfile.h\"\n #include \"environment.h\"\n #include \"url.h\"\n+#include \"urlmatch.h\"\n #include \"version.h\"\n \n struct promisor_remote_config {\n@@ -657,6 +658,90 @@ static bool has_control_char(const char *s)\n \treturn false;\n }\n \n+struct allowed_url {\n+\tchar *remote_name;\n+\tchar *url_pattern;\n+\tstruct url_info pattern_info;\n+};\n+\n+static void allowed_url_free(void *util, const char *str UNUSED)\n+{\n+\tstruct allowed_url *allowed = util;\n+\n+\tif (!allowed)\n+\t\treturn;\n+\n+\t/* Depending on prefix, free either remote_name or url_pattern */\n+\tfree(allowed->remote_name ? allowed->remote_name : allowed->url_pattern);\n+\tfree(allowed->pattern_info.url);\n+\tfree(allowed);\n+}\n+\n+static struct allowed_url *valid_accept_url(const char *url)\n+{\n+\tchar *dup, *p;\n+\tstruct allowed_url *allowed;\n+\n+\tif (!url)\n+\t\treturn NULL;\n+\n+\tdup = xstrdup(url);\n+\tp = strchr(dup, '=');\n+\tif (p) {\n+\t\t*p = '\\0';\n+\t\tif (!valid_remote_name(dup)) {\n+\t\t\twarning(_(\"invalid remote name '%s' before '=' sign \"\n+\t\t\t\t  \"in '%s' from promisor.acceptFromServerUrl config\"),\n+\t\t\t\tdup, url);\n+\t\t\tfree(dup);\n+\t\t\treturn NULL;\n+\t\t}\n+\t\tp++;\n+\t} else {\n+\t\tp = dup;\n+\t}\n+\n+\tif (has_control_char(p)) {\n+\t\twarning(_(\"invalid url pattern '%s' \"\n+\t\t\t  \"in '%s' from promisor.acceptFromServerUrl config\"), p, url);\n+\t\tfree(dup);\n+\t\treturn NULL;\n+\t}\n+\n+\tallowed = xmalloc(sizeof(*allowed));\n+\tallowed->remote_name = (p == dup) ? NULL : dup;\n+\tallowed->url_pattern = p;\n+\tallowed->pattern_info.url = url_normalize_pattern(p, &allowed->pattern_info);\n+\tif (!allowed->pattern_info.url) {\n+\t\twarning(_(\"invalid url pattern '%s' \"\n+\t\t\t  \"in '%s' from promisor.acceptFromServerUrl config\"), p, url);\n+\t\tfree(dup);\n+\t\tfree(allowed);\n+\t\treturn NULL;\n+\t}\n+\n+\treturn allowed;\n+}\n+\n+static void load_accept_from_server_url(struct repository *repo,\n+\t\t\t\t\tstruct string_list *accept_urls)\n+{\n+\tconst struct string_list *config_urls;\n+\n+\tif (!repo_config_get_string_multi(repo, \"promisor.acceptfromserverurl\", &config_urls)) {\n+\t\tstruct string_list_item *item;\n+\n+\t\tfor_each_string_list_item(item, config_urls) {\n+\t\t\tstruct allowed_url *allowed = valid_accept_url(item->string);\n+\t\t\tif (allowed) {\n+\t\t\t\tstruct string_list_item *new;\n+\t\t\t\tnew = string_list_append(accept_urls, item->string);\n+\t\t\t\tnew->util = allowed;\n+\t\t\t}\n+\t\t}\n+\t}\n+}\n+\n static int should_accept_remote(enum accept_promisor accept,\n \t\t\t\tstruct promisor_info *advertised,\n \t\t\t\tstruct string_list *config_info)\n@@ -901,6 +986,10 @@ static void filter_promisor_remote(struct repository *repo,\n \tstruct string_list_item *item;\n \tbool reload_config = false;\n \tenum accept_promisor accept = accept_from_server(repo);\n+\tstruct string_list accept_urls = STRING_LIST_INIT_DUP;\n+\n+\t/* Load and validate the acceptFromServerUrl config */\n+\tload_accept_from_server_url(repo, &accept_urls);\n \n \tif (accept == ACCEPT_NONE)\n \t\treturn;\n@@ -934,6 +1023,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\t}\n \t}\n \n+\tstring_list_clear_func(&accept_urls, allowed_url_free);\n \tpromisor_info_list_clear(&config_info);\n \tstring_list_clear(&remote_info, 0);\n \tstore_info_free(store_info);\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex bf1cc54605..3b39505380 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -387,6 +387,27 @@ test_expect_success \"clone with 'KnownUrl' and empty url, so not advertised\" '\n \tcheck_missing_objects server 1 \"$oid\"\n '\n \n+test_expect_success \"clone with invalid promisor.acceptFromServerUrl\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\t# As \"bad name\" contains a space, which is not a valid remote name,\n+\t# the pattern should be rejected with a warning and no remote created.\n+\tGIT_NO_LAZY_FETCH=0 git clone \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c \"promisor.acceptFromServerUrl=bad name=https://example.com/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client 2>err &&\n+\n+\t# Check that a warning was emitted\n+\ttest_grep \"invalid remote name '\\''bad name'\\''\" err &&\n+\n+\t# Check that the largest object is not missing on the server\n+\tcheck_missing_objects server 0 \"\" &&\n+\n+\t# Reinitialize server so that the largest object is missing again\n+\tinitialize_server 1 \"$oid\"\n+'\n+\n test_expect_success \"clone with promisor.sendFields\" '\n \tgit -C server config promisor.advertise true &&\n \ttest_when_finished \"rm -rf client\" &&\n-- \n2.54.0.134.gbbe8e27878.dirty\n\n"},{"id":"543651","messageId":"20260519153808.494105-7-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260519153808.494105-1-christian.couder@gmail.com","subject":"[PATCH v3 6/8] promisor-remote: trust known remotes matching acceptFromServerUrl","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-05-19T15:38:06Z","receivedAt":"2026-05-19T15:38:37Z","isPatch":true,"body":"A previous commit introduced the `promisor.acceptFromServerUrl` config\nvariable along with the machinery to parse and validate the URL glob\npatterns and optional remote name prefixes it contains. However, these\nURL patterns are not yet tied into the client's acceptance logic.\n\nWhen a promisor remote is already configured locally, its fields (like\nauthentication tokens) may occasionally need to be refreshed by the\nserver. If `promisor.acceptFromServer` is set to the secure default\n(\"None\"), these updates are rejected, potentially causing future\nfetches to fail.\n\nTo enable such targeted updates for trusted URLs, let's use the URL\npatterns from `promisor.acceptFromServerUrl` as an additional URL\nbased allowlist.\n\nConcretely, let's check the advertised URLs against the URL glob\npatterns by introducing a new small helper function called\nurl_matches_accept_list(), which iterates over the glob patterns and\nreturns the first matching allowed_url entry (or NULL).\n\nThe URL matching is done component by component: scheme and port are\ncompared exactly, the host and path are matched with wildmatch().\nBefore matching, the advertised URL is passed through url_normalize()\nso that case variations in the scheme/host, percent-encoding tricks,\nand \"..\" path segments cannot bypass the allowlist.\n\nThe username and password components of the URL are intentionally\nignored during matching to allow servers to rotate them, though using\nthe 'token' field of the capability is preferred over embedding\ncredentials in the URL.\n\nLet's then use this helper in should_accept_remote() so that, a known\nremote whose URL matches the allowlist is accepted.\n\nTo prepare for this new logic, let's also:\n\n - Add an 'accept_urls' parameter to should_accept_remote().\n\n - Replace the BUG() guard in the ACCEPT_KNOWN_URL case with an\n   explicit 'if (accept == ACCEPT_KNOWN_URL) return' and a new\n   BUG() guard in the ACCEPT_NONE case.\n\n - Call accept_from_server_url() from filter_promisor_remote()\n   and relax its early return so that the function is entered when\n   `accept_urls` has entries even if `accept == ACCEPT_NONE`.\n\nWith this, many organizations may only need something like:\n\n  git config set --global \\\n          promisor.acceptFromServerUrl \"https://my-org.com/*\"\n\nto accept only their own remotes. And if they need to accept additional\nremotes in some specific repos, they can also set:\n\n  git config set promisor.acceptFromServer knownUrl\n\nand configure the additional remote manually only in the repos where\nthey are needed.\n\nLet's then properly document `promisor.acceptFromServerUrl` in\n\"promisor.adoc\" as an additive security allowlist for known remotes,\nincluding the URL normalization behavior and the component-wise\nmatching, and let's mention it in \"gitprotocol-v2.adoc\".\n\nAlso let's clarify in the documentation how\n`promisor.acceptFromServerUrl` interacts with\n`promisor.acceptFromServer`:\n\n - Precedence: when both options are set,\n   `promisor.acceptFromServerUrl` is consulted first. If a matching\n   pattern leads to acceptance, the remote is accepted regardless of\n   `promisor.acceptFromServer`. Otherwise the decision is left to\n   `promisor.acceptFromServer`.\n\n - URL-mismatch guard: even when the advertised URL matches the\n   allowlist, an already-existing client-side remote whose configured\n   URL differs from the advertised one is not accepted through\n   `promisor.acceptFromServerUrl`. `promisor.acceptFromServer=all` and\n   `=knownName` keep their pre-existing, looser semantics.\n\nThe precedence paragraph is intentionally scoped here to known remotes\nonly (field updates). A following commit that introduces auto-creation\nof unknown remotes will extend it to cover that case as well.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    |  74 +++++++++++++++++++\n Documentation/gitprotocol-v2.adoc     |   9 ++-\n promisor-remote.c                     | 102 +++++++++++++++++++++++---\n t/t5710-promisor-remote-capability.sh |  71 ++++++++++++++++++\n 4 files changed, 242 insertions(+), 14 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex b0fa43b839..4a1ecb4425 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -51,6 +51,80 @@ promisor.acceptFromServer::\n \tto \"fetch\" and \"clone\" requests from the client. Name and URL\n \tcomparisons are case sensitive. See linkgit:gitprotocol-v2[5].\n \n+promisor.acceptFromServerUrl::\n+\tA glob pattern to specify which server-advertised URLs a\n+\tclient is allowed to act on. When a URL matches, the client\n+\twill accept the advertised remote as a promisor remote and may\n+\tautomatically accept field updates (such as authentication\n+\ttokens) from the server, even if `promisor.acceptFromServer`\n+\tis set to `none` (the default).\n++\n+This option can appear multiple times in config files. An advertised\n+URL will be accepted if it matches _ANY_ glob pattern specified by\n+this option in _ANY_ config file read by Git.\n++\n+When both `promisor.acceptFromServer` and `promisor.acceptFromServerUrl`\n+are set, `promisor.acceptFromServerUrl` is consulted first and takes\n+precedence: if a matching pattern leads to acceptance (by accepting\n+field updates for a known remote whose URL matches both the local\n+configuration and the allowlist), the advertised remote is accepted\n+regardless of the `promisor.acceptFromServer` setting. If no pattern\n+in `promisor.acceptFromServerUrl` triggers acceptance, the decision\n+is left to `promisor.acceptFromServer`.\n++\n+Note however that, even when an advertised URL matches a pattern in\n+`promisor.acceptFromServerUrl`, an already-existing remote on the\n+client whose name matches the advertised name but whose configured URL\n+differs from the advertised one will _NOT_ be accepted through\n+`promisor.acceptFromServerUrl`. This prevents a server from silently\n+re-pointing an existing client-side remote at a different URL. (Such a\n+remote may still be accepted through `promisor.acceptFromServer=all`\n+or `=knownName`, which have their own, looser semantics; see the\n+documentation of that option.)\n++\n+Be _VERY_ careful with these patterns: `*` matches any sequence of\n+characters within the 'host' and 'path' parts of a URL (but cannot\n+cross part boundaries). An overly broad pattern is a major security\n+risk, as a matching URL allows a server to update fields (such as\n+authentication tokens) on known remotes without further confirmation.\n+To minimize security risks, follow these guidelines:\n++\n+1. Start with a secure protocol scheme, like `https://` or `ssh://`.\n++\n+2. Only allow domain names or paths where you control and trust _ALL_\n+   the content. Be especially careful with shared hosting platforms\n+   like `github.com` or `gitlab.com`. A broad pattern like\n+   `https://gitlab.com/*` is dangerous because it trusts every\n+   repository on the entire platform. Always restrict such patterns to\n+   your specific organization or namespace (e.g.,\n+   `https://gitlab.com/your-org/*`).\n++\n+3. Never use globs at the end of domain names. For example,\n+   `https://cdn.your-org.com/*` might be safe, but\n+   `https://cdn.your-org.com*/*` is a major security risk because\n+   the latter matches `https://cdn.your-org.com.hacker.net/repo`.\n++\n+4. Be careful using globs at the beginning of domain names. While the\n+   code ensures a `*` in the host cannot cross into the path, a\n+   pattern like `https://*.example.com/*` will still match any\n+   subdomain. This is extremely dangerous on shared hosting platforms\n+   (e.g., `https://*.github.io/*` trusts every user's site on the\n+   entire platform).\n++\n+Before matching, both the advertised URL and the pattern are\n+normalized: the scheme and host are lowercased, percent-encoded\n+characters are decoded where possible, and path segments like `..`\n+are resolved. The port must also match exactly (e.g.,\n+`https://example.com:8080/*` will not match a URL advertised on\n+port 9999). The username and password components of the URL are\n+ignored during matching. Note that embedding credentials in URLs is\n+discouraged. Passing authentication tokens via the `token` field of\n+the `promisor-remote` capability is strongly preferred.\n++\n+For the security implications of accepting a promisor remote, see the\n+documentation of `promisor.acceptFromServer`. For details on the\n+protocol, see linkgit:gitprotocol-v2[5].\n+\n promisor.checkFields::\n \tA comma or space separated list of additional remote related\n \tfield names. A client checks if the values of these fields\ndiff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\nindex befa697d21..2beb70595f 100644\n--- a/Documentation/gitprotocol-v2.adoc\n+++ b/Documentation/gitprotocol-v2.adoc\n@@ -866,10 +866,11 @@ the server advertised, the client shouldn't advertise the\n \n On the server side, the \"promisor.advertise\" and \"promisor.sendFields\"\n configuration options can be used to control what it advertises. On\n-the client side, the \"promisor.acceptFromServer\" configuration option\n-can be used to control what it accepts, and the \"promisor.storeFields\"\n-option, to control what it stores. See the documentation of these\n-configuration options in linkgit:git-config[1] for more information.\n+the client side, the \"promisor.acceptFromServer\" and\n+\"promisor.acceptFromServerUrl\" configuration options can be used to\n+control what it accepts, and the \"promisor.storeFields\" option, to\n+control what it stores. See the documentation of these configuration\n+options in linkgit:git-config[1] for more information.\n \n Note that in the future it would be nice if the \"promisor-remote\"\n protocol capability could be used by the server, when responding to\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 3f3924f587..ac4f54c590 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -14,6 +14,7 @@\n #include \"url.h\"\n #include \"urlmatch.h\"\n #include \"version.h\"\n+#include \"wildmatch.h\"\n \n struct promisor_remote_config {\n \tstruct promisor_remote *promisors;\n@@ -742,8 +743,79 @@ static void load_accept_from_server_url(struct repository *repo,\n \t}\n }\n \n+static bool match_pattern_url(const char *pat, size_t pat_len,\n+\t\t\t      const char *url, size_t url_len)\n+{\n+\tchar *p_str = xstrndup(pat, pat_len);\n+\tchar *u_str = xstrndup(url, url_len);\n+\tbool res = !wildmatch(p_str, u_str, 0);\n+\n+\tfree(p_str);\n+\tfree(u_str);\n+\n+\treturn res;\n+}\n+\n+static bool match_one_url(const struct url_info *pi, const struct url_info *ui)\n+{\n+\tconst char *pat = pi->url;\n+\tconst char *url = ui->url;\n+\n+\t/*\n+\t * Schemes must match exactly. They are case-folded by\n+\t * url_normalize(), so strncmp() suffices.\n+\t */\n+\tif (pi->scheme_len != ui->scheme_len || strncmp(pat, url, pi->scheme_len))\n+\t\treturn false;\n+\n+\t/*\n+\t * Ports must match exactly. url_normalize() strips default\n+\t * ports (like 443 for https), so length and content\n+\t * comparisons are sufficient.\n+\t */\n+\tif (pi->port_len != ui->port_len ||\n+\t    strncmp(pat + pi->port_off, url + ui->port_off, pi->port_len))\n+\t\treturn false;\n+\n+\t/*\n+\t * Match host and path separately to prevent a '*' in the host\n+\t * portion of the pattern from matching across the '/'\n+\t * boundary into the path.\n+\t */\n+\n+\treturn match_pattern_url(pat + pi->host_off, pi->host_len,\n+\t\t\t\t url + ui->host_off, ui->host_len) &&\n+\t\tmatch_pattern_url(pat + pi->path_off, pi->path_len,\n+\t\t\t\t  url + ui->path_off, ui->path_len);\n+}\n+\n+static struct allowed_url *url_matches_accept_list(\n+\t\tstruct string_list *accept_urls, const char *url)\n+{\n+\tstruct string_list_item *item;\n+\tstruct url_info url_info;\n+\n+\turl_info.url = url_normalize(url, &url_info);\n+\n+\tif (!url_info.url)\n+\t\treturn NULL;\n+\n+\tfor_each_string_list_item(item, accept_urls) {\n+\t\tstruct allowed_url *allowed = item->util;\n+\n+\t\tif (match_one_url(&allowed->pattern_info, &url_info)) {\n+\t\t\tfree(url_info.url);\n+\t\t\treturn allowed;\n+\t\t}\n+\t}\n+\n+\tfree(url_info.url);\n+\treturn NULL;\n+}\n+\n static int should_accept_remote(enum accept_promisor accept,\n \t\t\t\tstruct promisor_info *advertised,\n+\t\t\t\tstruct string_list *accept_urls,\n \t\t\t\tstruct string_list *config_info)\n {\n \tstruct promisor_info *p;\n@@ -756,23 +828,27 @@ static int should_accept_remote(enum accept_promisor accept,\n \t\t    \"this remote should have been rejected earlier\",\n \t\t    remote_name);\n \n-\tif (accept == ACCEPT_ALL)\n-\t\treturn all_fields_match(advertised, config_info, NULL);\n-\n \t/* Get config info for that promisor remote */\n \titem = string_list_lookup(config_info, remote_name);\n \n-\tif (!item)\n+\tif (!item) {\n \t\t/* We don't know about that remote */\n+\t\tif (accept == ACCEPT_ALL)\n+\t\t\treturn all_fields_match(advertised, config_info, NULL);\n \t\treturn 0;\n+\t}\n \n \tp = item->util;\n \n-\tif (accept == ACCEPT_KNOWN_NAME)\n+\t/* Known remote in the allowlist? */\n+\tif (!strcmp(p->url, remote_url) && url_matches_accept_list(accept_urls, remote_url))\n \t\treturn all_fields_match(advertised, config_info, p);\n \n-\tif (accept != ACCEPT_KNOWN_URL)\n-\t\tBUG(\"Unhandled 'enum accept_promisor' value '%d'\", accept);\n+\tif (accept == ACCEPT_ALL)\n+\t\treturn all_fields_match(advertised, config_info, NULL);\n+\n+\tif (accept == ACCEPT_KNOWN_NAME)\n+\t\treturn all_fields_match(advertised, config_info, p);\n \n \tif (strcmp(p->url, remote_url)) {\n \t\twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s', \"\n@@ -781,7 +857,13 @@ static int should_accept_remote(enum accept_promisor accept,\n \t\treturn 0;\n \t}\n \n-\treturn all_fields_match(advertised, config_info, p);\n+\tif (accept == ACCEPT_KNOWN_URL)\n+\t\treturn all_fields_match(advertised, config_info, p);\n+\n+\tif (accept != ACCEPT_NONE)\n+\t\tBUG(\"Unhandled 'enum accept_promisor' value '%d'\", accept);\n+\n+\treturn 0;\n }\n \n static int skip_field_name_prefix(const char *elem, const char *field_name, const char **value)\n@@ -991,7 +1073,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t/* Load and validate the acceptFromServerUrl config */\n \tload_accept_from_server_url(repo, &accept_urls);\n \n-\tif (accept == ACCEPT_NONE)\n+\tif (accept == ACCEPT_NONE && !accept_urls.nr)\n \t\treturn;\n \n \t/* Parse remote info received */\n@@ -1011,7 +1093,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\t\tstring_list_sort(&config_info);\n \t\t}\n \n-\t\tif (should_accept_remote(accept, advertised, &config_info)) {\n+\t\tif (should_accept_remote(accept, advertised, &accept_urls, &config_info)) {\n \t\t\tif (!store_info)\n \t\t\t\tstore_info = store_info_new(repo);\n \t\t\tif (promisor_store_advertised_fields(advertised, store_info))\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex 3b39505380..0659b2ac15 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -387,6 +387,77 @@ test_expect_success \"clone with 'KnownUrl' and empty url, so not advertised\" '\n \tcheck_missing_objects server 1 \"$oid\"\n '\n \n+test_expect_success \"clone with 'None' but URL allowlisted\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\tGIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \\\n+\t\t-c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n+\t\t-c remote.lop.url=\"$TRASH_DIRECTORY_URL/lop\" \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\"\n+'\n+\n+test_expect_success \"clone with 'None' but URL not in allowlist\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\tGIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \\\n+\t\t-c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n+\t\t-c remote.lop.url=\"$TRASH_DIRECTORY_URL/lop\" \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c promisor.acceptFromServerUrl=\"https://example.com/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that the largest object is not missing on the server\n+\tcheck_missing_objects server 0 \"\" &&\n+\n+\t# Reinitialize server so that the largest object is missing again\n+\tinitialize_server 1 \"$oid\"\n+'\n+\n+test_expect_success \"clone with 'None' but URL allowlisted in one pattern out of two\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\tGIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \\\n+\t\t-c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n+\t\t-c remote.lop.url=\"$TRASH_DIRECTORY_URL/lop\" \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c promisor.acceptFromServerUrl=\"https://example.com/*\" \\\n+\t\t-c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\"\n+'\n+\n+test_expect_success \"clone with 'None', URL allowlisted, but client has different URL\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\t# The client configures \"lop\" with a different URL (serverTwo) than\n+\t# what the server advertises (lop). Even though the advertised URL\n+\t# matches the allowlist, the remote is rejected because the\n+\t# configured URL does not match the advertised one.\n+\tGIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \\\n+\t\t-c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n+\t\t-c remote.lop.url=\"$TRASH_DIRECTORY_URL/serverTwo\" \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that the largest object is not missing on the server\n+\tcheck_missing_objects server 0 \"\" &&\n+\n+\t# Reinitialize server so that the largest object is missing again\n+\tinitialize_server 1 \"$oid\"\n+'\n+\n test_expect_success \"clone with invalid promisor.acceptFromServerUrl\" '\n \tgit -C server config promisor.advertise true &&\n \ttest_when_finished \"rm -rf client\" &&\n-- \n2.54.0.134.gbbe8e27878.dirty\n\n"},{"id":"543652","messageId":"20260519153808.494105-8-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260519153808.494105-1-christian.couder@gmail.com","subject":"[PATCH v3 7/8] promisor-remote: auto-configure unknown remotes","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-05-19T15:38:07Z","receivedAt":"2026-05-19T15:38:39Z","isPatch":true,"body":"Previous commits have introduced the `promisor.acceptFromServerUrl`\nconfig variable to allowlist some URLs advertised by a server through\nthe \"promisor-remote\" protocol capability.\n\nHowever the new `promisor.acceptFromServerUrl` mechanism, like the old\n`promisor.acceptFromServer` mechanism, still requires a remote to\nalready exist in the client's local configuration before it can be\naccepted. This places a significant manual burden on users to\npre-configure these remotes, and creates friction for administrators\nwho have to troubleshoot or manually provision these setups for their\nteams.\n\nTo eliminate this burden, let's automatically create a new `[remote]`\nsection in the client's config when a server advertises an unknown\nremote whose URL matches a `promisor.acceptFromServerUrl` glob pattern.\n\nConcretely, let's add four helpers:\n\n - sanitize_remote_name(): turn an arbitrary URL-derived string into a\n   valid remote name by replacing non-alphanumeric characters,\n   collapsing runs of '-', and prepending \"promisor-auto-\".\n\n - promisor_remote_name_from_url(): normalize the URL and extract\n   host+port+path to build a human-readable base name, then pass it\n   through sanitize_remote_name().\n\n - configure_auto_promisor_remote(): write the remote.*.url,\n   remote.*.promisor and remote.*.advertisedAs keys to the repo\n   config.\n\n - handle_matching_allowed_url(): pick the final name (user-supplied\n   alias or auto-generated), handle collisions by appending \"-1\",\n   \"-2\", etc., then call configure_auto_promisor_remote().\n\nLet's also add should_accept_new_remote_url() which reuses the\nurl_matches_accept_list() helper introduced in a previous commit to\nfind a matching pattern, then delegates to handle_matching_allowed_url()\nto create the remote.\n\nAnd then let's call should_accept_new_remote_url() from the '!item'\n(unknown remote) branch of should_accept_remote(), setting\n`reload_config` so that the newly-written config is picked up.\n\nFinally let's document all that by:\n\n - expanding the `promisor.acceptFromServerUrl` entry to describe\n   auto-creation, the optional \"name=\" prefix syntax, the\n   \"promisor-auto-*\" generation rules, and numeric-suffix collision\n   handling, and by\n - adding a \"remote.<name>.advertisedAs\" entry to \"remote.adoc\".\n\nAlso let's extend the precedence paragraph added by a previous commit\nto mention this new acceptance path: until now, the only way for\n`promisor.acceptFromServerUrl` to trigger acceptance was to allow\nfield updates for a known remote. With this commit, it can also trigger\nauto-creation of a previously-unknown remote whose advertised URL\nmatches the allowlist.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    |  39 +++--\n Documentation/config/remote.adoc      |   9 ++\n promisor-remote.c                     | 201 +++++++++++++++++++++++++-\n t/t5710-promisor-remote-capability.sh | 104 +++++++++++++\n 4 files changed, 340 insertions(+), 13 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex 4a1ecb4425..11b5716294 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -54,7 +54,8 @@ promisor.acceptFromServer::\n promisor.acceptFromServerUrl::\n \tA glob pattern to specify which server-advertised URLs a\n \tclient is allowed to act on. When a URL matches, the client\n-\twill accept the advertised remote as a promisor remote and may\n+\twill accept the advertised remote as a promisor remote, may\n+\tautomatically create a new remote configuration for it and may\n \tautomatically accept field updates (such as authentication\n \ttokens) from the server, even if `promisor.acceptFromServer`\n \tis set to `none` (the default).\n@@ -65,12 +66,13 @@ this option in _ANY_ config file read by Git.\n +\n When both `promisor.acceptFromServer` and `promisor.acceptFromServerUrl`\n are set, `promisor.acceptFromServerUrl` is consulted first and takes\n-precedence: if a matching pattern leads to acceptance (by accepting\n-field updates for a known remote whose URL matches both the local\n-configuration and the allowlist), the advertised remote is accepted\n-regardless of the `promisor.acceptFromServer` setting. If no pattern\n-in `promisor.acceptFromServerUrl` triggers acceptance, the decision\n-is left to `promisor.acceptFromServer`.\n+precedence: if a matching pattern leads to acceptance (either by\n+auto-configuring an unknown remote or by accepting field updates for\n+a known remote whose URL matches both the local configuration and the\n+allowlist), the advertised remote is accepted regardless of the\n+`promisor.acceptFromServer` setting. If no pattern in\n+`promisor.acceptFromServerUrl` triggers acceptance, the decision is\n+left to `promisor.acceptFromServer`.\n +\n Note however that, even when an advertised URL matches a pattern in\n `promisor.acceptFromServerUrl`, an already-existing remote on the\n@@ -85,9 +87,10 @@ documentation of that option.)\n Be _VERY_ careful with these patterns: `*` matches any sequence of\n characters within the 'host' and 'path' parts of a URL (but cannot\n cross part boundaries). An overly broad pattern is a major security\n-risk, as a matching URL allows a server to update fields (such as\n-authentication tokens) on known remotes without further confirmation.\n-To minimize security risks, follow these guidelines:\n+risk, as a matching URL allows a server to auto-configure new remotes\n+and to update fields (such as authentication tokens) on known remotes\n+without further confirmation. To minimize security risks, follow these\n+guidelines:\n +\n 1. Start with a secure protocol scheme, like `https://` or `ssh://`.\n +\n@@ -121,6 +124,22 @@ ignored during matching. Note that embedding credentials in URLs is\n discouraged. Passing authentication tokens via the `token` field of\n the `promisor-remote` capability is strongly preferred.\n +\n+The glob pattern can optionally be prefixed with a remote name and an\n+equals sign (e.g., `cdn=https://cdn.example.com/*`). If such a prefix\n+is provided, accepted remotes will be saved under that name. If no\n+such prefix is provided, a safe remote name will be automatically\n+generated by sanitizing the URL and prefixing it with\n+`promisor-auto-`.\n++\n+If a remote with the chosen name already exists but points to a\n+different URL, Git will append a numeric suffix (e.g., `-1`, `-2`) to\n+the name to prevent overwriting existing configurations. You should\n+make sure that this doesn't happen often though, as remotes will be\n+rejected if the numeric suffix increases too much. In all cases, the\n+original name advertised by the server is recorded in the\n+`remote.<name>.advertisedAs` configuration variable for tracing and\n+debugging purposes.\n++\n For the security implications of accepting a promisor remote, see the\n documentation of `promisor.acceptFromServer`. For details on the\n protocol, see linkgit:gitprotocol-v2[5].\ndiff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc\nindex 91e46f66f5..6e2bbdf457 100644\n--- a/Documentation/config/remote.adoc\n+++ b/Documentation/config/remote.adoc\n@@ -91,6 +91,15 @@ remote.<name>.promisor::\n \tWhen set to true, this remote will be used to fetch promisor\n \tobjects.\n \n+remote.<name>.advertisedAs::\n+\tWhen a promisor remote is automatically configured using\n+\tinformation advertised by a server through the\n+\t`promisor-remote` protocol capability (see\n+\t`promisor.acceptFromServerUrl`), the server's originally\n+\tadvertised name is saved in this variable. This is for\n+\tinformation, tracing and debugging purposes. Users should not\n+\ttypically modify or create such configuration entries.\n+\n remote.<name>.partialclonefilter::\n \tThe filter that will be applied when fetching from this\tpromisor remote.\n \tChanging or clearing this value will only affect fetches for new commits.\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex ac4f54c590..cbfe6672a3 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -813,10 +813,197 @@ static struct allowed_url *url_matches_accept_list(\n \treturn NULL;\n }\n \n-static int should_accept_remote(enum accept_promisor accept,\n+/*\n+ * Sanitize the buffer to make it a valid remote name coming from the\n+ * server by:\n+ *\n+ * - replacing any non alphanumeric character with a '-'\n+ * - stripping any leading '-',\n+ * - condensing multiple '-' into one,\n+ * - prepending \"promisor-auto-\",\n+ * - validating the result.\n+ */\n+static int sanitize_remote_name(struct strbuf *buf, const char *url)\n+{\n+\tchar prev = '-';\n+\tfor (size_t i = 0; i < buf->len; ) {\n+\t\tif (!isalnum(buf->buf[i]))\n+\t\t\tbuf->buf[i] = '-';\n+\t\tif (prev == '-' && buf->buf[i] == '-') {\n+\t\t\tstrbuf_remove(buf, i, 1);\n+\t\t} else {\n+\t\t\tprev = buf->buf[i];\n+\t\t\ti++;\n+\t\t}\n+\t}\n+\n+\tstrbuf_strip_suffix(buf, \"-\");\n+\n+\tif (!buf->len) {\n+\t\twarning(_(\"couldn't generate a valid remote name from \"\n+\t\t\t  \"advertised url '%s', ignoring this remote\"), url);\n+\t\treturn -1;\n+\t}\n+\n+\tstrbuf_insertstr(buf, 0, \"promisor-auto-\");\n+\n+\tif (!valid_remote_name(buf->buf)) {\n+\t\twarning(_(\"generated remote name '%s' from advertised url '%s' \"\n+\t\t\t  \"is invalid, ignoring this remote\"), buf->buf, url);\n+\t\treturn -1;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static char *promisor_remote_name_from_url(const char *url)\n+{\n+\tstruct url_info url_info = { 0 };\n+\tchar *normalized = url_normalize(url, &url_info);\n+\tstruct strbuf buf = STRBUF_INIT;\n+\n+\tif (!normalized) {\n+\t\twarning(_(\"couldn't normalize advertised url '%s', \"\n+\t\t\t  \"ignoring this remote\"), url);\n+\t\treturn NULL;\n+\t}\n+\n+\tif (url_info.host_len) {\n+\t\tstrbuf_add(&buf, normalized + url_info.host_off, url_info.host_len);\n+\t\tstrbuf_addch(&buf, '-');\n+\t}\n+\n+\tif (url_info.port_len) {\n+\t\tstrbuf_add(&buf, normalized + url_info.port_off, url_info.port_len);\n+\t\tstrbuf_addch(&buf, '-');\n+\t}\n+\n+\tif (url_info.path_len) {\n+\t\tstrbuf_add(&buf, normalized + url_info.path_off, url_info.path_len);\n+\t\tstrbuf_trim_trailing_dir_sep(&buf);\n+\t\tstrbuf_strip_suffix(&buf, \".git\");\n+\t}\n+\n+\tfree(normalized);\n+\n+\tif (sanitize_remote_name(&buf, url)) {\n+\t\tstrbuf_release(&buf);\n+\t\treturn NULL;\n+\t}\n+\n+\treturn strbuf_detach(&buf, NULL);\n+}\n+\n+static void configure_auto_promisor_remote(struct repository *repo,\n+\t\t\t\t\t   const char *name,\n+\t\t\t\t\t   const char *url,\n+\t\t\t\t\t   const char *advertised_as,\n+\t\t\t\t\t   bool reuse)\n+{\n+\tchar *key;\n+\n+\tif (!reuse) {\n+\t\tfprintf(stderr, _(\"Auto-creating promisor remote '%s' for URL '%s'\\n\"),\n+\t\t\tname, url);\n+\n+\t\tkey = xstrfmt(\"remote.%s.url\", name);\n+\t\trepo_config_set_gently(repo, key, url);\n+\t\tfree(key);\n+\t}\n+\n+\t/* NB: when reusing, this promotes an existing non-promisor remote */\n+\tkey = xstrfmt(\"remote.%s.promisor\", name);\n+\trepo_config_set_gently(repo, key, \"true\");\n+\tfree(key);\n+\n+\tif (advertised_as) {\n+\t\tkey = xstrfmt(\"remote.%s.advertisedAs\", name);\n+\t\trepo_config_set_gently(repo, key, advertised_as);\n+\t\tfree(key);\n+\t}\n+}\n+\n+#define MAX_REMOTES_WITH_SIMILAR_NAMES 20\n+\n+/* Return the allocated local name, or NULL on failure */\n+static char *handle_matching_allowed_url(struct repository *repo,\n+\t\t\t\t\t char *allowed_name,\n+\t\t\t\t\t const char *remote_url,\n+\t\t\t\t\t const char *remote_name)\n+{\n+\tchar *name;\n+\tchar *basename = allowed_name ?\n+\t\txstrdup(allowed_name) :\n+\t\tpromisor_remote_name_from_url(remote_url);\n+\tint i = 0;\n+\tbool reuse = false;\n+\n+\tif (!basename)\n+\t\treturn NULL;\n+\n+\tname = xstrdup(basename);\n+\n+\twhile (i < MAX_REMOTES_WITH_SIMILAR_NAMES) {\n+\t\tchar *url_key = xstrfmt(\"remote.%s.url\", name);\n+\t\tconst char *existing_url;\n+\t\tint exists = !repo_config_get_string_tmp(repo, url_key, &existing_url);\n+\n+\t\tfree(url_key);\n+\n+\t\tif (!exists)\n+\t\t\tbreak; /* Free to use */\n+\n+\t\tif (!strcmp(existing_url, remote_url)) {\n+\t\t\treuse = true;\n+\t\t\tbreak; /* Same URL, so safe to reuse */\n+\t\t}\n+\n+\t\ti++;\n+\t\tfree(name);\n+\t\tname = xstrfmt(\"%s-%d\", basename, i);\n+\t}\n+\n+\tif (i < MAX_REMOTES_WITH_SIMILAR_NAMES) {\n+\t\tconfigure_auto_promisor_remote(repo, name,\n+\t\t\t\t\t       remote_url, remote_name,\n+\t\t\t\t\t       reuse);\n+\t} else {\n+\t\twarning(_(\"too many remotes accepted with name like '%s-X', \"\n+\t\t\t  \"ignoring this remote\"), basename);\n+\t\tFREE_AND_NULL(name);\n+\t}\n+\n+\tfree(basename);\n+\treturn name;\n+}\n+\n+static int should_accept_new_remote_url(struct repository *repo,\n+\t\t\t\t\tstruct string_list *accept_urls,\n+\t\t\t\t\tstruct promisor_info *advertised)\n+{\n+\tstruct allowed_url *allowed = url_matches_accept_list(accept_urls,\n+\t\t\t\t\t\t\t     advertised->url);\n+\tif (allowed) {\n+\t\tchar *name = handle_matching_allowed_url(repo,\n+\t\t\t\t\t\t\t allowed->remote_name,\n+\t\t\t\t\t\t\t advertised->url,\n+\t\t\t\t\t\t\t advertised->name);\n+\t\tif (name) {\n+\t\t\tfree((char *)advertised->local_name);\n+\t\t\tadvertised->local_name = name;\n+\t\t\treturn 1;\n+\t\t}\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static int should_accept_remote(struct repository *repo,\n+\t\t\t\tenum accept_promisor accept,\n \t\t\t\tstruct promisor_info *advertised,\n \t\t\t\tstruct string_list *accept_urls,\n-\t\t\t\tstruct string_list *config_info)\n+\t\t\t\tstruct string_list *config_info,\n+\t\t\t\tbool *reload_config)\n {\n \tstruct promisor_info *p;\n \tstruct string_list_item *item;\n@@ -833,6 +1020,13 @@ static int should_accept_remote(enum accept_promisor accept,\n \n \tif (!item) {\n \t\t/* We don't know about that remote */\n+\n+\t\tint res = should_accept_new_remote_url(repo, accept_urls, advertised);\n+\t\tif (res) {\n+\t\t\t*reload_config = true;\n+\t\t\treturn res;\n+\t\t}\n+\n \t\tif (accept == ACCEPT_ALL)\n \t\t\treturn all_fields_match(advertised, config_info, NULL);\n \t\treturn 0;\n@@ -1093,7 +1287,8 @@ static void filter_promisor_remote(struct repository *repo,\n \t\t\tstring_list_sort(&config_info);\n \t\t}\n \n-\t\tif (should_accept_remote(accept, advertised, &accept_urls, &config_info)) {\n+\t\tif (should_accept_remote(repo, accept, advertised, &accept_urls,\n+\t\t\t\t\t &config_info, &reload_config)) {\n \t\t\tif (!store_info)\n \t\t\t\tstore_info = store_info_new(repo);\n \t\t\tif (promisor_store_advertised_fields(advertised, store_info))\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex 0659b2ac15..549acff23f 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -458,6 +458,107 @@ test_expect_success \"clone with 'None', URL allowlisted, but client has differen\n \tinitialize_server 1 \"$oid\"\n '\n \n+test_expect_success \"clone with URL allowlisted and no remote already configured\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\ttest_when_finished \"rm -f full_names\" &&\n+\n+\tGIT_NO_LAZY_FETCH=0 git clone \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that exactly one remote has been auto-created, identified\n+\t# by \"remote.<name>.advertisedAs\" == \"lop\".\n+\tgit -C client config get --all --show-names --regexp \\\n+\t\t\"remote\\..*\\.advertisedas\" >full_names &&\n+\ttest_line_count = 1 full_names &&\n+\tREMOTE_NAME=$(sed \"s/^remote\\.\\(.*\\)\\.advertisedas .*$/\\1/\" full_names) &&\n+\n+\t# Check \".url\" and \".promisor\" values\n+\tprintf \"%s\\n\" \"$TRASH_DIRECTORY_URL/lop\" \"true\" >expect &&\n+\tgit -C client config \"remote.$REMOTE_NAME.url\" >actual &&\n+\tgit -C client config \"remote.$REMOTE_NAME.promisor\" >>actual &&\n+\ttest_cmp expect actual &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\"\n+'\n+\n+test_expect_success \"clone with named URL allowlisted and no pre-configured remote\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\tGIT_NO_LAZY_FETCH=0 git clone \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c promisor.acceptFromServerUrl=\"cdn=$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that a remote has been auto-created with the right \"cdn\" name and fields.\n+\tprintf \"%s\\n\" \"$TRASH_DIRECTORY_URL/lop\" \"true\" \"lop\" >expect &&\n+\tgit -C client config \"remote.cdn.url\" >actual &&\n+\tgit -C client config \"remote.cdn.promisor\" >>actual &&\n+\tgit -C client config \"remote.cdn.advertisedAs\" >>actual &&\n+\ttest_cmp expect actual &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\"\n+'\n+\n+test_expect_success \"clone with URL allowlisted but colliding name\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\tGIT_NO_LAZY_FETCH=0 git clone -c remote.cdn.promisor=true \\\n+\t\t-c remote.cdn.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n+\t\t-c remote.cdn.url=\"https://example.com/cdn\" \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c promisor.acceptFromServerUrl=\"cdn=$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that a remote has been auto-created with the right \"cdn-1\" name and fields.\n+\tprintf \"%s\\n\" \"$TRASH_DIRECTORY_URL/lop\" \"true\" \"lop\" >expect &&\n+\tgit -C client config \"remote.cdn-1.url\" >actual &&\n+\tgit -C client config \"remote.cdn-1.promisor\" >>actual &&\n+\tgit -C client config \"remote.cdn-1.advertisedAs\" >>actual &&\n+\ttest_cmp expect actual &&\n+\n+\t# Check that the original \"cdn\" remote was not overwritten.\n+\tprintf \"%s\\n\" \"https://example.com/cdn\" \"true\" >expect &&\n+\tgit -C client config \"remote.cdn.url\" >actual &&\n+\tgit -C client config \"remote.cdn.promisor\" >>actual &&\n+\ttest_cmp expect actual &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\"\n+'\n+\n+test_expect_success \"clone with URL allowlisted and reusable remote\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\tGIT_NO_LAZY_FETCH=0 git clone \\\n+\t\t-c remote.cdn.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n+\t\t-c remote.cdn.url=\"$TRASH_DIRECTORY_URL/lop\" \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c promisor.acceptFromServerUrl=\"cdn=$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that the existing \"cdn\" remote has been properly updated.\n+\tprintf \"%s\\n\" \"$TRASH_DIRECTORY_URL/lop\" \"true\" \"lop\" \"+refs/heads/*:refs/remotes/lop/*\" >expect &&\n+\tgit -C client config \"remote.cdn.url\" >actual &&\n+\tgit -C client config \"remote.cdn.promisor\" >>actual &&\n+\tgit -C client config \"remote.cdn.advertisedAs\" >>actual &&\n+\tgit -C client config \"remote.cdn.fetch\" >>actual &&\n+\ttest_cmp expect actual &&\n+\n+\t# Check that no new \"cdn-1\" remote has been created.\n+\ttest_must_fail git -C client config \"remote.cdn-1.url\" &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\"\n+'\n+\n test_expect_success \"clone with invalid promisor.acceptFromServerUrl\" '\n \tgit -C server config promisor.advertise true &&\n \ttest_when_finished \"rm -rf client\" &&\n@@ -472,6 +573,9 @@ test_expect_success \"clone with invalid promisor.acceptFromServerUrl\" '\n \t# Check that a warning was emitted\n \ttest_grep \"invalid remote name '\\''bad name'\\''\" err &&\n \n+\t# Check that no remote was auto-created\n+\ttest_must_fail git -C client config get --regexp \"remote\\..*\\.advertisedas\" &&\n+\n \t# Check that the largest object is not missing on the server\n \tcheck_missing_objects server 0 \"\" &&\n \n-- \n2.54.0.134.gbbe8e27878.dirty\n\n"},{"id":"543653","messageId":"20260519153808.494105-9-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260519153808.494105-1-christian.couder@gmail.com","subject":"[PATCH v3 8/8] doc: promisor: improve acceptFromServer entry","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-05-19T15:38:08Z","receivedAt":"2026-05-19T15:38:40Z","isPatch":true,"body":"The entry for the `promisor.acceptFromServer` in\n\"Documentation/config/promisor.adoc\" has a number of issues:\n\n- it's not clear if new remotes and URLs can be created,\n- it looks like a big block of text,\n- it's not easy to see all the options,\n- it's not easy to see which option is the default one,\n- for \"knownName\", it says \"advertised by the client\" instead of\n  \"advertised by the server\",\n- it doesn't refer to the new related `acceptFromServerUrl`\n  option.\n\nLet's address all these issues by rewording large parts of it\nand using bullet points for the different options.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc | 53 ++++++++++++++++++++----------\n 1 file changed, 35 insertions(+), 18 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex 11b5716294..cc728bb0b5 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -32,24 +32,41 @@ variable is set to \"true\", and the \"name\" and \"url\" fields are always\n advertised regardless of this setting.\n \n promisor.acceptFromServer::\n-\tIf set to \"all\", a client will accept all the promisor remotes\n-\ta server might advertise using the \"promisor-remote\"\n-\tcapability. If set to \"knownName\" the client will accept\n-\tpromisor remotes which are already configured on the client\n-\tand have the same name as those advertised by the client. This\n-\tis not very secure, but could be used in a corporate setup\n-\twhere servers and clients are trusted to not switch name and\n-\tURLs. If set to \"knownUrl\", the client will accept promisor\n-\tremotes which have both the same name and the same URL\n-\tconfigured on the client as the name and URL advertised by the\n-\tserver. This is more secure than \"all\" or \"knownName\", so it\n-\tshould be used if possible instead of those options. Default\n-\tis \"none\", which means no promisor remote advertised by a\n-\tserver will be accepted. By accepting a promisor remote, the\n-\tclient agrees that the server might omit objects that are\n-\tlazily fetchable from this promisor remote from its responses\n-\tto \"fetch\" and \"clone\" requests from the client. Name and URL\n-\tcomparisons are case sensitive. See linkgit:gitprotocol-v2[5].\n+\tControls which promisor remotes advertised by a server (using the\n+\t\"promisor-remote\" protocol capability) a client will accept. By\n+\taccepting a promisor remote, the client agrees that the server\n+\tmight omit objects that are lazily fetchable from this promisor\n+\tremote from its responses to \"fetch\" and \"clone\" requests.\n++\n+Note that this option does not cause new remotes to be automatically\n+created in the client's configuration. It only allows remotes which\n+are somehow already configured to be trusted for the current\n+operation, or their fields to be updated (if `promisor.storeFields` is\n+set and the remote already exists locally). To allow Git to\n+automatically create and persist new remotes from server\n+advertisements, use `promisor.acceptFromServerUrl`.\n++\n+The available options are:\n++\n+* `none` (default): No promisor remote advertised by a server will be\n+  accepted.\n++\n+* `knownUrl`: The client will accept promisor remotes that are already\n+  configured on the client and have both the same name and the same URL\n+  as advertised by the server. This is more secure than `all` or\n+  `knownName`, and should be used if possible instead of those options.\n++\n+* `knownName`: The client will accept promisor remotes that are already\n+  configured on the client and have the same name as those advertised\n+  by the server. This is not very secure, but could be used in a corporate\n+  setup where servers and clients are trusted to not switch names and URLs.\n++\n+* `all`: The client will accept all the promisor remotes a server might\n+  advertise. This is the least secure option and should only be used in\n+  fully trusted environments.\n++\n+Name and URL comparisons are case-sensitive. See linkgit:gitprotocol-v2[5]\n+for protocol details.\n \n promisor.acceptFromServerUrl::\n \tA glob pattern to specify which server-advertised URLs a\n-- \n2.54.0.134.gbbe8e27878.dirty\n\n"},{"id":"543718","messageId":"87a4tvq6pr.fsf@gitster.g","threadId":"64670","inReplyTo":"20260519153808.494105-5-christian.couder@gmail.com","subject":"Re: [PATCH v3 4/8] promisor-remote: add 'local_name' to 'struct promisor_info'","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-05-20T00:12:48Z","receivedAt":"2026-05-20T00:12:53Z","isPatch":true,"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n>  struct promisor_info {\n> -\tconst char *name;\n> +\tconst char *name;\t/* name the server advertised */\n> +\tconst char *local_name;\t/* name used locally (may be auto-generated) */\n\nOK.\n\n> @@ -449,6 +450,7 @@ struct promisor_info {\n>  static void promisor_info_free(struct promisor_info *p)\n>  {\n>  \tfree((char *)p->name);\n> +\tfree((char *)p->local_name);\n>  \tfree((char *)p->url);\n>  \tfree((char *)p->filter);\n>  \tfree((char *)p->token);\n\nHaving to cast away constness is irritating, but to the users of the\nstruct it may be safer to mark the members const so that they do not\ntouch them, perhaps.  It is not a new problem with this patch but is\ninherited from the existing code, so let's not worry too much about\nit.\n\n> +static const char *promisor_info_internal_name(struct promisor_info *p)\n> +{\n> +\treturn p->local_name ? p->local_name : p->name;\n> +}\n\nHmph.\n\n> @@ -829,7 +836,7 @@ static bool promisor_store_advertised_fields(struct promisor_info *advertised,\n>  {\n>  \tstruct promisor_info *p;\n>  \tstruct string_list_item *item;\n> -\tconst char *remote_name = advertised->name;\n> +\tconst char *remote_name = promisor_info_internal_name(advertised);\n\nIs this really a \"remote_name\", though?  As ...\n\n> @@ -937,7 +944,8 @@ static void filter_promisor_remote(struct repository *repo,\n>  \t/* Apply accepted remotes to the stable repo state */\n>  \tfor_each_string_list_item(item, accepted_remotes) {\n>  \t\tstruct promisor_info *info = item->util;\n> -\t\tstruct promisor_remote *r = repo_promisor_remote_find(repo, info->name);\n> +\t\tconst char *local = promisor_info_internal_name(info);\n\n... this name \"local\" is \"the name the thing is locally known to\nus\", promisor_info_local_name() might be a better name?  I dunno.\nI jsut found it odd that the return value of the same function is\nstored in variables named \"remote\" and \"local\" at the same time ;-)\n"},{"id":"543979","messageId":"97b9f2cd-7c82-4d4c-b574-31176074e566@app.fastmail.com","threadId":"64670","inReplyTo":"20260519153808.494105-7-christian.couder@gmail.com","subject":"Re: [PATCH v3 6/8] promisor-remote: trust known remotes matching acceptFromServerUrl","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-05-23T15:17:20Z","receivedAt":"2026-05-23T15:17:42Z","isPatch":true,"body":"On Tue, May 19, 2026, at 17:38, Christian Couder wrote:\n>[snip]\n>\n> Let's then use this helper in should_accept_remote() so that, a known\n> remote whose URL matches the allowlist is accepted.\n\nI don’t understand this comma break?\n\n>\n> To prepare for this new logic, let's also:\n>\n>[snip]\n>\n> Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n\nThe rest of the commit message looks good to me.\n\n> ---\n>  Documentation/config/promisor.adoc    |  74 +++++++++++++++++++\n>  Documentation/gitprotocol-v2.adoc     |   9 ++-\n>  promisor-remote.c                     | 102 +++++++++++++++++++++++---\n>  t/t5710-promisor-remote-capability.sh |  71 ++++++++++++++++++\n>  4 files changed, 242 insertions(+), 14 deletions(-)\n>\n> diff --git a/Documentation/config/promisor.adoc\n>[snip]\n> ++\n> +Be _VERY_ careful with these patterns: `*` matches any sequence of\n> +characters within the 'host' and 'path' parts of a URL (but cannot\n> +cross part boundaries). An overly broad pattern is a major security\n> +risk, as a matching URL allows a server to update fields (such as\n> +authentication tokens) on known remotes without further confirmation.\n> +To minimize security risks, follow these guidelines:\n> ++\n\nSo this introduces a list of precautions to take.\n\n> +1. Start with a secure protocol scheme, like `https://` or `ssh://`.\n> ++\n> +2. Only allow domain names or paths where you control and trust _ALL_\n> +   the content. Be especially careful with shared hosting platforms\n> +   like `github.com` or `gitlab.com`. A broad pattern like\n> +   `https://gitlab.com/*` is dangerous because it trusts every\n> +   repository on the entire platform. Always restrict such patterns to\n> +   your specific organization or namespace (e.g.,\n> +   `https://gitlab.com/your-org/*`).\n> ++\n> +3. Never use globs at the end of domain names. For example,\n> +   `https://cdn.your-org.com/*` might be safe, but\n> +   `https://cdn.your-org.com*/*` is a major security risk because\n> +   the latter matches `https://cdn.your-org.com.hacker.net/repo`.\n> ++\n> +4. Be careful using globs at the beginning of domain names. While the\n> +   code ensures a `*` in the host cannot cross into the path, a\n> +   pattern like `https://*.example.com/*` will still match any\n> +   subdomain. This is extremely dangerous on shared hosting platforms\n> +   (e.g., `https://*.github.io/*` trusts every user's site on the\n> +   entire platform).\n\nThe list seems to end here, because...\n\n> ++\n> +Before matching, both the advertised URL and the pattern are\n> +normalized: the scheme and host are lowercased, percent-encoded\n\nThis next paragraph seems to go back to describing how things work. But\nthis paragraph as well as all of the following ones belong to this list\nitem:\n\n      4.   Be careful using globs [...]\n\n           Before matching, [...]\n\n           The glob pattern can [...]\n\n           If a remote with the [...]\n\n           For the security implications [...]\n\n    promisor.checkFields\n    [...]\n\nI don’t know what the intent is. But using an open block will delimit\nthe ordered list.\n\n    diff --git Documentation/config/promisor.adoc Documentation/config/promisor.adoc\n    index cc728bb0b5e..f07a2e883bd 100644\n    --- Documentation/config/promisor.adoc\n    +++ Documentation/config/promisor.adoc\n    @@ -109,6 +109,7 @@ and to update fields (such as authentication tokens) on known remotes\n     without further confirmation. To minimize security risks, follow these\n     guidelines:\n     +\n    +--\n     1. Start with a secure protocol scheme, like `https://` or `ssh://`.\n     +\n     2. Only allow domain names or paths where you control and trust _ALL_\n    @@ -130,6 +131,7 @@ guidelines:\n        subdomain. This is extremely dangerous on shared hosting platforms\n        (e.g., `https://*.github.io/*` trusts every user's site on the\n        entire platform).\n    +--\n     +\n     Before matching, both the advertised URL and the pattern are\n     normalized: the scheme and host are lowercased, percent-encoded\n\n>[snip]\n"},{"id":"544156","messageId":"20260527140820.1438165-1-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260519153808.494105-1-christian.couder@gmail.com","subject":"[PATCH v4 0/8] Auto-configure advertised remotes via URL allowlist","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-05-27T14:08:12Z","receivedAt":"2026-05-27T14:08:41Z","isPatch":true,"body":"Currently, the \"promisor-remote\" protocol capability allows a server\nto advertise promisor remotes (and their tokens/filters), but the\nclient's `promisor.acceptFromServer` mechanism requires these remotes\nto already exist in the config.\n\nThis is a significant burden for users and administrators who have to\npre-configure remotes.\n\nThis patch series improves on this by introducing a new\n`promisor.acceptFromServerUrl` config option, which provides an\nadditive, URL-based security allowlist.\n\nMultiple `promisor.acceptFromServerUrl` config options can be provided\nin different config files. Each one should contain a URL glob pattern\nwhich can optionally be prefixed with a remote name in the\n\"[<name>=]<pattern>\" format.\n\nThe goal is for something like a simple:\n\n  git config set --global promisor.acceptFromServerUrl \"https://my-org.com/*\"\n\nto be all that is needed for internal work in many organizations. \n\nWith this new config option:\n\n - The server can update fields (like tokens) for known remotes,\n   provided their URL matches the allowlist, even if\n   `acceptFromServer` is set to `None`.\n\n - Unknown remotes advertised by the server can be automatically\n   configured on the client if their URL matches the allowlist.\n\n - If there is no `<name>` prefix before the glob pattern matched, the\n   auto-configured remote is named using the\n   \"promisor-auto-<sanitized-url>\" format. So the same auto-configured\n   remote config entry will be reused for the same URL.\n\n - If a `<name>` prefix is provided, it will be used for the\n   auto-configured remote config entry.\n\n - If the chosen name (auto-generated or prefixed) already exists but\n   points to a different URL, overwriting the existing config is\n   prevented by appending a numeric suffix (e.g., -1, -2) to the name\n   and auto-configuring using that name.\n\n - The server's originally advertised name is always saved in the\n   `remote.<name>.advertisedAs` config variable of the auto-configured\n   remote for tracing and debugging.\n\nSecurity considerations:\n\n - Advertised URLs and glob patterns are routed through\n   url_normalize() / url_normalize_pattern() before matching, to\n   prevent percent-encoding, case variation, or path-traversal (..)\n   bypasses.\n\n - URL matching is done component by component: scheme and port\n   must match exactly (no wildcards), the host is matched with\n   WM_PATHNAME so a '*' cannot cross the '/' boundary into the\n   path, and the path is matched without WM_PATHNAME so '*' can\n   still span multi-level paths.\n\n - Auto-generated remote names are sanitized (non-alphanumeric\n   characters are replaced with '-', runs of '-' are collapsed)\n   and prefixed with 'promisor-auto-'. User-supplied names (from\n   the 'name=<pattern>' syntax) are validated with\n   valid_remote_name(). Together, these prevent a server from\n   maliciously overwriting standard remotes (like 'origin').\n\n - If the auto-generated or user-supplied name collides with an\n   existing remote configured to a different URL, a numeric\n   suffix ('-1', '-2', ...) is appended, up to a bounded limit,\n   so a server cannot hijack an existing remote by name.\n\n - Known remotes are still subject to URL consistency checks:\n   even if an advertised URL matches the allowlist, it is only\n   accepted for a known remote if it matches the URL already\n   configured locally for that remote.\n\n - The documentation explains in detail how to write secure glob\n   patterns in `promisor.acceptFromServerUrl`, and highlights the\n   risks of overly broad patterns on shared hosting platforms.\n\nHigh level description of the patches\n=====================================\n\n - Patch 1/8 is a very small preparatory patch that simplifies some\n   tests a bit.\n\n - Patches 2/8 and 3/8 expose and adapt a url_normalize_pattern()\n   helper function in the urlmatch API.\n\n - Patch 4/8 adapts `struct promisor_info` by adding a new\n   `local_name` member to it to prepare for the next patches.\n\n - Patches 5/8 to 7/8 implement the core feature. They introduce the\n   parsing machinery, add the additive allowlist for known remotes\n   (with url_normalize() security), and finally implement the\n   auto-creation and collision resolution for unknown remotes.\n\n - Patch 8/8 cleans up and modernizes the existing\n   `promisor.acceptFromServer` documentation.\n\nChanges compared to v3\n======================\n\nThanks to Toon, Kristoffer, Patrick and Junio for reviewing the\nprevious versions of this series and of the preparatory series.\n\nThis has been rebased onto master @ 56a4f3c3a2 (The 8th batch,\n2026-05-25) to avoid a trivial conflict in \"urlmatch.c\".\n\nOnly minor changes have been made since v3, in the following patches:\n\n - Patch 4/8 (\"promisor-remote: add 'local_name' to 'struct\n   promisor_info'\"):\n\n   - The promisor_info_internal_name() function has been renamed\n     promisor_info_local_name() for clarity.\n\n   - A `const char *local` local variable has been renamed\n     `remote_name` for consistency with another similar variable.\n\n - Patch 6/8 (\"promisor-remote: trust known remotes matching\n   acceptFromServerUrl\"):\n\n   - A spurious comma in the commit message has been deleted.\n\n   - The `promisor.acceptFromServerUrl` documentation in\n     \"Documentation/config/promisor.adoc\" now uses `--` to separate a\n     numbered list from the surrounding paragraphs.\n\nCI tests\n========\n\nThey all pass, see:\n\nhttps://github.com/chriscool/git/actions/runs/26514308470\n\nRange diff since v3\n===================\n\n1:  ab231c0896 = 1:  9fcc7d9d5e t5710: simplify 'mkdir X' followed by 'git -C X init'\n2:  b3e66f329f ! 2:  ec558c2b9c urlmatch: change 'allow_globs' arg to bool\n    @@ urlmatch.c: static char *url_normalize_1(const char *url, struct url_info *out_i\n     +\treturn url_normalize_1(url, out_info, false);\n      }\n      \n    - static size_t url_match_prefix(const char *url,\n    + char *url_parse(const char *url_orig, struct url_info *out_info)\n     @@ urlmatch.c: int urlmatch_config_entry(const char *var, const char *value,\n      \t\tstruct url_info norm_info;\n      \n3:  813d748dd6 ! 3:  79ee353449 urlmatch: add url_normalize_pattern() helper\n    @@ urlmatch.c: char *url_normalize(const char *url, struct url_info *out_info)\n     +\treturn url_normalize_1(url, out_info, true);\n     +}\n     +\n    - static size_t url_match_prefix(const char *url,\n    - \t\t\t       const char *url_prefix,\n    - \t\t\t       size_t url_prefix_len)\n    + char *url_parse(const char *url_orig, struct url_info *out_info)\n    + {\n    + \tstruct strbuf url;\n     \n      ## urlmatch.h ##\n     @@ urlmatch.h: struct url_info {\n    - \n      char *url_normalize(const char *, struct url_info *);\n    + char *url_parse(const char *, struct url_info *);\n      \n     +/*\n     + * Like url_normalize(), but also allows '*' glob characters in the host\n4:  e92863bee8 ! 4:  037fd46ac7 promisor-remote: add 'local_name' to 'struct promisor_info'\n    @@ Commit message\n         To prepare for this change, let's add a new 'char *local_name' member\n         to 'struct promisor_info', and let's update the related functions.\n     \n    -    While at it, let's also add a small promisor_info_internal_name()\n    -    helper that returns `local_name` when set, `name` otherwise, and let's\n    -    use this small helper in promisor_store_advertised_fields() and in the\n    +    While at it, let's also add a small promisor_info_local_name() helper\n    +    that returns `local_name` when set, `name` otherwise, and let's use\n    +    this small helper in promisor_store_advertised_fields() and in the\n         post-loop of filter_promisor_remote() so that lookups against the local\n         repo configuration use the right name.\n     \n    @@ promisor-remote.c: static void promisor_info_list_clear(struct string_list *list\n      \tstring_list_clear(list, 0);\n      }\n      \n    -+static const char *promisor_info_internal_name(struct promisor_info *p)\n    ++static const char *promisor_info_local_name(struct promisor_info *p)\n     +{\n     +\treturn p->local_name ? p->local_name : p->name;\n     +}\n    @@ promisor-remote.c: static bool promisor_store_advertised_fields(struct promisor_\n      \tstruct promisor_info *p;\n      \tstruct string_list_item *item;\n     -\tconst char *remote_name = advertised->name;\n    -+\tconst char *remote_name = promisor_info_internal_name(advertised);\n    ++\tconst char *remote_name = promisor_info_local_name(advertised);\n      \tbool reload_config = false;\n      \n      \tif (!(store_info->store_filter || store_info->store_token))\n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n      \tfor_each_string_list_item(item, accepted_remotes) {\n      \t\tstruct promisor_info *info = item->util;\n     -\t\tstruct promisor_remote *r = repo_promisor_remote_find(repo, info->name);\n    -+\t\tconst char *local = promisor_info_internal_name(info);\n    -+\t\tstruct promisor_remote *r = repo_promisor_remote_find(repo, local);\n    ++\t\tconst char *remote_name = promisor_info_local_name(info);\n    ++\t\tstruct promisor_remote *r = repo_promisor_remote_find(repo, remote_name);\n      \n      \t\tif (r) {\n      \t\t\tr->accepted = 1;\n5:  7e1b106404 = 5:  532adb7ca9 promisor-remote: introduce promisor.acceptFromServerUrl\n6:  f00eed4bf2 ! 6:  b970f5647c promisor-remote: trust known remotes matching acceptFromServerUrl\n    @@ Commit message\n         the 'token' field of the capability is preferred over embedding\n         credentials in the URL.\n     \n    -    Let's then use this helper in should_accept_remote() so that, a known\n    +    Let's then use this helper in should_accept_remote() so that a known\n         remote whose URL matches the allowlist is accepted.\n     \n         To prepare for this new logic, let's also:\n    @@ Documentation/config/promisor.adoc: promisor.acceptFromServer::\n     +authentication tokens) on known remotes without further confirmation.\n     +To minimize security risks, follow these guidelines:\n     ++\n    ++--\n     +1. Start with a secure protocol scheme, like `https://` or `ssh://`.\n     ++\n     +2. Only allow domain names or paths where you control and trust _ALL_\n    @@ Documentation/config/promisor.adoc: promisor.acceptFromServer::\n     +   subdomain. This is extremely dangerous on shared hosting platforms\n     +   (e.g., `https://*.github.io/*` trusts every user's site on the\n     +   entire platform).\n    ++--\n     ++\n     +Before matching, both the advertised URL and the pattern are\n     +normalized: the scheme and host are lowercased, percent-encoded\n7:  af06fb31db ! 7:  1875228a7b promisor-remote: auto-configure unknown remotes\n    @@ Documentation/config/promisor.adoc: documentation of that option.)\n     +without further confirmation. To minimize security risks, follow these\n     +guidelines:\n      +\n    + --\n      1. Start with a secure protocol scheme, like `https://` or `ssh://`.\n    - +\n     @@ Documentation/config/promisor.adoc: ignored during matching. Note that embedding credentials in URLs is\n      discouraged. Passing authentication tokens via the `token` field of\n      the `promisor-remote` capability is strongly preferred.\n8:  92075d88d8 = 8:  351ece0b90 doc: promisor: improve acceptFromServer entry\n\n\nChristian Couder (8):\n  t5710: simplify 'mkdir X' followed by 'git -C X init'\n  urlmatch: change 'allow_globs' arg to bool\n  urlmatch: add url_normalize_pattern() helper\n  promisor-remote: add 'local_name' to 'struct promisor_info'\n  promisor-remote: introduce promisor.acceptFromServerUrl\n  promisor-remote: trust known remotes matching acceptFromServerUrl\n  promisor-remote: auto-configure unknown remotes\n  doc: promisor: improve acceptFromServer entry\n\n Documentation/config/promisor.adoc    | 148 +++++++--\n Documentation/config/remote.adoc      |   9 +\n Documentation/gitprotocol-v2.adoc     |   9 +-\n promisor-remote.c                     | 413 ++++++++++++++++++++++++--\n t/t5710-promisor-remote-capability.sh | 202 ++++++++++++-\n urlmatch.c                            |  11 +-\n urlmatch.h                            |  12 +\n 7 files changed, 756 insertions(+), 48 deletions(-)\n\n-- \n2.54.0.275.g96c817d129.dirty\n\n"},{"id":"544157","messageId":"20260527140820.1438165-2-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260527140820.1438165-1-christian.couder@gmail.com","subject":"[PATCH v4 1/8] t5710: simplify 'mkdir X' followed by 'git -C X init'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-05-27T14:08:13Z","receivedAt":"2026-05-27T14:08:43Z","isPatch":true,"body":"It's simpler and more efficient to just use `git init client` instead\nof `mkdir client && git -C client init`.\n\nSo let's replace the latter with the former.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n t/t5710-promisor-remote-capability.sh | 6 ++----\n 1 file changed, 2 insertions(+), 4 deletions(-)\n\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex b404ad9f0a..bf1cc54605 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -177,8 +177,7 @@ test_expect_success \"init + fetch with promisor.advertise set to 'true'\" '\n \tgit -C server config promisor.advertise true &&\n \ttest_when_finished \"rm -rf client\" &&\n \n-\tmkdir client &&\n-\tgit -C client init &&\n+\tgit init client &&\n \tgit -C client config remote.lop.promisor true &&\n \tgit -C client config remote.lop.fetch \"+refs/heads/*:refs/remotes/lop/*\" &&\n \tgit -C client config remote.lop.url \"$TRASH_DIRECTORY_URL/lop\" &&\n@@ -231,8 +230,7 @@ test_expect_success \"init + fetch two promisors but only one advertised\" '\n \t# Create a promisor that will be configured but not be used\n \tgit init --bare unused_lop &&\n \n-\tmkdir client &&\n-\tgit -C client init &&\n+\tgit init client &&\n \tgit -C client config remote.unused_lop.promisor true &&\n \tgit -C client config remote.unused_lop.fetch \"+refs/heads/*:refs/remotes/unused_lop/*\" &&\n \tgit -C client config remote.unused_lop.url \"$TRASH_DIRECTORY_URL/unused_lop\" &&\n-- \n2.54.0.275.g96c817d129.dirty\n\n"},{"id":"544158","messageId":"20260527140820.1438165-3-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260527140820.1438165-1-christian.couder@gmail.com","subject":"[PATCH v4 2/8] urlmatch: change 'allow_globs' arg to bool","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-05-27T14:08:14Z","receivedAt":"2026-05-27T14:08:44Z","isPatch":true,"body":"The last argument of url_normalize_1() is `char allow_globs` but it is\nused as a boolean, not as a char.\n\nLet's convert it to a `bool`, and while at it convert the two calls to\nurl_normalize_1() so they pass 'true' or 'false' instead of '1' or '0'.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n urlmatch.c | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/urlmatch.c b/urlmatch.c\nindex bf8cce6de9..b2d88a5289 100644\n--- a/urlmatch.c\n+++ b/urlmatch.c\n@@ -112,7 +112,7 @@ static int match_host(const struct url_info *url_info,\n \treturn (!url_len && !pat_len);\n }\n \n-static char *url_normalize_1(const char *url, struct url_info *out_info, char allow_globs)\n+static char *url_normalize_1(const char *url, struct url_info *out_info, bool allow_globs)\n {\n \t/*\n \t * Normalize NUL-terminated url using the following rules:\n@@ -438,7 +438,7 @@ static char *url_normalize_1(const char *url, struct url_info *out_info, char al\n \n char *url_normalize(const char *url, struct url_info *out_info)\n {\n-\treturn url_normalize_1(url, out_info, 0);\n+\treturn url_normalize_1(url, out_info, false);\n }\n \n char *url_parse(const char *url_orig, struct url_info *out_info)\n@@ -704,7 +704,7 @@ int urlmatch_config_entry(const char *var, const char *value,\n \t\tstruct url_info norm_info;\n \n \t\tconfig_url = xmemdupz(key, dot - key);\n-\t\tnorm_url = url_normalize_1(config_url, &norm_info, 1);\n+\t\tnorm_url = url_normalize_1(config_url, &norm_info, true);\n \t\tif (norm_url)\n \t\t\tretval = match_urls(url, &norm_info, &matched);\n \t\telse if (collect->fallback_match_fn)\n-- \n2.54.0.275.g96c817d129.dirty\n\n"},{"id":"544159","messageId":"20260527140820.1438165-4-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260527140820.1438165-1-christian.couder@gmail.com","subject":"[PATCH v4 3/8] urlmatch: add url_normalize_pattern() helper","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-05-27T14:08:15Z","receivedAt":"2026-05-27T14:08:46Z","isPatch":true,"body":"In a following commit, we will need to normalize a URL glob pattern\n(which may contain '*' in the host portion) and extract its component\noffsets (host, path, etc.) for separate matching. Let's export a\ndedicated helper function url_normalize_pattern() for that purpose.\n\nIt works like url_normalize(), but passes allow_globs=true to the\ninternal url_normalize_1(), so that '*' characters in the host are\naccepted rather than rejected.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n urlmatch.c |  5 +++++\n urlmatch.h | 12 ++++++++++++\n 2 files changed, 17 insertions(+)\n\ndiff --git a/urlmatch.c b/urlmatch.c\nindex b2d88a5289..20bc2d009c 100644\n--- a/urlmatch.c\n+++ b/urlmatch.c\n@@ -441,6 +441,11 @@ char *url_normalize(const char *url, struct url_info *out_info)\n \treturn url_normalize_1(url, out_info, false);\n }\n \n+char *url_normalize_pattern(const char *url, struct url_info *out_info)\n+{\n+\treturn url_normalize_1(url, out_info, true);\n+}\n+\n char *url_parse(const char *url_orig, struct url_info *out_info)\n {\n \tstruct strbuf url;\ndiff --git a/urlmatch.h b/urlmatch.h\nindex 6b3ce42858..db1a335e72 100644\n--- a/urlmatch.h\n+++ b/urlmatch.h\n@@ -37,6 +37,18 @@ struct url_info {\n char *url_normalize(const char *, struct url_info *);\n char *url_parse(const char *, struct url_info *);\n \n+/*\n+ * Like url_normalize(), but also allows '*' glob characters in the host\n+ * portion. Use this when normalizing URL patterns from user configuration.\n+ *\n+ * Note that '*' is a valid path character per RFC 3986 (as a sub-delim),\n+ * so glob patterns using '*' in the path are also accepted.\n+ *\n+ * Returns a newly allocated normalized string and fills out_info if\n+ * non-NULL, or NULL if the pattern is invalid.\n+ */\n+char *url_normalize_pattern(const char *url, struct url_info *out_info);\n+\n struct urlmatch_item {\n \tsize_t hostmatch_len;\n \tsize_t pathmatch_len;\n-- \n2.54.0.275.g96c817d129.dirty\n\n"},{"id":"544160","messageId":"20260527140820.1438165-5-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260527140820.1438165-1-christian.couder@gmail.com","subject":"[PATCH v4 4/8] promisor-remote: add 'local_name' to 'struct promisor_info'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-05-27T14:08:16Z","receivedAt":"2026-05-27T14:08:47Z","isPatch":true,"body":"In a following commit, we will store promisor remote information under\na remote name different than the one the server advertised.\n\nTo prepare for this change, let's add a new 'char *local_name' member\nto 'struct promisor_info', and let's update the related functions.\n\nWhile at it, let's also add a small promisor_info_local_name() helper\nthat returns `local_name` when set, `name` otherwise, and let's use\nthis small helper in promisor_store_advertised_fields() and in the\npost-loop of filter_promisor_remote() so that lookups against the local\nrepo configuration use the right name.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 22 +++++++++++++++-------\n 1 file changed, 15 insertions(+), 7 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 38fa050542..138a412893 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -434,13 +434,14 @@ static struct string_list *fields_stored(void)\n  * Struct for promisor remotes involved in the \"promisor-remote\"\n  * protocol capability.\n  *\n- * Except for \"name\", each <member> in this struct and its <value>\n- * should correspond (either on the client side or on the server side)\n- * to a \"remote.<name>.<member>\" config variable set to <value> where\n- * \"<name>\" is a promisor remote name.\n+ * Except for \"name\" and \"local_name\", each <member> in this struct\n+ * and its <value> should correspond (either on the client side or on\n+ * the server side) to a \"remote.<name>.<member>\" config variable set\n+ * to <value> where \"<name>\" is a promisor remote name.\n  */\n struct promisor_info {\n-\tconst char *name;\n+\tconst char *name;\t/* name the server advertised */\n+\tconst char *local_name;\t/* name used locally (may be auto-generated) */\n \tconst char *url;\n \tconst char *filter;\n \tconst char *token;\n@@ -449,6 +450,7 @@ struct promisor_info {\n static void promisor_info_free(struct promisor_info *p)\n {\n \tfree((char *)p->name);\n+\tfree((char *)p->local_name);\n \tfree((char *)p->url);\n \tfree((char *)p->filter);\n \tfree((char *)p->token);\n@@ -462,6 +464,11 @@ static void promisor_info_list_clear(struct string_list *list)\n \tstring_list_clear(list, 0);\n }\n \n+static const char *promisor_info_local_name(struct promisor_info *p)\n+{\n+\treturn p->local_name ? p->local_name : p->name;\n+}\n+\n static void set_one_field(struct promisor_info *p,\n \t\t\t  const char *field, const char *value)\n {\n@@ -829,7 +836,7 @@ static bool promisor_store_advertised_fields(struct promisor_info *advertised,\n {\n \tstruct promisor_info *p;\n \tstruct string_list_item *item;\n-\tconst char *remote_name = advertised->name;\n+\tconst char *remote_name = promisor_info_local_name(advertised);\n \tbool reload_config = false;\n \n \tif (!(store_info->store_filter || store_info->store_token))\n@@ -937,7 +944,8 @@ static void filter_promisor_remote(struct repository *repo,\n \t/* Apply accepted remotes to the stable repo state */\n \tfor_each_string_list_item(item, accepted_remotes) {\n \t\tstruct promisor_info *info = item->util;\n-\t\tstruct promisor_remote *r = repo_promisor_remote_find(repo, info->name);\n+\t\tconst char *remote_name = promisor_info_local_name(info);\n+\t\tstruct promisor_remote *r = repo_promisor_remote_find(repo, remote_name);\n \n \t\tif (r) {\n \t\t\tr->accepted = 1;\n-- \n2.54.0.275.g96c817d129.dirty\n\n"},{"id":"544161","messageId":"20260527140820.1438165-6-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260527140820.1438165-1-christian.couder@gmail.com","subject":"[PATCH v4 5/8] promisor-remote: introduce promisor.acceptFromServerUrl","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-05-27T14:08:17Z","receivedAt":"2026-05-27T14:08:49Z","isPatch":true,"body":"The \"promisor-remote\" protocol capability allows servers to advertise\npromisor remotes, but doesn't allow these remotes to be automatically\nconfigured on the client.\n\nLet's introduce a new `promisor.acceptFromServerUrl` config variable\nwhich contains a glob pattern, so that advertised remotes with a URL\nmatching that pattern will be automatically configured.\n\nThe glob pattern can optionally be prefixed with a remote name which\nwill be used as the name of the new local remote.\n\nFor now though, let's only introduce the functions to read and validate\nthe glob patterns and the optional prefixes.\n\nChecking if the URLs of the advertised remotes match the glob patterns\nand taking the appropriate action is left for a following commit.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c                     | 90 +++++++++++++++++++++++++++\n t/t5710-promisor-remote-capability.sh | 21 +++++++\n 2 files changed, 111 insertions(+)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 138a412893..8d4f6e0a72 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -12,6 +12,7 @@\n #include \"packfile.h\"\n #include \"environment.h\"\n #include \"url.h\"\n+#include \"urlmatch.h\"\n #include \"version.h\"\n \n struct promisor_remote_config {\n@@ -657,6 +658,90 @@ static bool has_control_char(const char *s)\n \treturn false;\n }\n \n+struct allowed_url {\n+\tchar *remote_name;\n+\tchar *url_pattern;\n+\tstruct url_info pattern_info;\n+};\n+\n+static void allowed_url_free(void *util, const char *str UNUSED)\n+{\n+\tstruct allowed_url *allowed = util;\n+\n+\tif (!allowed)\n+\t\treturn;\n+\n+\t/* Depending on prefix, free either remote_name or url_pattern */\n+\tfree(allowed->remote_name ? allowed->remote_name : allowed->url_pattern);\n+\tfree(allowed->pattern_info.url);\n+\tfree(allowed);\n+}\n+\n+static struct allowed_url *valid_accept_url(const char *url)\n+{\n+\tchar *dup, *p;\n+\tstruct allowed_url *allowed;\n+\n+\tif (!url)\n+\t\treturn NULL;\n+\n+\tdup = xstrdup(url);\n+\tp = strchr(dup, '=');\n+\tif (p) {\n+\t\t*p = '\\0';\n+\t\tif (!valid_remote_name(dup)) {\n+\t\t\twarning(_(\"invalid remote name '%s' before '=' sign \"\n+\t\t\t\t  \"in '%s' from promisor.acceptFromServerUrl config\"),\n+\t\t\t\tdup, url);\n+\t\t\tfree(dup);\n+\t\t\treturn NULL;\n+\t\t}\n+\t\tp++;\n+\t} else {\n+\t\tp = dup;\n+\t}\n+\n+\tif (has_control_char(p)) {\n+\t\twarning(_(\"invalid url pattern '%s' \"\n+\t\t\t  \"in '%s' from promisor.acceptFromServerUrl config\"), p, url);\n+\t\tfree(dup);\n+\t\treturn NULL;\n+\t}\n+\n+\tallowed = xmalloc(sizeof(*allowed));\n+\tallowed->remote_name = (p == dup) ? NULL : dup;\n+\tallowed->url_pattern = p;\n+\tallowed->pattern_info.url = url_normalize_pattern(p, &allowed->pattern_info);\n+\tif (!allowed->pattern_info.url) {\n+\t\twarning(_(\"invalid url pattern '%s' \"\n+\t\t\t  \"in '%s' from promisor.acceptFromServerUrl config\"), p, url);\n+\t\tfree(dup);\n+\t\tfree(allowed);\n+\t\treturn NULL;\n+\t}\n+\n+\treturn allowed;\n+}\n+\n+static void load_accept_from_server_url(struct repository *repo,\n+\t\t\t\t\tstruct string_list *accept_urls)\n+{\n+\tconst struct string_list *config_urls;\n+\n+\tif (!repo_config_get_string_multi(repo, \"promisor.acceptfromserverurl\", &config_urls)) {\n+\t\tstruct string_list_item *item;\n+\n+\t\tfor_each_string_list_item(item, config_urls) {\n+\t\t\tstruct allowed_url *allowed = valid_accept_url(item->string);\n+\t\t\tif (allowed) {\n+\t\t\t\tstruct string_list_item *new;\n+\t\t\t\tnew = string_list_append(accept_urls, item->string);\n+\t\t\t\tnew->util = allowed;\n+\t\t\t}\n+\t\t}\n+\t}\n+}\n+\n static int should_accept_remote(enum accept_promisor accept,\n \t\t\t\tstruct promisor_info *advertised,\n \t\t\t\tstruct string_list *config_info)\n@@ -901,6 +986,10 @@ static void filter_promisor_remote(struct repository *repo,\n \tstruct string_list_item *item;\n \tbool reload_config = false;\n \tenum accept_promisor accept = accept_from_server(repo);\n+\tstruct string_list accept_urls = STRING_LIST_INIT_DUP;\n+\n+\t/* Load and validate the acceptFromServerUrl config */\n+\tload_accept_from_server_url(repo, &accept_urls);\n \n \tif (accept == ACCEPT_NONE)\n \t\treturn;\n@@ -934,6 +1023,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\t}\n \t}\n \n+\tstring_list_clear_func(&accept_urls, allowed_url_free);\n \tpromisor_info_list_clear(&config_info);\n \tstring_list_clear(&remote_info, 0);\n \tstore_info_free(store_info);\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex bf1cc54605..3b39505380 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -387,6 +387,27 @@ test_expect_success \"clone with 'KnownUrl' and empty url, so not advertised\" '\n \tcheck_missing_objects server 1 \"$oid\"\n '\n \n+test_expect_success \"clone with invalid promisor.acceptFromServerUrl\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\t# As \"bad name\" contains a space, which is not a valid remote name,\n+\t# the pattern should be rejected with a warning and no remote created.\n+\tGIT_NO_LAZY_FETCH=0 git clone \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c \"promisor.acceptFromServerUrl=bad name=https://example.com/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client 2>err &&\n+\n+\t# Check that a warning was emitted\n+\ttest_grep \"invalid remote name '\\''bad name'\\''\" err &&\n+\n+\t# Check that the largest object is not missing on the server\n+\tcheck_missing_objects server 0 \"\" &&\n+\n+\t# Reinitialize server so that the largest object is missing again\n+\tinitialize_server 1 \"$oid\"\n+'\n+\n test_expect_success \"clone with promisor.sendFields\" '\n \tgit -C server config promisor.advertise true &&\n \ttest_when_finished \"rm -rf client\" &&\n-- \n2.54.0.275.g96c817d129.dirty\n\n"},{"id":"544162","messageId":"20260527140820.1438165-7-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260527140820.1438165-1-christian.couder@gmail.com","subject":"[PATCH v4 6/8] promisor-remote: trust known remotes matching acceptFromServerUrl","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-05-27T14:08:18Z","receivedAt":"2026-05-27T14:08:50Z","isPatch":true,"body":"A previous commit introduced the `promisor.acceptFromServerUrl` config\nvariable along with the machinery to parse and validate the URL glob\npatterns and optional remote name prefixes it contains. However, these\nURL patterns are not yet tied into the client's acceptance logic.\n\nWhen a promisor remote is already configured locally, its fields (like\nauthentication tokens) may occasionally need to be refreshed by the\nserver. If `promisor.acceptFromServer` is set to the secure default\n(\"None\"), these updates are rejected, potentially causing future\nfetches to fail.\n\nTo enable such targeted updates for trusted URLs, let's use the URL\npatterns from `promisor.acceptFromServerUrl` as an additional URL\nbased allowlist.\n\nConcretely, let's check the advertised URLs against the URL glob\npatterns by introducing a new small helper function called\nurl_matches_accept_list(), which iterates over the glob patterns and\nreturns the first matching allowed_url entry (or NULL).\n\nThe URL matching is done component by component: scheme and port are\ncompared exactly, the host and path are matched with wildmatch().\nBefore matching, the advertised URL is passed through url_normalize()\nso that case variations in the scheme/host, percent-encoding tricks,\nand \"..\" path segments cannot bypass the allowlist.\n\nThe username and password components of the URL are intentionally\nignored during matching to allow servers to rotate them, though using\nthe 'token' field of the capability is preferred over embedding\ncredentials in the URL.\n\nLet's then use this helper in should_accept_remote() so that a known\nremote whose URL matches the allowlist is accepted.\n\nTo prepare for this new logic, let's also:\n\n - Add an 'accept_urls' parameter to should_accept_remote().\n\n - Replace the BUG() guard in the ACCEPT_KNOWN_URL case with an\n   explicit 'if (accept == ACCEPT_KNOWN_URL) return' and a new\n   BUG() guard in the ACCEPT_NONE case.\n\n - Call accept_from_server_url() from filter_promisor_remote()\n   and relax its early return so that the function is entered when\n   `accept_urls` has entries even if `accept == ACCEPT_NONE`.\n\nWith this, many organizations may only need something like:\n\n  git config set --global \\\n          promisor.acceptFromServerUrl \"https://my-org.com/*\"\n\nto accept only their own remotes. And if they need to accept additional\nremotes in some specific repos, they can also set:\n\n  git config set promisor.acceptFromServer knownUrl\n\nand configure the additional remote manually only in the repos where\nthey are needed.\n\nLet's then properly document `promisor.acceptFromServerUrl` in\n\"promisor.adoc\" as an additive security allowlist for known remotes,\nincluding the URL normalization behavior and the component-wise\nmatching, and let's mention it in \"gitprotocol-v2.adoc\".\n\nAlso let's clarify in the documentation how\n`promisor.acceptFromServerUrl` interacts with\n`promisor.acceptFromServer`:\n\n - Precedence: when both options are set,\n   `promisor.acceptFromServerUrl` is consulted first. If a matching\n   pattern leads to acceptance, the remote is accepted regardless of\n   `promisor.acceptFromServer`. Otherwise the decision is left to\n   `promisor.acceptFromServer`.\n\n - URL-mismatch guard: even when the advertised URL matches the\n   allowlist, an already-existing client-side remote whose configured\n   URL differs from the advertised one is not accepted through\n   `promisor.acceptFromServerUrl`. `promisor.acceptFromServer=all` and\n   `=knownName` keep their pre-existing, looser semantics.\n\nThe precedence paragraph is intentionally scoped here to known remotes\nonly (field updates). A following commit that introduces auto-creation\nof unknown remotes will extend it to cover that case as well.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    |  76 +++++++++++++++++++\n Documentation/gitprotocol-v2.adoc     |   9 ++-\n promisor-remote.c                     | 102 +++++++++++++++++++++++---\n t/t5710-promisor-remote-capability.sh |  71 ++++++++++++++++++\n 4 files changed, 244 insertions(+), 14 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex b0fa43b839..605473c82f 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -51,6 +51,82 @@ promisor.acceptFromServer::\n \tto \"fetch\" and \"clone\" requests from the client. Name and URL\n \tcomparisons are case sensitive. See linkgit:gitprotocol-v2[5].\n \n+promisor.acceptFromServerUrl::\n+\tA glob pattern to specify which server-advertised URLs a\n+\tclient is allowed to act on. When a URL matches, the client\n+\twill accept the advertised remote as a promisor remote and may\n+\tautomatically accept field updates (such as authentication\n+\ttokens) from the server, even if `promisor.acceptFromServer`\n+\tis set to `none` (the default).\n++\n+This option can appear multiple times in config files. An advertised\n+URL will be accepted if it matches _ANY_ glob pattern specified by\n+this option in _ANY_ config file read by Git.\n++\n+When both `promisor.acceptFromServer` and `promisor.acceptFromServerUrl`\n+are set, `promisor.acceptFromServerUrl` is consulted first and takes\n+precedence: if a matching pattern leads to acceptance (by accepting\n+field updates for a known remote whose URL matches both the local\n+configuration and the allowlist), the advertised remote is accepted\n+regardless of the `promisor.acceptFromServer` setting. If no pattern\n+in `promisor.acceptFromServerUrl` triggers acceptance, the decision\n+is left to `promisor.acceptFromServer`.\n++\n+Note however that, even when an advertised URL matches a pattern in\n+`promisor.acceptFromServerUrl`, an already-existing remote on the\n+client whose name matches the advertised name but whose configured URL\n+differs from the advertised one will _NOT_ be accepted through\n+`promisor.acceptFromServerUrl`. This prevents a server from silently\n+re-pointing an existing client-side remote at a different URL. (Such a\n+remote may still be accepted through `promisor.acceptFromServer=all`\n+or `=knownName`, which have their own, looser semantics; see the\n+documentation of that option.)\n++\n+Be _VERY_ careful with these patterns: `*` matches any sequence of\n+characters within the 'host' and 'path' parts of a URL (but cannot\n+cross part boundaries). An overly broad pattern is a major security\n+risk, as a matching URL allows a server to update fields (such as\n+authentication tokens) on known remotes without further confirmation.\n+To minimize security risks, follow these guidelines:\n++\n+--\n+1. Start with a secure protocol scheme, like `https://` or `ssh://`.\n++\n+2. Only allow domain names or paths where you control and trust _ALL_\n+   the content. Be especially careful with shared hosting platforms\n+   like `github.com` or `gitlab.com`. A broad pattern like\n+   `https://gitlab.com/*` is dangerous because it trusts every\n+   repository on the entire platform. Always restrict such patterns to\n+   your specific organization or namespace (e.g.,\n+   `https://gitlab.com/your-org/*`).\n++\n+3. Never use globs at the end of domain names. For example,\n+   `https://cdn.your-org.com/*` might be safe, but\n+   `https://cdn.your-org.com*/*` is a major security risk because\n+   the latter matches `https://cdn.your-org.com.hacker.net/repo`.\n++\n+4. Be careful using globs at the beginning of domain names. While the\n+   code ensures a `*` in the host cannot cross into the path, a\n+   pattern like `https://*.example.com/*` will still match any\n+   subdomain. This is extremely dangerous on shared hosting platforms\n+   (e.g., `https://*.github.io/*` trusts every user's site on the\n+   entire platform).\n+--\n++\n+Before matching, both the advertised URL and the pattern are\n+normalized: the scheme and host are lowercased, percent-encoded\n+characters are decoded where possible, and path segments like `..`\n+are resolved. The port must also match exactly (e.g.,\n+`https://example.com:8080/*` will not match a URL advertised on\n+port 9999). The username and password components of the URL are\n+ignored during matching. Note that embedding credentials in URLs is\n+discouraged. Passing authentication tokens via the `token` field of\n+the `promisor-remote` capability is strongly preferred.\n++\n+For the security implications of accepting a promisor remote, see the\n+documentation of `promisor.acceptFromServer`. For details on the\n+protocol, see linkgit:gitprotocol-v2[5].\n+\n promisor.checkFields::\n \tA comma or space separated list of additional remote related\n \tfield names. A client checks if the values of these fields\ndiff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\nindex befa697d21..2beb70595f 100644\n--- a/Documentation/gitprotocol-v2.adoc\n+++ b/Documentation/gitprotocol-v2.adoc\n@@ -866,10 +866,11 @@ the server advertised, the client shouldn't advertise the\n \n On the server side, the \"promisor.advertise\" and \"promisor.sendFields\"\n configuration options can be used to control what it advertises. On\n-the client side, the \"promisor.acceptFromServer\" configuration option\n-can be used to control what it accepts, and the \"promisor.storeFields\"\n-option, to control what it stores. See the documentation of these\n-configuration options in linkgit:git-config[1] for more information.\n+the client side, the \"promisor.acceptFromServer\" and\n+\"promisor.acceptFromServerUrl\" configuration options can be used to\n+control what it accepts, and the \"promisor.storeFields\" option, to\n+control what it stores. See the documentation of these configuration\n+options in linkgit:git-config[1] for more information.\n \n Note that in the future it would be nice if the \"promisor-remote\"\n protocol capability could be used by the server, when responding to\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 8d4f6e0a72..04a5bb9939 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -14,6 +14,7 @@\n #include \"url.h\"\n #include \"urlmatch.h\"\n #include \"version.h\"\n+#include \"wildmatch.h\"\n \n struct promisor_remote_config {\n \tstruct promisor_remote *promisors;\n@@ -742,8 +743,79 @@ static void load_accept_from_server_url(struct repository *repo,\n \t}\n }\n \n+static bool match_pattern_url(const char *pat, size_t pat_len,\n+\t\t\t      const char *url, size_t url_len)\n+{\n+\tchar *p_str = xstrndup(pat, pat_len);\n+\tchar *u_str = xstrndup(url, url_len);\n+\tbool res = !wildmatch(p_str, u_str, 0);\n+\n+\tfree(p_str);\n+\tfree(u_str);\n+\n+\treturn res;\n+}\n+\n+static bool match_one_url(const struct url_info *pi, const struct url_info *ui)\n+{\n+\tconst char *pat = pi->url;\n+\tconst char *url = ui->url;\n+\n+\t/*\n+\t * Schemes must match exactly. They are case-folded by\n+\t * url_normalize(), so strncmp() suffices.\n+\t */\n+\tif (pi->scheme_len != ui->scheme_len || strncmp(pat, url, pi->scheme_len))\n+\t\treturn false;\n+\n+\t/*\n+\t * Ports must match exactly. url_normalize() strips default\n+\t * ports (like 443 for https), so length and content\n+\t * comparisons are sufficient.\n+\t */\n+\tif (pi->port_len != ui->port_len ||\n+\t    strncmp(pat + pi->port_off, url + ui->port_off, pi->port_len))\n+\t\treturn false;\n+\n+\t/*\n+\t * Match host and path separately to prevent a '*' in the host\n+\t * portion of the pattern from matching across the '/'\n+\t * boundary into the path.\n+\t */\n+\n+\treturn match_pattern_url(pat + pi->host_off, pi->host_len,\n+\t\t\t\t url + ui->host_off, ui->host_len) &&\n+\t\tmatch_pattern_url(pat + pi->path_off, pi->path_len,\n+\t\t\t\t  url + ui->path_off, ui->path_len);\n+}\n+\n+static struct allowed_url *url_matches_accept_list(\n+\t\tstruct string_list *accept_urls, const char *url)\n+{\n+\tstruct string_list_item *item;\n+\tstruct url_info url_info;\n+\n+\turl_info.url = url_normalize(url, &url_info);\n+\n+\tif (!url_info.url)\n+\t\treturn NULL;\n+\n+\tfor_each_string_list_item(item, accept_urls) {\n+\t\tstruct allowed_url *allowed = item->util;\n+\n+\t\tif (match_one_url(&allowed->pattern_info, &url_info)) {\n+\t\t\tfree(url_info.url);\n+\t\t\treturn allowed;\n+\t\t}\n+\t}\n+\n+\tfree(url_info.url);\n+\treturn NULL;\n+}\n+\n static int should_accept_remote(enum accept_promisor accept,\n \t\t\t\tstruct promisor_info *advertised,\n+\t\t\t\tstruct string_list *accept_urls,\n \t\t\t\tstruct string_list *config_info)\n {\n \tstruct promisor_info *p;\n@@ -756,23 +828,27 @@ static int should_accept_remote(enum accept_promisor accept,\n \t\t    \"this remote should have been rejected earlier\",\n \t\t    remote_name);\n \n-\tif (accept == ACCEPT_ALL)\n-\t\treturn all_fields_match(advertised, config_info, NULL);\n-\n \t/* Get config info for that promisor remote */\n \titem = string_list_lookup(config_info, remote_name);\n \n-\tif (!item)\n+\tif (!item) {\n \t\t/* We don't know about that remote */\n+\t\tif (accept == ACCEPT_ALL)\n+\t\t\treturn all_fields_match(advertised, config_info, NULL);\n \t\treturn 0;\n+\t}\n \n \tp = item->util;\n \n-\tif (accept == ACCEPT_KNOWN_NAME)\n+\t/* Known remote in the allowlist? */\n+\tif (!strcmp(p->url, remote_url) && url_matches_accept_list(accept_urls, remote_url))\n \t\treturn all_fields_match(advertised, config_info, p);\n \n-\tif (accept != ACCEPT_KNOWN_URL)\n-\t\tBUG(\"Unhandled 'enum accept_promisor' value '%d'\", accept);\n+\tif (accept == ACCEPT_ALL)\n+\t\treturn all_fields_match(advertised, config_info, NULL);\n+\n+\tif (accept == ACCEPT_KNOWN_NAME)\n+\t\treturn all_fields_match(advertised, config_info, p);\n \n \tif (strcmp(p->url, remote_url)) {\n \t\twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s', \"\n@@ -781,7 +857,13 @@ static int should_accept_remote(enum accept_promisor accept,\n \t\treturn 0;\n \t}\n \n-\treturn all_fields_match(advertised, config_info, p);\n+\tif (accept == ACCEPT_KNOWN_URL)\n+\t\treturn all_fields_match(advertised, config_info, p);\n+\n+\tif (accept != ACCEPT_NONE)\n+\t\tBUG(\"Unhandled 'enum accept_promisor' value '%d'\", accept);\n+\n+\treturn 0;\n }\n \n static int skip_field_name_prefix(const char *elem, const char *field_name, const char **value)\n@@ -991,7 +1073,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t/* Load and validate the acceptFromServerUrl config */\n \tload_accept_from_server_url(repo, &accept_urls);\n \n-\tif (accept == ACCEPT_NONE)\n+\tif (accept == ACCEPT_NONE && !accept_urls.nr)\n \t\treturn;\n \n \t/* Parse remote info received */\n@@ -1011,7 +1093,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\t\tstring_list_sort(&config_info);\n \t\t}\n \n-\t\tif (should_accept_remote(accept, advertised, &config_info)) {\n+\t\tif (should_accept_remote(accept, advertised, &accept_urls, &config_info)) {\n \t\t\tif (!store_info)\n \t\t\t\tstore_info = store_info_new(repo);\n \t\t\tif (promisor_store_advertised_fields(advertised, store_info))\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex 3b39505380..0659b2ac15 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -387,6 +387,77 @@ test_expect_success \"clone with 'KnownUrl' and empty url, so not advertised\" '\n \tcheck_missing_objects server 1 \"$oid\"\n '\n \n+test_expect_success \"clone with 'None' but URL allowlisted\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\tGIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \\\n+\t\t-c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n+\t\t-c remote.lop.url=\"$TRASH_DIRECTORY_URL/lop\" \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\"\n+'\n+\n+test_expect_success \"clone with 'None' but URL not in allowlist\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\tGIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \\\n+\t\t-c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n+\t\t-c remote.lop.url=\"$TRASH_DIRECTORY_URL/lop\" \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c promisor.acceptFromServerUrl=\"https://example.com/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that the largest object is not missing on the server\n+\tcheck_missing_objects server 0 \"\" &&\n+\n+\t# Reinitialize server so that the largest object is missing again\n+\tinitialize_server 1 \"$oid\"\n+'\n+\n+test_expect_success \"clone with 'None' but URL allowlisted in one pattern out of two\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\tGIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \\\n+\t\t-c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n+\t\t-c remote.lop.url=\"$TRASH_DIRECTORY_URL/lop\" \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c promisor.acceptFromServerUrl=\"https://example.com/*\" \\\n+\t\t-c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\"\n+'\n+\n+test_expect_success \"clone with 'None', URL allowlisted, but client has different URL\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\t# The client configures \"lop\" with a different URL (serverTwo) than\n+\t# what the server advertises (lop). Even though the advertised URL\n+\t# matches the allowlist, the remote is rejected because the\n+\t# configured URL does not match the advertised one.\n+\tGIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \\\n+\t\t-c remote.lop.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n+\t\t-c remote.lop.url=\"$TRASH_DIRECTORY_URL/serverTwo\" \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that the largest object is not missing on the server\n+\tcheck_missing_objects server 0 \"\" &&\n+\n+\t# Reinitialize server so that the largest object is missing again\n+\tinitialize_server 1 \"$oid\"\n+'\n+\n test_expect_success \"clone with invalid promisor.acceptFromServerUrl\" '\n \tgit -C server config promisor.advertise true &&\n \ttest_when_finished \"rm -rf client\" &&\n-- \n2.54.0.275.g96c817d129.dirty\n\n"},{"id":"544163","messageId":"20260527140820.1438165-8-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260527140820.1438165-1-christian.couder@gmail.com","subject":"[PATCH v4 7/8] promisor-remote: auto-configure unknown remotes","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-05-27T14:08:19Z","receivedAt":"2026-05-27T14:08:51Z","isPatch":true,"body":"Previous commits have introduced the `promisor.acceptFromServerUrl`\nconfig variable to allowlist some URLs advertised by a server through\nthe \"promisor-remote\" protocol capability.\n\nHowever the new `promisor.acceptFromServerUrl` mechanism, like the old\n`promisor.acceptFromServer` mechanism, still requires a remote to\nalready exist in the client's local configuration before it can be\naccepted. This places a significant manual burden on users to\npre-configure these remotes, and creates friction for administrators\nwho have to troubleshoot or manually provision these setups for their\nteams.\n\nTo eliminate this burden, let's automatically create a new `[remote]`\nsection in the client's config when a server advertises an unknown\nremote whose URL matches a `promisor.acceptFromServerUrl` glob pattern.\n\nConcretely, let's add four helpers:\n\n - sanitize_remote_name(): turn an arbitrary URL-derived string into a\n   valid remote name by replacing non-alphanumeric characters,\n   collapsing runs of '-', and prepending \"promisor-auto-\".\n\n - promisor_remote_name_from_url(): normalize the URL and extract\n   host+port+path to build a human-readable base name, then pass it\n   through sanitize_remote_name().\n\n - configure_auto_promisor_remote(): write the remote.*.url,\n   remote.*.promisor and remote.*.advertisedAs keys to the repo\n   config.\n\n - handle_matching_allowed_url(): pick the final name (user-supplied\n   alias or auto-generated), handle collisions by appending \"-1\",\n   \"-2\", etc., then call configure_auto_promisor_remote().\n\nLet's also add should_accept_new_remote_url() which reuses the\nurl_matches_accept_list() helper introduced in a previous commit to\nfind a matching pattern, then delegates to handle_matching_allowed_url()\nto create the remote.\n\nAnd then let's call should_accept_new_remote_url() from the '!item'\n(unknown remote) branch of should_accept_remote(), setting\n`reload_config` so that the newly-written config is picked up.\n\nFinally let's document all that by:\n\n - expanding the `promisor.acceptFromServerUrl` entry to describe\n   auto-creation, the optional \"name=\" prefix syntax, the\n   \"promisor-auto-*\" generation rules, and numeric-suffix collision\n   handling, and by\n - adding a \"remote.<name>.advertisedAs\" entry to \"remote.adoc\".\n\nAlso let's extend the precedence paragraph added by a previous commit\nto mention this new acceptance path: until now, the only way for\n`promisor.acceptFromServerUrl` to trigger acceptance was to allow\nfield updates for a known remote. With this commit, it can also trigger\nauto-creation of a previously-unknown remote whose advertised URL\nmatches the allowlist.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    |  39 +++--\n Documentation/config/remote.adoc      |   9 ++\n promisor-remote.c                     | 201 +++++++++++++++++++++++++-\n t/t5710-promisor-remote-capability.sh | 104 +++++++++++++\n 4 files changed, 340 insertions(+), 13 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex 605473c82f..455ce40be8 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -54,7 +54,8 @@ promisor.acceptFromServer::\n promisor.acceptFromServerUrl::\n \tA glob pattern to specify which server-advertised URLs a\n \tclient is allowed to act on. When a URL matches, the client\n-\twill accept the advertised remote as a promisor remote and may\n+\twill accept the advertised remote as a promisor remote, may\n+\tautomatically create a new remote configuration for it and may\n \tautomatically accept field updates (such as authentication\n \ttokens) from the server, even if `promisor.acceptFromServer`\n \tis set to `none` (the default).\n@@ -65,12 +66,13 @@ this option in _ANY_ config file read by Git.\n +\n When both `promisor.acceptFromServer` and `promisor.acceptFromServerUrl`\n are set, `promisor.acceptFromServerUrl` is consulted first and takes\n-precedence: if a matching pattern leads to acceptance (by accepting\n-field updates for a known remote whose URL matches both the local\n-configuration and the allowlist), the advertised remote is accepted\n-regardless of the `promisor.acceptFromServer` setting. If no pattern\n-in `promisor.acceptFromServerUrl` triggers acceptance, the decision\n-is left to `promisor.acceptFromServer`.\n+precedence: if a matching pattern leads to acceptance (either by\n+auto-configuring an unknown remote or by accepting field updates for\n+a known remote whose URL matches both the local configuration and the\n+allowlist), the advertised remote is accepted regardless of the\n+`promisor.acceptFromServer` setting. If no pattern in\n+`promisor.acceptFromServerUrl` triggers acceptance, the decision is\n+left to `promisor.acceptFromServer`.\n +\n Note however that, even when an advertised URL matches a pattern in\n `promisor.acceptFromServerUrl`, an already-existing remote on the\n@@ -85,9 +87,10 @@ documentation of that option.)\n Be _VERY_ careful with these patterns: `*` matches any sequence of\n characters within the 'host' and 'path' parts of a URL (but cannot\n cross part boundaries). An overly broad pattern is a major security\n-risk, as a matching URL allows a server to update fields (such as\n-authentication tokens) on known remotes without further confirmation.\n-To minimize security risks, follow these guidelines:\n+risk, as a matching URL allows a server to auto-configure new remotes\n+and to update fields (such as authentication tokens) on known remotes\n+without further confirmation. To minimize security risks, follow these\n+guidelines:\n +\n --\n 1. Start with a secure protocol scheme, like `https://` or `ssh://`.\n@@ -123,6 +126,22 @@ ignored during matching. Note that embedding credentials in URLs is\n discouraged. Passing authentication tokens via the `token` field of\n the `promisor-remote` capability is strongly preferred.\n +\n+The glob pattern can optionally be prefixed with a remote name and an\n+equals sign (e.g., `cdn=https://cdn.example.com/*`). If such a prefix\n+is provided, accepted remotes will be saved under that name. If no\n+such prefix is provided, a safe remote name will be automatically\n+generated by sanitizing the URL and prefixing it with\n+`promisor-auto-`.\n++\n+If a remote with the chosen name already exists but points to a\n+different URL, Git will append a numeric suffix (e.g., `-1`, `-2`) to\n+the name to prevent overwriting existing configurations. You should\n+make sure that this doesn't happen often though, as remotes will be\n+rejected if the numeric suffix increases too much. In all cases, the\n+original name advertised by the server is recorded in the\n+`remote.<name>.advertisedAs` configuration variable for tracing and\n+debugging purposes.\n++\n For the security implications of accepting a promisor remote, see the\n documentation of `promisor.acceptFromServer`. For details on the\n protocol, see linkgit:gitprotocol-v2[5].\ndiff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc\nindex 91e46f66f5..6e2bbdf457 100644\n--- a/Documentation/config/remote.adoc\n+++ b/Documentation/config/remote.adoc\n@@ -91,6 +91,15 @@ remote.<name>.promisor::\n \tWhen set to true, this remote will be used to fetch promisor\n \tobjects.\n \n+remote.<name>.advertisedAs::\n+\tWhen a promisor remote is automatically configured using\n+\tinformation advertised by a server through the\n+\t`promisor-remote` protocol capability (see\n+\t`promisor.acceptFromServerUrl`), the server's originally\n+\tadvertised name is saved in this variable. This is for\n+\tinformation, tracing and debugging purposes. Users should not\n+\ttypically modify or create such configuration entries.\n+\n remote.<name>.partialclonefilter::\n \tThe filter that will be applied when fetching from this\tpromisor remote.\n \tChanging or clearing this value will only affect fetches for new commits.\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 04a5bb9939..8fb5e40f67 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -813,10 +813,197 @@ static struct allowed_url *url_matches_accept_list(\n \treturn NULL;\n }\n \n-static int should_accept_remote(enum accept_promisor accept,\n+/*\n+ * Sanitize the buffer to make it a valid remote name coming from the\n+ * server by:\n+ *\n+ * - replacing any non alphanumeric character with a '-'\n+ * - stripping any leading '-',\n+ * - condensing multiple '-' into one,\n+ * - prepending \"promisor-auto-\",\n+ * - validating the result.\n+ */\n+static int sanitize_remote_name(struct strbuf *buf, const char *url)\n+{\n+\tchar prev = '-';\n+\tfor (size_t i = 0; i < buf->len; ) {\n+\t\tif (!isalnum(buf->buf[i]))\n+\t\t\tbuf->buf[i] = '-';\n+\t\tif (prev == '-' && buf->buf[i] == '-') {\n+\t\t\tstrbuf_remove(buf, i, 1);\n+\t\t} else {\n+\t\t\tprev = buf->buf[i];\n+\t\t\ti++;\n+\t\t}\n+\t}\n+\n+\tstrbuf_strip_suffix(buf, \"-\");\n+\n+\tif (!buf->len) {\n+\t\twarning(_(\"couldn't generate a valid remote name from \"\n+\t\t\t  \"advertised url '%s', ignoring this remote\"), url);\n+\t\treturn -1;\n+\t}\n+\n+\tstrbuf_insertstr(buf, 0, \"promisor-auto-\");\n+\n+\tif (!valid_remote_name(buf->buf)) {\n+\t\twarning(_(\"generated remote name '%s' from advertised url '%s' \"\n+\t\t\t  \"is invalid, ignoring this remote\"), buf->buf, url);\n+\t\treturn -1;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static char *promisor_remote_name_from_url(const char *url)\n+{\n+\tstruct url_info url_info = { 0 };\n+\tchar *normalized = url_normalize(url, &url_info);\n+\tstruct strbuf buf = STRBUF_INIT;\n+\n+\tif (!normalized) {\n+\t\twarning(_(\"couldn't normalize advertised url '%s', \"\n+\t\t\t  \"ignoring this remote\"), url);\n+\t\treturn NULL;\n+\t}\n+\n+\tif (url_info.host_len) {\n+\t\tstrbuf_add(&buf, normalized + url_info.host_off, url_info.host_len);\n+\t\tstrbuf_addch(&buf, '-');\n+\t}\n+\n+\tif (url_info.port_len) {\n+\t\tstrbuf_add(&buf, normalized + url_info.port_off, url_info.port_len);\n+\t\tstrbuf_addch(&buf, '-');\n+\t}\n+\n+\tif (url_info.path_len) {\n+\t\tstrbuf_add(&buf, normalized + url_info.path_off, url_info.path_len);\n+\t\tstrbuf_trim_trailing_dir_sep(&buf);\n+\t\tstrbuf_strip_suffix(&buf, \".git\");\n+\t}\n+\n+\tfree(normalized);\n+\n+\tif (sanitize_remote_name(&buf, url)) {\n+\t\tstrbuf_release(&buf);\n+\t\treturn NULL;\n+\t}\n+\n+\treturn strbuf_detach(&buf, NULL);\n+}\n+\n+static void configure_auto_promisor_remote(struct repository *repo,\n+\t\t\t\t\t   const char *name,\n+\t\t\t\t\t   const char *url,\n+\t\t\t\t\t   const char *advertised_as,\n+\t\t\t\t\t   bool reuse)\n+{\n+\tchar *key;\n+\n+\tif (!reuse) {\n+\t\tfprintf(stderr, _(\"Auto-creating promisor remote '%s' for URL '%s'\\n\"),\n+\t\t\tname, url);\n+\n+\t\tkey = xstrfmt(\"remote.%s.url\", name);\n+\t\trepo_config_set_gently(repo, key, url);\n+\t\tfree(key);\n+\t}\n+\n+\t/* NB: when reusing, this promotes an existing non-promisor remote */\n+\tkey = xstrfmt(\"remote.%s.promisor\", name);\n+\trepo_config_set_gently(repo, key, \"true\");\n+\tfree(key);\n+\n+\tif (advertised_as) {\n+\t\tkey = xstrfmt(\"remote.%s.advertisedAs\", name);\n+\t\trepo_config_set_gently(repo, key, advertised_as);\n+\t\tfree(key);\n+\t}\n+}\n+\n+#define MAX_REMOTES_WITH_SIMILAR_NAMES 20\n+\n+/* Return the allocated local name, or NULL on failure */\n+static char *handle_matching_allowed_url(struct repository *repo,\n+\t\t\t\t\t char *allowed_name,\n+\t\t\t\t\t const char *remote_url,\n+\t\t\t\t\t const char *remote_name)\n+{\n+\tchar *name;\n+\tchar *basename = allowed_name ?\n+\t\txstrdup(allowed_name) :\n+\t\tpromisor_remote_name_from_url(remote_url);\n+\tint i = 0;\n+\tbool reuse = false;\n+\n+\tif (!basename)\n+\t\treturn NULL;\n+\n+\tname = xstrdup(basename);\n+\n+\twhile (i < MAX_REMOTES_WITH_SIMILAR_NAMES) {\n+\t\tchar *url_key = xstrfmt(\"remote.%s.url\", name);\n+\t\tconst char *existing_url;\n+\t\tint exists = !repo_config_get_string_tmp(repo, url_key, &existing_url);\n+\n+\t\tfree(url_key);\n+\n+\t\tif (!exists)\n+\t\t\tbreak; /* Free to use */\n+\n+\t\tif (!strcmp(existing_url, remote_url)) {\n+\t\t\treuse = true;\n+\t\t\tbreak; /* Same URL, so safe to reuse */\n+\t\t}\n+\n+\t\ti++;\n+\t\tfree(name);\n+\t\tname = xstrfmt(\"%s-%d\", basename, i);\n+\t}\n+\n+\tif (i < MAX_REMOTES_WITH_SIMILAR_NAMES) {\n+\t\tconfigure_auto_promisor_remote(repo, name,\n+\t\t\t\t\t       remote_url, remote_name,\n+\t\t\t\t\t       reuse);\n+\t} else {\n+\t\twarning(_(\"too many remotes accepted with name like '%s-X', \"\n+\t\t\t  \"ignoring this remote\"), basename);\n+\t\tFREE_AND_NULL(name);\n+\t}\n+\n+\tfree(basename);\n+\treturn name;\n+}\n+\n+static int should_accept_new_remote_url(struct repository *repo,\n+\t\t\t\t\tstruct string_list *accept_urls,\n+\t\t\t\t\tstruct promisor_info *advertised)\n+{\n+\tstruct allowed_url *allowed = url_matches_accept_list(accept_urls,\n+\t\t\t\t\t\t\t     advertised->url);\n+\tif (allowed) {\n+\t\tchar *name = handle_matching_allowed_url(repo,\n+\t\t\t\t\t\t\t allowed->remote_name,\n+\t\t\t\t\t\t\t advertised->url,\n+\t\t\t\t\t\t\t advertised->name);\n+\t\tif (name) {\n+\t\t\tfree((char *)advertised->local_name);\n+\t\t\tadvertised->local_name = name;\n+\t\t\treturn 1;\n+\t\t}\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static int should_accept_remote(struct repository *repo,\n+\t\t\t\tenum accept_promisor accept,\n \t\t\t\tstruct promisor_info *advertised,\n \t\t\t\tstruct string_list *accept_urls,\n-\t\t\t\tstruct string_list *config_info)\n+\t\t\t\tstruct string_list *config_info,\n+\t\t\t\tbool *reload_config)\n {\n \tstruct promisor_info *p;\n \tstruct string_list_item *item;\n@@ -833,6 +1020,13 @@ static int should_accept_remote(enum accept_promisor accept,\n \n \tif (!item) {\n \t\t/* We don't know about that remote */\n+\n+\t\tint res = should_accept_new_remote_url(repo, accept_urls, advertised);\n+\t\tif (res) {\n+\t\t\t*reload_config = true;\n+\t\t\treturn res;\n+\t\t}\n+\n \t\tif (accept == ACCEPT_ALL)\n \t\t\treturn all_fields_match(advertised, config_info, NULL);\n \t\treturn 0;\n@@ -1093,7 +1287,8 @@ static void filter_promisor_remote(struct repository *repo,\n \t\t\tstring_list_sort(&config_info);\n \t\t}\n \n-\t\tif (should_accept_remote(accept, advertised, &accept_urls, &config_info)) {\n+\t\tif (should_accept_remote(repo, accept, advertised, &accept_urls,\n+\t\t\t\t\t &config_info, &reload_config)) {\n \t\t\tif (!store_info)\n \t\t\t\tstore_info = store_info_new(repo);\n \t\t\tif (promisor_store_advertised_fields(advertised, store_info))\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex 0659b2ac15..549acff23f 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -458,6 +458,107 @@ test_expect_success \"clone with 'None', URL allowlisted, but client has differen\n \tinitialize_server 1 \"$oid\"\n '\n \n+test_expect_success \"clone with URL allowlisted and no remote already configured\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\ttest_when_finished \"rm -f full_names\" &&\n+\n+\tGIT_NO_LAZY_FETCH=0 git clone \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c promisor.acceptFromServerUrl=\"$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that exactly one remote has been auto-created, identified\n+\t# by \"remote.<name>.advertisedAs\" == \"lop\".\n+\tgit -C client config get --all --show-names --regexp \\\n+\t\t\"remote\\..*\\.advertisedas\" >full_names &&\n+\ttest_line_count = 1 full_names &&\n+\tREMOTE_NAME=$(sed \"s/^remote\\.\\(.*\\)\\.advertisedas .*$/\\1/\" full_names) &&\n+\n+\t# Check \".url\" and \".promisor\" values\n+\tprintf \"%s\\n\" \"$TRASH_DIRECTORY_URL/lop\" \"true\" >expect &&\n+\tgit -C client config \"remote.$REMOTE_NAME.url\" >actual &&\n+\tgit -C client config \"remote.$REMOTE_NAME.promisor\" >>actual &&\n+\ttest_cmp expect actual &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\"\n+'\n+\n+test_expect_success \"clone with named URL allowlisted and no pre-configured remote\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\tGIT_NO_LAZY_FETCH=0 git clone \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c promisor.acceptFromServerUrl=\"cdn=$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that a remote has been auto-created with the right \"cdn\" name and fields.\n+\tprintf \"%s\\n\" \"$TRASH_DIRECTORY_URL/lop\" \"true\" \"lop\" >expect &&\n+\tgit -C client config \"remote.cdn.url\" >actual &&\n+\tgit -C client config \"remote.cdn.promisor\" >>actual &&\n+\tgit -C client config \"remote.cdn.advertisedAs\" >>actual &&\n+\ttest_cmp expect actual &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\"\n+'\n+\n+test_expect_success \"clone with URL allowlisted but colliding name\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\tGIT_NO_LAZY_FETCH=0 git clone -c remote.cdn.promisor=true \\\n+\t\t-c remote.cdn.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n+\t\t-c remote.cdn.url=\"https://example.com/cdn\" \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c promisor.acceptFromServerUrl=\"cdn=$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that a remote has been auto-created with the right \"cdn-1\" name and fields.\n+\tprintf \"%s\\n\" \"$TRASH_DIRECTORY_URL/lop\" \"true\" \"lop\" >expect &&\n+\tgit -C client config \"remote.cdn-1.url\" >actual &&\n+\tgit -C client config \"remote.cdn-1.promisor\" >>actual &&\n+\tgit -C client config \"remote.cdn-1.advertisedAs\" >>actual &&\n+\ttest_cmp expect actual &&\n+\n+\t# Check that the original \"cdn\" remote was not overwritten.\n+\tprintf \"%s\\n\" \"https://example.com/cdn\" \"true\" >expect &&\n+\tgit -C client config \"remote.cdn.url\" >actual &&\n+\tgit -C client config \"remote.cdn.promisor\" >>actual &&\n+\ttest_cmp expect actual &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\"\n+'\n+\n+test_expect_success \"clone with URL allowlisted and reusable remote\" '\n+\tgit -C server config promisor.advertise true &&\n+\ttest_when_finished \"rm -rf client\" &&\n+\n+\tGIT_NO_LAZY_FETCH=0 git clone \\\n+\t\t-c remote.cdn.fetch=\"+refs/heads/*:refs/remotes/lop/*\" \\\n+\t\t-c remote.cdn.url=\"$TRASH_DIRECTORY_URL/lop\" \\\n+\t\t-c promisor.acceptfromserver=None \\\n+\t\t-c promisor.acceptFromServerUrl=\"cdn=$ENCODED_TRASH_DIRECTORY_URL/*\" \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that the existing \"cdn\" remote has been properly updated.\n+\tprintf \"%s\\n\" \"$TRASH_DIRECTORY_URL/lop\" \"true\" \"lop\" \"+refs/heads/*:refs/remotes/lop/*\" >expect &&\n+\tgit -C client config \"remote.cdn.url\" >actual &&\n+\tgit -C client config \"remote.cdn.promisor\" >>actual &&\n+\tgit -C client config \"remote.cdn.advertisedAs\" >>actual &&\n+\tgit -C client config \"remote.cdn.fetch\" >>actual &&\n+\ttest_cmp expect actual &&\n+\n+\t# Check that no new \"cdn-1\" remote has been created.\n+\ttest_must_fail git -C client config \"remote.cdn-1.url\" &&\n+\n+\t# Check that the largest object is still missing on the server\n+\tcheck_missing_objects server 1 \"$oid\"\n+'\n+\n test_expect_success \"clone with invalid promisor.acceptFromServerUrl\" '\n \tgit -C server config promisor.advertise true &&\n \ttest_when_finished \"rm -rf client\" &&\n@@ -472,6 +573,9 @@ test_expect_success \"clone with invalid promisor.acceptFromServerUrl\" '\n \t# Check that a warning was emitted\n \ttest_grep \"invalid remote name '\\''bad name'\\''\" err &&\n \n+\t# Check that no remote was auto-created\n+\ttest_must_fail git -C client config get --regexp \"remote\\..*\\.advertisedas\" &&\n+\n \t# Check that the largest object is not missing on the server\n \tcheck_missing_objects server 0 \"\" &&\n \n-- \n2.54.0.275.g96c817d129.dirty\n\n"},{"id":"544164","messageId":"20260527140820.1438165-9-christian.couder@gmail.com","threadId":"64670","inReplyTo":"20260527140820.1438165-1-christian.couder@gmail.com","subject":"[PATCH v4 8/8] doc: promisor: improve acceptFromServer entry","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-05-27T14:08:20Z","receivedAt":"2026-05-27T14:08:53Z","isPatch":true,"body":"The entry for the `promisor.acceptFromServer` in\n\"Documentation/config/promisor.adoc\" has a number of issues:\n\n- it's not clear if new remotes and URLs can be created,\n- it looks like a big block of text,\n- it's not easy to see all the options,\n- it's not easy to see which option is the default one,\n- for \"knownName\", it says \"advertised by the client\" instead of\n  \"advertised by the server\",\n- it doesn't refer to the new related `acceptFromServerUrl`\n  option.\n\nLet's address all these issues by rewording large parts of it\nand using bullet points for the different options.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc | 53 ++++++++++++++++++++----------\n 1 file changed, 35 insertions(+), 18 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex 455ce40be8..f07a2e883b 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -32,24 +32,41 @@ variable is set to \"true\", and the \"name\" and \"url\" fields are always\n advertised regardless of this setting.\n \n promisor.acceptFromServer::\n-\tIf set to \"all\", a client will accept all the promisor remotes\n-\ta server might advertise using the \"promisor-remote\"\n-\tcapability. If set to \"knownName\" the client will accept\n-\tpromisor remotes which are already configured on the client\n-\tand have the same name as those advertised by the client. This\n-\tis not very secure, but could be used in a corporate setup\n-\twhere servers and clients are trusted to not switch name and\n-\tURLs. If set to \"knownUrl\", the client will accept promisor\n-\tremotes which have both the same name and the same URL\n-\tconfigured on the client as the name and URL advertised by the\n-\tserver. This is more secure than \"all\" or \"knownName\", so it\n-\tshould be used if possible instead of those options. Default\n-\tis \"none\", which means no promisor remote advertised by a\n-\tserver will be accepted. By accepting a promisor remote, the\n-\tclient agrees that the server might omit objects that are\n-\tlazily fetchable from this promisor remote from its responses\n-\tto \"fetch\" and \"clone\" requests from the client. Name and URL\n-\tcomparisons are case sensitive. See linkgit:gitprotocol-v2[5].\n+\tControls which promisor remotes advertised by a server (using the\n+\t\"promisor-remote\" protocol capability) a client will accept. By\n+\taccepting a promisor remote, the client agrees that the server\n+\tmight omit objects that are lazily fetchable from this promisor\n+\tremote from its responses to \"fetch\" and \"clone\" requests.\n++\n+Note that this option does not cause new remotes to be automatically\n+created in the client's configuration. It only allows remotes which\n+are somehow already configured to be trusted for the current\n+operation, or their fields to be updated (if `promisor.storeFields` is\n+set and the remote already exists locally). To allow Git to\n+automatically create and persist new remotes from server\n+advertisements, use `promisor.acceptFromServerUrl`.\n++\n+The available options are:\n++\n+* `none` (default): No promisor remote advertised by a server will be\n+  accepted.\n++\n+* `knownUrl`: The client will accept promisor remotes that are already\n+  configured on the client and have both the same name and the same URL\n+  as advertised by the server. This is more secure than `all` or\n+  `knownName`, and should be used if possible instead of those options.\n++\n+* `knownName`: The client will accept promisor remotes that are already\n+  configured on the client and have the same name as those advertised\n+  by the server. This is not very secure, but could be used in a corporate\n+  setup where servers and clients are trusted to not switch names and URLs.\n++\n+* `all`: The client will accept all the promisor remotes a server might\n+  advertise. This is the least secure option and should only be used in\n+  fully trusted environments.\n++\n+Name and URL comparisons are case-sensitive. See linkgit:gitprotocol-v2[5]\n+for protocol details.\n \n promisor.acceptFromServerUrl::\n \tA glob pattern to specify which server-advertised URLs a\n-- \n2.54.0.275.g96c817d129.dirty\n\n"},{"id":"544169","messageId":"CAP8UFD2kYbu09xd=AppjY=xfENRG7ZmGBSNCsxR72bwqXybZzA@mail.gmail.com","threadId":"64670","inReplyTo":"87a4tvq6pr.fsf@gitster.g","subject":"Re: [PATCH v3 4/8] promisor-remote: add 'local_name' to 'struct promisor_info'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-05-27T15:33:40Z","receivedAt":"2026-05-27T15:33:55Z","isPatch":true,"body":"On Wed, May 20, 2026 at 2:12 AM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Christian Couder <christian.couder@gmail.com> writes:\n\n> > +static const char *promisor_info_internal_name(struct promisor_info *p)\n> > +{\n> > +     return p->local_name ? p->local_name : p->name;\n> > +}\n>\n> Hmph.\n>\n> > @@ -829,7 +836,7 @@ static bool promisor_store_advertised_fields(struct promisor_info *advertised,\n> >  {\n> >       struct promisor_info *p;\n> >       struct string_list_item *item;\n> > -     const char *remote_name = advertised->name;\n> > +     const char *remote_name = promisor_info_internal_name(advertised);\n>\n> Is this really a \"remote_name\", though?  As ...\n>\n> > @@ -937,7 +944,8 @@ static void filter_promisor_remote(struct repository *repo,\n> >       /* Apply accepted remotes to the stable repo state */\n> >       for_each_string_list_item(item, accepted_remotes) {\n> >               struct promisor_info *info = item->util;\n> > -             struct promisor_remote *r = repo_promisor_remote_find(repo, info->name);\n> > +             const char *local = promisor_info_internal_name(info);\n>\n> ... this name \"local\" is \"the name the thing is locally known to\n> us\", promisor_info_local_name() might be a better name?  I dunno.\n> I jsut found it odd that the return value of the same function is\n> stored in variables named \"remote\" and \"local\" at the same time ;-)\n\nIn the v4 I just sent, I renamed promisor_info_internal_name() to\npromisor_info_local_name(), and \"remote_name\" is the name of the local\nvariable in both places to be more consistent.\n\nThanks.\n"},{"id":"544170","messageId":"CAP8UFD33N91V5dt3NL5xozfVm6rc=-6r11nTMq+RM=eNp+R2xg@mail.gmail.com","threadId":"64670","inReplyTo":"97b9f2cd-7c82-4d4c-b574-31176074e566@app.fastmail.com","subject":"Re: [PATCH v3 6/8] promisor-remote: trust known remotes matching acceptFromServerUrl","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-05-27T15:37:24Z","receivedAt":"2026-05-27T15:37:36Z","isPatch":true,"body":"On Sat, May 23, 2026 at 5:17 PM Kristoffer Haugsbakk\n<kristofferhaugsbakk@fastmail.com> wrote:\n>\n> On Tue, May 19, 2026, at 17:38, Christian Couder wrote:\n> >[snip]\n> >\n> > Let's then use this helper in should_accept_remote() so that, a known\n> > remote whose URL matches the allowlist is accepted.\n>\n> I don’t understand this comma break?\n\nI have removed it in the v4 I just sent. Sorry for the confusion.\n\n> > ++\n> > +Before matching, both the advertised URL and the pattern are\n> > +normalized: the scheme and host are lowercased, percent-encoded\n>\n> This next paragraph seems to go back to describing how things work. But\n> this paragraph as well as all of the following ones belong to this list\n> item:\n>\n>       4.   Be careful using globs [...]\n>\n>            Before matching, [...]\n>\n>            The glob pattern can [...]\n>\n>            If a remote with the [...]\n>\n>            For the security implications [...]\n>\n>     promisor.checkFields\n>     [...]\n>\n> I don’t know what the intent is. But using an open block will delimit\n> the ordered list.\n>\n>     diff --git Documentation/config/promisor.adoc Documentation/config/promisor.adoc\n>     index cc728bb0b5e..f07a2e883bd 100644\n>     --- Documentation/config/promisor.adoc\n>     +++ Documentation/config/promisor.adoc\n>     @@ -109,6 +109,7 @@ and to update fields (such as authentication tokens) on known remotes\n>      without further confirmation. To minimize security risks, follow these\n>      guidelines:\n>      +\n>     +--\n>      1. Start with a secure protocol scheme, like `https://` or `ssh://`.\n>      +\n>      2. Only allow domain names or paths where you control and trust _ALL_\n>     @@ -130,6 +131,7 @@ guidelines:\n>         subdomain. This is extremely dangerous on shared hosting platforms\n>         (e.g., `https://*.github.io/*` trusts every user's site on the\n>         entire platform).\n>     +--\n>      +\n>      Before matching, both the advertised URL and the pattern are\n>      normalized: the scheme and host are lowercased, percent-encoded\n\nThanks for the suggestion, it is indeed much better this way, and this\nis what is used in v4.\n"},{"id":"545021","messageId":"87ik7s16sg.fsf@emacs.iotcl.com","threadId":"64670","inReplyTo":"20260527140820.1438165-1-christian.couder@gmail.com","subject":"Re: [PATCH v4 0/8] Auto-configure advertised remotes via URL allowlist","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-06-09T08:01:35Z","receivedAt":"2026-06-09T08:01:54Z","isPatch":true,"body":"_(resend because it seems I accidentally didn't reply-all)_\n\nChristian Couder <christian.couder@gmail.com> writes:\n\n> Changes compared to v3\n> ======================\n>\n> Thanks to Toon, Kristoffer, Patrick and Junio for reviewing the\n> previous versions of this series and of the preparatory series.\n>\n> This has been rebased onto master @ 56a4f3c3a2 (The 8th batch,\n> 2026-05-25) to avoid a trivial conflict in \"urlmatch.c\".\n>\n> Only minor changes have been made since v3, in the following patches:\n>\n>  - Patch 4/8 (\"promisor-remote: add 'local_name' to 'struct\n>    promisor_info'\"):\n>\n>    - The promisor_info_internal_name() function has been renamed\n>      promisor_info_local_name() for clarity.\n>\n>    - A `const char *local` local variable has been renamed\n>      `remote_name` for consistency with another similar variable.\n\nI can really appreciate these two changes. Both make things more\nconsistent and cleaner.\n\n>  - Patch 6/8 (\"promisor-remote: trust known remotes matching\n>    acceptFromServerUrl\"):\n\nI previously reviewed v2 and compared to that I like the changes you've\nmade toward being clear about precedence. And this consistency carries\nthrough in PATCH 7/8.\n\nAnd thanks for mentioning username and password components are ignored\nintentionally.\n\nBut I previously mentioned I felt the naming of 'acceptFromServer' and\n'acceptFromServerUrl' are a bit confusing. So I'm wondering whether we\ncan consider another proposal:\n\nWhat if 'acceptFromServer' would configure if 'acceptFromServerUrl'\nshould be used? I mean, imagine we put this in the config:\n\n    [promisor]\n        acceptFromServer = Match\n        acceptFromServerUrl = https://my-org.com/*\n\n(we can still argue over naming, but to get the idea)\n\nSo the value \"Match\" for 'acceptFromServer' would inform Git to use\n'acceptFromServerUrl'. This way precedence isn't a concern no more,\nbecause every value for 'acceptFromServer' is mutually exclusive.\n\nThis has one downside though, you can no longer combine\nacceptFromServer=KnownUrl with a 'acceptFromServerUrl'. So URLs\nadvertised by the server can no longer fall-through to\n'acceptFromServer' if they don't match 'acceptFromServerUrl'. You can\nargue whether that's a good thing or not.\n\nWhat do you think? If you disagree, I'm fine with the current approach\nand I think this version looks good.\n\n-- \nCheers,\nToon\n"},{"id":"545022","messageId":"CAP8UFD0r96KxU3kW2khJ_MySgtv0ZpU26KR1vNimp_FwigQfXA@mail.gmail.com","threadId":"64670","inReplyTo":"87ik7s16sg.fsf@emacs.iotcl.com","subject":"Re: [PATCH v4 0/8] Auto-configure advertised remotes via URL allowlist","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-06-09T08:30:13Z","receivedAt":"2026-06-09T08:30:27Z","isPatch":true,"body":"Hi Toon,\n\nOn Tue, Jun 9, 2026 at 10:01 AM Toon Claes <toon@iotcl.com> wrote:\n>\n> _(resend because it seems I accidentally didn't reply-all)_\n>\n> Christian Couder <christian.couder@gmail.com> writes:\n>\n> > Changes compared to v3\n> > ======================\n> >\n> > Thanks to Toon, Kristoffer, Patrick and Junio for reviewing the\n> > previous versions of this series and of the preparatory series.\n> >\n> > This has been rebased onto master @ 56a4f3c3a2 (The 8th batch,\n> > 2026-05-25) to avoid a trivial conflict in \"urlmatch.c\".\n> >\n> > Only minor changes have been made since v3, in the following patches:\n> >\n> >  - Patch 4/8 (\"promisor-remote: add 'local_name' to 'struct\n> >    promisor_info'\"):\n> >\n> >    - The promisor_info_internal_name() function has been renamed\n> >      promisor_info_local_name() for clarity.\n> >\n> >    - A `const char *local` local variable has been renamed\n> >      `remote_name` for consistency with another similar variable.\n>\n> I can really appreciate these two changes. Both make things more\n> consistent and cleaner.\n>\n> >  - Patch 6/8 (\"promisor-remote: trust known remotes matching\n> >    acceptFromServerUrl\"):\n>\n> I previously reviewed v2 and compared to that I like the changes you've\n> made toward being clear about precedence. And this consistency carries\n> through in PATCH 7/8.\n>\n> And thanks for mentioning username and password components are ignored\n> intentionally.\n\nThanks.\n\n> But I previously mentioned I felt the naming of 'acceptFromServer' and\n> 'acceptFromServerUrl' are a bit confusing. So I'm wondering whether we\n> can consider another proposal:\n>\n> What if 'acceptFromServer' would configure if 'acceptFromServerUrl'\n> should be used? I mean, imagine we put this in the config:\n>\n>     [promisor]\n>         acceptFromServer = Match\n>         acceptFromServerUrl = https://my-org.com/*\n>\n> (we can still argue over naming, but to get the idea)\n>\n> So the value \"Match\" for 'acceptFromServer' would inform Git to use\n> 'acceptFromServerUrl'. This way precedence isn't a concern no more,\n> because every value for 'acceptFromServer' is mutually exclusive.\n\nIn this case I would prefer to remove 'acceptFromServerUrl' entirely\nand to make acceptFromServer accept values like:\n\n    match:https://my-org.com/*\n\nBy the way \"match\" might not be the best term. Maybe something like\n\"auto-configure\" would be better.\n\n> This has one downside though, you can no longer combine\n> acceptFromServer=KnownUrl with a 'acceptFromServerUrl'. So URLs\n> advertised by the server can no longer fall-through to\n> 'acceptFromServer' if they don't match 'acceptFromServerUrl'. You can\n> argue whether that's a good thing or not.\n\nI think it's a good thing to have this fall-through. It allows setting\nup things like this:\n\nIn the global config:\n\n[promisor]\n        acceptFromServerUrl = https://my-org.com/*\n\nIn the config of only a few repo that need it:\n\n[promisor]\n        acceptFromServer = knownUrl\n\nThis way remotes from my-org.com are accepted in all the repos, while\nother remotes are accepted only if their name and URLs have already\nbeen configured in the repos that need them.\n\nThis allows relatively lenient security for internal repos and more\nstrict security for external ones, and I suspect that many users will\nwant something like that.\n\nWhat you suggest doesn't allow that. It could force users to choose\nfor each repo between either URL based allowlist or local\nconfiguration of every remote.\n\nAlso I think it's easier to explain that 'acceptFromServerUrl' is a\ndifferent mechanism (that allows auto-configuration, contrary to\n'acceptFromServer') if these two variables are independent.\n\n> What do you think? If you disagree, I'm fine with the current approach\n> and I think this version looks good.\n\nThanks for your review and for being fine with the current approach if\nI disagree.\n\nBest,\nChristian.\n"},{"id":"545064","messageId":"877bo7294j.fsf@emacs.iotcl.com","threadId":"64670","inReplyTo":"CAP8UFD0r96KxU3kW2khJ_MySgtv0ZpU26KR1vNimp_FwigQfXA@mail.gmail.com","subject":"Re: [PATCH v4 0/8] Auto-configure advertised remotes via URL allowlist","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-06-09T12:25:48Z","receivedAt":"2026-06-09T12:25:56Z","isPatch":true,"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n>> But I previously mentioned I felt the naming of 'acceptFromServer' and\n>> 'acceptFromServerUrl' are a bit confusing. So I'm wondering whether we\n>> can consider another proposal:\n>>\n>> What if 'acceptFromServer' would configure if 'acceptFromServerUrl'\n>> should be used? I mean, imagine we put this in the config:\n>>\n>>     [promisor]\n>>         acceptFromServer = Match\n>>         acceptFromServerUrl = https://my-org.com/*\n>>\n>> (we can still argue over naming, but to get the idea)\n>>\n>> So the value \"Match\" for 'acceptFromServer' would inform Git to use\n>> 'acceptFromServerUrl'. This way precedence isn't a concern no more,\n>> because every value for 'acceptFromServer' is mutually exclusive.\n>\n> In this case I would prefer to remove 'acceptFromServerUrl' entirely\n> and to make acceptFromServer accept values like:\n>\n>     match:https://my-org.com/*\n>\n> By the way \"match\" might not be the best term. Maybe something like\n> \"auto-configure\" would be better.\n\nI think that's too complicated. Let's not do that.\n\n>> This has one downside though, you can no longer combine\n>> acceptFromServer=KnownUrl with a 'acceptFromServerUrl'. So URLs\n>> advertised by the server can no longer fall-through to\n>> 'acceptFromServer' if they don't match 'acceptFromServerUrl'. You can\n>> argue whether that's a good thing or not.\n>\n> I think it's a good thing to have this fall-through. It allows setting\n> up things like this:\n>\n> In the global config:\n>\n> [promisor]\n>         acceptFromServerUrl = https://my-org.com/*\n>\n> In the config of only a few repo that need it:\n>\n> [promisor]\n>         acceptFromServer = knownUrl\n>\n> This way remotes from my-org.com are accepted in all the repos, while\n> other remotes are accepted only if their name and URLs have already\n> been configured in the repos that need them.\n>\n> This allows relatively lenient security for internal repos and more\n> strict security for external ones, and I suspect that many users will\n> want something like that.\n>\n> What you suggest doesn't allow that. It could force users to choose\n> for each repo between either URL based allowlist or local\n> configuration of every remote.\n\nWell yes, that's why I mentioned:\n\n> You can argue whether that's a good thing or not.\n\nIf it's intentional and as you mention there's a valid use-case for\nthis, then I agree with your approach in this series.\n\n> Also I think it's easier to explain that 'acceptFromServerUrl' is a\n> different mechanism (that allows auto-configuration, contrary to\n> 'acceptFromServer') if these two variables are independent.\n\nTrue, although naming-wise it doesn't feel like that. But I no longer\ngonna keep picking on that, so ignore this comment please. :-)\n\n>> What do you think? If you disagree, I'm fine with the current approach\n>> and I think this version looks good.\n>\n> Thanks for your review and for being fine with the current approach if\n> I disagree.\n\nThanks for explaining, I still agree moving on like this.\n\n-- \nCheers,\nToon\n"},{"id":"545163","messageId":"xmqqh5naxwfc.fsf@gitster.g","threadId":"64670","inReplyTo":"877bo7294j.fsf@emacs.iotcl.com","subject":"Re: [PATCH v4 0/8] Auto-configure advertised remotes via URL allowlist","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-10T15:11:19Z","receivedAt":"2026-06-10T15:11:21Z","isPatch":true,"body":"Toon Claes <toon@iotcl.com> writes:\n\n>> Also I think it's easier to explain that 'acceptFromServerUrl' is a\n>> different mechanism (that allows auto-configuration, contrary to\n>> 'acceptFromServer') if these two variables are independent.\n>\n> True, although naming-wise it doesn't feel like that. But I no longer\n> gonna keep picking on that, so ignore this comment please. :-)\n>\n>>> What do you think? If you disagree, I'm fine with the current approach\n>>> and I think this version looks good.\n>>\n>> Thanks for your review and for being fine with the current approach if\n>> I disagree.\n>\n> Thanks for explaining, I still agree moving on like this.\n\nSounds good.  Shall we mark the topic for 'next' then?\n\nThanks, all.\n"},{"id":"545282","messageId":"CAP8UFD1gNGHXKufTK-Vwc7qgpcW3tJDq1ovV0WhhSAuPsGEVwQ@mail.gmail.com","threadId":"64670","inReplyTo":"xmqqh5naxwfc.fsf@gitster.g","subject":"Re: [PATCH v4 0/8] Auto-configure advertised remotes via URL allowlist","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-06-11T13:29:56Z","receivedAt":"2026-06-11T13:30:13Z","isPatch":true,"body":"On Wed, Jun 10, 2026 at 5:11 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Toon Claes <toon@iotcl.com> writes:\n\n> > Thanks for explaining, I still agree moving on like this.\n>\n> Sounds good.  Shall we mark the topic for 'next' then?\n\nYes please. I think it's ready.\n\nThanks.\n"},{"id":"547136","messageId":"4320d6a6-eda7-472e-b416-65bced3e9481@app.fastmail.com","threadId":"64670","inReplyTo":"20260527140820.1438165-9-christian.couder@gmail.com","subject":"Re: [PATCH v4 8/8] doc: promisor: improve acceptFromServer entry","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-07-04T09:49:27Z","receivedAt":"2026-07-04T09:49:50Z","isPatch":true,"body":"On Wed, May 27, 2026, at 16:08, Christian Couder wrote:\n> The entry for the `promisor.acceptFromServer` in\n> \"Documentation/config/promisor.adoc\" has a number of issues:\n\n(This series is now in `next` so this is not a review comment)\n\n>[snip]\n>  Documentation/config/promisor.adoc | 53 ++++++++++++++++++++----------\n>  1 file changed, 35 insertions(+), 18 deletions(-)\n>\n> diff --git a/Documentation/config/promisor.adoc\n> b/Documentation/config/promisor.adoc\n> index 455ce40be8..f07a2e883b 100644\n> --- a/Documentation/config/promisor.adoc\n> +++ b/Documentation/config/promisor.adoc\n> @@ -32,24 +32,41 @@ variable is set to \"true\", and the \"name\" and \"url\"\n> fields are always\n>  advertised regardless of this setting.\n>[snip]\n> ++\n> +The available options are:\n> ++\n> +* `none` (default): No promisor remote advertised by a server will be\n> +  accepted.\n\nWhy did you use an unordered/bullet list instead of a description list?\n\n> ++\n> +* `knownUrl`: The client will accept promisor remotes that are already\n> +  configured on the client and have both the same name and the same URL\n> +  as advertised by the server. This is more secure than `all` or\n> +  `knownName`, and should be used if possible instead of those options.\n> ++\n> +* `knownName`: The client will accept promisor remotes that are already\n> +  configured on the client and have the same name as those advertised\n> +  by the server. This is not very secure, but could be used in a corporate\n> +  setup where servers and clients are trusted to not switch names and URLs.\n> ++\n> +* `all`: The client will accept all the promisor remotes a server might\n> +  advertise. This is the least secure option and should only be used in\n> +  fully trusted environments.\n> ++\n> +Name and URL comparisons are case-sensitive. See linkgit:gitprotocol-v2[5]\n> +for protocol details.\n>\n>  promisor.acceptFromServerUrl::\n>  \tA glob pattern to specify which server-advertised URLs a\n> --\n> 2.54.0.275.g96c817d129.dirty\n"}]}