{"thread":{"id":"63293","subject":"[PATCH 0/4] Make the \"promisor-remote\" capability support extra fields","startedAt":"2025-04-14T16:04:12Z","lastAt":"2025-09-08T17:34:17Z","messageCount":107,"participants":["Christian Couder","Junio C Hamano","Patrick Steinhardt","Karthik Nayak","Justin Tobler","Jean-Noël Avila"],"isPatch":true,"patchVersion":1,"patchTotal":4},"messages":[{"id":"516109","messageId":"20250414160343.2216312-1-christian.couder@gmail.com","threadId":"63293","inReplyTo":null,"subject":"[PATCH 0/4] Make the \"promisor-remote\" capability support extra fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-04-14T16:03:39Z","receivedAt":"2025-04-14T16:04:12Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"The \"promisor-remote\" capability can only be used to pass the names\nand URLs of the promisor remotes from the server to the client. After\nthat the client can use this information to decide if it accepts the\nremotes or not.\n\nIt would be nice if the server could pass more fields about its\nremotes and if the client could use that extra information to decide\nabout the remotes by comparing it with its local information about the\nremotes.\n\nThis patch series implements this by adding the\n\"promisor.sendExtraFields\" on the server side and the\n\"promisor.checkExtraFields\" on the client side.\n\nFor example, if \"promisor.sendExtraFields\" is set to\n\"partialCloneFilter\", and the server has the remote \"foo\" configured\nlike this:\n\n[remote \"foo\"]\n        url = file:///tmp/foo.git\n\tpartialCloneFilter = blob:none\n\nthen \"name=foo,url=file:///tmp/foo.git,partialCloneFilter=blob:none\"\nwill be sent by the server for this remote.\n\nAll the information passed through the \"promisor-remote\" capability is\nstill only used to decide if the remotes are accepted or not. The\nclient doesn't store it and doesn't use it for any other purpose.\n\nOn the technical side, we get rid of 'struct strvec' and we use mostly\n'struct string_list' instead, which is more flexible. This matches\nsome suggestions made when the series that introduced the\n\"promisor-remote\" capability was reviewed.\n\nRight now in this series, the fields names (like \"partialCloneFilter\"\nin the example) that are passed to the client are compared case\nsensitively to the local config keys. I think this is a bug and they\nshould be compared case insensitively, (while values should still be\ncompared case sensitively). I am planning to fix this in the next\niteration of this series. I am also planning to add more tests then.\n\nCI tests are not finished but look fine so far:\n\nhttps://github.com/chriscool/git/actions/runs/14449949491\n\nChristian Couder (4):\n  config: move is_config_key_char() to \"config.h\"\n  promisor-remote: refactor to get rid of 'struct strvec'\n  promisor-remote: allow a server to advertise extra fields\n  promisor-remote: allow a client to check extra fields\n\n Documentation/config/promisor.adoc    |  32 +++\n Documentation/gitprotocol-v2.adoc     |  42 ++--\n config.c                              |  11 +-\n config.h                              |   9 +\n promisor-remote.c                     | 287 +++++++++++++++++++++-----\n t/t5710-promisor-remote-capability.sh |  67 ++++++\n 6 files changed, 374 insertions(+), 74 deletions(-)\n\n-- \n2.49.0.158.g6ac6832dc3.dirty\n\n"},{"id":"516110","messageId":"20250414160343.2216312-2-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250414160343.2216312-1-christian.couder@gmail.com","subject":"[PATCH 1/4] config: move is_config_key_char() to \"config.h\"","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-04-14T16:03:40Z","receivedAt":"2025-04-14T16:04:14Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"The iskeychar() function in \"config.c\" checks if a character is valid\nfor the section or variable name part of a config key.\n\nIn a follow up commit we will want to check outside \"config.c\" if a\nstring can be a valid variable name of a config key, so will will want\nto reuse that fonction.\n\nLet's then move it from \"config.c\" to \"config.h\", and, while at it,\nlet's rename it to is_config_key_char().\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n config.c | 11 +++--------\n config.h |  9 +++++++++\n 2 files changed, 12 insertions(+), 8 deletions(-)\n\ndiff --git a/config.c b/config.c\nindex e127afaa8f..f529cb4cbe 100644\n--- a/config.c\n+++ b/config.c\n@@ -531,11 +531,6 @@ void git_config_push_env(const char *spec)\n \tfree(key);\n }\n \n-static inline int iskeychar(int c)\n-{\n-\treturn isalnum(c) || c == '-';\n-}\n-\n /*\n  * Auxiliary function to sanity-check and split the key into the section\n  * identifier and variable name.\n@@ -585,7 +580,7 @@ int git_config_parse_key(const char *key, char **store_key, size_t *baselen_)\n \t\t\tdot = 1;\n \t\t/* Leave the extended basename untouched.. */\n \t\tif (!dot || i > baselen) {\n-\t\t\tif (!iskeychar(c) ||\n+\t\t\tif (!is_config_key_char(c) ||\n \t\t\t    (i == baselen + 1 && !isalpha(c))) {\n \t\t\t\terror(_(\"invalid key: %s\"), key);\n \t\t\t\tgoto out_free_ret_1;\n@@ -906,7 +901,7 @@ static int get_value(struct config_source *cs, struct key_value_info *kvi,\n \t\tc = get_next_char(cs);\n \t\tif (cs->eof)\n \t\t\tbreak;\n-\t\tif (!iskeychar(c))\n+\t\tif (!is_config_key_char(c))\n \t\t\tbreak;\n \t\tstrbuf_addch(name, tolower(c));\n \t}\n@@ -984,7 +979,7 @@ static int get_base_var(struct config_source *cs, struct strbuf *name)\n \t\t\treturn 0;\n \t\tif (isspace(c))\n \t\t\treturn get_extended_base_var(cs, name, c);\n-\t\tif (!iskeychar(c) && c != '.')\n+\t\tif (!is_config_key_char(c) && c != '.')\n \t\t\treturn -1;\n \t\tstrbuf_addch(name, tolower(c));\n \t}\ndiff --git a/config.h b/config.h\nindex 29a0277483..16df47f446 100644\n--- a/config.h\n+++ b/config.h\n@@ -340,6 +340,15 @@ int repo_config_set_worktree_gently(struct repository *, const char *, const cha\n  */\n void repo_config_set(struct repository *, const char *, const char *);\n \n+/**\n+ * Is this char a valid char for the section or variable name part of\n+ * a config key?\n+ */\n+static inline int is_config_key_char(int c)\n+{\n+\treturn isalnum(c) || c == '-';\n+}\n+\n int git_config_parse_key(const char *, char **, size_t *);\n \n /*\n-- \n2.49.0.158.g6ac6832dc3.dirty\n\n"},{"id":"516112","messageId":"20250414160343.2216312-3-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250414160343.2216312-1-christian.couder@gmail.com","subject":"[PATCH 2/4] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-04-14T16:03:41Z","receivedAt":"2025-04-14T16:04:15Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"In a following commit, we will use the new 'promisor-remote' protocol\ncapability introduced by d460267613 (Add 'promisor-remote' capability\nto protocol v2, 2025-02-18) to pass and process more information\nabout promisor remotes than just their name and url.\n\nFor that purpose, we will need to store information about other\nfields, especially information that might or might not be available\nfor different promisor remotes. Unfortunately using 'struct strvec',\nas we currently do, to store information about the promisor remotes\nwith one 'struct strvec' for each field like \"name\" or \"url\" does not\nscale easily in that case.\n\nLet's refactor this and introduce a new 'struct promisor_info' linked\nlist that contains a 'struct string_list fields'. This string_list\nstores the field names, like \"name\" and \"url\", in the 'string' member\nof its items, and the field values in the 'util' member of its items.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 124 ++++++++++++++++++++++++++++++++--------------\n 1 file changed, 87 insertions(+), 37 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 5801ebfd9b..0fb07f25af 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -314,10 +314,38 @@ static int allow_unsanitized(char ch)\n \treturn ch > 32 && ch < 127;\n }\n \n-static void promisor_info_vecs(struct repository *repo,\n-\t\t\t       struct strvec *names,\n-\t\t\t       struct strvec *urls)\n+/*\n+ * Linked list for promisor remotes.\n+ *\n+ * 'fields' should not be sorted, as we will rely on the order we put\n+ * things into it. So, for example, 'string_list_append()' should be\n+ * used instead of 'string_list_insert()'.\n+ */\n+struct promisor_info {\n+\tstruct promisor_info *next;\n+\tstruct string_list fields;\n+};\n+\n+static void free_info_list(struct promisor_info *p)\n+{\n+\tstruct promisor_info *next;\n+\n+\tfor (; p; p = next) {\n+\t\tnext = p->next;\n+\t\tstring_list_clear(&p->fields, 0);\n+\t\tfree(p);\n+\t}\n+}\n+\n+/*\n+ * Prepare a 'struct promisor_info' linked list of promisor\n+ * remotes. For each promisor remote, some of its fields, starting\n+ * with \"name\" and \"url\", are put in the 'fields' string_list.\n+ */\n+static struct promisor_info *promisor_info_list(struct repository *repo)\n {\n+\tstruct promisor_info *infos = NULL;\n+\tstruct promisor_info **last_info = &infos;\n \tstruct promisor_remote *r;\n \n \tpromisor_remote_init(repo);\n@@ -328,57 +356,75 @@ static void promisor_info_vecs(struct repository *repo,\n \n \t\t/* Only add remotes with a non empty URL */\n \t\tif (!git_config_get_string_tmp(url_key, &url) && *url) {\n-\t\t\tstrvec_push(names, r->name);\n-\t\t\tstrvec_push(urls, url);\n+\t\t\tstruct promisor_info *new_info = xcalloc(1, sizeof(*new_info));\n+\n+\t\t\tstring_list_init_dup(&new_info->fields);\n+\n+\t\t\tstring_list_append(&new_info->fields, \"name\")->util = (char *)r->name;\n+\t\t\tstring_list_append(&new_info->fields, \"url\")->util = (char *)url;\n+\n+\t\t\t*last_info = new_info;\n+\t\t\tlast_info = &new_info->next;\n \t\t}\n \n \t\tfree(url_key);\n \t}\n+\n+\treturn infos;\n }\n \n char *promisor_remote_info(struct repository *repo)\n {\n \tstruct strbuf sb = STRBUF_INIT;\n \tint advertise_promisors = 0;\n-\tstruct strvec names = STRVEC_INIT;\n-\tstruct strvec urls = STRVEC_INIT;\n+\tstruct promisor_info *info_list;\n+\tstruct promisor_info *r, *p;\n \n \tgit_config_get_bool(\"promisor.advertise\", &advertise_promisors);\n \n \tif (!advertise_promisors)\n \t\treturn NULL;\n \n-\tpromisor_info_vecs(repo, &names, &urls);\n+\tinfo_list = promisor_info_list(repo);\n \n-\tif (!names.nr)\n+\tif (!info_list)\n \t\treturn NULL;\n \n-\tfor (size_t i = 0; i < names.nr; i++) {\n-\t\tif (i)\n+\tfor (p = NULL, r = info_list; r; p = r, r = r->next) {\n+\t\tstruct string_list_item *item;\n+\t\tint first = 1;\n+\n+\t\tif (r != info_list)\n \t\t\tstrbuf_addch(&sb, ';');\n-\t\tstrbuf_addstr(&sb, \"name=\");\n-\t\tstrbuf_addstr_urlencode(&sb, names.v[i], allow_unsanitized);\n-\t\tstrbuf_addstr(&sb, \",url=\");\n-\t\tstrbuf_addstr_urlencode(&sb, urls.v[i], allow_unsanitized);\n+\n+\t\tfor_each_string_list_item(item, &r->fields) {\n+\t\t\tif (first)\n+\t\t\t\tfirst = 0;\n+\t\t\telse\n+\t\t\t\tstrbuf_addch(&sb, ',');\n+\t\t\tstrbuf_addf(&sb, \"%s=\", item->string);\n+\t\t\tstrbuf_addstr_urlencode(&sb, (char *)item->util, allow_unsanitized);\n+\t\t}\n \t}\n \n-\tstrvec_clear(&names);\n-\tstrvec_clear(&urls);\n+\tfree_info_list(p);\n \n \treturn strbuf_detach(&sb, NULL);\n }\n \n /*\n- * Find first index of 'nicks' where there is 'nick'. 'nick' is\n- * compared case sensitively to the strings in 'nicks'. If not found\n- * 'nicks->nr' is returned.\n+ * Find first element of 'p' where the 'name' field is 'nick'. 'nick'\n+ * is compared case sensitively to the strings in 'p'. If not found\n+ * NULL is returned.\n  */\n-static size_t remote_nick_find(struct strvec *nicks, const char *nick)\n+static struct promisor_info *remote_nick_find(struct promisor_info *p, const char *nick)\n {\n-\tfor (size_t i = 0; i < nicks->nr; i++)\n-\t\tif (!strcmp(nicks->v[i], nick))\n-\t\t\treturn i;\n-\treturn nicks->nr;\n+\tfor (; p; p = p->next) {\n+\t\tassert(!strcmp(p->fields.items[0].string, \"name\"));\n+\t\tif (!strcmp(p->fields.items[0].util, nick))\n+\t\t\treturn p;\n+\t}\n+\treturn NULL;\n }\n \n enum accept_promisor {\n@@ -390,16 +436,17 @@ enum accept_promisor {\n \n static int should_accept_remote(enum accept_promisor accept,\n \t\t\t\tconst char *remote_name, const char *remote_url,\n-\t\t\t\tstruct strvec *names, struct strvec *urls)\n+\t\t\t\tstruct promisor_info *info_list)\n {\n-\tsize_t i;\n+\tstruct promisor_info *p;\n+\tconst char *local_url;\n \n \tif (accept == ACCEPT_ALL)\n \t\treturn 1;\n \n-\ti = remote_nick_find(names, remote_name);\n+\tp = remote_nick_find(info_list, remote_name);\n \n-\tif (i >= names->nr)\n+\tif (!p)\n \t\t/* We don't know about that remote */\n \t\treturn 0;\n \n@@ -414,11 +461,16 @@ static int should_accept_remote(enum accept_promisor accept,\n \t\treturn 0;\n \t}\n \n-\tif (!strcmp(urls->v[i], remote_url))\n+\tif (strcmp(p->fields.items[1].string, \"url\"))\n+\t\tBUG(\"Bad info_list for remote '%s'\", remote_name);\n+\n+\tlocal_url = p->fields.items[1].util;\n+\n+\tif (!strcmp(local_url, remote_url))\n \t\treturn 1;\n \n \twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s'\"),\n-\t\tremote_name, urls->v[i], remote_url);\n+\t\tremote_name, local_url, remote_url);\n \n \treturn 0;\n }\n@@ -430,8 +482,7 @@ static void filter_promisor_remote(struct repository *repo,\n \tstruct strbuf **remotes;\n \tconst char *accept_str;\n \tenum accept_promisor accept = ACCEPT_NONE;\n-\tstruct strvec names = STRVEC_INIT;\n-\tstruct strvec urls = STRVEC_INIT;\n+\tstruct promisor_info *info_list = NULL;\n \n \tif (!git_config_get_string_tmp(\"promisor.acceptfromserver\", &accept_str)) {\n \t\tif (!*accept_str || !strcasecmp(\"None\", accept_str))\n@@ -451,7 +502,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\treturn;\n \n \tif (accept != ACCEPT_ALL)\n-\t\tpromisor_info_vecs(repo, &names, &urls);\n+\t\tinfo_list = promisor_info_list(repo);\n \n \t/* Parse remote info received */\n \n@@ -482,7 +533,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tif (remote_url)\n \t\t\tdecoded_url = url_percent_decode(remote_url);\n \n-\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, &names, &urls))\n+\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, info_list))\n \t\t\tstrvec_push(accepted, decoded_name);\n \n \t\tstrbuf_list_free(elems);\n@@ -490,8 +541,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tfree(decoded_url);\n \t}\n \n-\tstrvec_clear(&names);\n-\tstrvec_clear(&urls);\n+\tfree_info_list(info_list);\n \tstrbuf_list_free(remotes);\n }\n \n-- \n2.49.0.158.g6ac6832dc3.dirty\n\n"},{"id":"516113","messageId":"20250414160343.2216312-4-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250414160343.2216312-1-christian.couder@gmail.com","subject":"[PATCH 3/4] promisor-remote: allow a server to advertise extra fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-04-14T16:03:42Z","receivedAt":"2025-04-14T16:04:17Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"For now the \"promisor-remote\" protocol capability can only pass \"name\"\nand \"url\" information from a server to a client in the form\n\"name=<remote_name>,url=<remote_url>\".\n\nLet's make it possible to pass more information by introducing a new\n\"promisor.sendExtraFields\" configuration variable. This variable\nshould contain a comma or space separated list of fields that will be\nlooked up in the configuration of the remote on the server to find the\nvalues that will be passed to the client.\n\nFor example if \"promisor.sendExtraFields\" is set to \"partialCloneFilter\",\nand the server has the \"remote.<name>.partialCloneFilter\" config\nvariable set to a value for a remote, then that value will be passed\nin the form \"partialCloneFilter=<value>\" after the \"name\" and \"url\"\nfields.\n\nA following commit will allow the client to use the extra information\nto decide if it accepts the remote or not. For now the client doesn't\ndo anything with the extra information it receives.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    | 15 ++++++\n Documentation/gitprotocol-v2.adoc     | 42 +++++++++------\n promisor-remote.c                     | 75 ++++++++++++++++++++++++---\n t/t5710-promisor-remote-capability.sh | 32 ++++++++++++\n 4 files changed, 139 insertions(+), 25 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex 2638b01f83..bc08999733 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -9,6 +9,21 @@ promisor.advertise::\n \t\"false\", which means the \"promisor-remote\" capability is not\n \tadvertised.\n \n+promisor.sendExtraFields::\n+\tA comma or space separated list of additional remote related\n+\tfields that a server will send while advertising its promisor\n+\tremotes using the \"promisor-remote\" capability, see\n+\tlinkgit:gitprotocol-v2[5]. When a field named \"bar\" is part of\n+\tthis list and a corresponding \"remote.foo.bar\" config variable\n+\tis set on the server to a non empty value, for example \"baz\",\n+\tthen the field and its value, so \"bar=baz\", will be sent when\n+\tadvertising the promisor remote \"foo\". This list has no effect\n+\tunless the \"promisor.advertise\" config variable, see above, is\n+\tset to \"true\", and if that's the case, then whatever this list\n+\tcontains, the \"name\" and \"url\" fields are advertised anyway\n+\tand contain the remote name and URL respectively, so there is\n+\tno need to add \"name\" or \"url\" to this list.\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\"\ndiff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\nindex 5598c93e67..f649745837 100644\n--- a/Documentation/gitprotocol-v2.adoc\n+++ b/Documentation/gitprotocol-v2.adoc\n@@ -785,33 +785,39 @@ retrieving the header from a bundle at the indicated URI, and thus\n save themselves and the server(s) the request(s) needed to inspect the\n headers of that bundle or bundles.\n \n-promisor-remote=<pr-infos>\n+promisor-remote=<pr-info>\n ~~~~~~~~~~~~~~~~~~~~~~~~~~\n \n The server may advertise some promisor remotes it is using or knows\n about to a client which may want to use them as its promisor remotes,\n-instead of this repository. In this case <pr-infos> should be of the\n+instead of this repository. In this case <pr-info> should be of the\n form:\n \n-\tpr-infos = pr-info | pr-infos \";\" pr-info\n+\tpr-info = pr-fields | pr-info \";\" pr-info\n \n-\tpr-info = \"name=\" pr-name | \"name=\" pr-name \",\" \"url=\" pr-url\n+\tpr-fields = fld-key \"=\" fld-value | pr-fields \",\" pr-fields\n \n-where `pr-name` is the urlencoded name of a promisor remote, and\n-`pr-url` the urlencoded URL of that promisor remote.\n+where all the `fld-key` and `fld-value` in a given `pr-fields` are\n+field keys and values related to a single promisor remote.\n \n-In this case, if the client decides to use one or more promisor\n-remotes the server advertised, it can reply with\n-\"promisor-remote=<pr-names>\" where <pr-names> should be of the form:\n+The server MUST advertise at least the \"name\" and \"url\" field keys\n+along with the associated field values, which are the name of a valid\n+remote and its URL, in each `pr-fields`.\n \n-\tpr-names = pr-name | pr-names \";\" pr-name\n+`fld-key` MUST start with an alphabetic character and contain only\n+alphanumeric or '-' characters, and `fld-value` MUST be urlencoded.\n+\n+If the client decides to use one or more promisor remotes the server\n+advertised, it can reply with \"promisor-remote=<pr-names>\" where\n+<pr-names> should be of the form:\n+\n+\tpr-names = pr-name | pr-names \";\" pr-names\n \n where `pr-name` is the urlencoded name of a promisor remote the server\n advertised and the client accepts.\n \n-Note that, everywhere in this document, `pr-name` MUST be a valid\n-remote name, and the ';' and ',' characters MUST be encoded if they\n-appear in `pr-name` or `pr-url`.\n+Note that, everywhere in this document, the ';' and ',' characters\n+MUST be encoded if they appear in `pr-name` or `fld-value`.\n \n If the server doesn't know any promisor remote that could be good for\n a client to use, or prefers a client not to use any promisor remote it\n@@ -822,10 +828,12 @@ In this case, or if the client doesn't want to use any promisor remote\n the server advertised, the client shouldn't advertise the\n \"promisor-remote\" capability at all in its reply.\n \n-The \"promisor.advertise\" and \"promisor.acceptFromServer\" configuration\n-options can be used on the server and client side to control what they\n-advertise or accept respectively. See the documentation of these\n-configuration options for more information.\n+On the server side, the \"promisor.advertise\" and\n+\"promisor.sendExtraFields\" configuration options can be used to\n+control what it advertises. On the client side, the\n+\"promisor.acceptFromServer\" configuration option can be used to\n+control what it accepts. See the documentation of these configuration\n+options 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 0fb07f25af..424d88d4a1 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -314,6 +314,65 @@ static int allow_unsanitized(char ch)\n \treturn ch > 32 && ch < 127;\n }\n \n+/*\n+ * A valid extra field \"foo\" should correspond to a\n+ * \"remote.<name>.foo\" config variable, so, like config variables\n+ * keys, it should start with an alphabetic character and otherwise\n+ * each character should satisfy is_config_key_char().\n+ */\n+static int valid_extra_field(struct string_list_item *item, void *cb_data)\n+{\n+\tconst char *field = item->string;\n+\tconst char *config_key = (const char *)cb_data;\n+\n+\tfor (size_t i = 0; field[i]; i++)\n+\t\tif (i ? !is_config_key_char(field[i]) : !isalpha(field[i])) {\n+\t\t\twarning(_(\"invalid field '%s' in '%s' config\"), field, config_key);\n+\t\t\treturn 0;\n+\t\t}\n+\treturn 1;\n+}\n+\n+static char *fields_from_config(struct string_list *fields_list, const char *config_key)\n+{\n+\tchar *extras = NULL;\n+\n+\tif (!git_config_get_string(config_key, &extras) && *extras) {\n+\t\tstring_list_split_in_place(fields_list, extras, \", \", -1);\n+\t\tfilter_string_list(fields_list, 0, valid_extra_field, (void *)config_key);\n+\t}\n+\n+\treturn extras;\n+}\n+\n+static struct string_list *extra_fields_sent(void)\n+{\n+\tstatic struct string_list fields_list = STRING_LIST_INIT_NODUP;\n+\tstatic int initialized = 0;\n+\n+\tif (!initialized) {\n+\t\tfields_from_config(&fields_list, \"promisor.sendExtraFields\");\n+\t\tinitialized = 1;\n+\t}\n+\n+\treturn &fields_list;\n+}\n+\n+static void append_extra_fields(struct string_list *fields,\n+\t\t\t\tstruct string_list *extra_fields,\n+\t\t\t\tconst char *name)\n+{\n+\tstruct string_list_item *item;\n+\n+\tfor_each_string_list_item(item, extra_fields) {\n+\t\tchar *key = xstrfmt(\"remote.%s.%s\", name, item->string);\n+\t\tconst char *val;\n+\t\tif (!git_config_get_string_tmp(key, &val) && *val)\n+\t\t\tstring_list_append(fields, item->string)->util = (char *)val;\n+\t\tfree(key);\n+\t}\n+}\n+\n /*\n  * Linked list for promisor remotes.\n  *\n@@ -342,7 +401,8 @@ static void free_info_list(struct promisor_info *p)\n  * remotes. For each promisor remote, some of its fields, starting\n  * with \"name\" and \"url\", are put in the 'fields' string_list.\n  */\n-static struct promisor_info *promisor_info_list(struct repository *repo)\n+static struct promisor_info *promisor_info_list(struct repository *repo,\n+\t\t\t\t\t\tstruct string_list *extra_fields)\n {\n \tstruct promisor_info *infos = NULL;\n \tstruct promisor_info **last_info = &infos;\n@@ -363,6 +423,9 @@ static struct promisor_info *promisor_info_list(struct repository *repo)\n \t\t\tstring_list_append(&new_info->fields, \"name\")->util = (char *)r->name;\n \t\t\tstring_list_append(&new_info->fields, \"url\")->util = (char *)url;\n \n+\t\t\tif (extra_fields)\n+\t\t\t\tappend_extra_fields(&new_info->fields, extra_fields, r->name);\n+\n \t\t\t*last_info = new_info;\n \t\t\tlast_info = &new_info->next;\n \t\t}\n@@ -385,7 +448,7 @@ char *promisor_remote_info(struct repository *repo)\n \tif (!advertise_promisors)\n \t\treturn NULL;\n \n-\tinfo_list = promisor_info_list(repo);\n+\tinfo_list = promisor_info_list(repo, extra_fields_sent());\n \n \tif (!info_list)\n \t\treturn NULL;\n@@ -502,7 +565,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\treturn;\n \n \tif (accept != ACCEPT_ALL)\n-\t\tinfo_list = promisor_info_list(repo);\n+\t\tinfo_list = promisor_info_list(repo, NULL);\n \n \t/* Parse remote info received */\n \n@@ -519,13 +582,9 @@ static void filter_promisor_remote(struct repository *repo,\n \t\telems = strbuf_split(remotes[i], ',');\n \n \t\tfor (size_t j = 0; elems[j]; j++) {\n-\t\t\tint res;\n \t\t\tstrbuf_strip_suffix(elems[j], \",\");\n-\t\t\tres = skip_prefix(elems[j]->buf, \"name=\", &remote_name) ||\n+\t\t\tif (!skip_prefix(elems[j]->buf, \"name=\", &remote_name))\n \t\t\t\tskip_prefix(elems[j]->buf, \"url=\", &remote_url);\n-\t\t\tif (!res)\n-\t\t\t\twarning(_(\"unknown element '%s' from remote info\"),\n-\t\t\t\t\telems[j]->buf);\n \t\t}\n \n \t\tif (remote_name)\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex b35b774235..26f3c63112 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -289,6 +289,38 @@ 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 promisor.sendExtraFields\" '\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.id \"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+\tgit -C server config promisor.sendExtraFields \"id, partialCloneFilter\" &&\n+\ttest_when_finished \"git -C server config unset promisor.sendExtraFields\" &&\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 promisor.acceptfromserver=All \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that extra fields are properly transmitted\n+\tENCODED_URL=$(echo \"file://$(pwd)/lop\" | sed -e \"s/ /%20/g\") &&\n+\tPR1=\"name=lop,url=$ENCODED_URL,partialCloneFilter=blob:none\" &&\n+\tPR2=\"name=otherLop,url=https://invalid.invalid,id=fooBar,partialCloneFilter=blob:limit=10k\" &&\n+\ttest_grep \"clone< promisor-remote=$PR1;$PR2\" trace &&\n+\ttest_grep \"clone> promisor-remote=lop;otherLop\" 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 \n-- \n2.49.0.158.g6ac6832dc3.dirty\n\n"},{"id":"516114","messageId":"20250414160343.2216312-5-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250414160343.2216312-1-christian.couder@gmail.com","subject":"[PATCH 4/4] promisor-remote: allow a client to check extra fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-04-14T16:03:43Z","receivedAt":"2025-04-14T16:04:19Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"A previous commit allowed a server to pass extra fields through the\n\"promisor-remote\" protocol capability after the \"name\" and \"url\"\nfields.\n\nLet's make it possible for a client to check if these extra fields\nmatch what it expects before accepting a promisor remote.\n\nWe allow this by introducing a new \"promisor.checkExtraFields\"\nconfiguration variable. It should contain a comma or space\nseparated list of fields that will be checked.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    |  17 +++++\n promisor-remote.c                     | 102 +++++++++++++++++++++++---\n t/t5710-promisor-remote-capability.sh |  35 +++++++++\n 3 files changed, 143 insertions(+), 11 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex bc08999733..75207de2ac 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -43,3 +43,20 @@ promisor.acceptFromServer::\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+\n+promisor.checkExtraFields::\n+\tA comma or space separated list of additional remote related\n+\tfields that a client will check before accepting a promisor\n+\tremote. When a field named \"bar\" is part of this list and a\n+\tcorresponding \"remote.foo.bar\" config variable is set for\n+\tlocally for remote \"foo\", then the value of the\n+\t\"remote.foo.bar\" config variable will be checked against the\n+\tvalue of the \"bar\" field passed by the server through the\n+\t\"promisor-remote\" capability for the remote \"foo\". The remote\n+\t\"foo\" will be rejected if the values don't match. The fields\n+\tshould be passed by the server through the \"promisor-remote\"\n+\tcapability by using the `promisor.sendExtraFields` config\n+\tvariable, see above. The extra fields will be checked only if\n+\tthe `promisor.acceptFromServer` config variable, see above, is\n+\tnot set to \"None\". If is set to \"None\", this config variable\n+\twill have no effect. See linkgit:gitprotocol-v2[5].\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 424d88d4a1..3645093f2f 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -358,6 +358,19 @@ static struct string_list *extra_fields_sent(void)\n \treturn &fields_list;\n }\n \n+static struct string_list *extra_fields_checked(void)\n+{\n+\tstatic struct string_list fields_list = STRING_LIST_INIT_NODUP;\n+\tstatic int initialized = 0;\n+\n+\tif (!initialized) {\n+\t\tfields_from_config(&fields_list, \"promisor.checkExtraFields\");\n+\t\tinitialized = 1;\n+\t}\n+\n+\treturn &fields_list;\n+}\n+\n static void append_extra_fields(struct string_list *fields,\n \t\t\t\tstruct string_list *extra_fields,\n \t\t\t\tconst char *name)\n@@ -497,15 +510,65 @@ enum accept_promisor {\n \tACCEPT_ALL\n };\n \n+static int check_extra_field_one(struct string_list_item *item_value,\n+\t\t\t\t struct promisor_info *p)\n+{\n+\tstruct string_list_item *item;\n+\n+\titem = unsorted_string_list_lookup(&p->fields, item_value->string);\n+\tif (!item)\n+\t\treturn 0;\n+\n+\treturn !strcmp(item->util, item_value->util);\n+}\n+\n+\n+static int check_extra_field(struct string_list_item *item_value,\n+\t\t\t     struct promisor_info *p, int in_list)\n+{\n+\tif (!in_list)\n+\t\treturn check_extra_field_one(item_value, p);\n+\n+\tfor (; p; p = p->next)\n+\t\tif (check_extra_field_one(item_value, p))\n+\t\t\treturn 1;\n+\n+\treturn 0;\n+}\n+\n+static int check_all_extra_fields(struct string_list* extra_values,\n+\t\t\t\t  struct promisor_info *p,\n+\t\t\t\t  int in_list)\n+{\n+\tstruct string_list* fields_checked = extra_fields_checked();\n+\tstruct string_list_item *item_checked;\n+\n+\tstring_list_sort(extra_values);\n+\n+\tfor_each_string_list_item(item_checked, fields_checked) {\n+\t\tstruct string_list_item *item_value;\n+\n+\t\titem_value = string_list_lookup(extra_values, item_checked->string);\n+\t\tif (!item_value)\n+\t\t\treturn 0;\n+\t\tif (!check_extra_field(item_value, p, in_list))\n+\t\t\treturn 0;\n+\t}\n+\n+\treturn 1;\n+}\n+\n static int should_accept_remote(enum accept_promisor accept,\n-\t\t\t\tconst char *remote_name, const char *remote_url,\n+\t\t\t\tconst char *remote_name,\n+\t\t\t\tconst char *remote_url,\n+\t\t\t\tstruct string_list* extras,\n \t\t\t\tstruct promisor_info *info_list)\n {\n \tstruct promisor_info *p;\n \tconst char *local_url;\n \n \tif (accept == ACCEPT_ALL)\n-\t\treturn 1;\n+\t\treturn check_all_extra_fields(extras, info_list, 1);\n \n \tp = remote_nick_find(info_list, remote_name);\n \n@@ -514,7 +577,7 @@ static int should_accept_remote(enum accept_promisor accept,\n \t\treturn 0;\n \n \tif (accept == ACCEPT_KNOWN_NAME)\n-\t\treturn 1;\n+\t\treturn check_all_extra_fields(extras, p, 0);\n \n \tif (accept != ACCEPT_KNOWN_URL)\n \t\tBUG(\"Unhandled 'enum accept_promisor' value '%d'\", accept);\n@@ -530,7 +593,7 @@ static int should_accept_remote(enum accept_promisor accept,\n \tlocal_url = p->fields.items[1].util;\n \n \tif (!strcmp(local_url, remote_url))\n-\t\treturn 1;\n+\t\treturn check_all_extra_fields(extras, p, 0);\n \n \twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s'\"),\n \t\tremote_name, local_url, remote_url);\n@@ -564,9 +627,6 @@ static void filter_promisor_remote(struct repository *repo,\n \tif (accept == ACCEPT_NONE)\n \t\treturn;\n \n-\tif (accept != ACCEPT_ALL)\n-\t\tinfo_list = promisor_info_list(repo, NULL);\n-\n \t/* Parse remote info received */\n \n \tremotes = strbuf_split_str(info, ';', 0);\n@@ -577,14 +637,27 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tconst char *remote_url = NULL;\n \t\tchar *decoded_name = NULL;\n \t\tchar *decoded_url = NULL;\n+\t\tstruct string_list extras = STRING_LIST_INIT_NODUP;\n \n \t\tstrbuf_strip_suffix(remotes[i], \";\");\n \t\telems = strbuf_split(remotes[i], ',');\n \n \t\tfor (size_t j = 0; elems[j]; j++) {\n+\t\t\tchar *p;\n+\n \t\t\tstrbuf_strip_suffix(elems[j], \",\");\n-\t\t\tif (!skip_prefix(elems[j]->buf, \"name=\", &remote_name))\n-\t\t\t\tskip_prefix(elems[j]->buf, \"url=\", &remote_url);\n+\t\t\tif (skip_prefix(elems[j]->buf, \"name=\", &remote_name) ||\n+\t\t\t    skip_prefix(elems[j]->buf, \"url=\", &remote_url))\n+\t\t\t\tcontinue;\n+\n+\t\t\tp = strchr(elems[j]->buf, '=');\n+\t\t\tif (p) {\n+\t\t\t\t*p = '\\0';\n+\t\t\t\tstring_list_append(&extras, elems[j]->buf)->util = p + 1;\n+\t\t\t} else {\n+\t\t\t\twarning(_(\"invalid element '%s' from remote info\"),\n+\t\t\t\t\telems[j]->buf);\n+\t\t\t}\n \t\t}\n \n \t\tif (remote_name)\n@@ -592,9 +665,16 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tif (remote_url)\n \t\t\tdecoded_url = url_percent_decode(remote_url);\n \n-\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, info_list))\n-\t\t\tstrvec_push(accepted, decoded_name);\n+\t\tif (decoded_name) {\n+\t\t\tif (!info_list)\n+\t\t\t\tinfo_list = promisor_info_list(repo, extra_fields_checked());\n+\n+\t\t\tif (should_accept_remote(accept, decoded_name, decoded_url,\n+\t\t\t\t\t\t &extras, info_list))\n+\t\t\t\tstrvec_push(accepted, decoded_name);\n+\t\t}\n \n+\t\tstring_list_clear(&extras, 0);\n \t\tstrbuf_list_free(elems);\n \t\tfree(decoded_name);\n \t\tfree(decoded_url);\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex 26f3c63112..858b34587d 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -321,6 +321,41 @@ test_expect_success \"clone with promisor.sendExtraFields\" '\n \tcheck_missing_objects server 1 \"$oid\"\n '\n \n+test_expect_success \"clone with promisor.checkExtraFields\" '\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.id \"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+\tgit -C server config promisor.sendExtraFields \"id, partialCloneFilter\" &&\n+\ttest_when_finished \"git -C server config unset promisor.sendExtraFields\" &&\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.partialCloneFilter=\"blob:none\" \\\n+\t\t-c promisor.acceptfromserver=All \\\n+\t\t-c promisor.checkExtraFields=partialCloneFilter \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that extra fields are properly transmitted\n+\tENCODED_URL=$(echo \"file://$(pwd)/lop\" | sed -e \"s/ /%20/g\") &&\n+\tPR1=\"name=lop,url=$ENCODED_URL,partialCloneFilter=blob:none\" &&\n+\tPR2=\"name=otherLop,url=https://invalid.invalid,id=fooBar,partialCloneFilter=blob:limit=10k\" &&\n+\ttest_grep \"clone< promisor-remote=$PR1;$PR2\" trace &&\n+\ttest_grep \"clone> promisor-remote=lop\" trace &&\n+\ttest_grep ! \"clone> promisor-remote=lop;otherLop\" 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 \n-- \n2.49.0.158.g6ac6832dc3.dirty\n\n"},{"id":"516151","messageId":"xmqq34eapfhk.fsf@gitster.g","threadId":"63293","inReplyTo":"20250414160343.2216312-4-christian.couder@gmail.com","subject":"Re: [PATCH 3/4] promisor-remote: allow a server to advertise extra fields","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-04-14T22:04:07Z","receivedAt":"2025-04-14T22:04:11Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n>  \n> +promisor.sendExtraFields::\n> +\tA comma or space separated list of additional remote related\n> +\tfields that a server will send while advertising its promisor\n> +\tremotes using the \"promisor-remote\" capability, see\n> +\tlinkgit:gitprotocol-v2[5]. When a field named \"bar\" is part of\n> +\tthis list and a corresponding \"remote.foo.bar\" config variable\n> +\tis set on the server to a non empty value, for example \"baz\",\n> +\tthen the field and its value, so \"bar=baz\", will be sent when\n> +\tadvertising the promisor remote \"foo\". This list has no effect\n> +\tunless the \"promisor.advertise\" config variable, see above, is\n> +\tset to \"true\", and if that's the case, then whatever this list\n> +\tcontains, the \"name\" and \"url\" fields are advertised anyway\n> +\tand contain the remote name and URL respectively, so there is\n> +\tno need to add \"name\" or \"url\" to this list.\n\nAs a description of overall syntax of the protocol, this and ...\n\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> diff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\n> index 5598c93e67..f649745837 100644\n> --- a/Documentation/gitprotocol-v2.adoc\n> +++ b/Documentation/gitprotocol-v2.adoc\n> @@ -785,33 +785,39 @@ retrieving the header from a bundle at the indicated URI, and thus\n>  save themselves and the server(s) the request(s) needed to inspect the\n>  headers of that bundle or bundles.\n>  \n> -promisor-remote=<pr-infos>\n> +promisor-remote=<pr-info>\n>  ~~~~~~~~~~~~~~~~~~~~~~~~~~\n>  \n>  The server may advertise some promisor remotes it is using or knows\n>  about to a client which may want to use them as its promisor remotes,\n> -instead of this repository. In this case <pr-infos> should be of the\n> +instead of this repository. In this case <pr-info> should be of the\n>  form:\n>  \n> -\tpr-infos = pr-info | pr-infos \";\" pr-info\n> +\tpr-info = pr-fields | pr-info \";\" pr-info\n>  \n> -\tpr-info = \"name=\" pr-name | \"name=\" pr-name \",\" \"url=\" pr-url\n> +\tpr-fields = fld-key \"=\" fld-value | pr-fields \",\" pr-fields\n>  \n> -where `pr-name` is the urlencoded name of a promisor remote, and\n> -`pr-url` the urlencoded URL of that promisor remote.\n> +where all the `fld-key` and `fld-value` in a given `pr-fields` are\n> +field keys and values related to a single promisor remote.\n\n... this may work, but as we are defining protocol between two\nparties, in order to ensure interoperable reimplementations, we need\nto also specify semantics, what are the defined \"fields\", and what\neach of them mean.  Proposing nebulous \"with this framework your\nimagination is the limit, you can invent anything\" may work for\nother parts of the system, but not for the part that is about\ncommunication between two repositories.\n\nIOW, we shouldn't be internally calling these \"extra\".  From the\npoint of view of \"core\" they may be \"extra\", but to the developers\nand certainly to the end-users, they shouldn't be \"extra\" at all.\nThey are all supported parts of the system with defined semantics,\nright?\n\nAnother reason why I hate seeing this nebulous \"with this, we can\nsend anything extra\" is because such a thing will have a wrong\nsecurity posture.  If we truly *need* to be able to carry\n*anything*, we need to make sure how values are quoted/escaped, and\nthe code for dequoting/unescaping are robustly written to avoid\npassing malformed input and misinterpreting it as something else,\nwhich would give a new attack vector.  If we can enumerate supported\nfields, their syntax and their possible values, we can make the\nattack surface a lot smaller.\n\nThanks.\n"},{"id":"516487","messageId":"aAdrzlUX61TK1x8_@pks.im","threadId":"63293","inReplyTo":"20250414160343.2216312-3-christian.couder@gmail.com","subject":"Re: [PATCH 2/4] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-22T10:13:34Z","receivedAt":"2025-04-22T10:13:43Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Apr 14, 2025 at 06:03:41PM +0200, Christian Couder wrote:\n> diff --git a/promisor-remote.c b/promisor-remote.c\n> index 5801ebfd9b..0fb07f25af 100644\n> --- a/promisor-remote.c\n> +++ b/promisor-remote.c\n> @@ -314,10 +314,38 @@ static int allow_unsanitized(char ch)\n>  \treturn ch > 32 && ch < 127;\n>  }\n>  \n> -static void promisor_info_vecs(struct repository *repo,\n> -\t\t\t       struct strvec *names,\n> -\t\t\t       struct strvec *urls)\n> +/*\n> + * Linked list for promisor remotes.\n> + *\n> + * 'fields' should not be sorted, as we will rely on the order we put\n> + * things into it. So, for example, 'string_list_append()' should be\n> + * used instead of 'string_list_insert()'.\n> + */\n> +struct promisor_info {\n> +\tstruct promisor_info *next;\n> +\tstruct string_list fields;\n> +};\n> +\n> +static void free_info_list(struct promisor_info *p)\n\nNit: nowadays we would call this something like\n`promisor_info_list_free()`, with the name of the subsystem coming\nfirst.\n\n>  char *promisor_remote_info(struct repository *repo)\n>  {\n>  \tstruct strbuf sb = STRBUF_INIT;\n>  \tint advertise_promisors = 0;\n> -\tstruct strvec names = STRVEC_INIT;\n> -\tstruct strvec urls = STRVEC_INIT;\n> +\tstruct promisor_info *info_list;\n> +\tstruct promisor_info *r, *p;\n>  \n>  \tgit_config_get_bool(\"promisor.advertise\", &advertise_promisors);\n>  \n>  \tif (!advertise_promisors)\n>  \t\treturn NULL;\n>  \n> -\tpromisor_info_vecs(repo, &names, &urls);\n> +\tinfo_list = promisor_info_list(repo);\n>  \n> -\tif (!names.nr)\n> +\tif (!info_list)\n>  \t\treturn NULL;\n>  \n> -\tfor (size_t i = 0; i < names.nr; i++) {\n> -\t\tif (i)\n> +\tfor (p = NULL, r = info_list; r; p = r, r = r->next) {\n> +\t\tstruct string_list_item *item;\n> +\t\tint first = 1;\n> +\n> +\t\tif (r != info_list)\n>  \t\t\tstrbuf_addch(&sb, ';');\n> -\t\tstrbuf_addstr(&sb, \"name=\");\n> -\t\tstrbuf_addstr_urlencode(&sb, names.v[i], allow_unsanitized);\n> -\t\tstrbuf_addstr(&sb, \",url=\");\n> -\t\tstrbuf_addstr_urlencode(&sb, urls.v[i], allow_unsanitized);\n> +\n> +\t\tfor_each_string_list_item(item, &r->fields) {\n> +\t\t\tif (first)\n> +\t\t\t\tfirst = 0;\n> +\t\t\telse\n> +\t\t\t\tstrbuf_addch(&sb, ',');\n> +\t\t\tstrbuf_addf(&sb, \"%s=\", item->string);\n> +\t\t\tstrbuf_addstr_urlencode(&sb, (char *)item->util, allow_unsanitized);\n> +\t\t}\n>  \t}\n>  \n> -\tstrvec_clear(&names);\n> -\tstrvec_clear(&urls);\n> +\tfree_info_list(p);\n\nI don't quite follow the usage pattern of `info_list` here. My\nexpectation is that we'd:\n\n  1. Acquire the promisor info list.\n\n  2. Iterate through each of its items.\n\n  3. Free the complete list.\n\nBut why do we free `p` here? Shouldn't we free `info_list`? And if we\ndid so, can't we drop `p` completely and just iterate through the list\nvia `r`?\n\n>  \treturn strbuf_detach(&sb, NULL);\n>  }\n>  \n>  /*\n> - * Find first index of 'nicks' where there is 'nick'. 'nick' is\n> - * compared case sensitively to the strings in 'nicks'. If not found\n> - * 'nicks->nr' is returned.\n> + * Find first element of 'p' where the 'name' field is 'nick'. 'nick'\n> + * is compared case sensitively to the strings in 'p'. If not found\n> + * NULL is returned.\n>   */\n> -static size_t remote_nick_find(struct strvec *nicks, const char *nick)\n> +static struct promisor_info *remote_nick_find(struct promisor_info *p, const char *nick)\n>  {\n> -\tfor (size_t i = 0; i < nicks->nr; i++)\n> -\t\tif (!strcmp(nicks->v[i], nick))\n> -\t\t\treturn i;\n> -\treturn nicks->nr;\n> +\tfor (; p; p = p->next) {\n> +\t\tassert(!strcmp(p->fields.items[0].string, \"name\"));\n\nWhy do we add this assert now? And if we want to keep it, shouldn't it\nrather be `BUG()`?\n\n> @@ -414,11 +461,16 @@ static int should_accept_remote(enum accept_promisor accept,\n>  \t\treturn 0;\n>  \t}\n>  \n> -\tif (!strcmp(urls->v[i], remote_url))\n> +\tif (strcmp(p->fields.items[1].string, \"url\"))\n> +\t\tBUG(\"Bad info_list for remote '%s'\", remote_name);\n\nIt feels somewhat fragile to assume hardcoded locations of each of the\nkeys in `fields.items`. Would it be preferable to instead have a\nfunction that looks up the index by key?\n\nPatrick\n"},{"id":"516488","messageId":"aAdr2Q9jthNV699i@pks.im","threadId":"63293","inReplyTo":"xmqq34eapfhk.fsf@gitster.g","subject":"Re: [PATCH 3/4] promisor-remote: allow a server to advertise extra fields","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-22T10:13:45Z","receivedAt":"2025-04-22T10:13:48Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Apr 14, 2025 at 03:04:07PM -0700, Junio C Hamano wrote:\n> Christian Couder <christian.couder@gmail.com> writes:\n> \n> >  \n> > +promisor.sendExtraFields::\n> > +\tA comma or space separated list of additional remote related\n> > +\tfields that a server will send while advertising its promisor\n> > +\tremotes using the \"promisor-remote\" capability, see\n> > +\tlinkgit:gitprotocol-v2[5]. When a field named \"bar\" is part of\n> > +\tthis list and a corresponding \"remote.foo.bar\" config variable\n> > +\tis set on the server to a non empty value, for example \"baz\",\n> > +\tthen the field and its value, so \"bar=baz\", will be sent when\n> > +\tadvertising the promisor remote \"foo\". This list has no effect\n> > +\tunless the \"promisor.advertise\" config variable, see above, is\n> > +\tset to \"true\", and if that's the case, then whatever this list\n> > +\tcontains, the \"name\" and \"url\" fields are advertised anyway\n> > +\tand contain the remote name and URL respectively, so there is\n> > +\tno need to add \"name\" or \"url\" to this list.\n> \n> As a description of overall syntax of the protocol, this and ...\n> \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> > diff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\n> > index 5598c93e67..f649745837 100644\n> > --- a/Documentation/gitprotocol-v2.adoc\n> > +++ b/Documentation/gitprotocol-v2.adoc\n> > @@ -785,33 +785,39 @@ retrieving the header from a bundle at the indicated URI, and thus\n> >  save themselves and the server(s) the request(s) needed to inspect the\n> >  headers of that bundle or bundles.\n> >  \n> > -promisor-remote=<pr-infos>\n> > +promisor-remote=<pr-info>\n> >  ~~~~~~~~~~~~~~~~~~~~~~~~~~\n> >  \n> >  The server may advertise some promisor remotes it is using or knows\n> >  about to a client which may want to use them as its promisor remotes,\n> > -instead of this repository. In this case <pr-infos> should be of the\n> > +instead of this repository. In this case <pr-info> should be of the\n> >  form:\n> >  \n> > -\tpr-infos = pr-info | pr-infos \";\" pr-info\n> > +\tpr-info = pr-fields | pr-info \";\" pr-info\n> >  \n> > -\tpr-info = \"name=\" pr-name | \"name=\" pr-name \",\" \"url=\" pr-url\n> > +\tpr-fields = fld-key \"=\" fld-value | pr-fields \",\" pr-fields\n> >  \n> > -where `pr-name` is the urlencoded name of a promisor remote, and\n> > -`pr-url` the urlencoded URL of that promisor remote.\n> > +where all the `fld-key` and `fld-value` in a given `pr-fields` are\n> > +field keys and values related to a single promisor remote.\n> \n> ... this may work, but as we are defining protocol between two\n> parties, in order to ensure interoperable reimplementations, we need\n> to also specify semantics, what are the defined \"fields\", and what\n> each of them mean.  Proposing nebulous \"with this framework your\n> imagination is the limit, you can invent anything\" may work for\n> other parts of the system, but not for the part that is about\n> communication between two repositories.\n> \n> IOW, we shouldn't be internally calling these \"extra\".  From the\n> point of view of \"core\" they may be \"extra\", but to the developers\n> and certainly to the end-users, they shouldn't be \"extra\" at all.\n> They are all supported parts of the system with defined semantics,\n> right?\n> \n> Another reason why I hate seeing this nebulous \"with this, we can\n> send anything extra\" is because such a thing will have a wrong\n> security posture.  If we truly *need* to be able to carry\n> *anything*, we need to make sure how values are quoted/escaped, and\n> the code for dequoting/unescaping are robustly written to avoid\n> passing malformed input and misinterpreting it as something else,\n> which would give a new attack vector.  If we can enumerate supported\n> fields, their syntax and their possible values, we can make the\n> attack surface a lot smaller.\n\nI agree that we should properly specify which fields are accepted and\nwhat their respective format as well as semantics would be. Otherwise,\nwithout such a definition, hosting sites may eventually end up with\nslightly incompatible semantics. I also don't expect that there should\nbe all that many fields.\n\nThis raises a question though: what would happen if a field was\nadvertised that the client doesn't understand? Should the client simply\nignore such a field? Should they bail out? I think we need to also think\nabout this edge case and specify client-side behaviour. I think in the\nend, both ways would be rather limiting:\n\n  - If we simply ignored all unknown fields our hands might be bound if\n    we ever had to introduce changes that aren't backwards compatible.\n\n  - If we always bail out on an unknown field our hands would be bound\n    equally, as we cannot ever introduce a new field.\n\nWhich raises the question whether we need to be able to dynamically\nfigure out fields. This could be in the form of capability negotiation\nor protocol versions. But in any case, I think we need to have something\nready so that we can change behaviour depending on which features are\nsupported by a client.\n\nPatrick\n"},{"id":"516951","messageId":"20250429145243.992252-1-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250414160343.2216312-1-christian.couder@gmail.com","subject":"[PATCH v2 0/3] Make the \"promisor-remote\" capability support more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-04-29T14:52:40Z","receivedAt":"2025-04-29T14:53:16Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"The \"promisor-remote\" capability can only be used to pass the names\nand URLs of the promisor remotes from the server to the client. After\nthat the client can use this information to decide if it accepts the\nremotes or not.\n\nIt would be nice if the server could pass more fields about its\nremotes and if the client could use that additional information to\ndecide about the remotes by comparing it with its local information\nabout the remotes.\n\nThis patch series implements this by adding the \"promisor.sendFields\"\non the server side and the \"promisor.checkFields\" on the client side.\n\nFor example, if \"promisor.sendFields\" is set to \"partialCloneFilter\",\nand the server has the remote \"foo\" configured like this:\n\n[remote \"foo\"]\n        url = file:///tmp/foo.git\n\tpartialCloneFilter = blob:none\n\nthen \"name=foo,url=file:///tmp/foo.git,partialCloneFilter=blob:none\"\nwill be sent by the server for this remote.\n\nAll the information passed through the \"promisor-remote\" capability is\nstill only used to decide if the remotes are accepted or not. The\nclient doesn't store it and doesn't use it for any other purpose.\n\nThe fields that can be passed are limited to \"partialCloneFilter\" and\n\"token\".\n\nOn the technical side, we get rid of 'struct strvec' and we use mostly\n'struct string_list' instead, which is more flexible. This matches\nsome suggestions made when the series that introduced the\n\"promisor-remote\" capability was reviewed.\n\nRight now in this series, the fields names (like \"partialCloneFilter\"\nin the example) that are passed to the client are compared case\nsensitively to the local config keys. I think this is a bug and they\nshould be compared case insensitively, (while values should still be\ncompared case sensitively). I am planning to fix this in the next\niteration of this series. I am also planning to add more tests then.\n\nChanges since v1\n----------------\n\nThanks to Patrick and Junio for their comments on the v1.\n\n* Patch 1/4 in v1 has been removed. It isn't necessary anymore because\n  we limit the possible field names to some harcoded values. So we\n  don't need to check if the field names contain only valid\n  characters.\n\n* In patch 1/3, the commit message has been improved to explain why we\n  use \"field\" and to tell that they should correspond to config\n  variables.\n\n* In patch 1/3, the following improvements have been made to the code:\n\n  - the comment before the definition of 'struct promisor_info' has\n    been improved to better describe the fields and their possible\n    names\n\n  - free_info_list() has been renamed promisor_info_list_free()\n\n  - field names are now compared case insensitively by setting the\n    'cmp' member of 'struct string_list' to 'strcasecmp'\n\n  - a memory leak in promisor_info_vecs() has been fixed and the code\n    simplified, as we used to free only a single 'struct\n    promisor_info' instance instead of all the instances\n\n  - an assert() has been replaced by a BUG()\n\n  - a BUG() message has been improved\n\n* In patch 2/3, there are a number of general changes:\n\n  - the name of the \"promisor.sendExtraFields\" config variable has\n    been changed to \"promisor.sendFields\"\n\n  - in general we just talk about \"fields\" and not \"extra fields\"\n\n  - only the \"partialCloneFilter\" and \"token\" fields are allowed on\n    top of the pre-existing \"name\" and \"url\" fields\n\n* In patch 2/3, in \"Documentation/gitprotocol-v2.adoc\", the\n  documentation of the \"promisor-remote\" capability has been improved\n  in a few ways:\n\n  - `fld-key` has been renamed `fld-name` to be consistent with\n    everywhere else where we talk about field names\n\n  - it's stated that \"Clients SHOULD ignore fields they don't\n    recognize to allow for future protocol extensions\"\n\n* In patch 2/3, the following improvements have been made to the code:\n\n  - a `static const char *allowed_fields[]` is used to store the\n    allowed field names in an extensible way\n\n  - a new is_allowed_field() function is introduced and used in\n    valid_field() to check if a field name is valid\n\n  - \"extra\" has been removed from all function and variable names\n\n* In patch 2/3, the tests have been improved to use \"token\" instead of\n  \"id\" as \"token\" is among the list of allowed field names.\n\n* In patch 3/3, there are the same general changes as in patch 2/3.\n\n* In patch 3/3, the commit message says that limiting the fields\n  allows both server and client have a shared understanding of field\n  semantics and usage.\n\n* In patch 3/3, the following improvements have been made to the code:\n\n  - \"extra\" has been removed from all function and variable names\n\n  - field names are compared case insensitively by setting the 'cmp'\n    member of 'struct string_list' to 'strcasecmp'\n  \n* In patch 3/3, the tests have been improved to use \"token\" instead of\n  \"id\" as \"token\" is among the list of allowed field names.\n\nCI tests\n--------\n\nHere they are:\n\nhttps://github.com/chriscool/git/actions/runs/14729278162\n\nThey have all passed except the \"pedantic(fedora:latest)\" one which\nfailed with the following error:\n\n  t/unit-tests/generate-clar-suites.sh: line 63: awk: command not found\n\nRange diff compared to v1\n-------------------------\n\n1:  fe49a861b9 < -:  ---------- config: move is_config_key_char() to \"config.h\"\n2:  6ebff6d784 ! 1:  d4e12afbc1 promisor-remote: refactor to get rid of 'struct strvec'\n    @@ Commit message\n         stores the field names, like \"name\" and \"url\", in the 'string' member\n         of its items, and the field values in the 'util' member of its items.\n     \n    +    Except for \"name\", each \"<field_name>/<field_value>\" pair should\n    +    correspond to a \"remote.<name>.<field_name>\" config variable set to\n    +    <field_value> where \"<name>\" is a promisor remote name.\n    +\n    +    Previously in Git, the part after the last dot in a configuration\n    +    variable key, for example \"c\" for \"a.b.c\", was called the \"variable\n    +    name part\" of a configuration key. It would be very confusing to use\n    +    \"variable name part\" or some similar terms in the context of the\n    +    'promisor-remote' protocol though, so let's forget about it, and just\n    +    use \"field\", \"field name\" and \"field value\" instead.\n    +\n         Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n     \n      ## promisor-remote.c ##\n    @@ promisor-remote.c: static int allow_unsanitized(char ch)\n     -\t\t\t       struct strvec *names,\n     -\t\t\t       struct strvec *urls)\n     +/*\n    -+ * Linked list for promisor remotes.\n    ++ * Linked list for promisor remotes involved in the \"promisor-remote\"\n    ++ * protocol capability.\n    ++ *\n    ++ * 'fields' contains a defined set of field name/value pairs for\n    ++ * each promisor remote. Field names are stored in the 'string'\n    ++ * member, and values in the 'util' member.\n    ++ *\n    ++ * Currently supported field names:\n    ++ * - \"name\": The name of the promisor remote.\n    ++ * - \"url\": The URL of the promisor remote.\n    ++ *\n    ++ * Except for \"name\", each \"<field_name>/<field_value>\" pair should\n    ++ * correspond to a \"remote.<name>.<field_name>\" config variable set to\n    ++ * <field_value> where \"<name>\" is a promisor remote name.\n     + *\n     + * 'fields' should not be sorted, as we will rely on the order we put\n     + * things into it. So, for example, 'string_list_append()' should be\n    @@ promisor-remote.c: static int allow_unsanitized(char ch)\n     +\tstruct string_list fields;\n     +};\n     +\n    -+static void free_info_list(struct promisor_info *p)\n    ++static void promisor_info_list_free(struct promisor_info *p)\n     +{\n     +\tstruct promisor_info *next;\n     +\n    @@ promisor-remote.c: static void promisor_info_vecs(struct repository *repo,\n     +\t\t\tstruct promisor_info *new_info = xcalloc(1, sizeof(*new_info));\n     +\n     +\t\t\tstring_list_init_dup(&new_info->fields);\n    ++\t\t\tnew_info->fields.cmp = strcasecmp;\n     +\n     +\t\t\tstring_list_append(&new_info->fields, \"name\")->util = (char *)r->name;\n     +\t\t\tstring_list_append(&new_info->fields, \"url\")->util = (char *)url;\n    @@ promisor-remote.c: static void promisor_info_vecs(struct repository *repo,\n     -\tstruct strvec names = STRVEC_INIT;\n     -\tstruct strvec urls = STRVEC_INIT;\n     +\tstruct promisor_info *info_list;\n    -+\tstruct promisor_info *r, *p;\n    ++\tstruct promisor_info *r;\n      \n      \tgit_config_get_bool(\"promisor.advertise\", &advertise_promisors);\n      \n    @@ promisor-remote.c: static void promisor_info_vecs(struct repository *repo,\n      \n     -\tfor (size_t i = 0; i < names.nr; i++) {\n     -\t\tif (i)\n    -+\tfor (p = NULL, r = info_list; r; p = r, r = r->next) {\n    ++\tfor (r = info_list; r; r = r->next) {\n     +\t\tstruct string_list_item *item;\n     +\t\tint first = 1;\n     +\n    @@ promisor-remote.c: static void promisor_info_vecs(struct repository *repo,\n      \n     -\tstrvec_clear(&names);\n     -\tstrvec_clear(&urls);\n    -+\tfree_info_list(p);\n    ++\tpromisor_info_list_free(info_list);\n      \n      \treturn strbuf_detach(&sb, NULL);\n      }\n    @@ promisor-remote.c: static void promisor_info_vecs(struct repository *repo,\n     -\t\t\treturn i;\n     -\treturn nicks->nr;\n     +\tfor (; p; p = p->next) {\n    -+\t\tassert(!strcmp(p->fields.items[0].string, \"name\"));\n    ++\t\tif (strcmp(p->fields.items[0].string, \"name\"))\n    ++\t\t\tBUG(\"First field of promisor info should be 'name', but was '%s'.\",\n    ++\t\t\t    p->fields.items[0].string);\n     +\t\tif (!strcmp(p->fields.items[0].util, nick))\n     +\t\t\treturn p;\n     +\t}\n    @@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n      \n     -\tif (!strcmp(urls->v[i], remote_url))\n     +\tif (strcmp(p->fields.items[1].string, \"url\"))\n    -+\t\tBUG(\"Bad info_list for remote '%s'\", remote_name);\n    ++\t\tBUG(\"Bad info_list for remote '%s'.\\n\"\n    ++\t\t    \"Second field of promisor info should be 'url', but was '%s'.\",\n    ++\t\t    remote_name, p->fields.items[1].string);\n     +\n     +\tlocal_url = p->fields.items[1].util;\n     +\n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n      \n     -\tstrvec_clear(&names);\n     -\tstrvec_clear(&urls);\n    -+\tfree_info_list(info_list);\n    ++\tpromisor_info_list_free(info_list);\n      \tstrbuf_list_free(remotes);\n      }\n      \n3:  f8b875d1a7 ! 2:  d6d4f99768 promisor-remote: allow a server to advertise extra fields\n    @@ Metadata\n     Author: Christian Couder <chriscool@tuxfamily.org>\n     \n      ## Commit message ##\n    -    promisor-remote: allow a server to advertise extra fields\n    +    promisor-remote: allow a server to advertise more fields\n     \n         For now the \"promisor-remote\" protocol capability can only pass \"name\"\n         and \"url\" information from a server to a client in the form\n         \"name=<remote_name>,url=<remote_url>\".\n     \n         Let's make it possible to pass more information by introducing a new\n    -    \"promisor.sendExtraFields\" configuration variable. This variable\n    -    should contain a comma or space separated list of fields that will be\n    -    looked up in the configuration of the remote on the server to find the\n    -    values that will be passed to the client.\n    +    \"promisor.sendFields\" configuration variable. This variable should\n    +    contain a comma or space separated list of fields that will be looked\n    +    up in the configuration of the remote on the server to find the values\n    +    that will be passed to the client.\n     \n    -    For example if \"promisor.sendExtraFields\" is set to \"partialCloneFilter\",\n    +    Only a set of predefined fields are allowed. The only fields in this\n    +    set are \"partialCloneFilter\" and \"token\".\n    +\n    +    For example if \"promisor.sendFields\" is set to \"partialCloneFilter\",\n         and the server has the \"remote.<name>.partialCloneFilter\" config\n         variable set to a value for a remote, then that value will be passed\n         in the form \"partialCloneFilter=<value>\" after the \"name\" and \"url\"\n         fields.\n     \n    -    A following commit will allow the client to use the extra information\n    -    to decide if it accepts the remote or not. For now the client doesn't\n    -    do anything with the extra information it receives.\n    +    A following commit will allow the client to use the information to\n    +    decide if it accepts the remote or not. For now the client doesn't do\n    +    anything with the additional information it receives.\n     \n         Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n     \n    @@ Documentation/config/promisor.adoc: promisor.advertise::\n      \t\"false\", which means the \"promisor-remote\" capability is not\n      \tadvertised.\n      \n    -+promisor.sendExtraFields::\n    ++promisor.sendFields::\n     +\tA comma or space separated list of additional remote related\n     +\tfields that a server will send while advertising its promisor\n     +\tremotes using the \"promisor-remote\" capability, see\n    -+\tlinkgit:gitprotocol-v2[5]. When a field named \"bar\" is part of\n    -+\tthis list and a corresponding \"remote.foo.bar\" config variable\n    -+\tis set on the server to a non empty value, for example \"baz\",\n    -+\tthen the field and its value, so \"bar=baz\", will be sent when\n    -+\tadvertising the promisor remote \"foo\". This list has no effect\n    -+\tunless the \"promisor.advertise\" config variable, see above, is\n    -+\tset to \"true\", and if that's the case, then whatever this list\n    -+\tcontains, the \"name\" and \"url\" fields are advertised anyway\n    -+\tand contain the remote name and URL respectively, so there is\n    -+\tno need to add \"name\" or \"url\" to this list.\n    ++\tlinkgit:gitprotocol-v2[5]. Currently, only the\n    ++\t\"partialCloneFilter\" and \"token\" fields are supported. The\n    ++\t\"partialCloneFilter\" field contains the partial clone filter\n    ++\tused for the remote, and the \"token\" field contains an\n    ++\tauthentication token for the remote.\n    +++\n    ++When a field is part of this list and a corresponding\n    ++\"remote.foo.<field>\" config variable is set on the server to a\n    ++non-empty value, then the field and its value will be sent when\n    ++advertising the promisor remote \"foo\". This list has no effect unless\n    ++the \"promisor.advertise\" config variable is set to \"true\", and the\n    ++\"name\" and \"url\" fields are always advertised regardless of this\n    ++setting.\n     +\n      promisor.acceptFromServer::\n      \tIf set to \"all\", a client will accept all the promisor remotes\n    @@ Documentation/gitprotocol-v2.adoc: retrieving the header from a bundle at the in\n     +\tpr-info = pr-fields | pr-info \";\" pr-info\n      \n     -\tpr-info = \"name=\" pr-name | \"name=\" pr-name \",\" \"url=\" pr-url\n    -+\tpr-fields = fld-key \"=\" fld-value | pr-fields \",\" pr-fields\n    ++\tpr-fields = fld-name \"=\" fld-value | pr-fields \",\" pr-fields\n      \n     -where `pr-name` is the urlencoded name of a promisor remote, and\n     -`pr-url` the urlencoded URL of that promisor remote.\n    -+where all the `fld-key` and `fld-value` in a given `pr-fields` are\n    -+field keys and values related to a single promisor remote.\n    ++where all the `fld-name` and `fld-value` in a given `pr-fields` are\n    ++field names and values related to a single promisor remote.\n      \n     -In this case, if the client decides to use one or more promisor\n     -remotes the server advertised, it can reply with\n     -\"promisor-remote=<pr-names>\" where <pr-names> should be of the form:\n    -+The server MUST advertise at least the \"name\" and \"url\" field keys\n    ++The server MUST advertise at least the \"name\" and \"url\" field names\n     +along with the associated field values, which are the name of a valid\n     +remote and its URL, in each `pr-fields`.\n      \n     -\tpr-names = pr-name | pr-names \";\" pr-name\n    -+`fld-key` MUST start with an alphabetic character and contain only\n    -+alphanumeric or '-' characters, and `fld-value` MUST be urlencoded.\n    ++The server MAY advertise the following optional fields:\n    ++\n    ++- \"partialCloneFilter\": Filter used for partial clone, corresponding\n    ++  to the \"remote.<name>.partialCloneFilter\" config setting.\n    ++- \"token\": Authentication token for the remote, corresponding\n    ++  to the \"remote.<name>.token\" config setting.\n    ++\n    ++No other fields are defined by the protocol at this time. Clients SHOULD\n    ++ignore fields they don't recognize to allow for 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    ++\n    ++Field values MUST be urlencoded.\n     +\n     +If the client decides to use one or more promisor remotes the server\n     +advertised, it can reply with \"promisor-remote=<pr-names>\" where\n    @@ Documentation/gitprotocol-v2.adoc: In this case, or if the client doesn't want t\n     -The \"promisor.advertise\" and \"promisor.acceptFromServer\" configuration\n     -options can be used on the server and client side to control what they\n     -advertise or accept respectively. See the documentation of these\n    --configuration options for more information.\n    -+On the server side, the \"promisor.advertise\" and\n    -+\"promisor.sendExtraFields\" configuration options can be used to\n    -+control what it advertises. On the client side, the\n    -+\"promisor.acceptFromServer\" configuration option can be used to\n    -+control what it accepts. See the documentation of these configuration\n    -+options for more information.\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      \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     \n      ## promisor-remote.c ##\n     @@ promisor-remote.c: static int allow_unsanitized(char ch)\n    @@ promisor-remote.c: static int allow_unsanitized(char ch)\n      }\n      \n     +/*\n    -+ * A valid extra field \"foo\" should correspond to a\n    -+ * \"remote.<name>.foo\" config variable, so, like config variables\n    -+ * keys, it should start with an alphabetic character and otherwise\n    -+ * each character should satisfy is_config_key_char().\n    ++ * List of field names allowed to be used in the \"promisor-remote\"\n    ++ * protocol capability. Each field should correspond to a configurable\n    ++ * property of a remote that can be relevant for the client.\n    ++ */\n    ++static const char *allowed_fields[] = {\n    ++\t\"partialCloneFilter\", /* Filter used for partial clone */\n    ++\t\"token\",              /* Authentication token for the remote */\n    ++\tNULL\n    ++};\n    ++\n    ++/*\n    ++ * Check if 'field' is in the list of allowed field names for the\n    ++ * \"promisor-remote\" protocol capability.\n     + */\n    -+static int valid_extra_field(struct string_list_item *item, void *cb_data)\n    ++static int is_allowed_field(const char *field)\n    ++{\n    ++\tconst char **p;\n    ++\n    ++\tfor (p = allowed_fields; *p; p++)\n    ++\t\tif (!strcasecmp(*p, field))\n    ++\t\t\treturn 1;\n    ++\treturn 0;\n    ++}\n    ++\n    ++static int valid_field(struct string_list_item *item, void *cb_data)\n     +{\n     +\tconst char *field = item->string;\n     +\tconst char *config_key = (const char *)cb_data;\n     +\n    -+\tfor (size_t i = 0; field[i]; i++)\n    -+\t\tif (i ? !is_config_key_char(field[i]) : !isalpha(field[i])) {\n    -+\t\t\twarning(_(\"invalid field '%s' in '%s' config\"), field, config_key);\n    -+\t\t\treturn 0;\n    -+\t\t}\n    ++\tif (!is_allowed_field(field)) {\n    ++\t\twarning(_(\"unsupported field '%s' in '%s' config\"), field, config_key);\n    ++\t\treturn 0;\n    ++\t}\n     +\treturn 1;\n     +}\n     +\n     +static char *fields_from_config(struct string_list *fields_list, const char *config_key)\n     +{\n    -+\tchar *extras = NULL;\n    ++\tchar *fields = NULL;\n     +\n    -+\tif (!git_config_get_string(config_key, &extras) && *extras) {\n    -+\t\tstring_list_split_in_place(fields_list, extras, \", \", -1);\n    -+\t\tfilter_string_list(fields_list, 0, valid_extra_field, (void *)config_key);\n    ++\tif (!git_config_get_string(config_key, &fields) && *fields) {\n    ++\t\tstring_list_split_in_place(fields_list, fields, \", \", -1);\n    ++\t\tfilter_string_list(fields_list, 0, valid_field, (void *)config_key);\n     +\t}\n     +\n    -+\treturn extras;\n    ++\treturn fields;\n     +}\n     +\n    -+static struct string_list *extra_fields_sent(void)\n    ++static struct string_list *fields_sent(void)\n     +{\n     +\tstatic struct string_list fields_list = STRING_LIST_INIT_NODUP;\n     +\tstatic int initialized = 0;\n     +\n     +\tif (!initialized) {\n    -+\t\tfields_from_config(&fields_list, \"promisor.sendExtraFields\");\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     +}\n     +\n    -+static void append_extra_fields(struct string_list *fields,\n    -+\t\t\t\tstruct string_list *extra_fields,\n    -+\t\t\t\tconst char *name)\n    ++static void append_fields(struct string_list *fields,\n    ++\t\t\t  struct string_list *field_names,\n    ++\t\t\t  const char *name)\n     +{\n     +\tstruct string_list_item *item;\n     +\n    -+\tfor_each_string_list_item(item, extra_fields) {\n    ++\tfor_each_string_list_item(item, field_names) {\n     +\t\tchar *key = xstrfmt(\"remote.%s.%s\", name, item->string);\n     +\t\tconst char *val;\n     +\t\tif (!git_config_get_string_tmp(key, &val) && *val)\n    @@ promisor-remote.c: static int allow_unsanitized(char ch)\n     +}\n     +\n      /*\n    -  * Linked list for promisor remotes.\n    +  * Linked list for promisor remotes involved in the \"promisor-remote\"\n    +  * protocol capability.\n    +@@ promisor-remote.c: static int allow_unsanitized(char ch)\n    +  * member, and values in the 'util' member.\n    +  *\n    +  * Currently supported field names:\n    +- * - \"name\": The name of the promisor remote.\n    +- * - \"url\": The URL of the promisor remote.\n    ++ * - \"name\": The name of the promisor remote,\n    ++ * - \"url\": The URL of the promisor remote,\n    ++ * - the fields in 'allowed_fields[]' above.\n       *\n    -@@ promisor-remote.c: static void free_info_list(struct promisor_info *p)\n    +  * Except for \"name\", each \"<field_name>/<field_value>\" pair should\n    +  * correspond to a \"remote.<name>.<field_name>\" config variable set to\n    +@@ promisor-remote.c: static void promisor_info_list_free(struct promisor_info *p)\n       * remotes. For each promisor remote, some of its fields, starting\n       * with \"name\" and \"url\", are put in the 'fields' string_list.\n       */\n     -static struct promisor_info *promisor_info_list(struct repository *repo)\n     +static struct promisor_info *promisor_info_list(struct repository *repo,\n    -+\t\t\t\t\t\tstruct string_list *extra_fields)\n    ++\t\t\t\t\t\tstruct string_list *field_names)\n      {\n      \tstruct promisor_info *infos = NULL;\n      \tstruct promisor_info **last_info = &infos;\n    @@ promisor-remote.c: static struct promisor_info *promisor_info_list(struct reposi\n      \t\t\tstring_list_append(&new_info->fields, \"name\")->util = (char *)r->name;\n      \t\t\tstring_list_append(&new_info->fields, \"url\")->util = (char *)url;\n      \n    -+\t\t\tif (extra_fields)\n    -+\t\t\t\tappend_extra_fields(&new_info->fields, extra_fields, r->name);\n    ++\t\t\tif (field_names)\n    ++\t\t\t\tappend_fields(&new_info->fields, field_names, r->name);\n     +\n      \t\t\t*last_info = new_info;\n      \t\t\tlast_info = &new_info->next;\n    @@ promisor-remote.c: char *promisor_remote_info(struct repository *repo)\n      \t\treturn NULL;\n      \n     -\tinfo_list = promisor_info_list(repo);\n    -+\tinfo_list = promisor_info_list(repo, extra_fields_sent());\n    ++\tinfo_list = promisor_info_list(repo, fields_sent());\n      \n      \tif (!info_list)\n      \t\treturn NULL;\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 promisor.sendExtraFields\" '\n    ++test_expect_success \"clone with promisor.sendFields\" '\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.id \"fooBar\" &&\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    -+\tgit -C server config promisor.sendExtraFields \"id, partialCloneFilter\" &&\n    -+\ttest_when_finished \"git -C server config unset promisor.sendExtraFields\" &&\n    ++\tgit -C server config promisor.sendFields \"token, partialCloneFilter\" &&\n    ++\ttest_when_finished \"git -C server config unset promisor.sendFields\" &&\n     +\ttest_when_finished \"rm trace\" &&\n     +\n     +\t# Clone from server to create a client\n    @@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'KnownUrl\n     +\t\t-c promisor.acceptfromserver=All \\\n     +\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n     +\n    -+\t# Check that extra fields are properly transmitted\n    ++\t# Check that fields are properly transmitted\n     +\tENCODED_URL=$(echo \"file://$(pwd)/lop\" | sed -e \"s/ /%20/g\") &&\n     +\tPR1=\"name=lop,url=$ENCODED_URL,partialCloneFilter=blob:none\" &&\n    -+\tPR2=\"name=otherLop,url=https://invalid.invalid,id=fooBar,partialCloneFilter=blob:limit=10k\" &&\n    ++\tPR2=\"name=otherLop,url=https://invalid.invalid,token=fooBar,partialCloneFilter=blob:limit=10k\" &&\n     +\ttest_grep \"clone< promisor-remote=$PR1;$PR2\" trace &&\n     +\ttest_grep \"clone> promisor-remote=lop;otherLop\" trace &&\n     +\n4:  d3b09c1afe ! 3:  09af0369a6 promisor-remote: allow a client to check extra fields\n    @@ Metadata\n     Author: Christian Couder <chriscool@tuxfamily.org>\n     \n      ## Commit message ##\n    -    promisor-remote: allow a client to check extra fields\n    +    promisor-remote: allow a client to check fields\n     \n    -    A previous commit allowed a server to pass extra fields through the\n    -    \"promisor-remote\" protocol capability after the \"name\" and \"url\"\n    -    fields.\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    -    Let's make it possible for a client to check if these extra fields\n    -    match what it expects before accepting a promisor remote.\n    +    Let's make it possible for a client to check if these fields match\n    +    what it expects before accepting a promisor remote.\n     \n    -    We allow this by introducing a new \"promisor.checkExtraFields\"\n    -    configuration variable. It should contain a comma or space\n    -    separated list of fields that will be checked.\n    +    We allow this by introducing a new \"promisor.checkFields\"\n    +    configuration variable. It should contain a comma or space separated\n    +    list of fields that will be checked.\n    +\n    +    By limiting the protocol to specific well-defined fields, we ensure\n    +    both server and client have a shared understanding of field\n    +    semantics and usage.\n     \n         Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n     \n    @@ Documentation/config/promisor.adoc: 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.checkExtraFields::\n    ++promisor.checkFields::\n     +\tA comma or space separated list of additional remote related\n     +\tfields that a client will check before accepting a promisor\n    -+\tremote. When a field named \"bar\" is part of this list and a\n    -+\tcorresponding \"remote.foo.bar\" config variable is set for\n    -+\tlocally for remote \"foo\", then the value of the\n    -+\t\"remote.foo.bar\" config variable will be checked against the\n    -+\tvalue of the \"bar\" field passed by the server through the\n    -+\t\"promisor-remote\" capability for the remote \"foo\". The remote\n    -+\t\"foo\" will be rejected if the values don't match. The fields\n    -+\tshould be passed by the server through the \"promisor-remote\"\n    -+\tcapability by using the `promisor.sendExtraFields` config\n    -+\tvariable, see above. The extra fields will be checked only if\n    -+\tthe `promisor.acceptFromServer` config variable, see above, is\n    -+\tnot set to \"None\". If is set to \"None\", this config variable\n    -+\twill have no effect. See linkgit:gitprotocol-v2[5].\n    ++\tremote. Currently, only the \"partialCloneFilter\" and \"token\"\n    ++\tfields are supported.\n    +++\n    ++When a field is part of this list and a corresponding\n    ++\"remote.foo.<field>\" config variable is set locally for remote \"foo\",\n    ++then the value of this config variable will be checked against the\n    ++value of the same field passed by the server for the remote \"foo\". The\n    ++remote \"foo\" will be rejected if the values don't match.\n    +++\n    ++For the \"partialCloneFilter\" field, this allows the client to ensure\n    ++that the server's filter matches what it expects locally, preventing\n    ++inconsistencies in filtering behavior. For the \"token\" field, this can\n    ++be used to verify that authentication credentials match expected\n    ++values.\n    +++\n    ++The fields should be passed by the server through the\n    ++\"promisor-remote\" capability by using the `promisor.sendFields` config\n    ++variable. The fields will be checked only if the\n    ++`promisor.acceptFromServer` config variable is not set to \"None\".  If\n    ++set to \"None\", this config variable will have no effect.  See\n    ++linkgit:gitprotocol-v2[5].\n     \n      ## promisor-remote.c ##\n    -@@ promisor-remote.c: static struct string_list *extra_fields_sent(void)\n    +@@ promisor-remote.c: static struct string_list *fields_sent(void)\n      \treturn &fields_list;\n      }\n      \n    -+static struct string_list *extra_fields_checked(void)\n    ++static struct string_list *fields_checked(void)\n     +{\n     +\tstatic struct string_list fields_list = STRING_LIST_INIT_NODUP;\n     +\tstatic int initialized = 0;\n     +\n     +\tif (!initialized) {\n    -+\t\tfields_from_config(&fields_list, \"promisor.checkExtraFields\");\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     +}\n     +\n    - static void append_extra_fields(struct string_list *fields,\n    - \t\t\t\tstruct string_list *extra_fields,\n    - \t\t\t\tconst char *name)\n    + static void append_fields(struct string_list *fields,\n    + \t\t\t  struct string_list *field_names,\n    + \t\t\t  const char *name)\n     @@ promisor-remote.c: enum accept_promisor {\n      \tACCEPT_ALL\n      };\n      \n    -+static int check_extra_field_one(struct string_list_item *item_value,\n    -+\t\t\t\t struct promisor_info *p)\n    ++static int check_field_one(struct string_list_item *item_value,\n    ++\t\t\t   struct promisor_info *p)\n     +{\n     +\tstruct string_list_item *item;\n     +\n    @@ promisor-remote.c: enum accept_promisor {\n     +}\n     +\n     +\n    -+static int check_extra_field(struct string_list_item *item_value,\n    -+\t\t\t     struct promisor_info *p, int in_list)\n    ++static int check_field(struct string_list_item *item_value,\n    ++\t\t       struct promisor_info *p, int in_list)\n     +{\n     +\tif (!in_list)\n    -+\t\treturn check_extra_field_one(item_value, p);\n    ++\t\treturn check_field_one(item_value, p);\n     +\n     +\tfor (; p; p = p->next)\n    -+\t\tif (check_extra_field_one(item_value, p))\n    ++\t\tif (check_field_one(item_value, p))\n     +\t\t\treturn 1;\n     +\n     +\treturn 0;\n     +}\n     +\n    -+static int check_all_extra_fields(struct string_list* extra_values,\n    -+\t\t\t\t  struct promisor_info *p,\n    -+\t\t\t\t  int in_list)\n    ++static int check_all_fields(struct string_list* values,\n    ++\t\t\t    struct promisor_info *p,\n    ++\t\t\t    int in_list)\n     +{\n    -+\tstruct string_list* fields_checked = extra_fields_checked();\n    ++\tstruct string_list* fields = fields_checked();\n     +\tstruct string_list_item *item_checked;\n     +\n    -+\tstring_list_sort(extra_values);\n    ++\tstring_list_sort(values);\n     +\n    -+\tfor_each_string_list_item(item_checked, fields_checked) {\n    ++\tfor_each_string_list_item(item_checked, fields) {\n     +\t\tstruct string_list_item *item_value;\n     +\n    -+\t\titem_value = string_list_lookup(extra_values, item_checked->string);\n    ++\t\titem_value = string_list_lookup(values, item_checked->string);\n     +\t\tif (!item_value)\n     +\t\t\treturn 0;\n    -+\t\tif (!check_extra_field(item_value, p, in_list))\n    ++\t\tif (!check_field(item_value, p, in_list))\n     +\t\t\treturn 0;\n     +\t}\n     +\n    @@ promisor-remote.c: enum accept_promisor {\n     -\t\t\t\tconst char *remote_name, const char *remote_url,\n     +\t\t\t\tconst char *remote_name,\n     +\t\t\t\tconst char *remote_url,\n    -+\t\t\t\tstruct string_list* extras,\n    ++\t\t\t\tstruct string_list* values,\n      \t\t\t\tstruct promisor_info *info_list)\n      {\n      \tstruct promisor_info *p;\n    @@ promisor-remote.c: enum accept_promisor {\n      \n      \tif (accept == ACCEPT_ALL)\n     -\t\treturn 1;\n    -+\t\treturn check_all_extra_fields(extras, info_list, 1);\n    ++\t\treturn check_all_fields(values, info_list, 1);\n      \n      \tp = remote_nick_find(info_list, remote_name);\n      \n    @@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n      \n      \tif (accept == ACCEPT_KNOWN_NAME)\n     -\t\treturn 1;\n    -+\t\treturn check_all_extra_fields(extras, p, 0);\n    ++\t\treturn check_all_fields(values, p, 0);\n      \n      \tif (accept != ACCEPT_KNOWN_URL)\n      \t\tBUG(\"Unhandled 'enum accept_promisor' value '%d'\", accept);\n    @@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n      \n      \tif (!strcmp(local_url, remote_url))\n     -\t\treturn 1;\n    -+\t\treturn check_all_extra_fields(extras, p, 0);\n    ++\t\treturn check_all_fields(values, p, 0);\n      \n      \twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s'\"),\n      \t\tremote_name, local_url, remote_url);\n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n      \t\tconst char *remote_url = NULL;\n      \t\tchar *decoded_name = NULL;\n      \t\tchar *decoded_url = NULL;\n    -+\t\tstruct string_list extras = STRING_LIST_INIT_NODUP;\n    ++\t\tstruct string_list field_values = STRING_LIST_INIT_NODUP;\n    ++\n    ++\t\tfield_values.cmp = strcasecmp;\n      \n      \t\tstrbuf_strip_suffix(remotes[i], \";\");\n      \t\telems = strbuf_split(remotes[i], ',');\n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n     +\t\t\tp = strchr(elems[j]->buf, '=');\n     +\t\t\tif (p) {\n     +\t\t\t\t*p = '\\0';\n    -+\t\t\t\tstring_list_append(&extras, elems[j]->buf)->util = p + 1;\n    ++\t\t\t\tstring_list_append(&field_values, elems[j]->buf)->util = p + 1;\n     +\t\t\t} else {\n     +\t\t\t\twarning(_(\"invalid element '%s' from remote info\"),\n     +\t\t\t\t\telems[j]->buf);\n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n     -\t\t\tstrvec_push(accepted, decoded_name);\n     +\t\tif (decoded_name) {\n     +\t\t\tif (!info_list)\n    -+\t\t\t\tinfo_list = promisor_info_list(repo, extra_fields_checked());\n    ++\t\t\t\tinfo_list = promisor_info_list(repo, fields_checked());\n     +\n     +\t\t\tif (should_accept_remote(accept, decoded_name, decoded_url,\n    -+\t\t\t\t\t\t &extras, info_list))\n    ++\t\t\t\t\t\t &field_values, info_list))\n     +\t\t\t\tstrvec_push(accepted, decoded_name);\n     +\t\t}\n      \n    -+\t\tstring_list_clear(&extras, 0);\n    ++\t\tstring_list_clear(&field_values, 0);\n      \t\tstrbuf_list_free(elems);\n      \t\tfree(decoded_name);\n      \t\tfree(decoded_url);\n     \n      ## t/t5710-promisor-remote-capability.sh ##\n    -@@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with promisor.sendExtraFields\" '\n    +@@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with promisor.sendFields\" '\n      \tcheck_missing_objects server 1 \"$oid\"\n      '\n      \n    -+test_expect_success \"clone with promisor.checkExtraFields\" '\n    ++test_expect_success \"clone with promisor.checkFields\" '\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.id \"fooBar\" &&\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    -+\tgit -C server config promisor.sendExtraFields \"id, partialCloneFilter\" &&\n    -+\ttest_when_finished \"git -C server config unset promisor.sendExtraFields\" &&\n    ++\tgit -C server config promisor.sendFields \"token, partialCloneFilter\" &&\n    ++\ttest_when_finished \"git -C server config unset promisor.sendFields\" &&\n     +\ttest_when_finished \"rm trace\" &&\n     +\n     +\t# Clone from server to create a client\n    @@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with promisor.\n     +\t\t-c remote.lop.url=\"file://$(pwd)/lop\" \\\n     +\t\t-c remote.lop.partialCloneFilter=\"blob:none\" \\\n     +\t\t-c promisor.acceptfromserver=All \\\n    -+\t\t-c promisor.checkExtraFields=partialCloneFilter \\\n    ++\t\t-c promisor.checkFields=partialcloneFilter \\\n     +\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n     +\n    -+\t# Check that extra fields are properly transmitted\n    ++\t# Check that fields are properly transmitted\n     +\tENCODED_URL=$(echo \"file://$(pwd)/lop\" | sed -e \"s/ /%20/g\") &&\n     +\tPR1=\"name=lop,url=$ENCODED_URL,partialCloneFilter=blob:none\" &&\n    -+\tPR2=\"name=otherLop,url=https://invalid.invalid,id=fooBar,partialCloneFilter=blob:limit=10k\" &&\n    ++\tPR2=\"name=otherLop,url=https://invalid.invalid,token=fooBar,partialCloneFilter=blob:limit=10k\" &&\n     +\ttest_grep \"clone< promisor-remote=$PR1;$PR2\" trace &&\n     +\ttest_grep \"clone> promisor-remote=lop\" trace &&\n     +\ttest_grep ! \"clone> promisor-remote=lop;otherLop\" trace &&\n\n\nChristian Couder (3):\n  promisor-remote: refactor to get rid of 'struct strvec'\n  promisor-remote: allow a server to advertise more fields\n  promisor-remote: allow a client to check fields\n\n Documentation/config/promisor.adoc    |  43 ++++\n Documentation/gitprotocol-v2.adoc     |  52 ++--\n promisor-remote.c                     | 328 ++++++++++++++++++++++----\n t/t5710-promisor-remote-capability.sh |  67 ++++++\n 4 files changed, 425 insertions(+), 65 deletions(-)\n\n-- \n2.49.0.157.g09af0369a6\n\n"},{"id":"516952","messageId":"20250429145243.992252-2-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250429145243.992252-1-christian.couder@gmail.com","subject":"[PATCH v2 1/3] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-04-29T14:52:41Z","receivedAt":"2025-04-29T14:53:16Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"In a following commit, we will use the new 'promisor-remote' protocol\ncapability introduced by d460267613 (Add 'promisor-remote' capability\nto protocol v2, 2025-02-18) to pass and process more information\nabout promisor remotes than just their name and url.\n\nFor that purpose, we will need to store information about other\nfields, especially information that might or might not be available\nfor different promisor remotes. Unfortunately using 'struct strvec',\nas we currently do, to store information about the promisor remotes\nwith one 'struct strvec' for each field like \"name\" or \"url\" does not\nscale easily in that case.\n\nLet's refactor this and introduce a new 'struct promisor_info' linked\nlist that contains a 'struct string_list fields'. This string_list\nstores the field names, like \"name\" and \"url\", in the 'string' member\nof its items, and the field values in the 'util' member of its items.\n\nExcept for \"name\", each \"<field_name>/<field_value>\" pair should\ncorrespond to a \"remote.<name>.<field_name>\" config variable set to\n<field_value> where \"<name>\" is a promisor remote name.\n\nPreviously in Git, the part after the last dot in a configuration\nvariable key, for example \"c\" for \"a.b.c\", was called the \"variable\nname part\" of a configuration key. It would be very confusing to use\n\"variable name part\" or some similar terms in the context of the\n'promisor-remote' protocol though, so let's forget about it, and just\nuse \"field\", \"field name\" and \"field value\" instead.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 142 ++++++++++++++++++++++++++++++++++------------\n 1 file changed, 105 insertions(+), 37 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 5801ebfd9b..24d0e70132 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -314,10 +314,51 @@ static int allow_unsanitized(char ch)\n \treturn ch > 32 && ch < 127;\n }\n \n-static void promisor_info_vecs(struct repository *repo,\n-\t\t\t       struct strvec *names,\n-\t\t\t       struct strvec *urls)\n+/*\n+ * Linked list for promisor remotes involved in the \"promisor-remote\"\n+ * protocol capability.\n+ *\n+ * 'fields' contains a defined set of field name/value pairs for\n+ * each promisor remote. Field names are stored in the 'string'\n+ * member, and values in the 'util' member.\n+ *\n+ * Currently supported field names:\n+ * - \"name\": The name of the promisor remote.\n+ * - \"url\": The URL of the promisor remote.\n+ *\n+ * Except for \"name\", each \"<field_name>/<field_value>\" pair should\n+ * correspond to a \"remote.<name>.<field_name>\" config variable set to\n+ * <field_value> where \"<name>\" is a promisor remote name.\n+ *\n+ * 'fields' should not be sorted, as we will rely on the order we put\n+ * things into it. So, for example, 'string_list_append()' should be\n+ * used instead of 'string_list_insert()'.\n+ */\n+struct promisor_info {\n+\tstruct promisor_info *next;\n+\tstruct string_list fields;\n+};\n+\n+static void promisor_info_list_free(struct promisor_info *p)\n+{\n+\tstruct promisor_info *next;\n+\n+\tfor (; p; p = next) {\n+\t\tnext = p->next;\n+\t\tstring_list_clear(&p->fields, 0);\n+\t\tfree(p);\n+\t}\n+}\n+\n+/*\n+ * Prepare a 'struct promisor_info' linked list of promisor\n+ * remotes. For each promisor remote, some of its fields, starting\n+ * with \"name\" and \"url\", are put in the 'fields' string_list.\n+ */\n+static struct promisor_info *promisor_info_list(struct repository *repo)\n {\n+\tstruct promisor_info *infos = NULL;\n+\tstruct promisor_info **last_info = &infos;\n \tstruct promisor_remote *r;\n \n \tpromisor_remote_init(repo);\n@@ -328,57 +369,78 @@ static void promisor_info_vecs(struct repository *repo,\n \n \t\t/* Only add remotes with a non empty URL */\n \t\tif (!git_config_get_string_tmp(url_key, &url) && *url) {\n-\t\t\tstrvec_push(names, r->name);\n-\t\t\tstrvec_push(urls, url);\n+\t\t\tstruct promisor_info *new_info = xcalloc(1, sizeof(*new_info));\n+\n+\t\t\tstring_list_init_dup(&new_info->fields);\n+\t\t\tnew_info->fields.cmp = strcasecmp;\n+\n+\t\t\tstring_list_append(&new_info->fields, \"name\")->util = (char *)r->name;\n+\t\t\tstring_list_append(&new_info->fields, \"url\")->util = (char *)url;\n+\n+\t\t\t*last_info = new_info;\n+\t\t\tlast_info = &new_info->next;\n \t\t}\n \n \t\tfree(url_key);\n \t}\n+\n+\treturn infos;\n }\n \n char *promisor_remote_info(struct repository *repo)\n {\n \tstruct strbuf sb = STRBUF_INIT;\n \tint advertise_promisors = 0;\n-\tstruct strvec names = STRVEC_INIT;\n-\tstruct strvec urls = STRVEC_INIT;\n+\tstruct promisor_info *info_list;\n+\tstruct promisor_info *r;\n \n \tgit_config_get_bool(\"promisor.advertise\", &advertise_promisors);\n \n \tif (!advertise_promisors)\n \t\treturn NULL;\n \n-\tpromisor_info_vecs(repo, &names, &urls);\n+\tinfo_list = promisor_info_list(repo);\n \n-\tif (!names.nr)\n+\tif (!info_list)\n \t\treturn NULL;\n \n-\tfor (size_t i = 0; i < names.nr; i++) {\n-\t\tif (i)\n+\tfor (r = info_list; r; r = r->next) {\n+\t\tstruct string_list_item *item;\n+\t\tint first = 1;\n+\n+\t\tif (r != info_list)\n \t\t\tstrbuf_addch(&sb, ';');\n-\t\tstrbuf_addstr(&sb, \"name=\");\n-\t\tstrbuf_addstr_urlencode(&sb, names.v[i], allow_unsanitized);\n-\t\tstrbuf_addstr(&sb, \",url=\");\n-\t\tstrbuf_addstr_urlencode(&sb, urls.v[i], allow_unsanitized);\n+\n+\t\tfor_each_string_list_item(item, &r->fields) {\n+\t\t\tif (first)\n+\t\t\t\tfirst = 0;\n+\t\t\telse\n+\t\t\t\tstrbuf_addch(&sb, ',');\n+\t\t\tstrbuf_addf(&sb, \"%s=\", item->string);\n+\t\t\tstrbuf_addstr_urlencode(&sb, (char *)item->util, allow_unsanitized);\n+\t\t}\n \t}\n \n-\tstrvec_clear(&names);\n-\tstrvec_clear(&urls);\n+\tpromisor_info_list_free(info_list);\n \n \treturn strbuf_detach(&sb, NULL);\n }\n \n /*\n- * Find first index of 'nicks' where there is 'nick'. 'nick' is\n- * compared case sensitively to the strings in 'nicks'. If not found\n- * 'nicks->nr' is returned.\n+ * Find first element of 'p' where the 'name' field is 'nick'. 'nick'\n+ * is compared case sensitively to the strings in 'p'. If not found\n+ * NULL is returned.\n  */\n-static size_t remote_nick_find(struct strvec *nicks, const char *nick)\n+static struct promisor_info *remote_nick_find(struct promisor_info *p, const char *nick)\n {\n-\tfor (size_t i = 0; i < nicks->nr; i++)\n-\t\tif (!strcmp(nicks->v[i], nick))\n-\t\t\treturn i;\n-\treturn nicks->nr;\n+\tfor (; p; p = p->next) {\n+\t\tif (strcmp(p->fields.items[0].string, \"name\"))\n+\t\t\tBUG(\"First field of promisor info should be 'name', but was '%s'.\",\n+\t\t\t    p->fields.items[0].string);\n+\t\tif (!strcmp(p->fields.items[0].util, nick))\n+\t\t\treturn p;\n+\t}\n+\treturn NULL;\n }\n \n enum accept_promisor {\n@@ -390,16 +452,17 @@ enum accept_promisor {\n \n static int should_accept_remote(enum accept_promisor accept,\n \t\t\t\tconst char *remote_name, const char *remote_url,\n-\t\t\t\tstruct strvec *names, struct strvec *urls)\n+\t\t\t\tstruct promisor_info *info_list)\n {\n-\tsize_t i;\n+\tstruct promisor_info *p;\n+\tconst char *local_url;\n \n \tif (accept == ACCEPT_ALL)\n \t\treturn 1;\n \n-\ti = remote_nick_find(names, remote_name);\n+\tp = remote_nick_find(info_list, remote_name);\n \n-\tif (i >= names->nr)\n+\tif (!p)\n \t\t/* We don't know about that remote */\n \t\treturn 0;\n \n@@ -414,11 +477,18 @@ static int should_accept_remote(enum accept_promisor accept,\n \t\treturn 0;\n \t}\n \n-\tif (!strcmp(urls->v[i], remote_url))\n+\tif (strcmp(p->fields.items[1].string, \"url\"))\n+\t\tBUG(\"Bad info_list for remote '%s'.\\n\"\n+\t\t    \"Second field of promisor info should be 'url', but was '%s'.\",\n+\t\t    remote_name, p->fields.items[1].string);\n+\n+\tlocal_url = p->fields.items[1].util;\n+\n+\tif (!strcmp(local_url, remote_url))\n \t\treturn 1;\n \n \twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s'\"),\n-\t\tremote_name, urls->v[i], remote_url);\n+\t\tremote_name, local_url, remote_url);\n \n \treturn 0;\n }\n@@ -430,8 +500,7 @@ static void filter_promisor_remote(struct repository *repo,\n \tstruct strbuf **remotes;\n \tconst char *accept_str;\n \tenum accept_promisor accept = ACCEPT_NONE;\n-\tstruct strvec names = STRVEC_INIT;\n-\tstruct strvec urls = STRVEC_INIT;\n+\tstruct promisor_info *info_list = NULL;\n \n \tif (!git_config_get_string_tmp(\"promisor.acceptfromserver\", &accept_str)) {\n \t\tif (!*accept_str || !strcasecmp(\"None\", accept_str))\n@@ -451,7 +520,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\treturn;\n \n \tif (accept != ACCEPT_ALL)\n-\t\tpromisor_info_vecs(repo, &names, &urls);\n+\t\tinfo_list = promisor_info_list(repo);\n \n \t/* Parse remote info received */\n \n@@ -482,7 +551,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tif (remote_url)\n \t\t\tdecoded_url = url_percent_decode(remote_url);\n \n-\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, &names, &urls))\n+\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, info_list))\n \t\t\tstrvec_push(accepted, decoded_name);\n \n \t\tstrbuf_list_free(elems);\n@@ -490,8 +559,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tfree(decoded_url);\n \t}\n \n-\tstrvec_clear(&names);\n-\tstrvec_clear(&urls);\n+\tpromisor_info_list_free(info_list);\n \tstrbuf_list_free(remotes);\n }\n \n-- \n2.49.0.157.g09af0369a6\n\n"},{"id":"516953","messageId":"20250429145243.992252-3-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250429145243.992252-1-christian.couder@gmail.com","subject":"[PATCH v2 2/3] promisor-remote: allow a server to advertise more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-04-29T14:52:42Z","receivedAt":"2025-04-29T14:53:17Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"For now the \"promisor-remote\" protocol capability can only pass \"name\"\nand \"url\" information from a server to a client in the form\n\"name=<remote_name>,url=<remote_url>\".\n\nLet's make it possible to pass more information by introducing a new\n\"promisor.sendFields\" configuration variable. This variable should\ncontain a comma or space separated list of fields that will be looked\nup in the configuration of the remote on the server to find the values\nthat will be passed to the client.\n\nOnly a set of predefined fields are allowed. The only fields in this\nset are \"partialCloneFilter\" and \"token\".\n\nFor example if \"promisor.sendFields\" is set to \"partialCloneFilter\",\nand the server has the \"remote.<name>.partialCloneFilter\" config\nvariable set to a value for a remote, then that value will be passed\nin the form \"partialCloneFilter=<value>\" after the \"name\" and \"url\"\nfields.\n\nA following commit will allow the client to use the information to\ndecide if it accepts the remote or not. For now the client doesn't do\nanything with the additional information it receives.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    | 18 +++++\n Documentation/gitprotocol-v2.adoc     | 52 +++++++++-----\n promisor-remote.c                     | 99 ++++++++++++++++++++++++---\n t/t5710-promisor-remote-capability.sh | 32 +++++++++\n 4 files changed, 175 insertions(+), 26 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex 2638b01f83..71311b70c8 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -9,6 +9,24 @@ promisor.advertise::\n \t\"false\", which means the \"promisor-remote\" capability is not\n \tadvertised.\n \n+promisor.sendFields::\n+\tA comma or space separated list of additional remote related\n+\tfields that a server will send while advertising its promisor\n+\tremotes using the \"promisor-remote\" capability, see\n+\tlinkgit:gitprotocol-v2[5]. Currently, only the\n+\t\"partialCloneFilter\" and \"token\" fields are supported. The\n+\t\"partialCloneFilter\" field contains the partial clone filter\n+\tused for the remote, and the \"token\" field contains an\n+\tauthentication token for the remote.\n++\n+When a field is part of this list and a corresponding\n+\"remote.foo.<field>\" config variable is set on the server to a\n+non-empty value, then the field and its value will be sent when\n+advertising the promisor remote \"foo\". This list has no effect unless\n+the \"promisor.advertise\" config variable is set to \"true\", and the\n+\"name\" and \"url\" fields are always advertised regardless of this\n+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\"\ndiff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\nindex 5598c93e67..b4648a7ce6 100644\n--- a/Documentation/gitprotocol-v2.adoc\n+++ b/Documentation/gitprotocol-v2.adoc\n@@ -785,33 +785,52 @@ retrieving the header from a bundle at the indicated URI, and thus\n save themselves and the server(s) the request(s) needed to inspect the\n headers of that bundle or bundles.\n \n-promisor-remote=<pr-infos>\n+promisor-remote=<pr-info>\n ~~~~~~~~~~~~~~~~~~~~~~~~~~\n \n The server may advertise some promisor remotes it is using or knows\n about to a client which may want to use them as its promisor remotes,\n-instead of this repository. In this case <pr-infos> should be of the\n+instead of this repository. In this case <pr-info> should be of the\n form:\n \n-\tpr-infos = pr-info | pr-infos \";\" pr-info\n+\tpr-info = pr-fields | pr-info \";\" pr-info\n \n-\tpr-info = \"name=\" pr-name | \"name=\" pr-name \",\" \"url=\" pr-url\n+\tpr-fields = fld-name \"=\" fld-value | pr-fields \",\" pr-fields\n \n-where `pr-name` is the urlencoded name of a promisor remote, and\n-`pr-url` the urlencoded URL of that promisor remote.\n+where all the `fld-name` and `fld-value` in a given `pr-fields` are\n+field names and values related to a single promisor remote.\n \n-In this case, if the client decides to use one or more promisor\n-remotes the server advertised, it can reply with\n-\"promisor-remote=<pr-names>\" where <pr-names> should be of the form:\n+The server MUST advertise at least the \"name\" and \"url\" field names\n+along with the associated field values, which are the name of a valid\n+remote and its URL, in each `pr-fields`.\n \n-\tpr-names = pr-name | pr-names \";\" pr-name\n+The server MAY advertise the following optional fields:\n+\n+- \"partialCloneFilter\": Filter used for partial clone, corresponding\n+  to the \"remote.<name>.partialCloneFilter\" config setting.\n+- \"token\": Authentication token for the remote, corresponding\n+  to the \"remote.<name>.token\" config setting.\n+\n+No other fields are defined by the protocol at this time. Clients SHOULD\n+ignore fields they don't recognize to allow for 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+\n+Field values MUST be urlencoded.\n+\n+If the client decides to use one or more promisor remotes the server\n+advertised, it can reply with \"promisor-remote=<pr-names>\" where\n+<pr-names> should be of the form:\n+\n+\tpr-names = pr-name | pr-names \";\" pr-names\n \n where `pr-name` is the urlencoded name of a promisor remote the server\n advertised and the client accepts.\n \n-Note that, everywhere in this document, `pr-name` MUST be a valid\n-remote name, and the ';' and ',' characters MUST be encoded if they\n-appear in `pr-name` or `pr-url`.\n+Note that, everywhere in this document, the ';' and ',' characters\n+MUST be encoded if they appear in `pr-name` or `fld-value`.\n \n If the server doesn't know any promisor remote that could be good for\n a client to use, or prefers a client not to use any promisor remote it\n@@ -822,9 +841,10 @@ In this case, or if the client doesn't want to use any promisor remote\n the server advertised, the client shouldn't advertise the\n \"promisor-remote\" capability at all in its reply.\n \n-The \"promisor.advertise\" and \"promisor.acceptFromServer\" configuration\n-options can be used on the server and client side to control what they\n-advertise or accept respectively. See the documentation of these\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 \n Note that in the future it would be nice if the \"promisor-remote\"\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 24d0e70132..70abec4c24 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -314,6 +314,84 @@ static int allow_unsanitized(char ch)\n \treturn ch > 32 && ch < 127;\n }\n \n+/*\n+ * List of field names allowed to be used in the \"promisor-remote\"\n+ * protocol capability. Each field should correspond to a configurable\n+ * property of a remote that can be relevant for the client.\n+ */\n+static const char *allowed_fields[] = {\n+\t\"partialCloneFilter\", /* Filter used for partial clone */\n+\t\"token\",              /* Authentication token for the remote */\n+\tNULL\n+};\n+\n+/*\n+ * Check if 'field' is in the list of allowed field names for the\n+ * \"promisor-remote\" protocol capability.\n+ */\n+static int is_allowed_field(const char *field)\n+{\n+\tconst char **p;\n+\n+\tfor (p = allowed_fields; *p; p++)\n+\t\tif (!strcasecmp(*p, field))\n+\t\t\treturn 1;\n+\treturn 0;\n+}\n+\n+static int valid_field(struct string_list_item *item, void *cb_data)\n+{\n+\tconst char *field = item->string;\n+\tconst char *config_key = (const char *)cb_data;\n+\n+\tif (!is_allowed_field(field)) {\n+\t\twarning(_(\"unsupported field '%s' in '%s' config\"), field, config_key);\n+\t\treturn 0;\n+\t}\n+\treturn 1;\n+}\n+\n+static char *fields_from_config(struct string_list *fields_list, const char *config_key)\n+{\n+\tchar *fields = NULL;\n+\n+\tif (!git_config_get_string(config_key, &fields) && *fields) {\n+\t\tstring_list_split_in_place(fields_list, fields, \", \", -1);\n+\t\tfilter_string_list(fields_list, 0, valid_field, (void *)config_key);\n+\t}\n+\n+\treturn fields;\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 = 0;\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+}\n+\n+static void append_fields(struct string_list *fields,\n+\t\t\t  struct string_list *field_names,\n+\t\t\t  const char *name)\n+{\n+\tstruct string_list_item *item;\n+\n+\tfor_each_string_list_item(item, field_names) {\n+\t\tchar *key = xstrfmt(\"remote.%s.%s\", name, item->string);\n+\t\tconst char *val;\n+\t\tif (!git_config_get_string_tmp(key, &val) && *val)\n+\t\t\tstring_list_append(fields, item->string)->util = (char *)val;\n+\t\tfree(key);\n+\t}\n+}\n+\n /*\n  * Linked list for promisor remotes involved in the \"promisor-remote\"\n  * protocol capability.\n@@ -323,8 +401,9 @@ static int allow_unsanitized(char ch)\n  * member, and values in the 'util' member.\n  *\n  * Currently supported field names:\n- * - \"name\": The name of the promisor remote.\n- * - \"url\": The URL of the promisor remote.\n+ * - \"name\": The name of the promisor remote,\n+ * - \"url\": The URL of the promisor remote,\n+ * - the fields in 'allowed_fields[]' above.\n  *\n  * Except for \"name\", each \"<field_name>/<field_value>\" pair should\n  * correspond to a \"remote.<name>.<field_name>\" config variable set to\n@@ -355,7 +434,8 @@ static void promisor_info_list_free(struct promisor_info *p)\n  * remotes. For each promisor remote, some of its fields, starting\n  * with \"name\" and \"url\", are put in the 'fields' string_list.\n  */\n-static struct promisor_info *promisor_info_list(struct repository *repo)\n+static struct promisor_info *promisor_info_list(struct repository *repo,\n+\t\t\t\t\t\tstruct string_list *field_names)\n {\n \tstruct promisor_info *infos = NULL;\n \tstruct promisor_info **last_info = &infos;\n@@ -377,6 +457,9 @@ static struct promisor_info *promisor_info_list(struct repository *repo)\n \t\t\tstring_list_append(&new_info->fields, \"name\")->util = (char *)r->name;\n \t\t\tstring_list_append(&new_info->fields, \"url\")->util = (char *)url;\n \n+\t\t\tif (field_names)\n+\t\t\t\tappend_fields(&new_info->fields, field_names, r->name);\n+\n \t\t\t*last_info = new_info;\n \t\t\tlast_info = &new_info->next;\n \t\t}\n@@ -399,7 +482,7 @@ char *promisor_remote_info(struct repository *repo)\n \tif (!advertise_promisors)\n \t\treturn NULL;\n \n-\tinfo_list = promisor_info_list(repo);\n+\tinfo_list = promisor_info_list(repo, fields_sent());\n \n \tif (!info_list)\n \t\treturn NULL;\n@@ -520,7 +603,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\treturn;\n \n \tif (accept != ACCEPT_ALL)\n-\t\tinfo_list = promisor_info_list(repo);\n+\t\tinfo_list = promisor_info_list(repo, NULL);\n \n \t/* Parse remote info received */\n \n@@ -537,13 +620,9 @@ static void filter_promisor_remote(struct repository *repo,\n \t\telems = strbuf_split(remotes[i], ',');\n \n \t\tfor (size_t j = 0; elems[j]; j++) {\n-\t\t\tint res;\n \t\t\tstrbuf_strip_suffix(elems[j], \",\");\n-\t\t\tres = skip_prefix(elems[j]->buf, \"name=\", &remote_name) ||\n+\t\t\tif (!skip_prefix(elems[j]->buf, \"name=\", &remote_name))\n \t\t\t\tskip_prefix(elems[j]->buf, \"url=\", &remote_url);\n-\t\t\tif (!res)\n-\t\t\t\twarning(_(\"unknown element '%s' from remote info\"),\n-\t\t\t\t\telems[j]->buf);\n \t\t}\n \n \t\tif (remote_name)\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex b35b774235..4038c076f1 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -289,6 +289,38 @@ 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 promisor.sendFields\" '\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+\tgit -C server config promisor.sendFields \"token, partialCloneFilter\" &&\n+\ttest_when_finished \"git -C server config unset promisor.sendFields\" &&\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 promisor.acceptfromserver=All \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that fields are properly transmitted\n+\tENCODED_URL=$(echo \"file://$(pwd)/lop\" | sed -e \"s/ /%20/g\") &&\n+\tPR1=\"name=lop,url=$ENCODED_URL,partialCloneFilter=blob:none\" &&\n+\tPR2=\"name=otherLop,url=https://invalid.invalid,token=fooBar,partialCloneFilter=blob:limit=10k\" &&\n+\ttest_grep \"clone< promisor-remote=$PR1;$PR2\" trace &&\n+\ttest_grep \"clone> promisor-remote=lop;otherLop\" 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 \n-- \n2.49.0.157.g09af0369a6\n\n"},{"id":"516954","messageId":"20250429145243.992252-4-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250429145243.992252-1-christian.couder@gmail.com","subject":"[PATCH v2 3/3] promisor-remote: allow a client to check fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-04-29T14:52:43Z","receivedAt":"2025-04-29T14:53:18Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"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\nLet's make it possible for a client to check if these fields match\nwhat it expects before accepting a promisor remote.\n\nWe allow this by introducing a new \"promisor.checkFields\"\nconfiguration variable. It should contain a comma or space separated\nlist of fields that will be checked.\n\nBy limiting the protocol to specific well-defined fields, we ensure\nboth server and client have a shared understanding of field\nsemantics and usage.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    |  25 ++++++\n promisor-remote.c                     | 105 +++++++++++++++++++++++---\n t/t5710-promisor-remote-capability.sh |  35 +++++++++\n 3 files changed, 154 insertions(+), 11 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex 71311b70c8..4147d2cf44 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -46,3 +46,28 @@ promisor.acceptFromServer::\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+\n+promisor.checkFields::\n+\tA comma or space separated list of additional remote related\n+\tfields that a client will check before accepting a promisor\n+\tremote. Currently, only the \"partialCloneFilter\" and \"token\"\n+\tfields are supported.\n++\n+When a field is part of this list and a corresponding\n+\"remote.foo.<field>\" config variable is set locally for remote \"foo\",\n+then the value of this config variable will be checked against the\n+value of the same field passed by the server for the remote \"foo\". The\n+remote \"foo\" will be rejected if the values don't match.\n++\n+For the \"partialCloneFilter\" field, this allows the client to ensure\n+that the server's filter matches what it expects locally, preventing\n+inconsistencies in filtering behavior. For the \"token\" field, this can\n+be used to verify that authentication credentials match expected\n+values.\n++\n+The fields should be passed by the server through the\n+\"promisor-remote\" capability by using the `promisor.sendFields` config\n+variable. The fields will be checked only if the\n+`promisor.acceptFromServer` config variable is not set to \"None\".  If\n+set to \"None\", this config variable will have no effect.  See\n+linkgit:gitprotocol-v2[5].\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 70abec4c24..97f81a0e08 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -377,6 +377,20 @@ static struct string_list *fields_sent(void)\n \treturn &fields_list;\n }\n \n+static struct string_list *fields_checked(void)\n+{\n+\tstatic struct string_list fields_list = STRING_LIST_INIT_NODUP;\n+\tstatic int initialized = 0;\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+}\n+\n static void append_fields(struct string_list *fields,\n \t\t\t  struct string_list *field_names,\n \t\t\t  const char *name)\n@@ -533,15 +547,65 @@ enum accept_promisor {\n \tACCEPT_ALL\n };\n \n+static int check_field_one(struct string_list_item *item_value,\n+\t\t\t   struct promisor_info *p)\n+{\n+\tstruct string_list_item *item;\n+\n+\titem = unsorted_string_list_lookup(&p->fields, item_value->string);\n+\tif (!item)\n+\t\treturn 0;\n+\n+\treturn !strcmp(item->util, item_value->util);\n+}\n+\n+\n+static int check_field(struct string_list_item *item_value,\n+\t\t       struct promisor_info *p, int in_list)\n+{\n+\tif (!in_list)\n+\t\treturn check_field_one(item_value, p);\n+\n+\tfor (; p; p = p->next)\n+\t\tif (check_field_one(item_value, p))\n+\t\t\treturn 1;\n+\n+\treturn 0;\n+}\n+\n+static int check_all_fields(struct string_list* values,\n+\t\t\t    struct promisor_info *p,\n+\t\t\t    int in_list)\n+{\n+\tstruct string_list* fields = fields_checked();\n+\tstruct string_list_item *item_checked;\n+\n+\tstring_list_sort(values);\n+\n+\tfor_each_string_list_item(item_checked, fields) {\n+\t\tstruct string_list_item *item_value;\n+\n+\t\titem_value = string_list_lookup(values, item_checked->string);\n+\t\tif (!item_value)\n+\t\t\treturn 0;\n+\t\tif (!check_field(item_value, p, in_list))\n+\t\t\treturn 0;\n+\t}\n+\n+\treturn 1;\n+}\n+\n static int should_accept_remote(enum accept_promisor accept,\n-\t\t\t\tconst char *remote_name, const char *remote_url,\n+\t\t\t\tconst char *remote_name,\n+\t\t\t\tconst char *remote_url,\n+\t\t\t\tstruct string_list* values,\n \t\t\t\tstruct promisor_info *info_list)\n {\n \tstruct promisor_info *p;\n \tconst char *local_url;\n \n \tif (accept == ACCEPT_ALL)\n-\t\treturn 1;\n+\t\treturn check_all_fields(values, info_list, 1);\n \n \tp = remote_nick_find(info_list, remote_name);\n \n@@ -550,7 +614,7 @@ static int should_accept_remote(enum accept_promisor accept,\n \t\treturn 0;\n \n \tif (accept == ACCEPT_KNOWN_NAME)\n-\t\treturn 1;\n+\t\treturn check_all_fields(values, p, 0);\n \n \tif (accept != ACCEPT_KNOWN_URL)\n \t\tBUG(\"Unhandled 'enum accept_promisor' value '%d'\", accept);\n@@ -568,7 +632,7 @@ static int should_accept_remote(enum accept_promisor accept,\n \tlocal_url = p->fields.items[1].util;\n \n \tif (!strcmp(local_url, remote_url))\n-\t\treturn 1;\n+\t\treturn check_all_fields(values, p, 0);\n \n \twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s'\"),\n \t\tremote_name, local_url, remote_url);\n@@ -602,9 +666,6 @@ static void filter_promisor_remote(struct repository *repo,\n \tif (accept == ACCEPT_NONE)\n \t\treturn;\n \n-\tif (accept != ACCEPT_ALL)\n-\t\tinfo_list = promisor_info_list(repo, NULL);\n-\n \t/* Parse remote info received */\n \n \tremotes = strbuf_split_str(info, ';', 0);\n@@ -615,14 +676,29 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tconst char *remote_url = NULL;\n \t\tchar *decoded_name = NULL;\n \t\tchar *decoded_url = NULL;\n+\t\tstruct string_list field_values = STRING_LIST_INIT_NODUP;\n+\n+\t\tfield_values.cmp = strcasecmp;\n \n \t\tstrbuf_strip_suffix(remotes[i], \";\");\n \t\telems = strbuf_split(remotes[i], ',');\n \n \t\tfor (size_t j = 0; elems[j]; j++) {\n+\t\t\tchar *p;\n+\n \t\t\tstrbuf_strip_suffix(elems[j], \",\");\n-\t\t\tif (!skip_prefix(elems[j]->buf, \"name=\", &remote_name))\n-\t\t\t\tskip_prefix(elems[j]->buf, \"url=\", &remote_url);\n+\t\t\tif (skip_prefix(elems[j]->buf, \"name=\", &remote_name) ||\n+\t\t\t    skip_prefix(elems[j]->buf, \"url=\", &remote_url))\n+\t\t\t\tcontinue;\n+\n+\t\t\tp = strchr(elems[j]->buf, '=');\n+\t\t\tif (p) {\n+\t\t\t\t*p = '\\0';\n+\t\t\t\tstring_list_append(&field_values, elems[j]->buf)->util = p + 1;\n+\t\t\t} else {\n+\t\t\t\twarning(_(\"invalid element '%s' from remote info\"),\n+\t\t\t\t\telems[j]->buf);\n+\t\t\t}\n \t\t}\n \n \t\tif (remote_name)\n@@ -630,9 +706,16 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tif (remote_url)\n \t\t\tdecoded_url = url_percent_decode(remote_url);\n \n-\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, info_list))\n-\t\t\tstrvec_push(accepted, decoded_name);\n+\t\tif (decoded_name) {\n+\t\t\tif (!info_list)\n+\t\t\t\tinfo_list = promisor_info_list(repo, fields_checked());\n+\n+\t\t\tif (should_accept_remote(accept, decoded_name, decoded_url,\n+\t\t\t\t\t\t &field_values, info_list))\n+\t\t\t\tstrvec_push(accepted, decoded_name);\n+\t\t}\n \n+\t\tstring_list_clear(&field_values, 0);\n \t\tstrbuf_list_free(elems);\n \t\tfree(decoded_name);\n \t\tfree(decoded_url);\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex 4038c076f1..2b54474ff8 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -321,6 +321,41 @@ test_expect_success \"clone with promisor.sendFields\" '\n \tcheck_missing_objects server 1 \"$oid\"\n '\n \n+test_expect_success \"clone with promisor.checkFields\" '\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+\tgit -C server config promisor.sendFields \"token, partialCloneFilter\" &&\n+\ttest_when_finished \"git -C server config unset promisor.sendFields\" &&\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.partialCloneFilter=\"blob:none\" \\\n+\t\t-c promisor.acceptfromserver=All \\\n+\t\t-c promisor.checkFields=partialcloneFilter \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that fields are properly transmitted\n+\tENCODED_URL=$(echo \"file://$(pwd)/lop\" | sed -e \"s/ /%20/g\") &&\n+\tPR1=\"name=lop,url=$ENCODED_URL,partialCloneFilter=blob:none\" &&\n+\tPR2=\"name=otherLop,url=https://invalid.invalid,token=fooBar,partialCloneFilter=blob:limit=10k\" &&\n+\ttest_grep \"clone< promisor-remote=$PR1;$PR2\" trace &&\n+\ttest_grep \"clone> promisor-remote=lop\" trace &&\n+\ttest_grep ! \"clone> promisor-remote=lop;otherLop\" 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 \n-- \n2.49.0.157.g09af0369a6\n\n"},{"id":"516955","messageId":"CAP8UFD3n09rzS6AMwAP6C=aADW+86r98onL8pci_N_OEF+FPDQ@mail.gmail.com","threadId":"63293","inReplyTo":"aAdrzlUX61TK1x8_@pks.im","subject":"Re: [PATCH 2/4] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-04-29T15:12:14Z","receivedAt":"2025-04-29T15:12:28Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Tue, Apr 22, 2025 at 12:13 PM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Mon, Apr 14, 2025 at 06:03:41PM +0200, Christian Couder wrote:\n\n> > +/*\n> > + * Linked list for promisor remotes.\n> > + *\n> > + * 'fields' should not be sorted, as we will rely on the order we put\n> > + * things into it. So, for example, 'string_list_append()' should be\n> > + * used instead of 'string_list_insert()'.\n> > + */\n> > +struct promisor_info {\n> > +     struct promisor_info *next;\n> > +     struct string_list fields;\n> > +};\n> > +\n> > +static void free_info_list(struct promisor_info *p)\n>\n> Nit: nowadays we would call this something like\n> `promisor_info_list_free()`, with the name of the subsystem coming\n> first.\n\nFine, I have changed it to `promisor_info_list_free()` in the next version.\n\n> >  char *promisor_remote_info(struct repository *repo)\n> >  {\n> >       struct strbuf sb = STRBUF_INIT;\n> >       int advertise_promisors = 0;\n> > -     struct strvec names = STRVEC_INIT;\n> > -     struct strvec urls = STRVEC_INIT;\n> > +     struct promisor_info *info_list;\n> > +     struct promisor_info *r, *p;\n> >\n> >       git_config_get_bool(\"promisor.advertise\", &advertise_promisors);\n> >\n> >       if (!advertise_promisors)\n> >               return NULL;\n> >\n> > -     promisor_info_vecs(repo, &names, &urls);\n> > +     info_list = promisor_info_list(repo);\n> >\n> > -     if (!names.nr)\n> > +     if (!info_list)\n> >               return NULL;\n> >\n> > -     for (size_t i = 0; i < names.nr; i++) {\n> > -             if (i)\n> > +     for (p = NULL, r = info_list; r; p = r, r = r->next) {\n> > +             struct string_list_item *item;\n> > +             int first = 1;\n> > +\n> > +             if (r != info_list)\n> >                       strbuf_addch(&sb, ';');\n> > -             strbuf_addstr(&sb, \"name=\");\n> > -             strbuf_addstr_urlencode(&sb, names.v[i], allow_unsanitized);\n> > -             strbuf_addstr(&sb, \",url=\");\n> > -             strbuf_addstr_urlencode(&sb, urls.v[i], allow_unsanitized);\n> > +\n> > +             for_each_string_list_item(item, &r->fields) {\n> > +                     if (first)\n> > +                             first = 0;\n> > +                     else\n> > +                             strbuf_addch(&sb, ',');\n> > +                     strbuf_addf(&sb, \"%s=\", item->string);\n> > +                     strbuf_addstr_urlencode(&sb, (char *)item->util, allow_unsanitized);\n> > +             }\n> >       }\n> >\n> > -     strvec_clear(&names);\n> > -     strvec_clear(&urls);\n> > +     free_info_list(p);\n>\n> I don't quite follow the usage pattern of `info_list` here. My\n> expectation is that we'd:\n>\n>   1. Acquire the promisor info list.\n>\n>   2. Iterate through each of its items.\n>\n>   3. Free the complete list.\n>\n> But why do we free `p` here? Shouldn't we free `info_list`? And if we\n> did so, can't we drop `p` completely and just iterate through the list\n> via `r`?\n\nYeah, it's a bug that is fixed in the next version.\n\n> >       return strbuf_detach(&sb, NULL);\n> >  }\n> >\n> >  /*\n> > - * Find first index of 'nicks' where there is 'nick'. 'nick' is\n> > - * compared case sensitively to the strings in 'nicks'. If not found\n> > - * 'nicks->nr' is returned.\n> > + * Find first element of 'p' where the 'name' field is 'nick'. 'nick'\n> > + * is compared case sensitively to the strings in 'p'. If not found\n> > + * NULL is returned.\n> >   */\n> > -static size_t remote_nick_find(struct strvec *nicks, const char *nick)\n> > +static struct promisor_info *remote_nick_find(struct promisor_info *p, const char *nick)\n> >  {\n> > -     for (size_t i = 0; i < nicks->nr; i++)\n> > -             if (!strcmp(nicks->v[i], nick))\n> > -                     return i;\n> > -     return nicks->nr;\n> > +     for (; p; p = p->next) {\n> > +             assert(!strcmp(p->fields.items[0].string, \"name\"));\n>\n> Why do we add this assert now? And if we want to keep it, shouldn't it\n> rather be `BUG()`?\n\nYeah, it's now BUG() in the next version.\n\n> > @@ -414,11 +461,16 @@ static int should_accept_remote(enum accept_promisor accept,\n> >               return 0;\n> >       }\n> >\n> > -     if (!strcmp(urls->v[i], remote_url))\n> > +     if (strcmp(p->fields.items[1].string, \"url\"))\n> > +             BUG(\"Bad info_list for remote '%s'\", remote_name);\n>\n> It feels somewhat fragile to assume hardcoded locations of each of the\n> keys in `fields.items`. Would it be preferable to instead have a\n> function that looks up the index by key?\n\nOn the other hand we always require a 'name' and a 'url' fields, so\nputting them first can simplify and speed things up.\n\nIf the checks look too ugly maybe they can be abstracted out in\nfunctions dedicated to get the name or the url?\n"},{"id":"516956","messageId":"CAP8UFD2WBqEiSB4YxoDCPuC7wyV81X-pPrLMJiEsv_kdktdEBg@mail.gmail.com","threadId":"63293","inReplyTo":"xmqq34eapfhk.fsf@gitster.g","subject":"Re: [PATCH 3/4] promisor-remote: allow a server to advertise extra fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-04-29T15:12:34Z","receivedAt":"2025-04-29T15:12:47Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Tue, Apr 15, 2025 at 12:04 AM Junio C Hamano <gitster@pobox.com> wrote:\n\n> As a description of overall syntax of the protocol, this and ...\n\n[...]\n\n> ... this may work, but as we are defining protocol between two\n> parties, in order to ensure interoperable reimplementations, we need\n> to also specify semantics, what are the defined \"fields\", and what\n> each of them mean.  Proposing nebulous \"with this framework your\n> imagination is the limit, you can invent anything\" may work for\n> other parts of the system, but not for the part that is about\n> communication between two repositories.\n\nYeah, we can specify a set of accepted fields and their semantics, and\nthat's what I have done in the next version where only\n\"partialCloneFilter\" and \"token\" are accepted so far.\n\nOne issue with this is that we might not know how user systems will\nwork and what they will need. For example if they use a special remote\nhelper they created to access a special remote, we might not know what\nkind of credentials the underlying system on that remote will need, so\nthey might be tempted to reuse some existing fields that weren't\ndesigned for that purpose if we don't make it easy enough to add what\nthey need.\n\nAnyway maybe we can deal with that problem later if it ever happens.\nThe \"token\" field might be enough or maybe there aren't so many kinds\nof credentials so adding a few more will be enough. And yeah, it's\neasier to be more lenient on this later than to restrict things later.\n\n> IOW, we shouldn't be internally calling these \"extra\".  From the\n> point of view of \"core\" they may be \"extra\", but to the developers\n> and certainly to the end-users, they shouldn't be \"extra\" at all.\n> They are all supported parts of the system with defined semantics,\n> right?\n\nI used \"extra\" to mean \"additional\" and \"optional\", compared to the\n\"name\" and \"url\" fields which are mandatory.\n\nBut yeah I have removed \"extra\" from all the names in the next version\nto avoid possible confusion.\n\n> Another reason why I hate seeing this nebulous \"with this, we can\n> send anything extra\" is because such a thing will have a wrong\n> security posture.  If we truly *need* to be able to carry\n> *anything*, we need to make sure how values are quoted/escaped, and\n> the code for dequoting/unescaping are robustly written to avoid\n> passing malformed input and misinterpreting it as something else,\n> which would give a new attack vector.  If we can enumerate supported\n> fields, their syntax and their possible values, we can make the\n> attack surface a lot smaller.\n\nYeah, maybe it will help a bit security wise.\n"},{"id":"516957","messageId":"CAP8UFD0UzX1-WsLzo0R6mND_dTpF6hPgD97Wvg4h3fqXO5-Bmg@mail.gmail.com","threadId":"63293","inReplyTo":"aAdr2Q9jthNV699i@pks.im","subject":"Re: [PATCH 3/4] promisor-remote: allow a server to advertise extra fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-04-29T15:12:46Z","receivedAt":"2025-04-29T15:13:01Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Tue, Apr 22, 2025 at 12:13 PM Patrick Steinhardt <ps@pks.im> wrote:\n\n> I agree that we should properly specify which fields are accepted and\n> what their respective format as well as semantics would be. Otherwise,\n> without such a definition, hosting sites may eventually end up with\n> slightly incompatible semantics. I also don't expect that there should\n> be all that many fields.\n\nYeah, I have done that in the next version where only\n\"partialCloneFilter\" and \"token\" are accepted and are documented.\n\n> This raises a question though: what would happen if a field was\n> advertised that the client doesn't understand? Should the client simply\n> ignore such a field?\n\nYes, for backward compatibility we need them to ignore fields they\ndon't understand.\n\n> Should they bail out?\n\nNo, this would prevent clients that are a bit old but could work with\na server to just not be able to work anymore as soon as the server\ntries new features.\n\n> I think we need to also think\n> about this edge case and specify client-side behaviour. I think in the\n> end, both ways would be rather limiting:\n>\n>   - If we simply ignored all unknown fields our hands might be bound if\n>     we ever had to introduce changes that aren't backwards compatible.\n\nYeah, but that still leaves backward compatible changes as OK.\n\n>   - If we always bail out on an unknown field our hands would be bound\n>     equally, as we cannot ever introduce a new field.\n\nYeah, that's worse.\n\n> Which raises the question whether we need to be able to dynamically\n> figure out fields. This could be in the form of capability negotiation\n> or protocol versions.\n\nThere is no real negotiation. The server advertises some remotes and\nassociated fields, and the client just accepts some of these remotes\nor not. We would need a separate protocol capability if we wanted a\nreal capability negotiation.\n\n> But in any case, I think we need to have something\n> ready so that we can change behaviour depending on which features are\n> supported by a client.\n\nIf the client ignores fields it doesn't know, a server can provide\nboth some fields for old clients and new fields for new clients. Then\nthe client itself will be able to use what looks best for itself.\n"},{"id":"517102","messageId":"CAP8UFD2LzijGsU3-aO-JBPtEfT+UtR_X_mqcdVigoOfdVAg1uA@mail.gmail.com","threadId":"63293","inReplyTo":"20250429145243.992252-1-christian.couder@gmail.com","subject":"Re: [PATCH v2 0/3] Make the \"promisor-remote\" capability support more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-02T09:34:29Z","receivedAt":"2025-05-02T09:34:43Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Tue, Apr 29, 2025 at 4:53 PM Christian Couder\n<christian.couder@gmail.com> wrote:\n\n\n> Right now in this series, the fields names (like \"partialCloneFilter\"\n> in the example) that are passed to the client are compared case\n> sensitively to the local config keys. I think this is a bug and they\n> should be compared case insensitively, (while values should still be\n> compared case sensitively). I am planning to fix this in the next\n> iteration of this series. I am also planning to add more tests then.\n\nSorry, I should have removed the above paragraph as now the field\nnames are compared case insensitively.\n"},{"id":"517464","messageId":"aBsZAKmaA_uGPgk2@pks.im","threadId":"63293","inReplyTo":"20250429145243.992252-2-christian.couder@gmail.com","subject":"Re: [PATCH v2 1/3] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-07T08:25:36Z","receivedAt":"2025-05-07T08:25:45Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Apr 29, 2025 at 04:52:41PM +0200, Christian Couder wrote:\n> diff --git a/promisor-remote.c b/promisor-remote.c\n> index 5801ebfd9b..24d0e70132 100644\n> --- a/promisor-remote.c\n> +++ b/promisor-remote.c\n> @@ -314,10 +314,51 @@ static int allow_unsanitized(char ch)\n>  \treturn ch > 32 && ch < 127;\n>  }\n>  \n> -static void promisor_info_vecs(struct repository *repo,\n> -\t\t\t       struct strvec *names,\n> -\t\t\t       struct strvec *urls)\n> +/*\n> + * Linked list for promisor remotes involved in the \"promisor-remote\"\n> + * protocol capability.\n> + *\n> + * 'fields' contains a defined set of field name/value pairs for\n> + * each promisor remote. Field names are stored in the 'string'\n> + * member, and values in the 'util' member.\n> + *\n> + * Currently supported field names:\n> + * - \"name\": The name of the promisor remote.\n> + * - \"url\": The URL of the promisor remote.\n> + *\n> + * Except for \"name\", each \"<field_name>/<field_value>\" pair should\n> + * correspond to a \"remote.<name>.<field_name>\" config variable set to\n> + * <field_value> where \"<name>\" is a promisor remote name.\n> + *\n> + * 'fields' should not be sorted, as we will rely on the order we put\n> + * things into it. So, for example, 'string_list_append()' should be\n> + * used instead of 'string_list_insert()'.\n> + */\n> +struct promisor_info {\n> +\tstruct promisor_info *next;\n> +\tstruct string_list fields;\n\nNow that we have a restricted set of accepted fields, wouldn't it be\neasier to store those as individual members of this struct directly?\n\n[snip]\n>  /*\n> - * Find first index of 'nicks' where there is 'nick'. 'nick' is\n> - * compared case sensitively to the strings in 'nicks'. If not found\n> - * 'nicks->nr' is returned.\n> + * Find first element of 'p' where the 'name' field is 'nick'. 'nick'\n> + * is compared case sensitively to the strings in 'p'. If not found\n> + * NULL is returned.\n>   */\n> -static size_t remote_nick_find(struct strvec *nicks, const char *nick)\n> +static struct promisor_info *remote_nick_find(struct promisor_info *p, const char *nick)\n>  {\n> -\tfor (size_t i = 0; i < nicks->nr; i++)\n> -\t\tif (!strcmp(nicks->v[i], nick))\n> -\t\t\treturn i;\n> -\treturn nicks->nr;\n> +\tfor (; p; p = p->next) {\n> +\t\tif (strcmp(p->fields.items[0].string, \"name\"))\n> +\t\t\tBUG(\"First field of promisor info should be 'name', but was '%s'.\",\n> +\t\t\t    p->fields.items[0].string);\n> +\t\tif (!strcmp(p->fields.items[0].util, nick))\n> +\t\t\treturn p;\n> +\t}\n> +\treturn NULL;\n>  }\n>  \n>  enum accept_promisor {\n\nIf so, we could also simplify code like this because we wouldn't have to\nassume any indices anymore.\n\nPatrick\n"},{"id":"517465","messageId":"aBsZBytP6TzMYCxl@pks.im","threadId":"63293","inReplyTo":"20250429145243.992252-3-christian.couder@gmail.com","subject":"Re: [PATCH v2 2/3] promisor-remote: allow a server to advertise more fields","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-07T08:25:43Z","receivedAt":"2025-05-07T08:25:47Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Apr 29, 2025 at 04:52:42PM +0200, Christian Couder wrote:\n> diff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\n> index 2638b01f83..71311b70c8 100644\n> --- a/Documentation/config/promisor.adoc\n> +++ b/Documentation/config/promisor.adoc\n> @@ -9,6 +9,24 @@ promisor.advertise::\n>  \t\"false\", which means the \"promisor-remote\" capability is not\n>  \tadvertised.\n>  \n> +promisor.sendFields::\n> +\tA comma or space separated list of additional remote related\n> +\tfields that a server will send while advertising its promisor\n> +\tremotes using the \"promisor-remote\" capability, see\n> +\tlinkgit:gitprotocol-v2[5]. Currently, only the\n> +\t\"partialCloneFilter\" and \"token\" fields are supported. The\n> +\t\"partialCloneFilter\" field contains the partial clone filter\n> +\tused for the remote, and the \"token\" field contains an\n> +\tauthentication token for the remote.\n> ++\n\nShould we maybe convert this into a list of accepted fields? Makes it\neasier to extend going forward.\n\nFurthermore, should we maybe refactor this to match the restrictive\ndesign where valid fields are explicitly specified? In other words,\nshould we have separate config keys for each of the accepted fields now?\n\nAlso, shouldn't this setting be per promisor remote that we want to\nadvertise? I expect that servers will want to send different partial\nclone filters for each of the advertised remotes, and they may also want\nto send different tokens. So it seems a bit too inflexible to only have\na single, global \"sendFields\" configuration that covers all promisors.\n\n> diff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\n> index 5598c93e67..b4648a7ce6 100644\n> --- a/Documentation/gitprotocol-v2.adoc\n> +++ b/Documentation/gitprotocol-v2.adoc\n> @@ -785,33 +785,52 @@ retrieving the header from a bundle at the indicated URI, and thus\n>  save themselves and the server(s) the request(s) needed to inspect the\n>  headers of that bundle or bundles.\n>  \n> -promisor-remote=<pr-infos>\n> +promisor-remote=<pr-info>\n>  ~~~~~~~~~~~~~~~~~~~~~~~~~~\n>  \n>  The server may advertise some promisor remotes it is using or knows\n>  about to a client which may want to use them as its promisor remotes,\n> -instead of this repository. In this case <pr-infos> should be of the\n> +instead of this repository. In this case <pr-info> should be of the\n>  form:\n>  \n> -\tpr-infos = pr-info | pr-infos \";\" pr-info\n> +\tpr-info = pr-fields | pr-info \";\" pr-info\n>  \n> -\tpr-info = \"name=\" pr-name | \"name=\" pr-name \",\" \"url=\" pr-url\n> +\tpr-fields = fld-name \"=\" fld-value | pr-fields \",\" pr-fields\n\nTiny nit, but can we maybe spell out \"fld\" fully? It doesn't buy us that\nmuch to abbreviate \"field\", and it did cause my reading to trip.\n\n> -where `pr-name` is the urlencoded name of a promisor remote, and\n> -`pr-url` the urlencoded URL of that promisor remote.\n> +where all the `fld-name` and `fld-value` in a given `pr-fields` are\n> +field names and values related to a single promisor remote.\n>  \n> -In this case, if the client decides to use one or more promisor\n> -remotes the server advertised, it can reply with\n> -\"promisor-remote=<pr-names>\" where <pr-names> should be of the form:\n> +The server MUST advertise at least the \"name\" and \"url\" field names\n> +along with the associated field values, which are the name of a valid\n> +remote and its URL, in each `pr-fields`.\n>  \n> -\tpr-names = pr-name | pr-names \";\" pr-name\n> +The server MAY advertise the following optional fields:\n> +\n> +- \"partialCloneFilter\": Filter used for partial clone, corresponding\n> +  to the \"remote.<name>.partialCloneFilter\" config setting.\n> +- \"token\": Authentication token for the remote, corresponding\n> +  to the \"remote.<name>.token\" config setting.\n\nI think we should define semantics of these fields more closely. What\nexactly is the consequence of a partial clone filter being defined? Does\nit mean that this promisor remote should only be used in case we do have\nthe exact same filter passed to git-clone(1)? Does it mean that the\nremote only contains objects that would've been filtered _out_ by such a\nfilter?\n\nFurthermore, we should specify how the token is supposed to be passed to\nthe remote.\n\n> +No other fields are defined by the protocol at this time. Clients SHOULD\n> +ignore fields they don't recognize to allow for future protocol extensions.\n\nShouldn't we require clients to ignore unknown fields? Otherwise, if\nit's only optional to ignore them, we still can't introduce new fields\nin the future without breaking existing clients that chose to ignore\nthis guidance.\n\n> diff --git a/promisor-remote.c b/promisor-remote.c\n> index 24d0e70132..70abec4c24 100644\n> --- a/promisor-remote.c\n> +++ b/promisor-remote.c\n> @@ -314,6 +314,84 @@ static int allow_unsanitized(char ch)\n>  \treturn ch > 32 && ch < 127;\n>  }\n>  \n> +/*\n> + * List of field names allowed to be used in the \"promisor-remote\"\n> + * protocol capability. Each field should correspond to a configurable\n> + * property of a remote that can be relevant for the client.\n> + */\n> +static const char *allowed_fields[] = {\n> +\t\"partialCloneFilter\", /* Filter used for partial clone */\n> +\t\"token\",              /* Authentication token for the remote */\n> +\tNULL\n> +};\n> +\n> +/*\n> + * Check if 'field' is in the list of allowed field names for the\n> + * \"promisor-remote\" protocol capability.\n> + */\n> +static int is_allowed_field(const char *field)\n> +{\n> +\tconst char **p;\n> +\n> +\tfor (p = allowed_fields; *p; p++)\n> +\t\tif (!strcasecmp(*p, field))\n> +\t\t\treturn 1;\n> +\treturn 0;\n> +}\n\nNit: it is a bit funny that we talk about allowed fields here, but\nthe recommendation is to just ignore unknown fields. So maybe this\nshould instead be called \"known_fields\".\n\nPatrick\n"},{"id":"517466","messageId":"aBsZC_MZw7BHxUiS@pks.im","threadId":"63293","inReplyTo":"20250429145243.992252-4-christian.couder@gmail.com","subject":"Re: [PATCH v2 3/3] promisor-remote: allow a client to check fields","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-07T08:25:47Z","receivedAt":"2025-05-07T08:25:50Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Apr 29, 2025 at 04:52:43PM +0200, Christian Couder wrote:\n> diff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\n> index 71311b70c8..4147d2cf44 100644\n> --- a/Documentation/config/promisor.adoc\n> +++ b/Documentation/config/promisor.adoc\n> @@ -46,3 +46,28 @@ promisor.acceptFromServer::\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> +\n> +promisor.checkFields::\n> +\tA comma or space separated list of additional remote related\n> +\tfields that a client will check before accepting a promisor\n> +\tremote. Currently, only the \"partialCloneFilter\" and \"token\"\n> +\tfields are supported.\n> ++\n> +When a field is part of this list and a corresponding\n> +\"remote.foo.<field>\" config variable is set locally for remote \"foo\",\n> +then the value of this config variable will be checked against the\n> +value of the same field passed by the server for the remote \"foo\". The\n> +remote \"foo\" will be rejected if the values don't match.\n> ++\n> +For the \"partialCloneFilter\" field, this allows the client to ensure\n> +that the server's filter matches what it expects locally, preventing\n> +inconsistencies in filtering behavior. For the \"token\" field, this can\n> +be used to verify that authentication credentials match expected\n> +values.\n> ++\n> +The fields should be passed by the server through the\n> +\"promisor-remote\" capability by using the `promisor.sendFields` config\n> +variable. The fields will be checked only if the\n> +`promisor.acceptFromServer` config variable is not set to \"None\".  If\n> +set to \"None\", this config variable will have no effect.  See\n> +linkgit:gitprotocol-v2[5].\n\nOne thought that came to my mind is that inevitably, users will\neventually want to specify different conditions and combinations. E.g.\n\"accept a promisor remote if it's announced by GitLab and if the partial\nfilter strips blobs, but not if it requires additional authentication\".\nI don't think that \"checkFields\" would be able to implement such a use\ncase.\n\nWhat is the vision where we want to end up here? Should we maybe provide\nsome more flexibility now already so that we don't have to retrofit such\na mechanism in the future?\n\nPatrick\n"},{"id":"517478","messageId":"CAOLa=ZRL6nRb0+Bf1u9b45UZGnsB1yPVJ81Mt884onEYhZ7JtA@mail.gmail.com","threadId":"63293","inReplyTo":"20250429145243.992252-2-christian.couder@gmail.com","subject":"Re: [PATCH v2 1/3] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-05-07T12:27:34Z","receivedAt":"2025-05-07T12:27:37Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n[snip]\n\n> +/*\n> + * Linked list for promisor remotes involved in the \"promisor-remote\"\n> + * protocol capability.\n> + *\n> + * 'fields' contains a defined set of field name/value pairs for\n> + * each promisor remote. Field names are stored in the 'string'\n> + * member, and values in the 'util' member.\n> + *\n> + * Currently supported field names:\n> + * - \"name\": The name of the promisor remote.\n> + * - \"url\": The URL of the promisor remote.\n> + *\n> + * Except for \"name\", each \"<field_name>/<field_value>\" pair should\n> + * correspond to a \"remote.<name>.<field_name>\" config variable set to\n> + * <field_value> where \"<name>\" is a promisor remote name.\n> + *\n> + * 'fields' should not be sorted, as we will rely on the order we put\n> + * things into it. So, for example, 'string_list_append()' should be\n> + * used instead of 'string_list_insert()'.\n> + */\n>\n\nDumb question: As I read through the patch, I realized we really care\nabout the order of the fields and it is mentioned here too. Why is the\norder important? Shouldn't the client be satisfied as long as the\nrequired fields are present? Or is this merely an implementation issue\nwhere we simply parse the information in a specific order?\n"},{"id":"517484","messageId":"CAOLa=ZSwzvfQ8MupFzuMEpHCKJRY8p06m-FfyE-deqOsgK_feg@mail.gmail.com","threadId":"63293","inReplyTo":"20250429145243.992252-3-christian.couder@gmail.com","subject":"Re: [PATCH v2 2/3] promisor-remote: allow a server to advertise more fields","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-05-07T12:44:59Z","receivedAt":"2025-05-07T12:45:01Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n[snip]\n\n> diff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\n> index 5598c93e67..b4648a7ce6 100644\n> --- a/Documentation/gitprotocol-v2.adoc\n> +++ b/Documentation/gitprotocol-v2.adoc\n> @@ -785,33 +785,52 @@ retrieving the header from a bundle at the indicated URI, and thus\n>  save themselves and the server(s) the request(s) needed to inspect the\n>  headers of that bundle or bundles.\n>\n> -promisor-remote=<pr-infos>\n> +promisor-remote=<pr-info>\n>  ~~~~~~~~~~~~~~~~~~~~~~~~~~\n>\n>  The server may advertise some promisor remotes it is using or knows\n>  about to a client which may want to use them as its promisor remotes,\n> -instead of this repository. In this case <pr-infos> should be of the\n> +instead of this repository. In this case <pr-info> should be of the\n>  form:\n>\n> -\tpr-infos = pr-info | pr-infos \";\" pr-info\n> +\tpr-info = pr-fields | pr-info \";\" pr-info\n>\n> -\tpr-info = \"name=\" pr-name | \"name=\" pr-name \",\" \"url=\" pr-url\n> +\tpr-fields = fld-name \"=\" fld-value | pr-fields \",\" pr-fields\n>\n\nFrom this, it seems like the order of the fields shouldn't matter, but\nthis is not the case. wouldn't it be better to say:\n\n  pr-infos = pr-info | pr-infos \";\" pr-info\n\n  pr-info = \"name=\" pr-name | \"name=\" pr-name \",\" \"url=\" pr-url\n  pr-info = pr-info | pr-info \",\" fld-name \"=\" fld-value\n\n[snip]\n\n> diff --git a/promisor-remote.c b/promisor-remote.c\n> index 24d0e70132..70abec4c24 100644\n> --- a/promisor-remote.c\n> +++ b/promisor-remote.c\n> @@ -314,6 +314,84 @@ static int allow_unsanitized(char ch)\n>  \treturn ch > 32 && ch < 127;\n>  }\n>\n> +/*\n> + * List of field names allowed to be used in the \"promisor-remote\"\n> + * protocol capability. Each field should correspond to a configurable\n> + * property of a remote that can be relevant for the client.\n> + */\n> +static const char *allowed_fields[] = {\n> +\t\"partialCloneFilter\", /* Filter used for partial clone */\n> +\t\"token\",              /* Authentication token for the remote */\n> +\tNULL\n> +};\n> +\n> +/*\n> + * Check if 'field' is in the list of allowed field names for the\n> + * \"promisor-remote\" protocol capability.\n> + */\n> +static int is_allowed_field(const char *field)\n> +{\n> +\tconst char **p;\n> +\n> +\tfor (p = allowed_fields; *p; p++)\n> +\t\tif (!strcasecmp(*p, field))\n> +\t\t\treturn 1;\n> +\treturn 0;\n> +}\n> +\n> +static int valid_field(struct string_list_item *item, void *cb_data)\n> +{\n\nNit: Shouldn't this be `is_valid_field` similar to `is_allowed_field`?\n\n> +\tconst char *field = item->string;\n> +\tconst char *config_key = (const char *)cb_data;\n> +\n> +\tif (!is_allowed_field(field)) {\n\nNit: Can't we just inline this?\n\n> +\t\twarning(_(\"unsupported field '%s' in '%s' config\"), field, config_key);\n> +\t\treturn 0;\n> +\t}\n> +\treturn 1;\n> +}\n> +\n> +static char *fields_from_config(struct string_list *fields_list, const char *config_key)\n> +{\n> +\tchar *fields = NULL;\n> +\n> +\tif (!git_config_get_string(config_key, &fields) && *fields) {\n> +\t\tstring_list_split_in_place(fields_list, fields, \", \", -1);\n> +\t\tfilter_string_list(fields_list, 0, valid_field, (void *)config_key);\n> +\t}\n> +\n> +\treturn fields;\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 = 0;\n> +\n> +\tif (!initialized) {\n> +\t\tfields_list.cmp = strcasecmp;\n> +\t\tfields_from_config(&fields_list, \"promisor.sendFields\");\n\nNit: Here too, can't this be inlined? While the modularity is nice, I'm\nnot sure the redirection is warranted for such small functions with very\nspecific usecases.\n\n[snip]\n\nApart from the nits, the patch looks good :)\n"},{"id":"518408","messageId":"CAP8UFD0c+LfCoHzvMyGYkmNmFd4PApEgmt65-TzLBvdap3LvdQ@mail.gmail.com","threadId":"63293","inReplyTo":"aBsZAKmaA_uGPgk2@pks.im","subject":"Re: [PATCH v2 1/3] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-19T14:10:16Z","receivedAt":"2025-05-19T14:10:30Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Wed, May 7, 2025 at 10:25 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Tue, Apr 29, 2025 at 04:52:41PM +0200, Christian Couder wrote:\n\n> > +/*\n> > + * Linked list for promisor remotes involved in the \"promisor-remote\"\n> > + * protocol capability.\n> > + *\n> > + * 'fields' contains a defined set of field name/value pairs for\n> > + * each promisor remote. Field names are stored in the 'string'\n> > + * member, and values in the 'util' member.\n> > + *\n> > + * Currently supported field names:\n> > + * - \"name\": The name of the promisor remote.\n> > + * - \"url\": The URL of the promisor remote.\n> > + *\n> > + * Except for \"name\", each \"<field_name>/<field_value>\" pair should\n> > + * correspond to a \"remote.<name>.<field_name>\" config variable set to\n> > + * <field_value> where \"<name>\" is a promisor remote name.\n> > + *\n> > + * 'fields' should not be sorted, as we will rely on the order we put\n> > + * things into it. So, for example, 'string_list_append()' should be\n> > + * used instead of 'string_list_insert()'.\n> > + */\n> > +struct promisor_info {\n> > +     struct promisor_info *next;\n> > +     struct string_list fields;\n>\n> Now that we have a restricted set of accepted fields, wouldn't it be\n> easier to store those as individual members of this struct directly?\n\nI have done so in the v3. The struct now looks like this at the end of\nthe series:\n\nstruct promisor_info {\n    struct promisor_info *next;\n    const char *name;\n    const char *url;\n    const char *filter;\n    const char *token;\n};\n\n> [snip]\n> >  /*\n> > - * Find first index of 'nicks' where there is 'nick'. 'nick' is\n> > - * compared case sensitively to the strings in 'nicks'. If not found\n> > - * 'nicks->nr' is returned.\n> > + * Find first element of 'p' where the 'name' field is 'nick'. 'nick'\n> > + * is compared case sensitively to the strings in 'p'. If not found\n> > + * NULL is returned.\n> >   */\n> > -static size_t remote_nick_find(struct strvec *nicks, const char *nick)\n> > +static struct promisor_info *remote_nick_find(struct promisor_info *p, const char *nick)\n> >  {\n> > -     for (size_t i = 0; i < nicks->nr; i++)\n> > -             if (!strcmp(nicks->v[i], nick))\n> > -                     return i;\n> > -     return nicks->nr;\n> > +     for (; p; p = p->next) {\n> > +             if (strcmp(p->fields.items[0].string, \"name\"))\n> > +                     BUG(\"First field of promisor info should be 'name', but was '%s'.\",\n> > +                         p->fields.items[0].string);\n> > +             if (!strcmp(p->fields.items[0].util, nick))\n> > +                     return p;\n> > +     }\n> > +     return NULL;\n> >  }\n> >\n> >  enum accept_promisor {\n>\n> If so, we could also simplify code like this because we wouldn't have to\n> assume any indices anymore.\n\nYeah, it simplifies things, but on the other hand in some places the\nfields cannot be handled in a generic way, so we have things like:\n\n        if (!strcmp(elem, promisor_field_name))\n            info->name = value;\n        else if (!strcmp(elem, promisor_field_url))\n            info->url = value;\n        else if (!strcasecmp(elem, promisor_field_filter))\n            info->filter = value;\n        else if (!strcasecmp(elem, promisor_field_token))\n            info->token = value;\n\nAnyway I think that it's not too bad.\n"},{"id":"518409","messageId":"CAP8UFD1xrtyv1hJhB2mvbgJb4bWeV3K1GZGdGbF+ptcp7T1Dew@mail.gmail.com","threadId":"63293","inReplyTo":"CAOLa=ZRL6nRb0+Bf1u9b45UZGnsB1yPVJ81Mt884onEYhZ7JtA@mail.gmail.com","subject":"Re: [PATCH v2 1/3] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-19T14:10:28Z","receivedAt":"2025-05-19T14:10:43Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Wed, May 7, 2025 at 2:27 PM Karthik Nayak <karthik.188@gmail.com> wrote:\n>\n> Christian Couder <christian.couder@gmail.com> writes:\n>\n> [snip]\n>\n> > +/*\n> > + * Linked list for promisor remotes involved in the \"promisor-remote\"\n> > + * protocol capability.\n> > + *\n> > + * 'fields' contains a defined set of field name/value pairs for\n> > + * each promisor remote. Field names are stored in the 'string'\n> > + * member, and values in the 'util' member.\n> > + *\n> > + * Currently supported field names:\n> > + * - \"name\": The name of the promisor remote.\n> > + * - \"url\": The URL of the promisor remote.\n> > + *\n> > + * Except for \"name\", each \"<field_name>/<field_value>\" pair should\n> > + * correspond to a \"remote.<name>.<field_name>\" config variable set to\n> > + * <field_value> where \"<name>\" is a promisor remote name.\n> > + *\n> > + * 'fields' should not be sorted, as we will rely on the order we put\n> > + * things into it. So, for example, 'string_list_append()' should be\n> > + * used instead of 'string_list_insert()'.\n> > + */\n> >\n>\n> Dumb question: As I read through the patch, I realized we really care\n> about the order of the fields and it is mentioned here too. Why is the\n> order important? Shouldn't the client be satisfied as long as the\n> required fields are present? Or is this merely an implementation issue\n> where we simply parse the information in a specific order?\n\nWe used to only care about the order for the first 2 mandatory fields\n(\"name\" and \"url\"), but this is relaxed in the v3.\n\nAnyway I think it's better if the 2 mandatory fields appear first as\nin many cases it might theoretically enable the client to reject an\nadvertised remote as soon as it has parsed the first 2 fields. So the\nclient could stop parsing the rest of the remote information in some\ncases which might reduce its workload a bit and speed things up.\n\nYeah, I don't think it matters now, and we can take care of this if it\never matters in the future. But if it ever matters, we might be happy\nto have specified for a long time that \"name\" and \"url\" should appear\nfirst.\n"},{"id":"518410","messageId":"CAP8UFD1+c11JgSKzs=A39-5EP2Senob-NxSXB7orU0usSLT83A@mail.gmail.com","threadId":"63293","inReplyTo":"aBsZBytP6TzMYCxl@pks.im","subject":"Re: [PATCH v2 2/3] promisor-remote: allow a server to advertise more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-19T14:11:18Z","receivedAt":"2025-05-19T14:11:32Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Wed, May 7, 2025 at 10:25 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Tue, Apr 29, 2025 at 04:52:42PM +0200, Christian Couder wrote:\n> > diff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\n> > index 2638b01f83..71311b70c8 100644\n> > --- a/Documentation/config/promisor.adoc\n> > +++ b/Documentation/config/promisor.adoc\n> > @@ -9,6 +9,24 @@ promisor.advertise::\n> >       \"false\", which means the \"promisor-remote\" capability is not\n> >       advertised.\n> >\n> > +promisor.sendFields::\n> > +     A comma or space separated list of additional remote related\n> > +     fields that a server will send while advertising its promisor\n> > +     remotes using the \"promisor-remote\" capability, see\n> > +     linkgit:gitprotocol-v2[5]. Currently, only the\n> > +     \"partialCloneFilter\" and \"token\" fields are supported. The\n> > +     \"partialCloneFilter\" field contains the partial clone filter\n> > +     used for the remote, and the \"token\" field contains an\n> > +     authentication token for the remote.\n> > ++\n>\n> Should we maybe convert this into a list of accepted fields? Makes it\n> easier to extend going forward.\n\nI am not sure I understand what you mean. This promisor.sendFields\nconfig variable is for the server side which advertises remotes. The\nserver advertises its remotes (if it wants to) before receiving\ninformation from the client, so it cannot know what the client\naccepts.\n\n> Furthermore, should we maybe refactor this to match the restrictive\n> design where valid fields are explicitly specified? In other words,\n> should we have separate config keys for each of the accepted fields now?\n\nMaybe I don't understand what you mean with \"accepted fields\".\n\n> Also, shouldn't this setting be per promisor remote that we want to\n> advertise? I expect that servers will want to send different partial\n> clone filters for each of the advertised remotes, and they may also want\n> to send different tokens. So it seems a bit too inflexible to only have\n> a single, global \"sendFields\" configuration that covers all promisors.\n\nFirst this setting already allows servers to send different partial\nclone filters for each of the advertised remotes. For each remote it\nadvertises, a server would send the partial clone filter that is\nalready configured for this remote on the server. Same for tokens.\n\nAlso we can extend this setting to be per promisor remote later if\nthere is a need for it. I don't think it would be difficult to do. And\nI don't think it's necessary right now, because it's likely that for\nsimplicity most servers will manage all their promisor remotes in the\nsame way (at least until usage of promisor remotes grows a lot).\n\n> > diff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\n> > index 5598c93e67..b4648a7ce6 100644\n> > --- a/Documentation/gitprotocol-v2.adoc\n> > +++ b/Documentation/gitprotocol-v2.adoc\n> > @@ -785,33 +785,52 @@ retrieving the header from a bundle at the indicated URI, and thus\n> >  save themselves and the server(s) the request(s) needed to inspect the\n> >  headers of that bundle or bundles.\n> >\n> > -promisor-remote=<pr-infos>\n> > +promisor-remote=<pr-info>\n> >  ~~~~~~~~~~~~~~~~~~~~~~~~~~\n> >\n> >  The server may advertise some promisor remotes it is using or knows\n> >  about to a client which may want to use them as its promisor remotes,\n> > -instead of this repository. In this case <pr-infos> should be of the\n> > +instead of this repository. In this case <pr-info> should be of the\n> >  form:\n> >\n> > -     pr-infos = pr-info | pr-infos \";\" pr-info\n> > +     pr-info = pr-fields | pr-info \";\" pr-info\n> >\n> > -     pr-info = \"name=\" pr-name | \"name=\" pr-name \",\" \"url=\" pr-url\n> > +     pr-fields = fld-name \"=\" fld-value | pr-fields \",\" pr-fields\n>\n> Tiny nit, but can we maybe spell out \"fld\" fully? It doesn't buy us that\n> much to abbreviate \"field\", and it did cause my reading to trip.\n\nFine, I have done this in v3.\n\n> > -where `pr-name` is the urlencoded name of a promisor remote, and\n> > -`pr-url` the urlencoded URL of that promisor remote.\n> > +where all the `fld-name` and `fld-value` in a given `pr-fields` are\n> > +field names and values related to a single promisor remote.\n> >\n> > -In this case, if the client decides to use one or more promisor\n> > -remotes the server advertised, it can reply with\n> > -\"promisor-remote=<pr-names>\" where <pr-names> should be of the form:\n> > +The server MUST advertise at least the \"name\" and \"url\" field names\n> > +along with the associated field values, which are the name of a valid\n> > +remote and its URL, in each `pr-fields`.\n> >\n> > -     pr-names = pr-name | pr-names \";\" pr-name\n> > +The server MAY advertise the following optional fields:\n> > +\n> > +- \"partialCloneFilter\": Filter used for partial clone, corresponding\n> > +  to the \"remote.<name>.partialCloneFilter\" config setting.\n> > +- \"token\": Authentication token for the remote, corresponding\n> > +  to the \"remote.<name>.token\" config setting.\n>\n> I think we should define semantics of these fields more closely. What\n> exactly is the consequence of a partial clone filter being defined?\n\nIt's just that the client will know what filter the server uses to\naccess the promisor remote. In a later patch in this series the client\nwill be able to decide to accept the remote or not based on that\ninformation (depending on whether the filter matches the filter\nalready configured on the client side). The client will still not use\nthe information for other things than deciding to accept or not the\nremote after this patch series.\n\n> Does\n> it mean that this promisor remote should only be used in case we do have\n> the exact same filter passed to git-clone(1)?\n\nIt's up to the client to decide, but yeah it will likely work better\nif the same filter is used. It should still work if a different filter\nis used though. In case the promisor remote doesn't have an object,\nthere should be a fallback to ask the main server for that object.\n\nAlso the filter mechanism already exists for a long time and this\nseries doesn't change how it works. It's already possible to have\ndifferent repos using the same promisor remote with different filters.\nSo documentation about what happens when they do that should not be\nspecific to this patch series.\n\n> Does it mean that the\n> remote only contains objects that would've been filtered _out_ by such a\n> filter?\n\nThe filter specification in general is always about what is filtered\nout from the repo accessing the promisor remote (not about what is\nfiltered out from the promisor remote). Again this is not specific to\nthis patch series. This is general partial clone information since\npartial clone has been developed a long time ago.\n\n> Furthermore, we should specify how the token is supposed to be passed to\n> the remote.\n\nFirst for now the token is not used for other things than deciding if\nthe client accepts the remote or not. (Same thing as for the filter,\nurl, name...) I think it can already be useful in some cases.\n\nNow, if we want to talk about a future patch series when the token is\npossibly used, fine, let's do it. Please consider the following:\n\n  - If the token is passed to the remote through a remote helper, then\nit's up to the remote helper to pass and use it, and it could do so in\nmany different ways that we can't anticipate and maybe we will not\nnecessarily know about them (for privately developed and used remote\nhelpers for example).\n\n  - If we develop a standard way in Git to use such a token, then we\nwill know about that standard way's use of the token at that time (not\nnecessarily about how remote helpers in the wild might use it though)\n. But to develop such a standard way, it's better to already have such\na token passed.\n\n  - Suppose we require this standard way to be developed alongside a\npatch series such as this one which passes a token. The main issue is\nthat this would prevent remote helpers from using such a token until\nthat standard way is developed. And for now we don't really know if\nsuch a standard way is actually needed or if people will only or\nmostly use remote helpers.\n\n  - What if this standard way to use a token is actually not needed\nbecause people use very different backends for large objects with very\ndifferent remote helpers developed by different people?\n\n  - There are not many technical similarities between code that passes\na token and code that possibly creates it and then uses it. So I don't\nthink there is anything technically that requires us to do these\nthings in the same patch series.\n\n  - There might be a catch 22 between a patch series such as this one\nand a patch series to develop a standard way to use a token. We might\nnot accept patch series adding a token like this one because the token\nis not used yet, while a patch series to develop a standard way to\naccess remote helpers using a token cannot work or even be developed\nbecause no token is passed yet.\n\nSo I strongly suggest accepting that we pass a token right now, and\nnot wait for possible ways to use it (other than deciding if the\nremote is accepted or not).\n\nIf 'token' is not put into the promisor.sendFields config variable by\nservers, the only cost is that clients might check if 'token' is\npassed when servers advertise their promisor remotes. It's a very\nsmall cost to enable something likely very much needed and important\nfor security reasons.\n\n> > +No other fields are defined by the protocol at this time. Clients SHOULD\n> > +ignore fields they don't recognize to allow for future protocol extensions.\n>\n> Shouldn't we require clients to ignore unknown fields? Otherwise, if\n> it's only optional to ignore them, we still can't introduce new fields\n> in the future without breaking existing clients that chose to ignore\n> this guidance.\n\nIn v3 this is now:\n\n\"No other fields are defined by the protocol at this time. Clients MUST\nignore fields they don't recognize to allow for future protocol\nextensions.\"\n\nWith \"MUST\" instead of \"SHOULD\", we now require clients to ignore\nunknown fields, so they shouldn't break if we introduce new fields.\n\n> > diff --git a/promisor-remote.c b/promisor-remote.c\n> > index 24d0e70132..70abec4c24 100644\n> > --- a/promisor-remote.c\n> > +++ b/promisor-remote.c\n> > @@ -314,6 +314,84 @@ static int allow_unsanitized(char ch)\n> >       return ch > 32 && ch < 127;\n> >  }\n> >\n> > +/*\n> > + * List of field names allowed to be used in the \"promisor-remote\"\n> > + * protocol capability. Each field should correspond to a configurable\n> > + * property of a remote that can be relevant for the client.\n> > + */\n> > +static const char *allowed_fields[] = {\n> > +     \"partialCloneFilter\", /* Filter used for partial clone */\n> > +     \"token\",              /* Authentication token for the remote */\n> > +     NULL\n> > +};\n> > +\n> > +/*\n> > + * Check if 'field' is in the list of allowed field names for the\n> > + * \"promisor-remote\" protocol capability.\n> > + */\n> > +static int is_allowed_field(const char *field)\n> > +{\n> > +     const char **p;\n> > +\n> > +     for (p = allowed_fields; *p; p++)\n> > +             if (!strcasecmp(*p, field))\n> > +                     return 1;\n> > +     return 0;\n> > +}\n>\n> Nit: it is a bit funny that we talk about allowed fields here, but\n> the recommendation is to just ignore unknown fields. So maybe this\n> should instead be called \"known_fields\".\n\nFine, it's now 'known_fields' in v3.\n\nThanks for the review!\n"},{"id":"518411","messageId":"CAP8UFD31PfhJY3jQpBmvbAB6eh43GFfsKzcXby7LshX76Qjekw@mail.gmail.com","threadId":"63293","inReplyTo":"CAOLa=ZSwzvfQ8MupFzuMEpHCKJRY8p06m-FfyE-deqOsgK_feg@mail.gmail.com","subject":"Re: [PATCH v2 2/3] promisor-remote: allow a server to advertise more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-19T14:11:24Z","receivedAt":"2025-05-19T14:11:38Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Wed, May 7, 2025 at 2:45 PM Karthik Nayak <karthik.188@gmail.com> wrote:\n>\n> Christian Couder <christian.couder@gmail.com> writes:\n>\n> [snip]\n>\n> > diff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\n> > index 5598c93e67..b4648a7ce6 100644\n> > --- a/Documentation/gitprotocol-v2.adoc\n> > +++ b/Documentation/gitprotocol-v2.adoc\n> > @@ -785,33 +785,52 @@ retrieving the header from a bundle at the indicated URI, and thus\n> >  save themselves and the server(s) the request(s) needed to inspect the\n> >  headers of that bundle or bundles.\n> >\n> > -promisor-remote=<pr-infos>\n> > +promisor-remote=<pr-info>\n> >  ~~~~~~~~~~~~~~~~~~~~~~~~~~\n> >\n> >  The server may advertise some promisor remotes it is using or knows\n> >  about to a client which may want to use them as its promisor remotes,\n> > -instead of this repository. In this case <pr-infos> should be of the\n> > +instead of this repository. In this case <pr-info> should be of the\n> >  form:\n> >\n> > -     pr-infos = pr-info | pr-infos \";\" pr-info\n> > +     pr-info = pr-fields | pr-info \";\" pr-info\n> >\n> > -     pr-info = \"name=\" pr-name | \"name=\" pr-name \",\" \"url=\" pr-url\n> > +     pr-fields = fld-name \"=\" fld-value | pr-fields \",\" pr-fields\n> >\n>\n> From this, it seems like the order of the fields shouldn't matter, but\n> this is not the case.\n\nI would prefer to keep the simpler version as I find these grammar\nnotations not very easy to understand.\n\nIn v3 there is:\n\n=> The \"name\" and \"url\" fields MUST appear first in each pr-fields, in\nthat order.\n\nAnd I think it's enough, especially since the parsing is relaxed so it\nwill work if they are not in this order.\n\n> wouldn't it be better to say:\n>\n>   pr-infos = pr-info | pr-infos \";\" pr-info\n>\n>   pr-info = \"name=\" pr-name | \"name=\" pr-name \",\" \"url=\" pr-url\n>   pr-info = pr-info | pr-info \",\" fld-name \"=\" fld-value\n\nCould this be interpreted to mean that it is Ok to have \"name=\"\npr-name \",\" \"url=\" pr-url several times in a single pr-info?\n\nThis seems to me really more complex than needed, and I think it's\nbetter to keep the simple version and then clarify things with the\nabove sentence.\n\n> [snip]\n>\n> > diff --git a/promisor-remote.c b/promisor-remote.c\n> > index 24d0e70132..70abec4c24 100644\n> > --- a/promisor-remote.c\n> > +++ b/promisor-remote.c\n> > @@ -314,6 +314,84 @@ static int allow_unsanitized(char ch)\n> >       return ch > 32 && ch < 127;\n> >  }\n> >\n> > +/*\n> > + * List of field names allowed to be used in the \"promisor-remote\"\n> > + * protocol capability. Each field should correspond to a configurable\n> > + * property of a remote that can be relevant for the client.\n> > + */\n> > +static const char *allowed_fields[] = {\n> > +     \"partialCloneFilter\", /* Filter used for partial clone */\n> > +     \"token\",              /* Authentication token for the remote */\n> > +     NULL\n> > +};\n> > +\n> > +/*\n> > + * Check if 'field' is in the list of allowed field names for the\n> > + * \"promisor-remote\" protocol capability.\n> > + */\n> > +static int is_allowed_field(const char *field)\n> > +{\n> > +     const char **p;\n> > +\n> > +     for (p = allowed_fields; *p; p++)\n> > +             if (!strcasecmp(*p, field))\n> > +                     return 1;\n> > +     return 0;\n> > +}\n> > +\n> > +static int valid_field(struct string_list_item *item, void *cb_data)\n> > +{\n>\n> Nit: Shouldn't this be `is_valid_field` similar to `is_allowed_field`?\n\nYeah, I have renamed it `is_valid_field`\n\n> > +     const char *field = item->string;\n> > +     const char *config_key = (const char *)cb_data;\n> > +\n> > +     if (!is_allowed_field(field)) {\n>\n> Nit: Can't we just inline this?\n\nWe could, but I think the is_allowed_field() makes sense on its own\ntoo, while the current one is very specific to be used in\nfilter_string_list(). So I prefer keeping is_allowed_field() separate.\n\n> > +             warning(_(\"unsupported field '%s' in '%s' config\"), field, config_key);\n> > +             return 0;\n> > +     }\n> > +     return 1;\n> > +}\n> > +\n> > +static char *fields_from_config(struct string_list *fields_list, const char *config_key)\n> > +{\n> > +     char *fields = NULL;\n> > +\n> > +     if (!git_config_get_string(config_key, &fields) && *fields) {\n> > +             string_list_split_in_place(fields_list, fields, \", \", -1);\n> > +             filter_string_list(fields_list, 0, valid_field, (void *)config_key);\n> > +     }\n> > +\n> > +     return fields;\n> > +}\n> > +\n> > +static struct string_list *fields_sent(void)\n> > +{\n> > +     static struct string_list fields_list = STRING_LIST_INIT_NODUP;\n> > +     static int initialized = 0;\n> > +\n> > +     if (!initialized) {\n> > +             fields_list.cmp = strcasecmp;\n> > +             fields_from_config(&fields_list, \"promisor.sendFields\");\n>\n> Nit: Here too, can't this be inlined? While the modularity is nice, I'm\n> not sure the redirection is warranted for such small functions with very\n> specific usecases.\n\nIn a follow up patch in this series we reuse fields_from_config() so I\nthink it's better to keep it separate.\n\n> [snip]\n>\n> Apart from the nits, the patch looks good :)\n\nThanks for the review!\n"},{"id":"518412","messageId":"CAP8UFD1mav=omMxSwWmrmORBnXKzKSp4C4FDNP8R9Q43hU77zw@mail.gmail.com","threadId":"63293","inReplyTo":"aBsZC_MZw7BHxUiS@pks.im","subject":"Re: [PATCH v2 3/3] promisor-remote: allow a client to check fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-19T14:11:58Z","receivedAt":"2025-05-19T14:12:12Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Wed, May 7, 2025 at 1:14 PM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Tue, Apr 29, 2025 at 04:52:43PM +0200, Christian Couder wrote:\n> > diff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\n> > index 71311b70c8..4147d2cf44 100644\n> > --- a/Documentation/config/promisor.adoc\n> > +++ b/Documentation/config/promisor.adoc\n> > @@ -46,3 +46,28 @@ promisor.acceptFromServer::\n> >       lazily fetchable from this promisor remote from its responses\n> >       to \"fetch\" and \"clone\" requests from the client. Name and URL\n> >       comparisons are case sensitive. See linkgit:gitprotocol-v2[5].\n> > +\n> > +promisor.checkFields::\n> > +     A comma or space separated list of additional remote related\n> > +     fields that a client will check before accepting a promisor\n> > +     remote. Currently, only the \"partialCloneFilter\" and \"token\"\n> > +     fields are supported.\n> > ++\n> > +When a field is part of this list and a corresponding\n> > +\"remote.foo.<field>\" config variable is set locally for remote \"foo\",\n> > +then the value of this config variable will be checked against the\n> > +value of the same field passed by the server for the remote \"foo\". The\n> > +remote \"foo\" will be rejected if the values don't match.\n> > ++\n> > +For the \"partialCloneFilter\" field, this allows the client to ensure\n> > +that the server's filter matches what it expects locally, preventing\n> > +inconsistencies in filtering behavior. For the \"token\" field, this can\n> > +be used to verify that authentication credentials match expected\n> > +values.\n> > ++\n> > +The fields should be passed by the server through the\n> > +\"promisor-remote\" capability by using the `promisor.sendFields` config\n> > +variable. The fields will be checked only if the\n> > +`promisor.acceptFromServer` config variable is not set to \"None\".  If\n> > +set to \"None\", this config variable will have no effect.  See\n> > +linkgit:gitprotocol-v2[5].\n>\n> One thought that came to my mind is that inevitably, users will\n> eventually want to specify different conditions and combinations. E.g.\n> \"accept a promisor remote if it's announced by GitLab and if the partial\n> filter strips blobs, but not if it requires additional authentication\".\n> I don't think that \"checkFields\" would be able to implement such a use\n> case.\n>\n> What is the vision where we want to end up here? Should we maybe provide\n> some more flexibility now already so that we don't have to retrofit such\n> a mechanism in the future?\n\nI'd prefer to wait until some concrete cases appear. It's possible\nthat the current very simple mechanism will be enough for some time,\nor it's possible that some users will come soon with a complex use\ncase where it might be necessary to have a hook or some kind of\nexternal script called. In those cases it would be sad if we added\nsome flexibility in advance but it appears to be too much or not\nenough.\n"},{"id":"518413","messageId":"20250519141259.3061550-2-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250519141259.3061550-1-christian.couder@gmail.com","subject":"[PATCH v3 1/5] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-19T14:12:55Z","receivedAt":"2025-05-19T14:13:20Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"In a following commit, we will use the new 'promisor-remote' protocol\ncapability introduced by d460267613 (Add 'promisor-remote' capability\nto protocol v2, 2025-02-18) to pass and process more information\nabout promisor remotes than just their name and url.\n\nFor that purpose, we will need to store information about other\nfields, especially information that might or might not be available\nfor different promisor remotes. Unfortunately using 'struct strvec',\nas we currently do, to store information about the promisor remotes\nwith one 'struct strvec' for each field like \"name\" or \"url\" does not\nscale easily in that case.\n\nLet's refactor this and introduce a new 'struct promisor_info' linked\nlist which for now only contains a 'next' pointer, a 'name' member for\nthe promisor remote name and an 'url' member for its URL.\n\nExplicit members are used within 'struct promisor_info' for type\nsafety and clarity regarding the specific information being handled,\nrather than a generic key-value store. We want to specify and document\neach field and its content, so adding new members to the struct as\nmore fields are supported is fine.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 107 +++++++++++++++++++++++++++++++---------------\n 1 file changed, 72 insertions(+), 35 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 9d058586df..94e87f2f48 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -314,10 +314,37 @@ static int allow_unsanitized(char ch)\n \treturn ch > 32 && ch < 127;\n }\n \n-static void promisor_info_vecs(struct repository *repo,\n-\t\t\t       struct strvec *names,\n-\t\t\t       struct strvec *urls)\n+/*\n+ * Linked list for promisor remotes involved in the \"promisor-remote\"\n+ * protocol capability.\n+ *\n+ * Except for \"next\" and \"name\", each <member> in this struct and its\n+ * <value> should correspond to a \"remote.<name>.<member>\" config\n+ * variable set to <value> where \"<name>\" is a promisor remote name.\n+ */\n+struct promisor_info {\n+\tstruct promisor_info *next;\n+\tconst char *name;\n+\tconst char *url;\n+};\n+\n+static void promisor_info_list_free(struct promisor_info *p)\n+{\n+\tstruct promisor_info *next;\n+\n+\tfor (; p; p = next) {\n+\t\tnext = p->next;\n+\t\tfree((char *)p->name);\n+\t\tfree((char *)p->url);\n+\t\tfree(p);\n+\t}\n+}\n+\n+/* Prepare a 'struct promisor_info' linked list with config information. */\n+static struct promisor_info *promisor_config_info_list(struct repository *repo)\n {\n+\tstruct promisor_info *infos = NULL;\n+\tstruct promisor_info **last_info = &infos;\n \tstruct promisor_remote *r;\n \n \tpromisor_remote_init(repo);\n@@ -328,57 +355,65 @@ static void promisor_info_vecs(struct repository *repo,\n \n \t\t/* Only add remotes with a non empty URL */\n \t\tif (!git_config_get_string_tmp(url_key, &url) && *url) {\n-\t\t\tstrvec_push(names, r->name);\n-\t\t\tstrvec_push(urls, url);\n+\t\t\tstruct promisor_info *new_info = xcalloc(1, sizeof(*new_info));\n+\n+\t\t\tnew_info->name = xstrdup(r->name);\n+\t\t\tnew_info->url = xstrdup(url);\n+\n+\t\t\t*last_info = new_info;\n+\t\t\tlast_info = &new_info->next;\n \t\t}\n \n \t\tfree(url_key);\n \t}\n+\n+\treturn infos;\n }\n \n char *promisor_remote_info(struct repository *repo)\n {\n \tstruct strbuf sb = STRBUF_INIT;\n \tint advertise_promisors = 0;\n-\tstruct strvec names = STRVEC_INIT;\n-\tstruct strvec urls = STRVEC_INIT;\n+\tstruct promisor_info *config_info;\n+\tstruct promisor_info *p;\n \n \tgit_config_get_bool(\"promisor.advertise\", &advertise_promisors);\n \n \tif (!advertise_promisors)\n \t\treturn NULL;\n \n-\tpromisor_info_vecs(repo, &names, &urls);\n+\tconfig_info = promisor_config_info_list(repo);\n \n-\tif (!names.nr)\n+\tif (!config_info)\n \t\treturn NULL;\n \n-\tfor (size_t i = 0; i < names.nr; i++) {\n-\t\tif (i)\n+\tfor (p = config_info; p; p = p->next) {\n+\t\tif (p != config_info)\n \t\t\tstrbuf_addch(&sb, ';');\n+\n \t\tstrbuf_addstr(&sb, \"name=\");\n-\t\tstrbuf_addstr_urlencode(&sb, names.v[i], allow_unsanitized);\n+\t\tstrbuf_addstr_urlencode(&sb, p->name, allow_unsanitized);\n \t\tstrbuf_addstr(&sb, \",url=\");\n-\t\tstrbuf_addstr_urlencode(&sb, urls.v[i], allow_unsanitized);\n+\t\tstrbuf_addstr_urlencode(&sb, p->url, allow_unsanitized);\n \t}\n \n-\tstrvec_clear(&names);\n-\tstrvec_clear(&urls);\n+\tpromisor_info_list_free(config_info);\n \n \treturn strbuf_detach(&sb, NULL);\n }\n \n /*\n- * Find first index of 'nicks' where there is 'nick'. 'nick' is\n- * compared case sensitively to the strings in 'nicks'. If not found\n- * 'nicks->nr' is returned.\n+ * Find first element of 'p' where the 'name' member is 'nick'. 'nick'\n+ * is compared case sensitively to the strings in 'p'. If not found\n+ * NULL is returned.\n  */\n-static size_t remote_nick_find(struct strvec *nicks, const char *nick)\n+static struct promisor_info *remote_nick_find(struct promisor_info *p, const char *nick)\n {\n-\tfor (size_t i = 0; i < nicks->nr; i++)\n-\t\tif (!strcmp(nicks->v[i], nick))\n-\t\t\treturn i;\n-\treturn nicks->nr;\n+\tfor (; p; p = p->next) {\n+\t\tif (!strcmp(p->name, nick))\n+\t\t\treturn p;\n+\t}\n+\treturn NULL;\n }\n \n enum accept_promisor {\n@@ -390,16 +425,16 @@ enum accept_promisor {\n \n static int should_accept_remote(enum accept_promisor accept,\n \t\t\t\tconst char *remote_name, const char *remote_url,\n-\t\t\t\tstruct strvec *names, struct strvec *urls)\n+\t\t\t\tstruct promisor_info *config_info)\n {\n-\tsize_t i;\n+\tstruct promisor_info *p;\n \n \tif (accept == ACCEPT_ALL)\n \t\treturn 1;\n \n-\ti = remote_nick_find(names, remote_name);\n+\tp = remote_nick_find(config_info, remote_name);\n \n-\tif (i >= names->nr)\n+\tif (!p)\n \t\t/* We don't know about that remote */\n \t\treturn 0;\n \n@@ -414,11 +449,15 @@ static int should_accept_remote(enum accept_promisor accept,\n \t\treturn 0;\n \t}\n \n-\tif (!strcmp(urls->v[i], remote_url))\n+\tif (!p->url)\n+\t\tBUG(\"Bad config_info (invalid URL) for remote '%s'.\\n\",\n+\t\t    remote_name);\n+\n+\tif (!strcmp(p->url, remote_url))\n \t\treturn 1;\n \n \twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s'\"),\n-\t\tremote_name, urls->v[i], remote_url);\n+\t\tremote_name, p->url, remote_url);\n \n \treturn 0;\n }\n@@ -430,8 +469,7 @@ static void filter_promisor_remote(struct repository *repo,\n \tstruct strbuf **remotes;\n \tconst char *accept_str;\n \tenum accept_promisor accept = ACCEPT_NONE;\n-\tstruct strvec names = STRVEC_INIT;\n-\tstruct strvec urls = STRVEC_INIT;\n+\tstruct promisor_info *config_info = NULL;\n \n \tif (!git_config_get_string_tmp(\"promisor.acceptfromserver\", &accept_str)) {\n \t\tif (!*accept_str || !strcasecmp(\"None\", accept_str))\n@@ -451,7 +489,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\treturn;\n \n \tif (accept != ACCEPT_ALL)\n-\t\tpromisor_info_vecs(repo, &names, &urls);\n+\t\tconfig_info = promisor_config_info_list(repo);\n \n \t/* Parse remote info received */\n \n@@ -482,7 +520,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tif (remote_url)\n \t\t\tdecoded_url = url_percent_decode(remote_url);\n \n-\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, &names, &urls))\n+\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, config_info))\n \t\t\tstrvec_push(accepted, decoded_name);\n \n \t\tstrbuf_list_free(elems);\n@@ -490,8 +528,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tfree(decoded_url);\n \t}\n \n-\tstrvec_clear(&names);\n-\tstrvec_clear(&urls);\n+\tpromisor_info_list_free(config_info);\n \tstrbuf_list_free(remotes);\n }\n \n-- \n2.49.0.596.g707f6eb7a2.dirty\n\n"},{"id":"518414","messageId":"20250519141259.3061550-1-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250429145243.992252-1-christian.couder@gmail.com","subject":"[PATCH v3 0/5] Make the \"promisor-remote\" capability support more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-19T14:12:54Z","receivedAt":"2025-05-19T14:13:20Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"The \"promisor-remote\" capability can only be used to pass the names\nand URLs of the promisor remotes from the server to the client. After\nthat the client can use this information to decide if it accepts the\nremotes or not.\n\nIt would be nice if the server could pass more fields about its\nremotes and if the client could use that additional information to\ndecide about the remotes by comparing it with its local information\nabout the remotes.\n\nThis patch series implements this by adding the \"promisor.sendFields\"\non the server side and the \"promisor.checkFields\" on the client side.\n\nFor example, if \"promisor.sendFields\" is set to \"partialCloneFilter\",\nand the server has the remote \"foo\" configured like this:\n\n[remote \"foo\"]\n        url = file:///tmp/foo.git\n\tpartialCloneFilter = blob:none\n\nthen \"name=foo,url=file:///tmp/foo.git,partialCloneFilter=blob:none\"\nwill be sent by the server for this remote.\n\nAll the information passed through the \"promisor-remote\" capability is\nstill only used to decide if the remotes are accepted or not. The\nclient doesn't store it and doesn't use it for any other purpose.\n\nThe fields that can be passed are limited to \"partialCloneFilter\" and\n\"token\".\n\nOn the technical side, we get rid of 'struct strvec' and we use a\n'struct promisor_info' linked list and some 'struct string_list'\ninstead. This matches some suggestions made when the series that\nintroduced the \"promisor-remote\" capability was reviewed.\n\nChanges since v2\n----------------\n\nThanks to Patrick, Junio and Karthik for their comments on the\nprevious versions.\n\n- A big change affecting all the patches is that instead of a linked\n  list, the 'struct promisor_info' now contains a `const char *`\n  member for each field (name, url, token, ...) that can be passed\n  throught the \"promisor-remote\" capability.\n\n- In patch 1/5, the commit message has been simplified a bit as\n  'struct promisor_info' doesn't contain a 'struct string_list fields'\n  anymore, and to void repeating information that is already in some\n  code comments.\n\n- In patch 1/5, the promisor_info_list() function has been renamed\n  promisor_config_info_list() to make it clear that it populates the\n  'struct promisor_info' it returns with information from the\n  config. For the same purpose, the `info_list` variable used to point\n  to the list returned by that function has been renamed\n  `config_info`.\n\n- In patch 2/5, there are a few documentation changes:\n\n  - \"fld-name\" and \"fld-value\" have been renamed \"field-name\" and\n    \"field-value\" respectively for clarity,\n\n  - some information about what the \"partialCloneFilter\" and \"token\"\n    fields should be used for has been provided,\n\n  - it's stated that the \"name\" and \"url\" fields MUST appear first in\n    each pr-fields, in that order,\n\n  - it's stated that clients MUST ignore fields they don't recognize.\n\n- In patch 2/5, in the code, there are the following changes:\n\n  - string constants are used instead of repeating the\n    \"partialCloneFilter\" and \"token\" literal strings,\n\n  - `allowed_fields` and `is_allowed_field()` have been renamed\n    `known_fields` and `is_known_field()` respectively,\n\n  - `valid_field()` has been renamed `is_valid_field()`\n\n  - `append_fields()` has been replaced by `set_one_field()` and\n    `set_fields()`.\n\n- In patch 2/5, the test has been adjusted to account for the fact\n  that fields are now processed in a different order due to the\n  changes in 'struct promisor_info'.\n\n- Patch 3/5 (\"promisor-remote: refactor how we parse advertised\n  fields\") is a new refactoring patch that prepares for the next\n  patch. It refactors into a new parse_one_advertised_remote()\n  function how remote information advertised by the server is\n  parsed. Instead of parsing it into variables, we now parse it into a\n  `struct promisor_info`.\n\n- Patch 4/5 used to be patch 3/3 in v2. In the documentation we now\n  cleary state under which condition having fields listed in\n  \"promisor.checkFields\" results in an accepted remote.\n\n- In patch 4/5, in the code, the main change is that the\n  `check_field_one()`, `check_field()` and `check_all_fields()`\n  functions have been replaced by the `match_field_against_config()`\n  and `all_fields_match()` functions.\n\n- In patch 4/5, the test has also been adjusted to account for the\n  fact that fields are now processed in a different order due to the\n  changes in 'struct promisor_info'.\n\n- Patch 5/5 (\"promisor-remote: use string constants for 'name' and\n  'url' too\") is a new clean up patch. It uses string constants for\n  \"name\" and \"url\" to match the fact that they are used for\n  \"partialCloneFilter\" and \"token\" since patch 2/5.\n\nCI tests\n--------\n\nHere they are:\n\nhttps://github.com/chriscool/git/actions/runs/15108739921\n\nThey all seem to have passed.\n\nRange diff compared to v2\n-------------------------\n\nThis range diff doesn't detect that patch 4/5 in v3 was patch 3/3 in\nv2 as there were a lot of changes. It might be useful for the first 2\npatches (1/5 and 2/5) though.\n\n1:  d4e12afbc1 ! 1:  12e753a358 promisor-remote: refactor to get rid of 'struct strvec'\n    @@ Commit message\n         scale easily in that case.\n     \n         Let's refactor this and introduce a new 'struct promisor_info' linked\n    -    list that contains a 'struct string_list fields'. This string_list\n    -    stores the field names, like \"name\" and \"url\", in the 'string' member\n    -    of its items, and the field values in the 'util' member of its items.\n    +    list which for now only contains a 'next' pointer, a 'name' member for\n    +    the promisor remote name and an 'url' member for its URL.\n     \n    -    Except for \"name\", each \"<field_name>/<field_value>\" pair should\n    -    correspond to a \"remote.<name>.<field_name>\" config variable set to\n    -    <field_value> where \"<name>\" is a promisor remote name.\n    -\n    -    Previously in Git, the part after the last dot in a configuration\n    -    variable key, for example \"c\" for \"a.b.c\", was called the \"variable\n    -    name part\" of a configuration key. It would be very confusing to use\n    -    \"variable name part\" or some similar terms in the context of the\n    -    'promisor-remote' protocol though, so let's forget about it, and just\n    -    use \"field\", \"field name\" and \"field value\" instead.\n    +    Explicit members are used within 'struct promisor_info' for type\n    +    safety and clarity regarding the specific information being handled,\n    +    rather than a generic key-value store. We want to specify and document\n    +    each field and its content, so adding new members to the struct as\n    +    more fields are supported is fine.\n     \n         Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n     \n    @@ promisor-remote.c: static int allow_unsanitized(char ch)\n     + * Linked list for promisor remotes involved in the \"promisor-remote\"\n     + * protocol capability.\n     + *\n    -+ * 'fields' contains a defined set of field name/value pairs for\n    -+ * each promisor remote. Field names are stored in the 'string'\n    -+ * member, and values in the 'util' member.\n    -+ *\n    -+ * Currently supported field names:\n    -+ * - \"name\": The name of the promisor remote.\n    -+ * - \"url\": The URL of the promisor remote.\n    -+ *\n    -+ * Except for \"name\", each \"<field_name>/<field_value>\" pair should\n    -+ * correspond to a \"remote.<name>.<field_name>\" config variable set to\n    -+ * <field_value> where \"<name>\" is a promisor remote name.\n    -+ *\n    -+ * 'fields' should not be sorted, as we will rely on the order we put\n    -+ * things into it. So, for example, 'string_list_append()' should be\n    -+ * used instead of 'string_list_insert()'.\n    ++ * Except for \"next\" and \"name\", each <member> in this struct and its\n    ++ * <value> should correspond to a \"remote.<name>.<member>\" config\n    ++ * variable set to <value> where \"<name>\" is a promisor remote name.\n     + */\n     +struct promisor_info {\n     +\tstruct promisor_info *next;\n    -+\tstruct string_list fields;\n    ++\tconst char *name;\n    ++\tconst char *url;\n     +};\n     +\n     +static void promisor_info_list_free(struct promisor_info *p)\n    @@ promisor-remote.c: static int allow_unsanitized(char ch)\n     +\n     +\tfor (; p; p = next) {\n     +\t\tnext = p->next;\n    -+\t\tstring_list_clear(&p->fields, 0);\n    ++\t\tfree((char *)p->name);\n    ++\t\tfree((char *)p->url);\n     +\t\tfree(p);\n     +\t}\n     +}\n     +\n    -+/*\n    -+ * Prepare a 'struct promisor_info' linked list of promisor\n    -+ * remotes. For each promisor remote, some of its fields, starting\n    -+ * with \"name\" and \"url\", are put in the 'fields' string_list.\n    -+ */\n    -+static struct promisor_info *promisor_info_list(struct repository *repo)\n    ++/* Prepare a 'struct promisor_info' linked list with config information. */\n    ++static struct promisor_info *promisor_config_info_list(struct repository *repo)\n      {\n     +\tstruct promisor_info *infos = NULL;\n     +\tstruct promisor_info **last_info = &infos;\n    @@ promisor-remote.c: static void promisor_info_vecs(struct repository *repo,\n     -\t\t\tstrvec_push(urls, url);\n     +\t\t\tstruct promisor_info *new_info = xcalloc(1, sizeof(*new_info));\n     +\n    -+\t\t\tstring_list_init_dup(&new_info->fields);\n    -+\t\t\tnew_info->fields.cmp = strcasecmp;\n    -+\n    -+\t\t\tstring_list_append(&new_info->fields, \"name\")->util = (char *)r->name;\n    -+\t\t\tstring_list_append(&new_info->fields, \"url\")->util = (char *)url;\n    ++\t\t\tnew_info->name = xstrdup(r->name);\n    ++\t\t\tnew_info->url = xstrdup(url);\n     +\n     +\t\t\t*last_info = new_info;\n     +\t\t\tlast_info = &new_info->next;\n    @@ promisor-remote.c: static void promisor_info_vecs(struct repository *repo,\n      \tint advertise_promisors = 0;\n     -\tstruct strvec names = STRVEC_INIT;\n     -\tstruct strvec urls = STRVEC_INIT;\n    -+\tstruct promisor_info *info_list;\n    -+\tstruct promisor_info *r;\n    ++\tstruct promisor_info *config_info;\n    ++\tstruct promisor_info *p;\n      \n      \tgit_config_get_bool(\"promisor.advertise\", &advertise_promisors);\n      \n    @@ promisor-remote.c: static void promisor_info_vecs(struct repository *repo,\n      \t\treturn NULL;\n      \n     -\tpromisor_info_vecs(repo, &names, &urls);\n    -+\tinfo_list = promisor_info_list(repo);\n    ++\tconfig_info = promisor_config_info_list(repo);\n      \n     -\tif (!names.nr)\n    -+\tif (!info_list)\n    ++\tif (!config_info)\n      \t\treturn NULL;\n      \n     -\tfor (size_t i = 0; i < names.nr; i++) {\n     -\t\tif (i)\n    -+\tfor (r = info_list; r; r = r->next) {\n    -+\t\tstruct string_list_item *item;\n    -+\t\tint first = 1;\n    -+\n    -+\t\tif (r != info_list)\n    ++\tfor (p = config_info; p; p = p->next) {\n    ++\t\tif (p != config_info)\n      \t\t\tstrbuf_addch(&sb, ';');\n    --\t\tstrbuf_addstr(&sb, \"name=\");\n    ++\n    + \t\tstrbuf_addstr(&sb, \"name=\");\n     -\t\tstrbuf_addstr_urlencode(&sb, names.v[i], allow_unsanitized);\n    --\t\tstrbuf_addstr(&sb, \",url=\");\n    ++\t\tstrbuf_addstr_urlencode(&sb, p->name, allow_unsanitized);\n    + \t\tstrbuf_addstr(&sb, \",url=\");\n     -\t\tstrbuf_addstr_urlencode(&sb, urls.v[i], allow_unsanitized);\n    -+\n    -+\t\tfor_each_string_list_item(item, &r->fields) {\n    -+\t\t\tif (first)\n    -+\t\t\t\tfirst = 0;\n    -+\t\t\telse\n    -+\t\t\t\tstrbuf_addch(&sb, ',');\n    -+\t\t\tstrbuf_addf(&sb, \"%s=\", item->string);\n    -+\t\t\tstrbuf_addstr_urlencode(&sb, (char *)item->util, allow_unsanitized);\n    -+\t\t}\n    ++\t\tstrbuf_addstr_urlencode(&sb, p->url, allow_unsanitized);\n      \t}\n      \n     -\tstrvec_clear(&names);\n     -\tstrvec_clear(&urls);\n    -+\tpromisor_info_list_free(info_list);\n    ++\tpromisor_info_list_free(config_info);\n      \n      \treturn strbuf_detach(&sb, NULL);\n      }\n    @@ promisor-remote.c: static void promisor_info_vecs(struct repository *repo,\n     - * Find first index of 'nicks' where there is 'nick'. 'nick' is\n     - * compared case sensitively to the strings in 'nicks'. If not found\n     - * 'nicks->nr' is returned.\n    -+ * Find first element of 'p' where the 'name' field is 'nick'. 'nick'\n    ++ * Find first element of 'p' where the 'name' member is 'nick'. 'nick'\n     + * is compared case sensitively to the strings in 'p'. If not found\n     + * NULL is returned.\n       */\n    @@ promisor-remote.c: static void promisor_info_vecs(struct repository *repo,\n     -\t\t\treturn i;\n     -\treturn nicks->nr;\n     +\tfor (; p; p = p->next) {\n    -+\t\tif (strcmp(p->fields.items[0].string, \"name\"))\n    -+\t\t\tBUG(\"First field of promisor info should be 'name', but was '%s'.\",\n    -+\t\t\t    p->fields.items[0].string);\n    -+\t\tif (!strcmp(p->fields.items[0].util, nick))\n    ++\t\tif (!strcmp(p->name, nick))\n     +\t\t\treturn p;\n     +\t}\n     +\treturn NULL;\n    @@ promisor-remote.c: enum accept_promisor {\n      static int should_accept_remote(enum accept_promisor accept,\n      \t\t\t\tconst char *remote_name, const char *remote_url,\n     -\t\t\t\tstruct strvec *names, struct strvec *urls)\n    -+\t\t\t\tstruct promisor_info *info_list)\n    ++\t\t\t\tstruct promisor_info *config_info)\n      {\n     -\tsize_t i;\n     +\tstruct promisor_info *p;\n    -+\tconst char *local_url;\n      \n      \tif (accept == ACCEPT_ALL)\n      \t\treturn 1;\n      \n     -\ti = remote_nick_find(names, remote_name);\n    -+\tp = remote_nick_find(info_list, remote_name);\n    ++\tp = remote_nick_find(config_info, remote_name);\n      \n     -\tif (i >= names->nr)\n     +\tif (!p)\n    @@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n      \t}\n      \n     -\tif (!strcmp(urls->v[i], remote_url))\n    -+\tif (strcmp(p->fields.items[1].string, \"url\"))\n    -+\t\tBUG(\"Bad info_list for remote '%s'.\\n\"\n    -+\t\t    \"Second field of promisor info should be 'url', but was '%s'.\",\n    -+\t\t    remote_name, p->fields.items[1].string);\n    -+\n    -+\tlocal_url = p->fields.items[1].util;\n    ++\tif (!p->url)\n    ++\t\tBUG(\"Bad config_info (invalid URL) for remote '%s'.\\n\",\n    ++\t\t    remote_name);\n     +\n    -+\tif (!strcmp(local_url, remote_url))\n    ++\tif (!strcmp(p->url, remote_url))\n      \t\treturn 1;\n      \n      \twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s'\"),\n     -\t\tremote_name, urls->v[i], remote_url);\n    -+\t\tremote_name, local_url, remote_url);\n    ++\t\tremote_name, p->url, remote_url);\n      \n      \treturn 0;\n      }\n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n      \tenum accept_promisor accept = ACCEPT_NONE;\n     -\tstruct strvec names = STRVEC_INIT;\n     -\tstruct strvec urls = STRVEC_INIT;\n    -+\tstruct promisor_info *info_list = NULL;\n    ++\tstruct promisor_info *config_info = NULL;\n      \n      \tif (!git_config_get_string_tmp(\"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      \n      \tif (accept != ACCEPT_ALL)\n     -\t\tpromisor_info_vecs(repo, &names, &urls);\n    -+\t\tinfo_list = promisor_info_list(repo);\n    ++\t\tconfig_info = promisor_config_info_list(repo);\n      \n      \t/* Parse remote info received */\n      \n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n      \t\t\tdecoded_url = url_percent_decode(remote_url);\n      \n     -\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, &names, &urls))\n    -+\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, info_list))\n    ++\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, config_info))\n      \t\t\tstrvec_push(accepted, decoded_name);\n      \n      \t\tstrbuf_list_free(elems);\n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n      \n     -\tstrvec_clear(&names);\n     -\tstrvec_clear(&urls);\n    -+\tpromisor_info_list_free(info_list);\n    ++\tpromisor_info_list_free(config_info);\n      \tstrbuf_list_free(remotes);\n      }\n      \n2:  d6d4f99768 ! 2:  29c6104827 promisor-remote: allow a server to advertise more fields\n    @@ Commit message\n         that will be passed to the client.\n     \n         Only a set of predefined fields are allowed. The only fields in this\n    -    set are \"partialCloneFilter\" and \"token\".\n    +    set are \"partialCloneFilter\" and \"token\". The \"partialCloneFilter\"\n    +    field specifies the filter definition used by the promisor remote,\n    +    and the \"token\" field can provide an authentication credential for\n    +    accessing it.\n     \n         For example if \"promisor.sendFields\" is set to \"partialCloneFilter\",\n         and the server has the \"remote.<name>.partialCloneFilter\" config\n    @@ Documentation/gitprotocol-v2.adoc: retrieving the header from a bundle at the in\n     +\tpr-info = pr-fields | pr-info \";\" pr-info\n      \n     -\tpr-info = \"name=\" pr-name | \"name=\" pr-name \",\" \"url=\" pr-url\n    -+\tpr-fields = fld-name \"=\" fld-value | pr-fields \",\" pr-fields\n    ++\tpr-fields = field-name \"=\" field-value | pr-fields \",\" pr-fields\n      \n     -where `pr-name` is the urlencoded name of a promisor remote, and\n     -`pr-url` the urlencoded URL of that promisor remote.\n    -+where all the `fld-name` and `fld-value` in a given `pr-fields` are\n    -+field names and values related to a single promisor remote.\n    ++where all the `field-name` and `field-value` in a given `pr-fields`\n    ++are field names and values related to a single promisor remote.\n      \n     -In this case, if the client decides to use one or more promisor\n     -remotes the server advertised, it can reply with\n     -\"promisor-remote=<pr-names>\" where <pr-names> should be of the form:\n     +The server MUST advertise at least the \"name\" and \"url\" field names\n     +along with the associated field values, which are the name of a valid\n    -+remote and its URL, in each `pr-fields`.\n    ++remote and its URL, in each `pr-fields`. The \"name\" and \"url\" fields\n    ++MUST appear first in each pr-fields, in that order.\n      \n     -\tpr-names = pr-name | pr-names \";\" pr-name\n    -+The server MAY advertise the following optional fields:\n    ++After these mandatory fields, the server MAY advertise the following\n    ++optional fields in any order:\n     +\n    -+- \"partialCloneFilter\": Filter used for partial clone, corresponding\n    -+  to the \"remote.<name>.partialCloneFilter\" config setting.\n    -+- \"token\": Authentication token for the remote, corresponding\n    -+  to the \"remote.<name>.token\" config setting.\n    ++- \"partialCloneFilter\": The filter specification used by the remote.\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     +\n    -+No other fields are defined by the protocol at this time. Clients SHOULD\n    -+ignore fields they don't recognize to allow for future protocol extensions.\n    ++- \"token\": An authentication token that clients can use when\n    ++connecting to the remote. It corresponds to the \"remote.<name>.token\"\n    ++config setting.\n    ++\n    ++No other fields are defined by the protocol at this time. Clients MUST\n    ++ignore fields they don't recognize to allow for future protocol\n    ++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    @@ Documentation/gitprotocol-v2.adoc: retrieving the header from a bundle at the in\n     -remote name, and the ';' and ',' characters MUST be encoded if they\n     -appear in `pr-name` or `pr-url`.\n     +Note that, everywhere in this document, the ';' and ',' characters\n    -+MUST be encoded if they appear in `pr-name` or `fld-value`.\n    ++MUST be encoded if they appear in `pr-name` or `field-value`.\n      \n      If the server doesn't know any promisor remote that could be good for\n      a client to use, or prefers a client not to use any promisor remote it\n    @@ promisor-remote.c: static int allow_unsanitized(char ch)\n      \treturn ch > 32 && ch < 127;\n      }\n      \n    ++static const char promisor_field_filter[] = \"partialCloneFilter\";\n    ++static const char promisor_field_token[] = \"token\";\n    ++\n     +/*\n    -+ * List of field names allowed to be used in the \"promisor-remote\"\n    -+ * protocol capability. Each field should correspond to a configurable\n    -+ * property of a remote that can be relevant for the client.\n    ++ * List of optional field names that can be used in the\n    ++ * \"promisor-remote\" protocol capability (others must be\n    ++ * ignored). Each field should correspond to a configurable property\n    ++ * of a remote that can be relevant for the client.\n     + */\n    -+static const char *allowed_fields[] = {\n    -+\t\"partialCloneFilter\", /* Filter used for partial clone */\n    -+\t\"token\",              /* Authentication token for the remote */\n    ++static const char *known_fields[] = {\n    ++\tpromisor_field_filter, /* Filter used for partial clone */\n    ++\tpromisor_field_token,  /* Authentication token for the remote */\n     +\tNULL\n     +};\n     +\n     +/*\n    -+ * Check if 'field' is in the list of allowed field names for the\n    ++ * Check if 'field' is in the list of the known field names for the\n     + * \"promisor-remote\" protocol capability.\n     + */\n    -+static int is_allowed_field(const char *field)\n    ++static int is_known_field(const char *field)\n     +{\n     +\tconst char **p;\n     +\n    -+\tfor (p = allowed_fields; *p; p++)\n    ++\tfor (p = known_fields; *p; p++)\n     +\t\tif (!strcasecmp(*p, field))\n     +\t\t\treturn 1;\n     +\treturn 0;\n     +}\n     +\n    -+static int valid_field(struct string_list_item *item, void *cb_data)\n    ++static int is_valid_field(struct string_list_item *item, void *cb_data)\n     +{\n     +\tconst char *field = item->string;\n     +\tconst char *config_key = (const char *)cb_data;\n     +\n    -+\tif (!is_allowed_field(field)) {\n    ++\tif (!is_known_field(field)) {\n     +\t\twarning(_(\"unsupported field '%s' in '%s' config\"), field, config_key);\n     +\t\treturn 0;\n     +\t}\n    @@ promisor-remote.c: static int allow_unsanitized(char ch)\n     +\n     +\tif (!git_config_get_string(config_key, &fields) && *fields) {\n     +\t\tstring_list_split_in_place(fields_list, fields, \", \", -1);\n    -+\t\tfilter_string_list(fields_list, 0, valid_field, (void *)config_key);\n    ++\t\tfilter_string_list(fields_list, 0, is_valid_field, (void *)config_key);\n     +\t}\n     +\n     +\treturn fields;\n    @@ promisor-remote.c: static int allow_unsanitized(char ch)\n     +\treturn &fields_list;\n     +}\n     +\n    -+static void append_fields(struct string_list *fields,\n    -+\t\t\t  struct string_list *field_names,\n    -+\t\t\t  const char *name)\n    + /*\n    +  * Linked list for promisor remotes involved in the \"promisor-remote\"\n    +  * protocol capability.\n    +@@ promisor-remote.c: struct promisor_info {\n    + \tstruct promisor_info *next;\n    + \tconst char *name;\n    + \tconst char *url;\n    ++\tconst char *filter;\n    ++\tconst char *token;\n    + };\n    + \n    + static void promisor_info_list_free(struct promisor_info *p)\n    +@@ promisor-remote.c: static void promisor_info_list_free(struct promisor_info *p)\n    + \t\tnext = p->next;\n    + \t\tfree((char *)p->name);\n    + \t\tfree((char *)p->url);\n    ++\t\tfree((char *)p->filter);\n    ++\t\tfree((char *)p->token);\n    + \t\tfree(p);\n    + \t}\n    + }\n    + \n    +-/* Prepare a 'struct promisor_info' linked list with config information. */\n    +-static struct promisor_info *promisor_config_info_list(struct repository *repo)\n    ++static void set_one_field(struct promisor_info *p,\n    ++\t\t\t  const char *field, const char *value)\n    ++{\n    ++\tif (!strcasecmp(field, promisor_field_filter))\n    ++\t\tp->filter = xstrdup(value);\n    ++\telse if (!strcasecmp(field, promisor_field_token))\n    ++\t\tp->token = xstrdup(value);\n    ++\telse\n    ++\t\tBUG(\"Invalid field '%s'\", field);\n    ++}\n    ++\n    ++static void set_fields(struct promisor_info *p,\n    ++\t\t       struct string_list *field_names)\n     +{\n     +\tstruct string_list_item *item;\n     +\n     +\tfor_each_string_list_item(item, field_names) {\n    -+\t\tchar *key = xstrfmt(\"remote.%s.%s\", name, item->string);\n    ++\t\tchar *key = xstrfmt(\"remote.%s.%s\", p->name, item->string);\n     +\t\tconst char *val;\n     +\t\tif (!git_config_get_string_tmp(key, &val) && *val)\n    -+\t\t\tstring_list_append(fields, item->string)->util = (char *)val;\n    ++\t\t\tset_one_field(p, item->string, val);\n     +\t\tfree(key);\n     +\t}\n     +}\n     +\n    - /*\n    -  * Linked list for promisor remotes involved in the \"promisor-remote\"\n    -  * protocol capability.\n    -@@ promisor-remote.c: static int allow_unsanitized(char ch)\n    -  * member, and values in the 'util' member.\n    -  *\n    -  * Currently supported field names:\n    -- * - \"name\": The name of the promisor remote.\n    -- * - \"url\": The URL of the promisor remote.\n    -+ * - \"name\": The name of the promisor remote,\n    -+ * - \"url\": The URL of the promisor remote,\n    -+ * - the fields in 'allowed_fields[]' above.\n    -  *\n    -  * Except for \"name\", each \"<field_name>/<field_value>\" pair should\n    -  * correspond to a \"remote.<name>.<field_name>\" config variable set to\n    -@@ promisor-remote.c: static void promisor_info_list_free(struct promisor_info *p)\n    -  * remotes. For each promisor remote, some of its fields, starting\n    -  * with \"name\" and \"url\", are put in the 'fields' string_list.\n    -  */\n    --static struct promisor_info *promisor_info_list(struct repository *repo)\n    -+static struct promisor_info *promisor_info_list(struct repository *repo,\n    -+\t\t\t\t\t\tstruct string_list *field_names)\n    ++/*\n    ++ * Prepare a 'struct promisor_info' linked list of promisor remotes\n    ++ * with config information. Only members of that struct specified by\n    ++ * the 'field_names' linked list are set (using values from the\n    ++ * configuration).\n    ++ */\n    ++static struct promisor_info *promisor_config_info_list(struct repository *repo,\n    ++\t\t\t\t\t\t       struct string_list *field_names)\n      {\n      \tstruct promisor_info *infos = NULL;\n      \tstruct promisor_info **last_info = &infos;\n    -@@ promisor-remote.c: static struct promisor_info *promisor_info_list(struct repository *repo)\n    - \t\t\tstring_list_append(&new_info->fields, \"name\")->util = (char *)r->name;\n    - \t\t\tstring_list_append(&new_info->fields, \"url\")->util = (char *)url;\n    +@@ promisor-remote.c: static struct promisor_info *promisor_config_info_list(struct repository *repo)\n    + \t\t\tnew_info->name = xstrdup(r->name);\n    + \t\t\tnew_info->url = xstrdup(url);\n      \n     +\t\t\tif (field_names)\n    -+\t\t\t\tappend_fields(&new_info->fields, field_names, r->name);\n    ++\t\t\t\tset_fields(new_info, field_names);\n     +\n      \t\t\t*last_info = new_info;\n      \t\t\tlast_info = &new_info->next;\n    @@ promisor-remote.c: char *promisor_remote_info(struct repository *repo)\n      \tif (!advertise_promisors)\n      \t\treturn NULL;\n      \n    --\tinfo_list = promisor_info_list(repo);\n    -+\tinfo_list = promisor_info_list(repo, fields_sent());\n    +-\tconfig_info = promisor_config_info_list(repo);\n    ++\tconfig_info = promisor_config_info_list(repo, fields_sent());\n      \n    - \tif (!info_list)\n    + \tif (!config_info)\n      \t\treturn NULL;\n    +@@ promisor-remote.c: char *promisor_remote_info(struct repository *repo)\n    + \t\tstrbuf_addstr_urlencode(&sb, p->name, allow_unsanitized);\n    + \t\tstrbuf_addstr(&sb, \",url=\");\n    + \t\tstrbuf_addstr_urlencode(&sb, p->url, allow_unsanitized);\n    ++\n    ++\t\tif (p->filter) {\n    ++\t\t\tstrbuf_addf(&sb, \",%s=\", promisor_field_filter);\n    ++\t\t\tstrbuf_addstr_urlencode(&sb, p->filter, allow_unsanitized);\n    ++\t\t}\n    ++\t\tif (p->token) {\n    ++\t\t\tstrbuf_addf(&sb, \",%s=\", promisor_field_token);\n    ++\t\t\tstrbuf_addstr_urlencode(&sb, p->token, allow_unsanitized);\n    ++\t\t}\n    + \t}\n    + \n    + \tpromisor_info_list_free(config_info);\n     @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n      \t\treturn;\n      \n      \tif (accept != ACCEPT_ALL)\n    --\t\tinfo_list = promisor_info_list(repo);\n    -+\t\tinfo_list = promisor_info_list(repo, NULL);\n    +-\t\tconfig_info = promisor_config_info_list(repo);\n    ++\t\tconfig_info = promisor_config_info_list(repo, NULL);\n      \n      \t/* Parse remote info received */\n      \n    @@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'KnownUrl\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    -+\tgit -C server config promisor.sendFields \"token, partialCloneFilter\" &&\n    ++\tgit -C server config promisor.sendFields \"partialCloneFilter, token\" &&\n     +\ttest_when_finished \"git -C server config unset promisor.sendFields\" &&\n     +\ttest_when_finished \"rm trace\" &&\n     +\n    @@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'KnownUrl\n     +\t# Check that fields are properly transmitted\n     +\tENCODED_URL=$(echo \"file://$(pwd)/lop\" | sed -e \"s/ /%20/g\") &&\n     +\tPR1=\"name=lop,url=$ENCODED_URL,partialCloneFilter=blob:none\" &&\n    -+\tPR2=\"name=otherLop,url=https://invalid.invalid,token=fooBar,partialCloneFilter=blob:limit=10k\" &&\n    ++\tPR2=\"name=otherLop,url=https://invalid.invalid,partialCloneFilter=blob:limit=10k,token=fooBar\" &&\n     +\ttest_grep \"clone< promisor-remote=$PR1;$PR2\" trace &&\n     +\ttest_grep \"clone> promisor-remote=lop;otherLop\" trace &&\n     +\n3:  09af0369a6 < -:  ---------- promisor-remote: allow a client to check fields\n-:  ---------- > 3:  6d8236bf70 promisor-remote: refactor how we parse advertised fields\n-:  ---------- > 4:  dab5823356 promisor-remote: allow a client to check fields\n-:  ---------- > 5:  9bd612cdb8 promisor-remote: use string constants for 'name' and 'url' too\n\n\nChristian Couder (5):\n  promisor-remote: refactor to get rid of 'struct strvec'\n  promisor-remote: allow a server to advertise more fields\n  promisor-remote: refactor how we parse advertised fields\n  promisor-remote: allow a client to check fields\n  promisor-remote: use string constants for 'name' and 'url' too\n\n Documentation/config/promisor.adoc    |  53 ++++\n Documentation/gitprotocol-v2.adoc     |  59 ++--\n promisor-remote.c                     | 383 +++++++++++++++++++++-----\n t/t5710-promisor-remote-capability.sh |  67 +++++\n 4 files changed, 479 insertions(+), 83 deletions(-)\n\n-- \n2.49.0.596.g707f6eb7a2.dirty\n\n"},{"id":"518415","messageId":"20250519141259.3061550-3-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250519141259.3061550-1-christian.couder@gmail.com","subject":"[PATCH v3 2/5] promisor-remote: allow a server to advertise more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-19T14:12:56Z","receivedAt":"2025-05-19T14:13:22Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"For now the \"promisor-remote\" protocol capability can only pass \"name\"\nand \"url\" information from a server to a client in the form\n\"name=<remote_name>,url=<remote_url>\".\n\nLet's make it possible to pass more information by introducing a new\n\"promisor.sendFields\" configuration variable. This variable should\ncontain a comma or space separated list of fields that will be looked\nup in the configuration of the remote on the server to find the values\nthat will be passed to the client.\n\nOnly a set of predefined fields are allowed. The only fields in this\nset are \"partialCloneFilter\" and \"token\". The \"partialCloneFilter\"\nfield specifies the filter definition used by the promisor remote,\nand the \"token\" field can provide an authentication credential for\naccessing it.\n\nFor example if \"promisor.sendFields\" is set to \"partialCloneFilter\",\nand the server has the \"remote.<name>.partialCloneFilter\" config\nvariable set to a value for a remote, then that value will be passed\nin the form \"partialCloneFilter=<value>\" after the \"name\" and \"url\"\nfields.\n\nA following commit will allow the client to use the information to\ndecide if it accepts the remote or not. For now the client doesn't do\nanything with the additional information it receives.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    |  18 ++++\n Documentation/gitprotocol-v2.adoc     |  59 ++++++++----\n promisor-remote.c                     | 128 ++++++++++++++++++++++++--\n t/t5710-promisor-remote-capability.sh |  32 +++++++\n 4 files changed, 212 insertions(+), 25 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex 2638b01f83..71311b70c8 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -9,6 +9,24 @@ promisor.advertise::\n \t\"false\", which means the \"promisor-remote\" capability is not\n \tadvertised.\n \n+promisor.sendFields::\n+\tA comma or space separated list of additional remote related\n+\tfields that a server will send while advertising its promisor\n+\tremotes using the \"promisor-remote\" capability, see\n+\tlinkgit:gitprotocol-v2[5]. Currently, only the\n+\t\"partialCloneFilter\" and \"token\" fields are supported. The\n+\t\"partialCloneFilter\" field contains the partial clone filter\n+\tused for the remote, and the \"token\" field contains an\n+\tauthentication token for the remote.\n++\n+When a field is part of this list and a corresponding\n+\"remote.foo.<field>\" config variable is set on the server to a\n+non-empty value, then the field and its value will be sent when\n+advertising the promisor remote \"foo\". This list has no effect unless\n+the \"promisor.advertise\" config variable is set to \"true\", and the\n+\"name\" and \"url\" fields are always advertised regardless of this\n+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\"\ndiff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\nindex 5598c93e67..3c12c158fd 100644\n--- a/Documentation/gitprotocol-v2.adoc\n+++ b/Documentation/gitprotocol-v2.adoc\n@@ -785,33 +785,59 @@ retrieving the header from a bundle at the indicated URI, and thus\n save themselves and the server(s) the request(s) needed to inspect the\n headers of that bundle or bundles.\n \n-promisor-remote=<pr-infos>\n+promisor-remote=<pr-info>\n ~~~~~~~~~~~~~~~~~~~~~~~~~~\n \n The server may advertise some promisor remotes it is using or knows\n about to a client which may want to use them as its promisor remotes,\n-instead of this repository. In this case <pr-infos> should be of the\n+instead of this repository. In this case <pr-info> should be of the\n form:\n \n-\tpr-infos = pr-info | pr-infos \";\" pr-info\n+\tpr-info = pr-fields | pr-info \";\" pr-info\n \n-\tpr-info = \"name=\" pr-name | \"name=\" pr-name \",\" \"url=\" pr-url\n+\tpr-fields = field-name \"=\" field-value | pr-fields \",\" pr-fields\n \n-where `pr-name` is the urlencoded name of a promisor remote, and\n-`pr-url` the urlencoded URL of that promisor remote.\n+where all the `field-name` and `field-value` in a given `pr-fields`\n+are field names and values related to a single promisor remote.\n \n-In this case, if the client decides to use one or more promisor\n-remotes the server advertised, it can reply with\n-\"promisor-remote=<pr-names>\" where <pr-names> should be of the form:\n+The server MUST advertise at least the \"name\" and \"url\" field names\n+along with the associated field values, which are the name of a valid\n+remote and its URL, in each `pr-fields`. The \"name\" and \"url\" fields\n+MUST appear first in each pr-fields, in that order.\n \n-\tpr-names = pr-name | pr-names \";\" pr-name\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+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+\n+- \"token\": An authentication token that clients can use when\n+connecting to the remote. It corresponds to the \"remote.<name>.token\"\n+config setting.\n+\n+No other fields are defined by the protocol at this time. Clients MUST\n+ignore fields they don't recognize to allow for future protocol\n+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+\n+Field values MUST be urlencoded.\n+\n+If the client decides to use one or more promisor remotes the server\n+advertised, it can reply with \"promisor-remote=<pr-names>\" where\n+<pr-names> should be of the form:\n+\n+\tpr-names = pr-name | pr-names \";\" pr-names\n \n where `pr-name` is the urlencoded name of a promisor remote the server\n advertised and the client accepts.\n \n-Note that, everywhere in this document, `pr-name` MUST be a valid\n-remote name, and the ';' and ',' characters MUST be encoded if they\n-appear in `pr-name` or `pr-url`.\n+Note that, everywhere in this document, the ';' and ',' characters\n+MUST be encoded if they appear in `pr-name` or `field-value`.\n \n If the server doesn't know any promisor remote that could be good for\n a client to use, or prefers a client not to use any promisor remote it\n@@ -822,9 +848,10 @@ In this case, or if the client doesn't want to use any promisor remote\n the server advertised, the client shouldn't advertise the\n \"promisor-remote\" capability at all in its reply.\n \n-The \"promisor.advertise\" and \"promisor.acceptFromServer\" configuration\n-options can be used on the server and client side to control what they\n-advertise or accept respectively. See the documentation of these\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 \n Note that in the future it would be nice if the \"promisor-remote\"\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 94e87f2f48..cde4079d8c 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -314,6 +314,73 @@ static int allow_unsanitized(char ch)\n \treturn ch > 32 && ch < 127;\n }\n \n+static const char promisor_field_filter[] = \"partialCloneFilter\";\n+static const char promisor_field_token[] = \"token\";\n+\n+/*\n+ * List of optional field names that can be used in the\n+ * \"promisor-remote\" protocol capability (others must be\n+ * ignored). Each field should correspond to a configurable property\n+ * of a remote that can be relevant for the client.\n+ */\n+static const char *known_fields[] = {\n+\tpromisor_field_filter, /* Filter used for partial clone */\n+\tpromisor_field_token,  /* Authentication token for the remote */\n+\tNULL\n+};\n+\n+/*\n+ * Check if 'field' is in the list of the known field names for the\n+ * \"promisor-remote\" protocol capability.\n+ */\n+static int is_known_field(const char *field)\n+{\n+\tconst char **p;\n+\n+\tfor (p = known_fields; *p; p++)\n+\t\tif (!strcasecmp(*p, field))\n+\t\t\treturn 1;\n+\treturn 0;\n+}\n+\n+static int is_valid_field(struct string_list_item *item, void *cb_data)\n+{\n+\tconst char *field = item->string;\n+\tconst char *config_key = (const char *)cb_data;\n+\n+\tif (!is_known_field(field)) {\n+\t\twarning(_(\"unsupported field '%s' in '%s' config\"), field, config_key);\n+\t\treturn 0;\n+\t}\n+\treturn 1;\n+}\n+\n+static char *fields_from_config(struct string_list *fields_list, const char *config_key)\n+{\n+\tchar *fields = NULL;\n+\n+\tif (!git_config_get_string(config_key, &fields) && *fields) {\n+\t\tstring_list_split_in_place(fields_list, fields, \", \", -1);\n+\t\tfilter_string_list(fields_list, 0, is_valid_field, (void *)config_key);\n+\t}\n+\n+\treturn fields;\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 = 0;\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+}\n+\n /*\n  * Linked list for promisor remotes involved in the \"promisor-remote\"\n  * protocol capability.\n@@ -326,6 +393,8 @@ struct promisor_info {\n \tstruct promisor_info *next;\n \tconst char *name;\n \tconst char *url;\n+\tconst char *filter;\n+\tconst char *token;\n };\n \n static void promisor_info_list_free(struct promisor_info *p)\n@@ -336,12 +405,45 @@ static void promisor_info_list_free(struct promisor_info *p)\n \t\tnext = p->next;\n \t\tfree((char *)p->name);\n \t\tfree((char *)p->url);\n+\t\tfree((char *)p->filter);\n+\t\tfree((char *)p->token);\n \t\tfree(p);\n \t}\n }\n \n-/* Prepare a 'struct promisor_info' linked list with config information. */\n-static struct promisor_info *promisor_config_info_list(struct repository *repo)\n+static void set_one_field(struct promisor_info *p,\n+\t\t\t  const char *field, const char *value)\n+{\n+\tif (!strcasecmp(field, promisor_field_filter))\n+\t\tp->filter = xstrdup(value);\n+\telse if (!strcasecmp(field, promisor_field_token))\n+\t\tp->token = xstrdup(value);\n+\telse\n+\t\tBUG(\"Invalid field '%s'\", field);\n+}\n+\n+static void set_fields(struct promisor_info *p,\n+\t\t       struct string_list *field_names)\n+{\n+\tstruct string_list_item *item;\n+\n+\tfor_each_string_list_item(item, field_names) {\n+\t\tchar *key = xstrfmt(\"remote.%s.%s\", p->name, item->string);\n+\t\tconst char *val;\n+\t\tif (!git_config_get_string_tmp(key, &val) && *val)\n+\t\t\tset_one_field(p, item->string, val);\n+\t\tfree(key);\n+\t}\n+}\n+\n+/*\n+ * Prepare a 'struct promisor_info' linked list of promisor remotes\n+ * with config information. Only members of that struct specified by\n+ * the 'field_names' linked list are set (using values from the\n+ * configuration).\n+ */\n+static struct promisor_info *promisor_config_info_list(struct repository *repo,\n+\t\t\t\t\t\t       struct string_list *field_names)\n {\n \tstruct promisor_info *infos = NULL;\n \tstruct promisor_info **last_info = &infos;\n@@ -360,6 +462,9 @@ static struct promisor_info *promisor_config_info_list(struct repository *repo)\n \t\t\tnew_info->name = xstrdup(r->name);\n \t\t\tnew_info->url = xstrdup(url);\n \n+\t\t\tif (field_names)\n+\t\t\t\tset_fields(new_info, field_names);\n+\n \t\t\t*last_info = new_info;\n \t\t\tlast_info = &new_info->next;\n \t\t}\n@@ -382,7 +487,7 @@ char *promisor_remote_info(struct repository *repo)\n \tif (!advertise_promisors)\n \t\treturn NULL;\n \n-\tconfig_info = promisor_config_info_list(repo);\n+\tconfig_info = promisor_config_info_list(repo, fields_sent());\n \n \tif (!config_info)\n \t\treturn NULL;\n@@ -395,6 +500,15 @@ char *promisor_remote_info(struct repository *repo)\n \t\tstrbuf_addstr_urlencode(&sb, p->name, allow_unsanitized);\n \t\tstrbuf_addstr(&sb, \",url=\");\n \t\tstrbuf_addstr_urlencode(&sb, p->url, allow_unsanitized);\n+\n+\t\tif (p->filter) {\n+\t\t\tstrbuf_addf(&sb, \",%s=\", promisor_field_filter);\n+\t\t\tstrbuf_addstr_urlencode(&sb, p->filter, allow_unsanitized);\n+\t\t}\n+\t\tif (p->token) {\n+\t\t\tstrbuf_addf(&sb, \",%s=\", promisor_field_token);\n+\t\t\tstrbuf_addstr_urlencode(&sb, p->token, allow_unsanitized);\n+\t\t}\n \t}\n \n \tpromisor_info_list_free(config_info);\n@@ -489,7 +603,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\treturn;\n \n \tif (accept != ACCEPT_ALL)\n-\t\tconfig_info = promisor_config_info_list(repo);\n+\t\tconfig_info = promisor_config_info_list(repo, NULL);\n \n \t/* Parse remote info received */\n \n@@ -506,13 +620,9 @@ static void filter_promisor_remote(struct repository *repo,\n \t\telems = strbuf_split(remotes[i], ',');\n \n \t\tfor (size_t j = 0; elems[j]; j++) {\n-\t\t\tint res;\n \t\t\tstrbuf_strip_suffix(elems[j], \",\");\n-\t\t\tres = skip_prefix(elems[j]->buf, \"name=\", &remote_name) ||\n+\t\t\tif (!skip_prefix(elems[j]->buf, \"name=\", &remote_name))\n \t\t\t\tskip_prefix(elems[j]->buf, \"url=\", &remote_url);\n-\t\t\tif (!res)\n-\t\t\t\twarning(_(\"unknown element '%s' from remote info\"),\n-\t\t\t\t\telems[j]->buf);\n \t\t}\n \n \t\tif (remote_name)\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex cb061b1f35..27c32b2573 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -295,6 +295,38 @@ 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 promisor.sendFields\" '\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+\tgit -C server config promisor.sendFields \"partialCloneFilter, token\" &&\n+\ttest_when_finished \"git -C server config unset promisor.sendFields\" &&\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 promisor.acceptfromserver=All \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that fields are properly transmitted\n+\tENCODED_URL=$(echo \"file://$(pwd)/lop\" | sed -e \"s/ /%20/g\") &&\n+\tPR1=\"name=lop,url=$ENCODED_URL,partialCloneFilter=blob:none\" &&\n+\tPR2=\"name=otherLop,url=https://invalid.invalid,partialCloneFilter=blob:limit=10k,token=fooBar\" &&\n+\ttest_grep \"clone< promisor-remote=$PR1;$PR2\" trace &&\n+\ttest_grep \"clone> promisor-remote=lop;otherLop\" 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 \n-- \n2.49.0.596.g707f6eb7a2.dirty\n\n"},{"id":"518416","messageId":"20250519141259.3061550-4-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250519141259.3061550-1-christian.couder@gmail.com","subject":"[PATCH v3 3/5] promisor-remote: refactor how we parse advertised fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-19T14:12:57Z","receivedAt":"2025-05-19T14:13:24Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"In a follow up commit we are going to parse more fields, like a filter\nand a token, coming from the server when it advertises promisor remotes\nusing the \"promisor-remote\" capability.\n\nTo prepare for this, let's refactor the code that parses the advertised\nfields coming from the server into a new parse_one_advertised_remote()\nfunction that will populate a `struct promisor_info` with the content\nof the fields it parsed.\n\nWhile at it, let's also pass this `struct promisor_info` to the\nshould_accept_remote() function, instead of passing it the parsed name\nand url.\n\nThese changes will make it simpler to both parse more fields and access\nthe content of these parsed fields in follow up commits.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 71 +++++++++++++++++++++++++++++++++--------------\n 1 file changed, 50 insertions(+), 21 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex cde4079d8c..13bfa817c3 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -538,10 +538,12 @@ enum accept_promisor {\n };\n \n static int should_accept_remote(enum accept_promisor accept,\n-\t\t\t\tconst char *remote_name, const char *remote_url,\n+\t\t\t\tstruct promisor_info *advertised,\n \t\t\t\tstruct promisor_info *config_info)\n {\n \tstruct promisor_info *p;\n+\tconst char *remote_name = advertised->name;\n+\tconst char *remote_url = advertised->url;\n \n \tif (accept == ACCEPT_ALL)\n \t\treturn 1;\n@@ -576,6 +578,46 @@ static int should_accept_remote(enum accept_promisor accept,\n \treturn 0;\n }\n \n+static struct promisor_info *parse_one_advertised_remote(struct strbuf *remote_info)\n+{\n+\tstruct promisor_info *info = xcalloc(1, sizeof(*info));\n+\tstruct strbuf **elems = strbuf_split(remote_info, ',');\n+\n+\tfor (size_t i = 0; elems[i]; i++) {\n+\t\tchar *elem = elems[i]->buf;\n+\t\tchar *value;\n+\t\tchar *p = strchr(elem, '=');\n+\n+\t\tstrbuf_strip_suffix(elems[i], \",\");\n+\n+\t\tif (!p) {\n+\t\t\twarning(_(\"invalid element '%s' from remote info\"), elem);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\t*p = '\\0';\n+\t\tvalue = url_percent_decode(p + 1);\n+\n+\t\tif (!strcmp(elem, \"name\"))\n+\t\t\tinfo->name = value;\n+\t\telse if (!strcmp(elem, \"url\"))\n+\t\t\tinfo->url = value;\n+\t\telse\n+\t\t\tfree(value);\n+\t}\n+\n+\tstrbuf_list_free(elems);\n+\n+\tif (!info->name || !info->url) {\n+\t\twarning(_(\"server advertised a promisor remote without a name or URL: %s\"),\n+\t\t\tremote_info->buf);\n+\t\tpromisor_info_list_free(info);\n+\t\treturn NULL;\n+\t}\n+\n+\treturn info;\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@@ -610,32 +652,19 @@ static void filter_promisor_remote(struct repository *repo,\n \tremotes = strbuf_split_str(info, ';', 0);\n \n \tfor (size_t i = 0; remotes[i]; i++) {\n-\t\tstruct strbuf **elems;\n-\t\tconst char *remote_name = NULL;\n-\t\tconst char *remote_url = NULL;\n-\t\tchar *decoded_name = NULL;\n-\t\tchar *decoded_url = NULL;\n+\t\tstruct promisor_info *advertised;\n \n \t\tstrbuf_strip_suffix(remotes[i], \";\");\n-\t\telems = strbuf_split(remotes[i], ',');\n \n-\t\tfor (size_t j = 0; elems[j]; j++) {\n-\t\t\tstrbuf_strip_suffix(elems[j], \",\");\n-\t\t\tif (!skip_prefix(elems[j]->buf, \"name=\", &remote_name))\n-\t\t\t\tskip_prefix(elems[j]->buf, \"url=\", &remote_url);\n-\t\t}\n+\t\tadvertised = parse_one_advertised_remote(remotes[i]);\n \n-\t\tif (remote_name)\n-\t\t\tdecoded_name = url_percent_decode(remote_name);\n-\t\tif (remote_url)\n-\t\t\tdecoded_url = url_percent_decode(remote_url);\n+\t\tif (!advertised)\n+\t\t\tcontinue;\n \n-\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, config_info))\n-\t\t\tstrvec_push(accepted, decoded_name);\n+\t\tif (should_accept_remote(accept, advertised, config_info))\n+\t\t\tstrvec_push(accepted, advertised->name);\n \n-\t\tstrbuf_list_free(elems);\n-\t\tfree(decoded_name);\n-\t\tfree(decoded_url);\n+\t\tpromisor_info_list_free(advertised);\n \t}\n \n \tpromisor_info_list_free(config_info);\n-- \n2.49.0.596.g707f6eb7a2.dirty\n\n"},{"id":"518417","messageId":"20250519141259.3061550-5-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250519141259.3061550-1-christian.couder@gmail.com","subject":"[PATCH v3 4/5] promisor-remote: allow a client to check fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-19T14:12:58Z","receivedAt":"2025-05-19T14:13:25Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"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\nLet's make it possible for a client to check if these fields match\nwhat it expects before accepting a promisor remote.\n\nWe allow this by introducing a new \"promisor.checkFields\"\nconfiguration variable. It should contain a comma or space separated\nlist of fields that will be checked.\n\nBy limiting the protocol to specific well-defined fields, we ensure\nboth server and client have a shared understanding of field\nsemantics and usage.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    | 35 ++++++++++++\n promisor-remote.c                     | 79 +++++++++++++++++++++++++--\n t/t5710-promisor-remote-capability.sh | 35 ++++++++++++\n 3 files changed, 143 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex 71311b70c8..c2443cabd8 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -46,3 +46,38 @@ promisor.acceptFromServer::\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+\n+promisor.checkFields::\n+\tA comma or space separated list of additional remote related\n+\tfields that a client will check before accepting a promisor\n+\tremote. Currently, \"partialCloneFilter\" and \"token\" are the only\n+\tsupported field names.\n++\n+If one of these field names (e.g., \"token\") is being checked for an\n+advertised promisor remote (e.g., \"foo\"), three conditions must be met\n+for the check of this specific field to pass:\n++\n+1. The corresponding local configuration (e.g., `remote.foo.token`)\n+   must be set.\n+2. The server must advertise the \"token\" field for remote \"foo\".\n+3. The value of the locally configured `remote.foo.token` must exactly\n+   match the value advertised by the server for the \"token\" field.\n++\n+If any of these conditions are not met for any field name listed in\n+`promisor.checkFields`, the advertised remote \"foo\" will be rejected.\n++\n+For the \"partialCloneFilter\" field, this allows the client to ensure\n+that the server's filter matches what it expects locally, preventing\n+inconsistencies in filtering behavior. For the \"token\" field, this can\n+be used to verify that authentication credentials match expected\n+values.\n++\n+The \"name\" and \"url\" fields are always checked according to the\n+`promisor.acceptFromServer` policy, independently of this setting.\n++\n+The fields should be passed by the server through the\n+\"promisor-remote\" capability by using the `promisor.sendFields` config\n+variable. The fields will be checked only if the\n+`promisor.acceptFromServer` config variable is not set to \"None\". If\n+set to \"None\", this config variable will have no effect. See\n+linkgit:gitprotocol-v2[5].\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 13bfa817c3..8ac1c99bed 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -381,6 +381,20 @@ static struct string_list *fields_sent(void)\n \treturn &fields_list;\n }\n \n+static struct string_list *fields_checked(void)\n+{\n+\tstatic struct string_list fields_list = STRING_LIST_INIT_NODUP;\n+\tstatic int initialized = 0;\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+}\n+\n /*\n  * Linked list for promisor remotes involved in the \"promisor-remote\"\n  * protocol capability.\n@@ -537,6 +551,55 @@ enum accept_promisor {\n \tACCEPT_ALL\n };\n \n+static int match_field_against_config(const char *field, const char *value,\n+\t\t\t\t      struct promisor_info *config_info)\n+{\n+\tif (config_info->filter && !strcasecmp(field, promisor_field_filter))\n+\t\treturn !strcmp(config_info->filter, value);\n+\telse if (config_info->token && !strcasecmp(field, promisor_field_token))\n+\t\treturn !strcmp(config_info->token, value);\n+\n+\treturn 0;\n+}\n+\n+static int all_fields_match(struct promisor_info *advertised,\n+\t\t\t    struct promisor_info *config_info,\n+\t\t\t    int in_list)\n+{\n+\tstruct string_list* fields = fields_checked();\n+\tstruct string_list_item *item_checked;\n+\n+\tfor_each_string_list_item(item_checked, fields) {\n+\t\tint match = 0;\n+\t\tconst char *field = item_checked->string;\n+\t\tconst char *value = NULL;\n+\n+\t\tif (!strcasecmp(field, promisor_field_filter))\n+\t\t\tvalue = advertised->filter;\n+\t\telse if (!strcasecmp(field, promisor_field_token))\n+\t\t\tvalue = advertised->token;\n+\n+\t\tif (!value)\n+\t\t\treturn 0;\n+\n+\t\tif (in_list) {\n+\t\t\tfor (struct promisor_info *p = config_info; p; p = p->next) {\n+\t\t\t\tif (match_field_against_config(field, value, p)) {\n+\t\t\t\t\tmatch = 1;\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t}\n+\t\t} else {\n+\t\t\tmatch = match_field_against_config(field, value, config_info);\n+\t\t}\n+\n+\t\tif (!match)\n+\t\t\treturn 0;\n+\t}\n+\n+\treturn 1;\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 promisor_info *config_info)\n@@ -546,7 +609,7 @@ static int should_accept_remote(enum accept_promisor accept,\n \tconst char *remote_url = advertised->url;\n \n \tif (accept == ACCEPT_ALL)\n-\t\treturn 1;\n+\t\treturn all_fields_match(advertised, config_info, 1);\n \n \tp = remote_nick_find(config_info, remote_name);\n \n@@ -555,7 +618,7 @@ static int should_accept_remote(enum accept_promisor accept,\n \t\treturn 0;\n \n \tif (accept == ACCEPT_KNOWN_NAME)\n-\t\treturn 1;\n+\t\treturn all_fields_match(advertised, p, 0);\n \n \tif (accept != ACCEPT_KNOWN_URL)\n \t\tBUG(\"Unhandled 'enum accept_promisor' value '%d'\", accept);\n@@ -570,7 +633,7 @@ static int should_accept_remote(enum accept_promisor accept,\n \t\t    remote_name);\n \n \tif (!strcmp(p->url, remote_url))\n-\t\treturn 1;\n+\t\treturn all_fields_match(advertised, p, 0);\n \n \twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s'\"),\n \t\tremote_name, p->url, remote_url);\n@@ -602,6 +665,10 @@ static struct promisor_info *parse_one_advertised_remote(struct strbuf *remote_i\n \t\t\tinfo->name = value;\n \t\telse if (!strcmp(elem, \"url\"))\n \t\t\tinfo->url = value;\n+\t\telse if (!strcasecmp(elem, promisor_field_filter))\n+\t\t\tinfo->filter = value;\n+\t\telse if (!strcasecmp(elem, promisor_field_token))\n+\t\t\tinfo->token = value;\n \t\telse\n \t\t\tfree(value);\n \t}\n@@ -644,9 +711,6 @@ static void filter_promisor_remote(struct repository *repo,\n \tif (accept == ACCEPT_NONE)\n \t\treturn;\n \n-\tif (accept != ACCEPT_ALL)\n-\t\tconfig_info = promisor_config_info_list(repo, NULL);\n-\n \t/* Parse remote info received */\n \n \tremotes = strbuf_split_str(info, ';', 0);\n@@ -661,6 +725,9 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tif (!advertised)\n \t\t\tcontinue;\n \n+\t\tif (!config_info)\n+\t\t\tconfig_info = promisor_config_info_list(repo, fields_checked());\n+\n \t\tif (should_accept_remote(accept, advertised, config_info))\n \t\t\tstrvec_push(accepted, advertised->name);\n \ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex 27c32b2573..3538aacfd5 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -327,6 +327,41 @@ test_expect_success \"clone with promisor.sendFields\" '\n \tcheck_missing_objects server 1 \"$oid\"\n '\n \n+test_expect_success \"clone with promisor.checkFields\" '\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+\tgit -C server config promisor.sendFields \"partialCloneFilter, token\" &&\n+\ttest_when_finished \"git -C server config unset promisor.sendFields\" &&\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.partialCloneFilter=\"blob:none\" \\\n+\t\t-c promisor.acceptfromserver=All \\\n+\t\t-c promisor.checkFields=partialcloneFilter \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that fields are properly transmitted\n+\tENCODED_URL=$(echo \"file://$(pwd)/lop\" | sed -e \"s/ /%20/g\") &&\n+\tPR1=\"name=lop,url=$ENCODED_URL,partialCloneFilter=blob:none\" &&\n+\tPR2=\"name=otherLop,url=https://invalid.invalid,partialCloneFilter=blob:limit=10k,token=fooBar\" &&\n+\ttest_grep \"clone< promisor-remote=$PR1;$PR2\" trace &&\n+\ttest_grep \"clone> promisor-remote=lop\" trace &&\n+\ttest_grep ! \"clone> promisor-remote=lop;otherLop\" 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 \n-- \n2.49.0.596.g707f6eb7a2.dirty\n\n"},{"id":"518418","messageId":"20250519141259.3061550-6-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250519141259.3061550-1-christian.couder@gmail.com","subject":"[PATCH v3 5/5] promisor-remote: use string constants for 'name' and 'url' too","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-19T14:12:59Z","receivedAt":"2025-05-19T14:13:26Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"A previous commit started to define `promisor_field_filter` and\n`promisor_field_token`, and used them instead of the\n\"partialCloneFilter\" and \"token\" string literals.\n\nLet's do the same for \"name\" and \"url\" to avoid repeating them\nseveral times and for consistency with the other fields.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 14 ++++++++++----\n 1 file changed, 10 insertions(+), 4 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 8ac1c99bed..201d767b74 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -314,6 +314,12 @@ static int allow_unsanitized(char ch)\n \treturn ch > 32 && ch < 127;\n }\n \n+/*\n+ * All the fields used in \"promisor-remote\" protocol capability,\n+ * including the mandatory \"name\" and \"url\" ones.\n+ */\n+static const char promisor_field_name[] = \"name\";\n+static const char promisor_field_url[] = \"url\";\n static const char promisor_field_filter[] = \"partialCloneFilter\";\n static const char promisor_field_token[] = \"token\";\n \n@@ -510,9 +516,9 @@ char *promisor_remote_info(struct repository *repo)\n \t\tif (p != config_info)\n \t\t\tstrbuf_addch(&sb, ';');\n \n-\t\tstrbuf_addstr(&sb, \"name=\");\n+\t\tstrbuf_addf(&sb, \"%s=\", promisor_field_name);\n \t\tstrbuf_addstr_urlencode(&sb, p->name, allow_unsanitized);\n-\t\tstrbuf_addstr(&sb, \",url=\");\n+\t\tstrbuf_addf(&sb, \",%s=\", promisor_field_url);\n \t\tstrbuf_addstr_urlencode(&sb, p->url, allow_unsanitized);\n \n \t\tif (p->filter) {\n@@ -661,9 +667,9 @@ static struct promisor_info *parse_one_advertised_remote(struct strbuf *remote_i\n \t\t*p = '\\0';\n \t\tvalue = url_percent_decode(p + 1);\n \n-\t\tif (!strcmp(elem, \"name\"))\n+\t\tif (!strcmp(elem, promisor_field_name))\n \t\t\tinfo->name = value;\n-\t\telse if (!strcmp(elem, \"url\"))\n+\t\telse if (!strcmp(elem, promisor_field_url))\n \t\t\tinfo->url = value;\n \t\telse if (!strcasecmp(elem, promisor_field_filter))\n \t\t\tinfo->filter = value;\n-- \n2.49.0.596.g707f6eb7a2.dirty\n\n"},{"id":"518481","messageId":"CAOLa=ZRnL_PLVpQrmHPMXtJd9e965B5iz8widkk5GNSDvh21Pg@mail.gmail.com","threadId":"63293","inReplyTo":"20250519141259.3061550-2-christian.couder@gmail.com","subject":"Re: [PATCH v3 1/5] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-05-20T09:37:43Z","receivedAt":"2025-05-20T09:37:47Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n[snip]\n\n>\n>  /*\n> - * Find first index of 'nicks' where there is 'nick'. 'nick' is\n> - * compared case sensitively to the strings in 'nicks'. If not found\n> - * 'nicks->nr' is returned.\n> + * Find first element of 'p' where the 'name' member is 'nick'. 'nick'\n> + * is compared case sensitively to the strings in 'p'. If not found\n> + * NULL is returned.\n>   */\n> -static size_t remote_nick_find(struct strvec *nicks, const char *nick)\n> +static struct promisor_info *remote_nick_find(struct promisor_info *p, const char *nick)\n\nNit: while we're here wouldn't be nicer to rename this to\n`promiser_info_list_find_name` or similar?\n\n[snip]\n"},{"id":"518486","messageId":"CAP8UFD2XtyPD7b=sE+t9d-kyXDt4hYMqVotwz7hJZEO+5TfXGg@mail.gmail.com","threadId":"63293","inReplyTo":"CAOLa=ZRnL_PLVpQrmHPMXtJd9e965B5iz8widkk5GNSDvh21Pg@mail.gmail.com","subject":"Re: [PATCH v3 1/5] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-20T13:32:56Z","receivedAt":"2025-05-20T13:33:10Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Tue, May 20, 2025 at 11:37 AM Karthik Nayak <karthik.188@gmail.com> wrote:\n>\n> Christian Couder <christian.couder@gmail.com> writes:\n>\n> [snip]\n>\n> >\n> >  /*\n> > - * Find first index of 'nicks' where there is 'nick'. 'nick' is\n> > - * compared case sensitively to the strings in 'nicks'. If not found\n> > - * 'nicks->nr' is returned.\n> > + * Find first element of 'p' where the 'name' member is 'nick'. 'nick'\n> > + * is compared case sensitively to the strings in 'p'. If not found\n> > + * NULL is returned.\n> >   */\n> > -static size_t remote_nick_find(struct strvec *nicks, const char *nick)\n> > +static struct promisor_info *remote_nick_find(struct promisor_info *p, const char *nick)\n>\n> Nit: while we're here wouldn't be nicer to rename this to\n> `promiser_info_list_find_name` or similar?\n\nJunio suggested this name in a discussion of a previous patch series:\n\nhttps://lore.kernel.org/git/xmqqa5bbq0nb.fsf@gitster.g/\n\nI am not sure that changing its first argument from a `struct strvec\n*` to a `struct promisor_info *` makes it worth renaming. If that's\nthe case, then I would be fine with renaming it like you suggest\nthough.\n"},{"id":"518507","messageId":"xmqqr00ji63l.fsf@gitster.g","threadId":"63293","inReplyTo":"CAP8UFD2XtyPD7b=sE+t9d-kyXDt4hYMqVotwz7hJZEO+5TfXGg@mail.gmail.com","subject":"Re: [PATCH v3 1/5] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-05-20T16:45:18Z","receivedAt":"2025-05-20T16:45:21Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> On Tue, May 20, 2025 at 11:37 AM Karthik Nayak <karthik.188@gmail.com> wrote:\n>>\n>> Christian Couder <christian.couder@gmail.com> writes:\n>>\n>> [snip]\n>>\n>> >\n>> >  /*\n>> > - * Find first index of 'nicks' where there is 'nick'. 'nick' is\n>> > - * compared case sensitively to the strings in 'nicks'. If not found\n>> > - * 'nicks->nr' is returned.\n>> > + * Find first element of 'p' where the 'name' member is 'nick'. 'nick'\n>> > + * is compared case sensitively to the strings in 'p'. If not found\n>> > + * NULL is returned.\n>> >   */\n>> > -static size_t remote_nick_find(struct strvec *nicks, const char *nick)\n>> > +static struct promisor_info *remote_nick_find(struct promisor_info *p, const char *nick)\n>>\n>> Nit: while we're here wouldn't be nicer to rename this to\n>> `promiser_info_list_find_name` or similar?\n>\n> Junio suggested this name in a discussion of a previous patch series:\n>\n> https://lore.kernel.org/git/xmqqa5bbq0nb.fsf@gitster.g/\n\nDon't blame me for that name ;-)\n\nThe name was for a hypothetical variant that took \"struct strvec *\"\nas its first parameter, and the name was given only because it did\nnot make much sense to call the helper after \"strvec\".  A function\nname that signals that we are finding (something) using \"nick\"-name\nwas a much better choice.  Since your final one finds in \"struct\npromisor_info *\", not a generic \"struct strvec *\", I wouldn't be\nsurprised if a name that is about promisor-info (whatever it is)\nmore clearly describes what it does.  If this is a file-local\nhelper, promisor_info_find() or find_promisor_info() may be\nsufficient, if \"by nickname\" is the primary and only way for the\napplication to find an instance of promisor_info.\n\nIs promisor_info primarily be found by their names before its other\nmembers are accessed?  If so, I wonder if a strmap or string_list\nthat uses the nickname as the key with a pointer to a promisor_info\nstructure as the data more appropriate than a hand-crafted linear\nlinked list.\n\nThanks.\n"},{"id":"518543","messageId":"CAP8UFD25fF6OdYmP54JFyY4Znpij=MR1ifdWk+cjca5Hes739A@mail.gmail.com","threadId":"63293","inReplyTo":"xmqqr00ji63l.fsf@gitster.g","subject":"Re: [PATCH v3 1/5] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-21T06:33:52Z","receivedAt":"2025-05-21T06:34:05Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Tue, May 20, 2025 at 6:45 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Christian Couder <christian.couder@gmail.com> writes:\n\n> > Junio suggested this name in a discussion of a previous patch series:\n> >\n> > https://lore.kernel.org/git/xmqqa5bbq0nb.fsf@gitster.g/\n>\n> Don't blame me for that name ;-)\n>\n> The name was for a hypothetical variant that took \"struct strvec *\"\n> as its first parameter, and the name was given only because it did\n> not make much sense to call the helper after \"strvec\".  A function\n> name that signals that we are finding (something) using \"nick\"-name\n> was a much better choice.  Since your final one finds in \"struct\n> promisor_info *\", not a generic \"struct strvec *\", I wouldn't be\n> surprised if a name that is about promisor-info (whatever it is)\n> more clearly describes what it does.  If this is a file-local\n> helper, promisor_info_find() or find_promisor_info() may be\n> sufficient, if \"by nickname\" is the primary and only way for the\n> application to find an instance of promisor_info.\n\nOK, I think I will use promisor_info_find() or\npromisor_info_find_by_name() in the next reroll, as it goes well with\nhow other functions related to \"struct promisor_info *\" are named.\n\n> Is promisor_info primarily be found by their names before its other\n> members are accessed?\n\nNo, there are different ways the promisor_info instances are iterated\non or searched, see promisor_remote_info() and all_fields_match()\nwhere we iterate on all the instances and then either use the instance\nmembers to generate an advertisement string, or check if some of the\nmembers match advertised values.\n\n> If so, I wonder if a strmap or string_list\n> that uses the nickname as the key with a pointer to a promisor_info\n> structure as the data more appropriate than a hand-crafted linear\n> linked list.\n\nI don't think it would bring a lot of benefits. Using an strmap or a\nsorted string list might make things faster if there are a lot of\npromisor remotes configured on the clients, but I don't think we are\nat a point where such an optimisation is worth it.\n"},{"id":"518584","messageId":"xmqqldqqc8kq.fsf@gitster.g","threadId":"63293","inReplyTo":"CAP8UFD25fF6OdYmP54JFyY4Znpij=MR1ifdWk+cjca5Hes739A@mail.gmail.com","subject":"Re: [PATCH v3 1/5] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-05-21T15:00:37Z","receivedAt":"2025-05-21T15:00:40Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> I don't think it would bring a lot of benefits. Using an strmap or a\n> sorted string list might make things faster if there are a lot of\n> promisor remotes configured on the clients, but I don't think we are\n> at a point where such an optimisation is worth it.\n\nWhat I was getting at using common collection types instead of\nrolling your own linked list was not primarily about performance.\nThey are more battle-tested and much easier to readers who are\nfamiliar with these existing types.\n"},{"id":"518614","messageId":"agvpcmvdxxpfwr34mw34dvatqr3rlc72zntgbrhlzynvn4h5rh@5mdgt4mdocnx","threadId":"63293","inReplyTo":"20250519141259.3061550-3-christian.couder@gmail.com","subject":"Re: [PATCH v3 2/5] promisor-remote: allow a server to advertise more fields","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-05-21T20:31:45Z","receivedAt":"2025-05-21T20:36:26Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/05/19 04:12PM, Christian Couder wrote:\n> For now the \"promisor-remote\" protocol capability can only pass \"name\"\n> and \"url\" information from a server to a client in the form\n> \"name=<remote_name>,url=<remote_url>\".\n> \n> Let's make it possible to pass more information by introducing a new\n> \"promisor.sendFields\" configuration variable. This variable should\n> contain a comma or space separated list of fields that will be looked\n> up in the configuration of the remote on the server to find the values\n> that will be passed to the client.\n> \n> Only a set of predefined fields are allowed. The only fields in this\n> set are \"partialCloneFilter\" and \"token\". The \"partialCloneFilter\"\n> field specifies the filter definition used by the promisor remote,\n> and the \"token\" field can provide an authentication credential for\n> accessing it.\n> \n> For example if \"promisor.sendFields\" is set to \"partialCloneFilter\",\n> and the server has the \"remote.<name>.partialCloneFilter\" config\n> variable set to a value for a remote, then that value will be passed\n> in the form \"partialCloneFilter=<value>\" after the \"name\" and \"url\"\n> fields.\n> \n> A following commit will allow the client to use the information to\n> decide if it accepts the remote or not. For now the client doesn't do\n> anything with the additional information it receives.\n> \n> Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n> ---\n>  Documentation/config/promisor.adoc    |  18 ++++\n>  Documentation/gitprotocol-v2.adoc     |  59 ++++++++----\n>  promisor-remote.c                     | 128 ++++++++++++++++++++++++--\n>  t/t5710-promisor-remote-capability.sh |  32 +++++++\n>  4 files changed, 212 insertions(+), 25 deletions(-)\n> \n> diff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\n> index 2638b01f83..71311b70c8 100644\n> --- a/Documentation/config/promisor.adoc\n> +++ b/Documentation/config/promisor.adoc\n> @@ -9,6 +9,24 @@ promisor.advertise::\n>  \t\"false\", which means the \"promisor-remote\" capability is not\n>  \tadvertised.\n>  \n> +promisor.sendFields::\n> +\tA comma or space separated list of additional remote related\n> +\tfields that a server will send while advertising its promisor\n> +\tremotes using the \"promisor-remote\" capability, see\n> +\tlinkgit:gitprotocol-v2[5]. Currently, only the\n> +\t\"partialCloneFilter\" and \"token\" fields are supported. The\n> +\t\"partialCloneFilter\" field contains the partial clone filter\n> +\tused for the remote, and the \"token\" field contains an\n> +\tauthentication token for the remote.\n> ++\n> +When a field is part of this list and a corresponding\n> +\"remote.foo.<field>\" config variable is set on the server to a\n> +non-empty value, then the field and its value will be sent when\n> +advertising the promisor remote \"foo\". This list has no effect unless\n> +the \"promisor.advertise\" config variable is set to \"true\", and the\n> +\"name\" and \"url\" fields are always advertised regardless of this\n> +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> diff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\n> index 5598c93e67..3c12c158fd 100644\n> --- a/Documentation/gitprotocol-v2.adoc\n> +++ b/Documentation/gitprotocol-v2.adoc\n> @@ -785,33 +785,59 @@ retrieving the header from a bundle at the indicated URI, and thus\n>  save themselves and the server(s) the request(s) needed to inspect the\n>  headers of that bundle or bundles.\n>  \n> -promisor-remote=<pr-infos>\n> +promisor-remote=<pr-info>\n>  ~~~~~~~~~~~~~~~~~~~~~~~~~~\n>  \n>  The server may advertise some promisor remotes it is using or knows\n>  about to a client which may want to use them as its promisor remotes,\n> -instead of this repository. In this case <pr-infos> should be of the\n> +instead of this repository. In this case <pr-info> should be of the\n>  form:\n>  \n> -\tpr-infos = pr-info | pr-infos \";\" pr-info\n> +\tpr-info = pr-fields | pr-info \";\" pr-info\n>  \n> -\tpr-info = \"name=\" pr-name | \"name=\" pr-name \",\" \"url=\" pr-url\n> +\tpr-fields = field-name \"=\" field-value | pr-fields \",\" pr-fields\n>  \n> -where `pr-name` is the urlencoded name of a promisor remote, and\n> -`pr-url` the urlencoded URL of that promisor remote.\n> +where all the `field-name` and `field-value` in a given `pr-fields`\n> +are field names and values related to a single promisor remote.\n>  \n> -In this case, if the client decides to use one or more promisor\n> -remotes the server advertised, it can reply with\n> -\"promisor-remote=<pr-names>\" where <pr-names> should be of the form:\n> +The server MUST advertise at least the \"name\" and \"url\" field names\n> +along with the associated field values, which are the name of a valid\n> +remote and its URL, in each `pr-fields`. The \"name\" and \"url\" fields\n> +MUST appear first in each pr-fields, in that order.\n>  \n> -\tpr-names = pr-name | pr-names \";\" pr-name\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> +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> +\n> +- \"token\": An authentication token that clients can use when\n> +connecting to the remote. It corresponds to the \"remote.<name>.token\"\n> +config setting.\n> +\n> +No other fields are defined by the protocol at this time. Clients MUST\n> +ignore fields they don't recognize to allow for future protocol\n> +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> +\n> +Field values MUST be urlencoded.\n> +\n> +If the client decides to use one or more promisor remotes the server\n> +advertised, it can reply with \"promisor-remote=<pr-names>\" where\n> +<pr-names> should be of the form:\n> +\n> +\tpr-names = pr-name | pr-names \";\" pr-names\n>  \n>  where `pr-name` is the urlencoded name of a promisor remote the server\n>  advertised and the client accepts.\n>  \n> -Note that, everywhere in this document, `pr-name` MUST be a valid\n> -remote name, and the ';' and ',' characters MUST be encoded if they\n> -appear in `pr-name` or `pr-url`.\n> +Note that, everywhere in this document, the ';' and ',' characters\n> +MUST be encoded if they appear in `pr-name` or `field-value`.\n>  \n>  If the server doesn't know any promisor remote that could be good for\n>  a client to use, or prefers a client not to use any promisor remote it\n> @@ -822,9 +848,10 @@ In this case, or if the client doesn't want to use any promisor remote\n>  the server advertised, the client shouldn't advertise the\n>  \"promisor-remote\" capability at all in its reply.\n>  \n> -The \"promisor.advertise\" and \"promisor.acceptFromServer\" configuration\n> -options can be used on the server and client side to control what they\n> -advertise or accept respectively. See the documentation of these\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>  \n>  Note that in the future it would be nice if the \"promisor-remote\"\n> diff --git a/promisor-remote.c b/promisor-remote.c\n> index 94e87f2f48..cde4079d8c 100644\n> --- a/promisor-remote.c\n> +++ b/promisor-remote.c\n> @@ -314,6 +314,73 @@ static int allow_unsanitized(char ch)\n>  \treturn ch > 32 && ch < 127;\n>  }\n>  \n> +static const char promisor_field_filter[] = \"partialCloneFilter\";\n> +static const char promisor_field_token[] = \"token\";\n> +\n> +/*\n> + * List of optional field names that can be used in the\n> + * \"promisor-remote\" protocol capability (others must be\n> + * ignored). Each field should correspond to a configurable property\n> + * of a remote that can be relevant for the client.\n> + */\n> +static const char *known_fields[] = {\n> +\tpromisor_field_filter, /* Filter used for partial clone */\n> +\tpromisor_field_token,  /* Authentication token for the remote */\n> +\tNULL\n> +};\n> +\n> +/*\n> + * Check if 'field' is in the list of the known field names for the\n> + * \"promisor-remote\" protocol capability.\n> + */\n> +static int is_known_field(const char *field)\n> +{\n> +\tconst char **p;\n> +\n> +\tfor (p = known_fields; *p; p++)\n> +\t\tif (!strcasecmp(*p, field))\n> +\t\t\treturn 1;\n> +\treturn 0;\n> +}\n> +\n> +static int is_valid_field(struct string_list_item *item, void *cb_data)\n> +{\n> +\tconst char *field = item->string;\n> +\tconst char *config_key = (const char *)cb_data;\n> +\n> +\tif (!is_known_field(field)) {\n> +\t\twarning(_(\"unsupported field '%s' in '%s' config\"), field, config_key);\n> +\t\treturn 0;\n> +\t}\n> +\treturn 1;\n> +}\n\nOk, so if the server has an unknown sendField value configured, the\nvalue is ignored by the server and a warning printed.\n\n> +\n> +static char *fields_from_config(struct string_list *fields_list, const char *config_key)\n> +{\n> +\tchar *fields = NULL;\n> +\n> +\tif (!git_config_get_string(config_key, &fields) && *fields) {\n\nWe could use `repo_config_get_string()` here instead and wire the nearby\n`struct repository`, but as the rest of the file is not doing so it's\nnot really a big deal.\n\n> +\t\tstring_list_split_in_place(fields_list, fields, \", \", -1);\n> +\t\tfilter_string_list(fields_list, 0, is_valid_field, (void *)config_key);\n> +\t}\n> +\n> +\treturn fields;\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 = 0;\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> +}\n\nAre there scenarios where `fields_sent()` is getting invoked more than\nonce? My understanding is that this is invoked only when the capability\nis being advertised. Regardless, I wonder if we really need the static\ninitialization here.\n\n> +\n>  /*\n>   * Linked list for promisor remotes involved in the \"promisor-remote\"\n>   * protocol capability.\n> @@ -326,6 +393,8 @@ struct promisor_info {\n>  \tstruct promisor_info *next;\n>  \tconst char *name;\n>  \tconst char *url;\n> +\tconst char *filter;\n> +\tconst char *token;\n>  };\n>  \n>  static void promisor_info_list_free(struct promisor_info *p)\n> @@ -336,12 +405,45 @@ static void promisor_info_list_free(struct promisor_info *p)\n>  \t\tnext = p->next;\n>  \t\tfree((char *)p->name);\n>  \t\tfree((char *)p->url);\n> +\t\tfree((char *)p->filter);\n> +\t\tfree((char *)p->token);\n>  \t\tfree(p);\n>  \t}\n>  }\n>  \n> -/* Prepare a 'struct promisor_info' linked list with config information. */\n> -static struct promisor_info *promisor_config_info_list(struct repository *repo)\n> +static void set_one_field(struct promisor_info *p,\n> +\t\t\t  const char *field, const char *value)\n> +{\n> +\tif (!strcasecmp(field, promisor_field_filter))\n> +\t\tp->filter = xstrdup(value);\n> +\telse if (!strcasecmp(field, promisor_field_token))\n> +\t\tp->token = xstrdup(value);\n> +\telse\n> +\t\tBUG(\"Invalid field '%s'\", field);\n> +}\n> +\n> +static void set_fields(struct promisor_info *p,\n> +\t\t       struct string_list *field_names)\n> +{\n> +\tstruct string_list_item *item;\n> +\n> +\tfor_each_string_list_item(item, field_names) {\n> +\t\tchar *key = xstrfmt(\"remote.%s.%s\", p->name, item->string);\n> +\t\tconst char *val;\n> +\t\tif (!git_config_get_string_tmp(key, &val) && *val)\n> +\t\t\tset_one_field(p, item->string, val);\n> +\t\tfree(key);\n> +\t}\n> +}\n> +\n> +/*\n> + * Prepare a 'struct promisor_info' linked list of promisor remotes\n> + * with config information. Only members of that struct specified by\n> + * the 'field_names' linked list are set (using values from the\n> + * configuration).\n> + */\n> +static struct promisor_info *promisor_config_info_list(struct repository *repo,\n> +\t\t\t\t\t\t       struct string_list *field_names)\n>  {\n>  \tstruct promisor_info *infos = NULL;\n>  \tstruct promisor_info **last_info = &infos;\n> @@ -360,6 +462,9 @@ static struct promisor_info *promisor_config_info_list(struct repository *repo)\n>  \t\t\tnew_info->name = xstrdup(r->name);\n>  \t\t\tnew_info->url = xstrdup(url);\n>  \n> +\t\t\tif (field_names)\n> +\t\t\t\tset_fields(new_info, field_names);\n> +\n>  \t\t\t*last_info = new_info;\n>  \t\t\tlast_info = &new_info->next;\n>  \t\t}\n> @@ -382,7 +487,7 @@ char *promisor_remote_info(struct repository *repo)\n>  \tif (!advertise_promisors)\n>  \t\treturn NULL;\n>  \n> -\tconfig_info = promisor_config_info_list(repo);\n> +\tconfig_info = promisor_config_info_list(repo, fields_sent());\n>  \n>  \tif (!config_info)\n>  \t\treturn NULL;\n> @@ -395,6 +500,15 @@ char *promisor_remote_info(struct repository *repo)\n>  \t\tstrbuf_addstr_urlencode(&sb, p->name, allow_unsanitized);\n>  \t\tstrbuf_addstr(&sb, \",url=\");\n>  \t\tstrbuf_addstr_urlencode(&sb, p->url, allow_unsanitized);\n> +\n> +\t\tif (p->filter) {\n> +\t\t\tstrbuf_addf(&sb, \",%s=\", promisor_field_filter);\n> +\t\t\tstrbuf_addstr_urlencode(&sb, p->filter, allow_unsanitized);\n> +\t\t}\n> +\t\tif (p->token) {\n> +\t\t\tstrbuf_addf(&sb, \",%s=\", promisor_field_token);\n> +\t\t\tstrbuf_addstr_urlencode(&sb, p->token, allow_unsanitized);\n> +\t\t}\n>  \t}\n>  \n>  \tpromisor_info_list_free(config_info);\n> @@ -489,7 +603,7 @@ static void filter_promisor_remote(struct repository *repo,\n>  \t\treturn;\n>  \n>  \tif (accept != ACCEPT_ALL)\n> -\t\tconfig_info = promisor_config_info_list(repo);\n> +\t\tconfig_info = promisor_config_info_list(repo, NULL);\n>  \n>  \t/* Parse remote info received */\n>  \n> @@ -506,13 +620,9 @@ static void filter_promisor_remote(struct repository *repo,\n>  \t\telems = strbuf_split(remotes[i], ',');\n>  \n>  \t\tfor (size_t j = 0; elems[j]; j++) {\n> -\t\t\tint res;\n>  \t\t\tstrbuf_strip_suffix(elems[j], \",\");\n> -\t\t\tres = skip_prefix(elems[j]->buf, \"name=\", &remote_name) ||\n> +\t\t\tif (!skip_prefix(elems[j]->buf, \"name=\", &remote_name))\n>  \t\t\t\tskip_prefix(elems[j]->buf, \"url=\", &remote_url);\n> -\t\t\tif (!res)\n> -\t\t\t\twarning(_(\"unknown element '%s' from remote info\"),\n> -\t\t\t\t\telems[j]->buf);\n>  \t\t}\n>  \n>  \t\tif (remote_name)\n> diff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\n> index cb061b1f35..27c32b2573 100755\n> --- a/t/t5710-promisor-remote-capability.sh\n> +++ b/t/t5710-promisor-remote-capability.sh\n> @@ -295,6 +295,38 @@ 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 promisor.sendFields\" '\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> +\tgit -C server config promisor.sendFields \"partialCloneFilter, token\" &&\n\nThis configuration results in the unsupported field warning message\nbeing printed. This is because of the SP following the comma resulting\nin an empty value in the middle after the string split.\n\n> +\ttest_when_finished \"git -C server config unset promisor.sendFields\" &&\n\nI think we could use `test_config` which would automatically unset the\nconfiguration after the test.\n\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 promisor.acceptfromserver=All \\\n> +\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n> +\n> +\t# Check that fields are properly transmitted\n> +\tENCODED_URL=$(echo \"file://$(pwd)/lop\" | sed -e \"s/ /%20/g\") &&\n> +\tPR1=\"name=lop,url=$ENCODED_URL,partialCloneFilter=blob:none\" &&\n> +\tPR2=\"name=otherLop,url=https://invalid.invalid,partialCloneFilter=blob:limit=10k,token=fooBar\" &&\n> +\ttest_grep \"clone< promisor-remote=$PR1;$PR2\" trace &&\n> +\ttest_grep \"clone> promisor-remote=lop;otherLop\" 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>  \n> -- \n> 2.49.0.596.g707f6eb7a2.dirty\n> \n> \n"},{"id":"518959","messageId":"aDVu4JmTU7Wz32Z7@pks.im","threadId":"63293","inReplyTo":"CAP8UFD1+c11JgSKzs=A39-5EP2Senob-NxSXB7orU0usSLT83A@mail.gmail.com","subject":"Re: [PATCH v2 2/3] promisor-remote: allow a server to advertise more fields","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-27T07:50:56Z","receivedAt":"2025-05-27T07:51:05Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, May 19, 2025 at 04:11:18PM +0200, Christian Couder wrote:\n> On Wed, May 7, 2025 at 10:25 AM Patrick Steinhardt <ps@pks.im> wrote:\n> >\n> > On Tue, Apr 29, 2025 at 04:52:42PM +0200, Christian Couder wrote:\n> > > diff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\n> > > index 2638b01f83..71311b70c8 100644\n> > > --- a/Documentation/config/promisor.adoc\n> > > +++ b/Documentation/config/promisor.adoc\n> > > @@ -9,6 +9,24 @@ promisor.advertise::\n> > >       \"false\", which means the \"promisor-remote\" capability is not\n> > >       advertised.\n> > >\n> > > +promisor.sendFields::\n> > > +     A comma or space separated list of additional remote related\n> > > +     fields that a server will send while advertising its promisor\n> > > +     remotes using the \"promisor-remote\" capability, see\n> > > +     linkgit:gitprotocol-v2[5]. Currently, only the\n> > > +     \"partialCloneFilter\" and \"token\" fields are supported. The\n> > > +     \"partialCloneFilter\" field contains the partial clone filter\n> > > +     used for the remote, and the \"token\" field contains an\n> > > +     authentication token for the remote.\n> > > ++\n> >\n> > Should we maybe convert this into a list of accepted fields? Makes it\n> > easier to extend going forward.\n> \n> I am not sure I understand what you mean. This promisor.sendFields\n> config variable is for the server side which advertises remotes. The\n> server advertises its remotes (if it wants to) before receiving\n> information from the client, so it cannot know what the client\n> accepts.\n\nIn the current form you need to reflow this whole paragraph every time a\nnew field is supported, and it's easy to miss the exact supported\nfields. So my idea was to maybe move the supported fields into a\nbulleted list. E.g.:\n\n    promisor.sendFields::\n         A comma or space separated list of additional remote related\n         fields that a server will send while advertising its promisor\n         remotes using the \"promisor-remote\" capability, see\n         linkgit:gitprotocol-v2[5]. The following fields are supported:\n    +\n    * \"partialCloneFilter\": contains the partial clone filter used for\n      the remote.\n    * \"token\": contains the authentication token for the remote.\n\n> > Furthermore, should we maybe refactor this to match the restrictive\n> > design where valid fields are explicitly specified? In other words,\n> > should we have separate config keys for each of the accepted fields now?\n> \n> Maybe I don't understand what you mean with \"accepted fields\".\n\nI think I had a misunderstanding on my side. I didn't get that this is\nonly configuring field _names_ that we'll end up sending to the remote\nside. So I thought that the user is expected to configure name-value\npairs here that are then sent to the client, not only the name.\n\nI guess this is mostly because the config documentation talks about\n\"fields\", but that term is used elsewhere to indicate a name-value pair.\n\n> > Also, shouldn't this setting be per promisor remote that we want to\n> > advertise? I expect that servers will want to send different partial\n> > clone filters for each of the advertised remotes, and they may also want\n> > to send different tokens. So it seems a bit too inflexible to only have\n> > a single, global \"sendFields\" configuration that covers all promisors.\n> \n> First this setting already allows servers to send different partial\n> clone filters for each of the advertised remotes. For each remote it\n> advertises, a server would send the partial clone filter that is\n> already configured for this remote on the server. Same for tokens.\n> \n> Also we can extend this setting to be per promisor remote later if\n> there is a need for it. I don't think it would be difficult to do. And\n> I don't think it's necessary right now, because it's likely that for\n> simplicity most servers will manage all their promisor remotes in the\n> same way (at least until usage of promisor remotes grows a lot).\n\nYeah, with my fixed understanding I agree that it's not necessary to\nconfigure this per remote as of now.\n\n[snip]\n> > Does\n> > it mean that this promisor remote should only be used in case we do have\n> > the exact same filter passed to git-clone(1)?\n> \n> It's up to the client to decide, but yeah it will likely work better\n> if the same filter is used. It should still work if a different filter\n> is used though. In case the promisor remote doesn't have an object,\n> there should be a fallback to ask the main server for that object.\n> \n> Also the filter mechanism already exists for a long time and this\n> series doesn't change how it works. It's already possible to have\n> different repos using the same promisor remote with different filters.\n> So documentation about what happens when they do that should not be\n> specific to this patch series.\n\nThat's fair enough, but spelling this out somewhere and drawing the\nbigger picture helps the reviewer understand the vision that you've got\nhere.\n\nPatrick\n"},{"id":"518960","messageId":"aDVu6O9MUicmd8y4@pks.im","threadId":"63293","inReplyTo":"20250519141259.3061550-3-christian.couder@gmail.com","subject":"Re: [PATCH v3 2/5] promisor-remote: allow a server to advertise more fields","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-27T07:51:04Z","receivedAt":"2025-05-27T07:51:08Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, May 19, 2025 at 04:12:56PM +0200, Christian Couder wrote:\n> diff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\n> index 2638b01f83..71311b70c8 100644\n> --- a/Documentation/config/promisor.adoc\n> +++ b/Documentation/config/promisor.adoc\n> @@ -9,6 +9,24 @@ promisor.advertise::\n>  \t\"false\", which means the \"promisor-remote\" capability is not\n>  \tadvertised.\n>  \n> +promisor.sendFields::\n> +\tA comma or space separated list of additional remote related\n> +\tfields that a server will send while advertising its promisor\n> +\tremotes using the \"promisor-remote\" capability, see\n> +\tlinkgit:gitprotocol-v2[5]. Currently, only the\n> +\t\"partialCloneFilter\" and \"token\" fields are supported. The\n> +\t\"partialCloneFilter\" field contains the partial clone filter\n> +\tused for the remote, and the \"token\" field contains an\n> +\tauthentication token for the remote.\n> ++\n> +When a field is part of this list and a corresponding\n> +\"remote.foo.<field>\" config variable is set on the server to a\n> +non-empty value, then the field and its value will be sent when\n> +advertising the promisor remote \"foo\". This list has no effect unless\n> +the \"promisor.advertise\" config variable is set to \"true\", and the\n> +\"name\" and \"url\" fields are always advertised regardless of this\n> +setting.\n\nI think this documentation should be clarified to explicitly talk about\n\"field names\". In v2 I misread these paragraphs to mean that the admin\nis expected to configure name-value pairs because you say \"fields\" here,\nand that term is specified elsewhere to be such a pair.\n\n> diff --git a/promisor-remote.c b/promisor-remote.c\n> index 94e87f2f48..cde4079d8c 100644\n> --- a/promisor-remote.c\n> +++ b/promisor-remote.c\n> @@ -314,6 +314,73 @@ static int allow_unsanitized(char ch)\n>  \treturn ch > 32 && ch < 127;\n>  }\n>  \n> +static const char promisor_field_filter[] = \"partialCloneFilter\";\n> +static const char promisor_field_token[] = \"token\";\n\nCurious. Why aren't these declared as mere string constants (static\nconst char *)? It might be a bit more idiomatic to have these as\nall-uppercase defines to make it obvious that those aren't a local\nvariable.\n\n    #define PROMISOR_FIELD_FILTER \"partialCloneFilter\"\n    #define PROMISOR_FIELD_TOKEN  \"token\"\n\n> @@ -326,6 +393,8 @@ struct promisor_info {\n>  \tstruct promisor_info *next;\n>  \tconst char *name;\n>  \tconst char *url;\n> +\tconst char *filter;\n> +\tconst char *token;\n>  };\n>  \n>  static void promisor_info_list_free(struct promisor_info *p)\n\nYup, this now follows my suggestion.\n\n> @@ -336,12 +405,45 @@ static void promisor_info_list_free(struct promisor_info *p)\n>  \t\tnext = p->next;\n>  \t\tfree((char *)p->name);\n>  \t\tfree((char *)p->url);\n> +\t\tfree((char *)p->filter);\n> +\t\tfree((char *)p->token);\n>  \t\tfree(p);\n>  \t}\n>  }\n>  \n> -/* Prepare a 'struct promisor_info' linked list with config information. */\n> -static struct promisor_info *promisor_config_info_list(struct repository *repo)\n> +static void set_one_field(struct promisor_info *p,\n> +\t\t\t  const char *field, const char *value)\n> +{\n> +\tif (!strcasecmp(field, promisor_field_filter))\n> +\t\tp->filter = xstrdup(value);\n> +\telse if (!strcasecmp(field, promisor_field_token))\n> +\t\tp->token = xstrdup(value);\n> +\telse\n> +\t\tBUG(\"Invalid field '%s'\", field);\n\ns/Invalid/invalid/\n\nPatrick\n"},{"id":"518996","messageId":"xmqqldqiqdfp.fsf@gitster.g","threadId":"63293","inReplyTo":"aDVu4JmTU7Wz32Z7@pks.im","subject":"Re: [PATCH v2 2/3] promisor-remote: allow a server to advertise more fields","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-05-27T15:30:02Z","receivedAt":"2025-05-27T15:30:06Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> In the current form you need to reflow this whole paragraph every time a\n> new field is supported, and it's easy to miss the exact supported\n> fields. So my idea was to maybe move the supported fields into a\n> bulleted list. E.g.:\n>\n>     promisor.sendFields::\n>          A comma or space separated list of additional remote related\n>          fields that a server will send while advertising its promisor\n>          remotes using the \"promisor-remote\" capability, see\n>          linkgit:gitprotocol-v2[5]. The following fields are supported:\n>     +\n>     * \"partialCloneFilter\": contains the partial clone filter used for\n>       the remote.\n>     * \"token\": contains the authentication token for the remote.\n\nExcellent.\n\n> I guess this is mostly because the config documentation talks about\n> \"fields\", but that term is used elsewhere to indicate a name-value pair.\n\n... which suggests the use of that word needs to be tightened a bit\nto save the next person from getting confused just like you did,\nperhaps?\n\n>> Also we can extend this setting to be per promisor remote later if\n>> there is a need for it. I don't think it would be difficult to do. And\n>> I don't think it's necessary right now, because it's likely that for\n>> simplicity most servers will manage all their promisor remotes in the\n>> same way (at least until usage of promisor remotes grows a lot).\n>\n> Yeah, with my fixed understanding I agree that it's not necessary to\n> configure this per remote as of now.\n\nAs long as we can extend later, starting small is always good.  We\ncan do \"promisor.<name>.sendFields\" later, for example.\n"},{"id":"520091","messageId":"20250611134506.2975856-1-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250519141259.3061550-1-christian.couder@gmail.com","subject":"[PATCH v4 0/5] Make the \"promisor-remote\" capability support more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-11T13:45:01Z","receivedAt":"2025-06-11T13:45:25Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"The \"promisor-remote\" capability can only be used to pass the names\nand URLs of the promisor remotes from the server to the client. After\nthat the client can use this information to decide if it accepts the\nremotes or not.\n\nIt would be nice if the server could pass more fields about its\nremotes and if the client could use that additional information to\ndecide about the remotes by comparing it with its local information\nabout the remotes.\n\nThis patch series implements this by adding the \"promisor.sendFields\"\non the server side and the \"promisor.checkFields\" on the client side.\n\nFor example, if \"promisor.sendFields\" is set to \"partialCloneFilter\",\nand the server has the remote \"foo\" configured like this:\n\n[remote \"foo\"]\n        url = file:///tmp/foo.git\n\tpartialCloneFilter = blob:none\n\nthen \"name=foo,url=file:///tmp/foo.git,partialCloneFilter=blob:none\"\nwill be sent by the server for this remote.\n\nAll the information passed through the \"promisor-remote\" capability is\nstill only used to decide if the remotes are accepted or not. The\nclient doesn't store it and doesn't use it for any other purpose.\n\nNote that the filter mechanism already exists for a long time and this\nseries doesn't change how it works. For example, it has already been\npossible for a long time to have different repos using the same\npromisor remote with different filters. See the existing partial clone\ndocumentation (like \"Documentation/technical/partial-clone.adoc\") for\nmore information on partial clone.\n\nThe fields that can be passed are limited to \"partialCloneFilter\" and\n\"token\".\n\nOn the technical side, we get rid of 'struct strvec' and we use\n'struct promisor_info' to store the data and 'struct string_list' to\nstore the 'struct promisor_info' instances instead. This matches the\nlatest suggestion from Junio.\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\nChanges since v3\n----------------\n\nThanks to Patrick, Junio, Karthik and Justin for their comments on the\nprevious versions.\n\n- A big change affecting all the patches is that 'struct string_list'\n  is used to store the 'struct promisor_info' instances which don't\n  contain a 'next' pointer anymore. The items in 'struct string_list'\n  have their 'string' pointer point to the remote name and their\n  'util' pointer point to a 'struct promisor_info' instance. The\n  'struct string_list' is sorted when useful.\n\n- Another global change is that \"field name\" is used instead of\n  \"fields\" where it makes sense.\n\n- In patch 1/5, the error message in a BUG() has been improved a bit.\n\n- In patch 2/5, bullet points are used to improve the doc of the new\n  \"promisor.sendFields\" config variable.\n\n- In patch 2/5, a call to string_list_remove_empty_items() has been\n  added to avoid issues with empty fields.\n\n- In patch 2/5, test_config is used instead of removing some config\n  with `test_when_finished`.\n\n- In patch 4/5, test_config is also used instead of removing some\n  config with `test_when_finished`.\n\nCI tests\n--------\n\nHere they are:\n\nhttps://github.com/chriscool/git/actions/runs/15585655648\n\nThey have all passed.\n\nRange diff compared to v3\n-------------------------\n\n1:  12e753a358 ! 1:  8a4df71d2c promisor-remote: refactor to get rid of 'struct strvec'\n    @@ Commit message\n         with one 'struct strvec' for each field like \"name\" or \"url\" does not\n         scale easily in that case.\n     \n    -    Let's refactor this and introduce a new 'struct promisor_info' linked\n    -    list which for now only contains a 'next' pointer, a 'name' member for\n    -    the promisor remote name and an 'url' member for its URL.\n    +    Let's refactor this and introduce a new 'struct promisor_info'.\n    +\n    +    It will only store promisor remote information in its members. For now\n    +    it has only a 'name' member for the promisor remote name and an 'url'\n    +    member for its URL. We will use use a 'struct string_list' to store\n    +    the instances of 'struct promisor_info'. For each 'item' in the\n    +    string_list, 'item->string' will point to the promisor remote name and\n    +    'item->util' will point to the corresponding 'struct promisor_info'\n    +    instance.\n     \n         Explicit members are used within 'struct promisor_info' for type\n         safety and clarity regarding the specific information being handled,\n    @@ promisor-remote.c: static int allow_unsanitized(char ch)\n     -\t\t\t       struct strvec *names,\n     -\t\t\t       struct strvec *urls)\n     +/*\n    -+ * Linked list for promisor remotes involved in the \"promisor-remote\"\n    ++ * Struct for promisor remotes involved in the \"promisor-remote\"\n     + * protocol capability.\n     + *\n    -+ * Except for \"next\" and \"name\", each <member> in this struct and its\n    -+ * <value> should correspond to a \"remote.<name>.<member>\" config\n    -+ * variable set to <value> where \"<name>\" is a promisor remote name.\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     + */\n     +struct promisor_info {\n    -+\tstruct promisor_info *next;\n     +\tconst char *name;\n     +\tconst char *url;\n     +};\n     +\n    -+static void promisor_info_list_free(struct promisor_info *p)\n    ++static void promisor_info_list_clear(struct string_list *list)\n     +{\n    -+\tstruct promisor_info *next;\n    -+\n    -+\tfor (; p; p = next) {\n    -+\t\tnext = p->next;\n    ++\tfor (size_t i = 0; i < list->nr; i++) {\n    ++\t\tstruct promisor_info *p = list->items[i].util;\n     +\t\tfree((char *)p->name);\n     +\t\tfree((char *)p->url);\n    -+\t\tfree(p);\n     +\t}\n    ++\tstring_list_clear(list, 1);\n     +}\n     +\n    -+/* Prepare a 'struct promisor_info' linked list with config information. */\n    -+static struct promisor_info *promisor_config_info_list(struct repository *repo)\n    ++/*\n    ++ * Populate 'list' with promisor remote information from the config.\n    ++ * The 'util' pointer of each list item will hold a 'struct promisor_info'.\n    ++ */\n    ++static void promisor_config_info_list(struct repository *repo, struct string_list *list)\n      {\n    -+\tstruct promisor_info *infos = NULL;\n    -+\tstruct promisor_info **last_info = &infos;\n      \tstruct promisor_remote *r;\n      \n    - \tpromisor_remote_init(repo);\n     @@ promisor-remote.c: static void promisor_info_vecs(struct repository *repo,\n      \n      \t\t/* Only add remotes with a non empty URL */\n    @@ promisor-remote.c: static void promisor_info_vecs(struct repository *repo,\n     -\t\t\tstrvec_push(names, r->name);\n     -\t\t\tstrvec_push(urls, url);\n     +\t\t\tstruct promisor_info *new_info = xcalloc(1, sizeof(*new_info));\n    ++\t\t\tstruct string_list_item *item;\n     +\n     +\t\t\tnew_info->name = xstrdup(r->name);\n     +\t\t\tnew_info->url = xstrdup(url);\n     +\n    -+\t\t\t*last_info = new_info;\n    -+\t\t\tlast_info = &new_info->next;\n    ++\t\t\titem = string_list_append(list, new_info->name);\n    ++\t\t\titem->util = new_info;\n      \t\t}\n      \n      \t\tfree(url_key);\n    - \t}\n    -+\n    -+\treturn infos;\n    - }\n    - \n    - char *promisor_remote_info(struct repository *repo)\n    +@@ promisor-remote.c: char *promisor_remote_info(struct repository *repo)\n      {\n      \tstruct strbuf sb = STRBUF_INIT;\n      \tint advertise_promisors = 0;\n     -\tstruct strvec names = STRVEC_INIT;\n     -\tstruct strvec urls = STRVEC_INIT;\n    -+\tstruct promisor_info *config_info;\n    -+\tstruct promisor_info *p;\n    ++\tstruct string_list config_info = STRING_LIST_INIT_NODUP;\n    ++\tstruct string_list_item *item;\n      \n      \tgit_config_get_bool(\"promisor.advertise\", &advertise_promisors);\n      \n    @@ promisor-remote.c: static void promisor_info_vecs(struct repository *repo,\n      \t\treturn NULL;\n      \n     -\tpromisor_info_vecs(repo, &names, &urls);\n    -+\tconfig_info = promisor_config_info_list(repo);\n    ++\tpromisor_config_info_list(repo, &config_info);\n      \n     -\tif (!names.nr)\n    -+\tif (!config_info)\n    ++\tif (!config_info.nr)\n      \t\treturn NULL;\n      \n     -\tfor (size_t i = 0; i < names.nr; i++) {\n     -\t\tif (i)\n    -+\tfor (p = config_info; p; p = p->next) {\n    -+\t\tif (p != config_info)\n    ++\tfor_each_string_list_item(item, &config_info) {\n    ++\t\tstruct promisor_info *p = item->util;\n    ++\n    ++\t\tif (item != config_info.items)\n      \t\t\tstrbuf_addch(&sb, ';');\n     +\n      \t\tstrbuf_addstr(&sb, \"name=\");\n    @@ promisor-remote.c: static void promisor_info_vecs(struct repository *repo,\n      \n     -\tstrvec_clear(&names);\n     -\tstrvec_clear(&urls);\n    -+\tpromisor_info_list_free(config_info);\n    ++\tpromisor_info_list_clear(&config_info);\n      \n      \treturn strbuf_detach(&sb, NULL);\n      }\n      \n    - /*\n    +-/*\n     - * Find first index of 'nicks' where there is 'nick'. 'nick' is\n     - * compared case sensitively to the strings in 'nicks'. If not found\n     - * 'nicks->nr' is returned.\n    -+ * Find first element of 'p' where the 'name' member is 'nick'. 'nick'\n    -+ * is compared case sensitively to the strings in 'p'. If not found\n    -+ * NULL is returned.\n    -  */\n    +- */\n     -static size_t remote_nick_find(struct strvec *nicks, const char *nick)\n    -+static struct promisor_info *remote_nick_find(struct promisor_info *p, const char *nick)\n    - {\n    +-{\n     -\tfor (size_t i = 0; i < nicks->nr; i++)\n     -\t\tif (!strcmp(nicks->v[i], nick))\n     -\t\t\treturn i;\n     -\treturn nicks->nr;\n    -+\tfor (; p; p = p->next) {\n    -+\t\tif (!strcmp(p->name, nick))\n    -+\t\t\treturn p;\n    -+\t}\n    -+\treturn NULL;\n    - }\n    - \n    +-}\n    +-\n      enum accept_promisor {\n    + \tACCEPT_NONE = 0,\n    + \tACCEPT_KNOWN_URL,\n     @@ promisor-remote.c: enum accept_promisor {\n      \n      static int should_accept_remote(enum accept_promisor accept,\n      \t\t\t\tconst char *remote_name, const char *remote_url,\n     -\t\t\t\tstruct strvec *names, struct strvec *urls)\n    -+\t\t\t\tstruct promisor_info *config_info)\n    ++\t\t\t\tstruct string_list *config_info)\n      {\n     -\tsize_t i;\n     +\tstruct promisor_info *p;\n    ++\tstruct string_list_item *item;\n      \n      \tif (accept == ACCEPT_ALL)\n      \t\treturn 1;\n      \n     -\ti = remote_nick_find(names, remote_name);\n    -+\tp = remote_nick_find(config_info, remote_name);\n    ++\t/* Get config info for that promisor remote */\n    ++\titem = string_list_lookup(config_info, remote_name);\n      \n     -\tif (i >= names->nr)\n    -+\tif (!p)\n    ++\tif (!item)\n      \t\t/* We don't know about that remote */\n      \t\treturn 0;\n      \n    ++\tp = item->util;\n    ++\n    + \tif (accept == ACCEPT_KNOWN_NAME)\n    + \t\treturn 1;\n    + \n     @@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n      \t\treturn 0;\n      \t}\n      \n     -\tif (!strcmp(urls->v[i], remote_url))\n     +\tif (!p->url)\n    -+\t\tBUG(\"Bad config_info (invalid URL) for remote '%s'.\\n\",\n    ++\t\tBUG(\"bad config_info (invalid URL) for remote '%s'\",\n     +\t\t    remote_name);\n     +\n     +\tif (!strcmp(p->url, remote_url))\n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n      \tenum accept_promisor accept = ACCEPT_NONE;\n     -\tstruct strvec names = STRVEC_INIT;\n     -\tstruct strvec urls = STRVEC_INIT;\n    -+\tstruct promisor_info *config_info = NULL;\n    ++\tstruct string_list config_info = STRING_LIST_INIT_NODUP;\n      \n      \tif (!git_config_get_string_tmp(\"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    + \tif (accept == ACCEPT_NONE)\n      \t\treturn;\n      \n    - \tif (accept != ACCEPT_ALL)\n    +-\tif (accept != ACCEPT_ALL)\n     -\t\tpromisor_info_vecs(repo, &names, &urls);\n    -+\t\tconfig_info = promisor_config_info_list(repo);\n    ++\tif (accept != ACCEPT_ALL) {\n    ++\t\tpromisor_config_info_list(repo, &config_info);\n    ++\t\tstring_list_sort(&config_info);\n    ++\t}\n      \n      \t/* Parse remote info received */\n      \n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n      \t\t\tdecoded_url = url_percent_decode(remote_url);\n      \n     -\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, &names, &urls))\n    -+\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, config_info))\n    ++\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, &config_info))\n      \t\t\tstrvec_push(accepted, decoded_name);\n      \n      \t\tstrbuf_list_free(elems);\n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n      \n     -\tstrvec_clear(&names);\n     -\tstrvec_clear(&urls);\n    -+\tpromisor_info_list_free(config_info);\n    ++\tpromisor_info_list_clear(&config_info);\n      \tstrbuf_list_free(remotes);\n      }\n      \n2:  29c6104827 ! 2:  8f3111b4f2 promisor-remote: allow a server to advertise more fields\n    @@ Commit message\n     \n         Let's make it possible to pass more information by introducing a new\n         \"promisor.sendFields\" configuration variable. This variable should\n    -    contain a comma or space separated list of fields that will be looked\n    -    up in the configuration of the remote on the server to find the values\n    -    that will be passed to the client.\n    +    contain a comma or space separated list of field names that will be\n    +    looked up in the configuration of the remote on the server to find the\n    +    values that will be passed to the client.\n     \n         Only a set of predefined fields are allowed. The only fields in this\n         set are \"partialCloneFilter\" and \"token\". The \"partialCloneFilter\"\n    @@ Commit message\n         and the \"token\" field can provide an authentication credential for\n         accessing it.\n     \n    -    For example if \"promisor.sendFields\" is set to \"partialCloneFilter\",\n    +    For example, if \"promisor.sendFields\" is set to \"partialCloneFilter\",\n         and the server has the \"remote.<name>.partialCloneFilter\" config\n         variable set to a value for a remote, then that value will be passed\n         in the form \"partialCloneFilter=<value>\" after the \"name\" and \"url\"\n    @@ Documentation/config/promisor.adoc: promisor.advertise::\n      \n     +promisor.sendFields::\n     +\tA comma or space separated list of additional remote related\n    -+\tfields that a server will send while advertising its promisor\n    -+\tremotes using the \"promisor-remote\" capability, see\n    -+\tlinkgit:gitprotocol-v2[5]. Currently, only the\n    -+\t\"partialCloneFilter\" and \"token\" fields are supported. The\n    -+\t\"partialCloneFilter\" field contains the partial clone filter\n    -+\tused for the remote, and the \"token\" field contains an\n    -+\tauthentication token for the remote.\n    ++\tfield names. A server will send these field names and the\n    ++\tassociated field values from its configuration when\n    ++\tadvertising its promisor remotes using the \"promisor-remote\"\n    ++\tcapability, see linkgit:gitprotocol-v2[5]. Currently, only the\n    ++\t\"partialCloneFilter\" and \"token\" field names are supported.\n     ++\n    -+When a field is part of this list and a corresponding\n    -+\"remote.foo.<field>\" config variable is set on the server to a\n    -+non-empty value, then the field and its value will be sent when\n    -+advertising the promisor remote \"foo\". This list has no effect unless\n    -+the \"promisor.advertise\" config variable is set to \"true\", and the\n    -+\"name\" and \"url\" fields are always advertised regardless of this\n    -+setting.\n    ++* \"partialCloneFilter\": contains the partial clone filter\n    ++  used for the remote.\n    +++\n    ++* \"token\": contains an authentication token for the remote.\n    +++\n    ++When a field name is part of this list and a corresponding\n    ++\"remote.foo.<field name>\" config variable is set on the server to a\n    ++non-empty value, then the field name and value will be sent when\n    ++advertising the promisor remote \"foo\".\n    +++\n    ++This list has no effect unless the \"promisor.advertise\" config\n    ++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    @@ promisor-remote.c: static int allow_unsanitized(char ch)\n     +\n     +\tif (!git_config_get_string(config_key, &fields) && *fields) {\n     +\t\tstring_list_split_in_place(fields_list, fields, \", \", -1);\n    ++\t\tstring_list_remove_empty_items(fields_list, 0);\n     +\t\tfilter_string_list(fields_list, 0, is_valid_field, (void *)config_key);\n     +\t}\n     +\n    @@ promisor-remote.c: static int allow_unsanitized(char ch)\n     +}\n     +\n      /*\n    -  * Linked list for promisor remotes involved in the \"promisor-remote\"\n    +  * Struct for promisor remotes involved in the \"promisor-remote\"\n       * protocol capability.\n    -@@ promisor-remote.c: struct promisor_info {\n    - \tstruct promisor_info *next;\n    +@@ promisor-remote.c: static int allow_unsanitized(char ch)\n    + struct promisor_info {\n      \tconst char *name;\n      \tconst char *url;\n     +\tconst char *filter;\n     +\tconst char *token;\n      };\n      \n    - static void promisor_info_list_free(struct promisor_info *p)\n    -@@ promisor-remote.c: static void promisor_info_list_free(struct promisor_info *p)\n    - \t\tnext = p->next;\n    + static void promisor_info_list_clear(struct string_list *list)\n    +@@ promisor-remote.c: static void promisor_info_list_clear(struct string_list *list)\n    + \t\tstruct promisor_info *p = list->items[i].util;\n      \t\tfree((char *)p->name);\n      \t\tfree((char *)p->url);\n     +\t\tfree((char *)p->filter);\n     +\t\tfree((char *)p->token);\n    - \t\tfree(p);\n      \t}\n    + \tstring_list_clear(list, 1);\n      }\n      \n    --/* Prepare a 'struct promisor_info' linked list with config information. */\n    --static struct promisor_info *promisor_config_info_list(struct repository *repo)\n     +static void set_one_field(struct promisor_info *p,\n     +\t\t\t  const char *field, const char *value)\n     +{\n    @@ promisor-remote.c: static void promisor_info_list_free(struct promisor_info *p)\n     +\telse if (!strcasecmp(field, promisor_field_token))\n     +\t\tp->token = xstrdup(value);\n     +\telse\n    -+\t\tBUG(\"Invalid field '%s'\", field);\n    ++\t\tBUG(\"invalid field '%s'\", field);\n     +}\n     +\n     +static void set_fields(struct promisor_info *p,\n    @@ promisor-remote.c: static void promisor_info_list_free(struct promisor_info *p)\n     +\t}\n     +}\n     +\n    -+/*\n    -+ * Prepare a 'struct promisor_info' linked list of promisor remotes\n    -+ * with config information. Only members of that struct specified by\n    -+ * the 'field_names' linked list are set (using values from the\n    -+ * configuration).\n    -+ */\n    -+static struct promisor_info *promisor_config_info_list(struct repository *repo,\n    -+\t\t\t\t\t\t       struct string_list *field_names)\n    + /*\n    +  * Populate 'list' with promisor remote information from the config.\n    +- * The 'util' pointer of each list item will hold a 'struct promisor_info'.\n    ++ * The 'util' pointer of each list item will hold a 'struct\n    ++ * promisor_info'. Except \"name\" and \"url\", only members of that\n    ++ * struct specified by the 'field_names' list are set (using values\n    ++ * from the configuration).\n    +  */\n    +-static void promisor_config_info_list(struct repository *repo, struct string_list *list)\n    ++static void promisor_config_info_list(struct repository *repo,\n    ++\t\t\t\t      struct string_list *list,\n    ++\t\t\t\t      struct string_list *field_names)\n      {\n    - \tstruct promisor_info *infos = NULL;\n    - \tstruct promisor_info **last_info = &infos;\n    -@@ promisor-remote.c: static struct promisor_info *promisor_config_info_list(struct repository *repo)\n    + \tstruct promisor_remote *r;\n    + \n    +@@ promisor-remote.c: static void promisor_config_info_list(struct repository *repo, struct string_lis\n      \t\t\tnew_info->name = xstrdup(r->name);\n      \t\t\tnew_info->url = xstrdup(url);\n      \n     +\t\t\tif (field_names)\n     +\t\t\t\tset_fields(new_info, field_names);\n     +\n    - \t\t\t*last_info = new_info;\n    - \t\t\tlast_info = &new_info->next;\n    + \t\t\titem = string_list_append(list, new_info->name);\n    + \t\t\titem->util = new_info;\n      \t\t}\n     @@ promisor-remote.c: char *promisor_remote_info(struct repository *repo)\n      \tif (!advertise_promisors)\n      \t\treturn NULL;\n      \n    --\tconfig_info = promisor_config_info_list(repo);\n    -+\tconfig_info = promisor_config_info_list(repo, fields_sent());\n    +-\tpromisor_config_info_list(repo, &config_info);\n    ++\tpromisor_config_info_list(repo, &config_info, fields_sent());\n      \n    - \tif (!config_info)\n    + \tif (!config_info.nr)\n      \t\treturn NULL;\n     @@ promisor-remote.c: char *promisor_remote_info(struct repository *repo)\n      \t\tstrbuf_addstr_urlencode(&sb, p->name, allow_unsanitized);\n    @@ promisor-remote.c: char *promisor_remote_info(struct repository *repo)\n     +\t\t}\n      \t}\n      \n    - \tpromisor_info_list_free(config_info);\n    + \tpromisor_info_list_clear(&config_info);\n     @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n      \t\treturn;\n      \n    - \tif (accept != ACCEPT_ALL)\n    --\t\tconfig_info = promisor_config_info_list(repo);\n    -+\t\tconfig_info = promisor_config_info_list(repo, NULL);\n    - \n    - \t/* Parse remote info received */\n    + \tif (accept != ACCEPT_ALL) {\n    +-\t\tpromisor_config_info_list(repo, &config_info);\n    ++\t\tpromisor_config_info_list(repo, &config_info, NULL);\n    + \t\tstring_list_sort(&config_info);\n    + \t}\n      \n     @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n      \t\telems = strbuf_split(remotes[i], ',');\n    @@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with 'KnownUrl\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    -+\tgit -C server config promisor.sendFields \"partialCloneFilter, token\" &&\n    -+\ttest_when_finished \"git -C server config unset promisor.sendFields\" &&\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\n3:  6d8236bf70 ! 3:  c91a1ba04a promisor-remote: refactor how we parse advertised fields\n    @@ Commit message\n         Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n     \n      ## promisor-remote.c ##\n    +@@ promisor-remote.c: struct promisor_info {\n    + \tconst char *token;\n    + };\n    + \n    ++static void promisor_info_free(struct promisor_info *p)\n    ++{\n    ++\tfree((char *)p->name);\n    ++\tfree((char *)p->url);\n    ++\tfree((char *)p->filter);\n    ++\tfree((char *)p->token);\n    ++\tfree(p);\n    ++}\n    ++\n    + static void promisor_info_list_clear(struct string_list *list)\n    + {\n    +-\tfor (size_t i = 0; i < list->nr; i++) {\n    +-\t\tstruct promisor_info *p = list->items[i].util;\n    +-\t\tfree((char *)p->name);\n    +-\t\tfree((char *)p->url);\n    +-\t\tfree((char *)p->filter);\n    +-\t\tfree((char *)p->token);\n    +-\t}\n    +-\tstring_list_clear(list, 1);\n    ++\tfor (size_t i = 0; i < list->nr; i++)\n    ++\t\tpromisor_info_free(list->items[i].util);\n    ++\tstring_list_clear(list, 0);\n    + }\n    + \n    + static void set_one_field(struct promisor_info *p,\n     @@ promisor-remote.c: enum accept_promisor {\n      };\n      \n      static int should_accept_remote(enum accept_promisor accept,\n     -\t\t\t\tconst char *remote_name, const char *remote_url,\n     +\t\t\t\tstruct promisor_info *advertised,\n    - \t\t\t\tstruct promisor_info *config_info)\n    + \t\t\t\tstruct string_list *config_info)\n      {\n      \tstruct promisor_info *p;\n    + \tstruct string_list_item *item;\n     +\tconst char *remote_name = advertised->name;\n     +\tconst char *remote_url = advertised->url;\n      \n    @@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n     +\tif (!info->name || !info->url) {\n     +\t\twarning(_(\"server advertised a promisor remote without a name or URL: %s\"),\n     +\t\t\tremote_info->buf);\n    -+\t\tpromisor_info_list_free(info);\n    ++\t\tpromisor_info_free(info);\n     +\t\treturn NULL;\n     +\t}\n     +\n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n     +\t\tif (!advertised)\n     +\t\t\tcontinue;\n      \n    --\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, config_info))\n    +-\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, &config_info))\n     -\t\t\tstrvec_push(accepted, decoded_name);\n    -+\t\tif (should_accept_remote(accept, advertised, config_info))\n    ++\t\tif (should_accept_remote(accept, advertised, &config_info))\n     +\t\t\tstrvec_push(accepted, advertised->name);\n      \n     -\t\tstrbuf_list_free(elems);\n     -\t\tfree(decoded_name);\n     -\t\tfree(decoded_url);\n    -+\t\tpromisor_info_list_free(advertised);\n    ++\t\tpromisor_info_free(advertised);\n      \t}\n      \n    - \tpromisor_info_list_free(config_info);\n    + \tpromisor_info_list_clear(&config_info);\n4:  dab5823356 ! 4:  ad68202057 promisor-remote: allow a client to check fields\n    @@ Documentation/config/promisor.adoc: promisor.acceptFromServer::\n     +\n     +promisor.checkFields::\n     +\tA comma or space separated list of additional remote related\n    -+\tfields that a client will check before accepting a promisor\n    -+\tremote. Currently, \"partialCloneFilter\" and \"token\" are the only\n    -+\tsupported field names.\n    ++\tfield names. A client will check if the values of these fields\n    ++\ttransmitted by a server correspond to the values of these\n    ++\tfields in its own configuration before accepting a promisor\n    ++\tremote. Currently, \"partialCloneFilter\" and \"token\" are the\n    ++\tonly supported field names.\n     ++\n     +If one of these field names (e.g., \"token\") is being checked for an\n     +advertised promisor remote (e.g., \"foo\"), three conditions must be met\n    @@ Documentation/config/promisor.adoc: promisor.acceptFromServer::\n     +3. The value of the locally configured `remote.foo.token` must exactly\n     +   match the value advertised by the server for the \"token\" field.\n     ++\n    -+If any of these conditions are not met for any field name listed in\n    ++If any of these conditions is not met for any field name listed in\n     +`promisor.checkFields`, the advertised remote \"foo\" will be rejected.\n     ++\n     +For the \"partialCloneFilter\" field, this allows the client to ensure\n    @@ Documentation/config/promisor.adoc: promisor.acceptFromServer::\n     +be used to verify that authentication credentials match expected\n     +values.\n     ++\n    ++Field names are compared case-insensitively. Field values are compared\n    ++case-sensitively.\n    +++\n     +The \"name\" and \"url\" fields are always checked according to the\n     +`promisor.acceptFromServer` policy, independently of this setting.\n     ++\n    -+The fields should be passed by the server through the\n    ++The field names and values should be passed by the server through the\n     +\"promisor-remote\" capability by using the `promisor.sendFields` config\n     +variable. The fields will be checked only if the\n     +`promisor.acceptFromServer` config variable is not set to \"None\". If\n    @@ promisor-remote.c: static struct string_list *fields_sent(void)\n     +}\n     +\n      /*\n    -  * Linked list for promisor remotes involved in the \"promisor-remote\"\n    +  * Struct for promisor remotes involved in the \"promisor-remote\"\n       * protocol capability.\n     @@ promisor-remote.c: enum accept_promisor {\n      \tACCEPT_ALL\n    @@ promisor-remote.c: enum accept_promisor {\n     +}\n     +\n     +static int all_fields_match(struct promisor_info *advertised,\n    -+\t\t\t    struct promisor_info *config_info,\n    ++\t\t\t    struct string_list *config_info,\n     +\t\t\t    int in_list)\n     +{\n     +\tstruct string_list* fields = fields_checked();\n    @@ promisor-remote.c: enum accept_promisor {\n     +\t\tint match = 0;\n     +\t\tconst char *field = item_checked->string;\n     +\t\tconst char *value = NULL;\n    ++\t\tstruct string_list_item *item;\n     +\n     +\t\tif (!strcasecmp(field, promisor_field_filter))\n     +\t\t\tvalue = advertised->filter;\n    @@ promisor-remote.c: enum accept_promisor {\n     +\t\t\treturn 0;\n     +\n     +\t\tif (in_list) {\n    -+\t\t\tfor (struct promisor_info *p = config_info; p; p = p->next) {\n    ++\t\t\tfor_each_string_list_item(item, config_info) {\n    ++\t\t\t\tstruct promisor_info *p = item->util;\n     +\t\t\t\tif (match_field_against_config(field, value, p)) {\n     +\t\t\t\t\tmatch = 1;\n     +\t\t\t\t\tbreak;\n     +\t\t\t\t}\n     +\t\t\t}\n     +\t\t} else {\n    -+\t\t\tmatch = match_field_against_config(field, value, config_info);\n    ++\t\t\titem = string_list_lookup(config_info, advertised->name);\n    ++\t\t\tif (item) {\n    ++\t\t\t\tstruct promisor_info *p = item->util;\n    ++\t\t\t\tmatch = match_field_against_config(field, value, p);\n    ++\t\t\t}\n     +\t\t}\n     +\n     +\t\tif (!match)\n    @@ promisor-remote.c: enum accept_promisor {\n     +\n      static int should_accept_remote(enum accept_promisor accept,\n      \t\t\t\tstruct promisor_info *advertised,\n    - \t\t\t\tstruct promisor_info *config_info)\n    + \t\t\t\tstruct string_list *config_info)\n     @@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n      \tconst char *remote_url = advertised->url;\n      \n    @@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n     -\t\treturn 1;\n     +\t\treturn all_fields_match(advertised, config_info, 1);\n      \n    - \tp = remote_nick_find(config_info, remote_name);\n    - \n    + \t/* Get config info for that promisor remote */\n    + \titem = string_list_lookup(config_info, remote_name);\n     @@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n    - \t\treturn 0;\n    + \tp = item->util;\n      \n      \tif (accept == ACCEPT_KNOWN_NAME)\n     -\t\treturn 1;\n    -+\t\treturn all_fields_match(advertised, p, 0);\n    ++\t\treturn all_fields_match(advertised, config_info, 0);\n      \n      \tif (accept != ACCEPT_KNOWN_URL)\n      \t\tBUG(\"Unhandled 'enum accept_promisor' value '%d'\", accept);\n    @@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n      \n      \tif (!strcmp(p->url, remote_url))\n     -\t\treturn 1;\n    -+\t\treturn all_fields_match(advertised, p, 0);\n    ++\t\treturn all_fields_match(advertised, config_info, 0);\n      \n      \twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s'\"),\n      \t\tremote_name, p->url, remote_url);\n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n      \tif (accept == ACCEPT_NONE)\n      \t\treturn;\n      \n    --\tif (accept != ACCEPT_ALL)\n    --\t\tconfig_info = promisor_config_info_list(repo, NULL);\n    +-\tif (accept != ACCEPT_ALL) {\n    +-\t\tpromisor_config_info_list(repo, &config_info, NULL);\n    +-\t\tstring_list_sort(&config_info);\n    +-\t}\n     -\n      \t/* Parse remote info received */\n      \n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n      \t\tif (!advertised)\n      \t\t\tcontinue;\n      \n    -+\t\tif (!config_info)\n    -+\t\t\tconfig_info = promisor_config_info_list(repo, fields_checked());\n    ++\t\tif (!config_info.nr) {\n    ++\t\t\tpromisor_config_info_list(repo, &config_info, fields_checked());\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\tstrvec_push(accepted, advertised->name);\n      \n     \n    @@ t/t5710-promisor-remote-capability.sh: test_expect_success \"clone with promisor.\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    -+\tgit -C server config promisor.sendFields \"partialCloneFilter, token\" &&\n    -+\ttest_when_finished \"git -C server config unset promisor.sendFields\" &&\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\n5:  9bd612cdb8 ! 5:  e8efe62b7f promisor-remote: use string constants for 'name' and 'url' too\n    @@ promisor-remote.c: static int allow_unsanitized(char ch)\n      static const char promisor_field_token[] = \"token\";\n      \n     @@ promisor-remote.c: char *promisor_remote_info(struct repository *repo)\n    - \t\tif (p != config_info)\n    + \t\tif (item != config_info.items)\n      \t\t\tstrbuf_addch(&sb, ';');\n      \n     -\t\tstrbuf_addstr(&sb, \"name=\");\n\n\nChristian Couder (5):\n  promisor-remote: refactor to get rid of 'struct strvec'\n  promisor-remote: allow a server to advertise more fields\n  promisor-remote: refactor how we parse advertised fields\n  promisor-remote: allow a client to check fields\n  promisor-remote: use string constants for 'name' and 'url' too\n\n Documentation/config/promisor.adoc    |  62 ++++\n Documentation/gitprotocol-v2.adoc     |  59 ++--\n promisor-remote.c                     | 395 +++++++++++++++++++++-----\n t/t5710-promisor-remote-capability.sh |  65 +++++\n 4 files changed, 493 insertions(+), 88 deletions(-)\n\n-- \n2.50.0.rc2.5.ge8efe62b7f\n\n"},{"id":"520090","messageId":"20250611134506.2975856-2-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250611134506.2975856-1-christian.couder@gmail.com","subject":"[PATCH v4 1/5] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-11T13:45:02Z","receivedAt":"2025-06-11T13:45:26Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"In a following commit, we will use the new 'promisor-remote' protocol\ncapability introduced by d460267613 (Add 'promisor-remote' capability\nto protocol v2, 2025-02-18) to pass and process more information\nabout promisor remotes than just their name and url.\n\nFor that purpose, we will need to store information about other\nfields, especially information that might or might not be available\nfor different promisor remotes. Unfortunately using 'struct strvec',\nas we currently do, to store information about the promisor remotes\nwith one 'struct strvec' for each field like \"name\" or \"url\" does not\nscale easily in that case.\n\nLet's refactor this and introduce a new 'struct promisor_info'.\n\nIt will only store promisor remote information in its members. For now\nit has only a 'name' member for the promisor remote name and an 'url'\nmember for its URL. We will use use a 'struct string_list' to store\nthe instances of 'struct promisor_info'. For each 'item' in the\nstring_list, 'item->string' will point to the promisor remote name and\n'item->util' will point to the corresponding 'struct promisor_info'\ninstance.\n\nExplicit members are used within 'struct promisor_info' for type\nsafety and clarity regarding the specific information being handled,\nrather than a generic key-value store. We want to specify and document\neach field and its content, so adding new members to the struct as\nmore fields are supported is fine.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 111 +++++++++++++++++++++++++++++-----------------\n 1 file changed, 70 insertions(+), 41 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 9d058586df..90a063ea53 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -314,9 +314,35 @@ static int allow_unsanitized(char ch)\n \treturn ch > 32 && ch < 127;\n }\n \n-static void promisor_info_vecs(struct repository *repo,\n-\t\t\t       struct strvec *names,\n-\t\t\t       struct strvec *urls)\n+/*\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+ */\n+struct promisor_info {\n+\tconst char *name;\n+\tconst char *url;\n+};\n+\n+static void promisor_info_list_clear(struct string_list *list)\n+{\n+\tfor (size_t i = 0; i < list->nr; i++) {\n+\t\tstruct promisor_info *p = list->items[i].util;\n+\t\tfree((char *)p->name);\n+\t\tfree((char *)p->url);\n+\t}\n+\tstring_list_clear(list, 1);\n+}\n+\n+/*\n+ * Populate 'list' with promisor remote information from the config.\n+ * The 'util' pointer of each list item will hold a 'struct promisor_info'.\n+ */\n+static void promisor_config_info_list(struct repository *repo, struct string_list *list)\n {\n \tstruct promisor_remote *r;\n \n@@ -328,8 +354,14 @@ static void promisor_info_vecs(struct repository *repo,\n \n \t\t/* Only add remotes with a non empty URL */\n \t\tif (!git_config_get_string_tmp(url_key, &url) && *url) {\n-\t\t\tstrvec_push(names, r->name);\n-\t\t\tstrvec_push(urls, url);\n+\t\t\tstruct promisor_info *new_info = xcalloc(1, sizeof(*new_info));\n+\t\t\tstruct string_list_item *item;\n+\n+\t\t\tnew_info->name = xstrdup(r->name);\n+\t\t\tnew_info->url = xstrdup(url);\n+\n+\t\t\titem = string_list_append(list, new_info->name);\n+\t\t\titem->util = new_info;\n \t\t}\n \n \t\tfree(url_key);\n@@ -340,47 +372,36 @@ char *promisor_remote_info(struct repository *repo)\n {\n \tstruct strbuf sb = STRBUF_INIT;\n \tint advertise_promisors = 0;\n-\tstruct strvec names = STRVEC_INIT;\n-\tstruct strvec urls = STRVEC_INIT;\n+\tstruct string_list config_info = STRING_LIST_INIT_NODUP;\n+\tstruct string_list_item *item;\n \n \tgit_config_get_bool(\"promisor.advertise\", &advertise_promisors);\n \n \tif (!advertise_promisors)\n \t\treturn NULL;\n \n-\tpromisor_info_vecs(repo, &names, &urls);\n+\tpromisor_config_info_list(repo, &config_info);\n \n-\tif (!names.nr)\n+\tif (!config_info.nr)\n \t\treturn NULL;\n \n-\tfor (size_t i = 0; i < names.nr; i++) {\n-\t\tif (i)\n+\tfor_each_string_list_item(item, &config_info) {\n+\t\tstruct promisor_info *p = item->util;\n+\n+\t\tif (item != config_info.items)\n \t\t\tstrbuf_addch(&sb, ';');\n+\n \t\tstrbuf_addstr(&sb, \"name=\");\n-\t\tstrbuf_addstr_urlencode(&sb, names.v[i], allow_unsanitized);\n+\t\tstrbuf_addstr_urlencode(&sb, p->name, allow_unsanitized);\n \t\tstrbuf_addstr(&sb, \",url=\");\n-\t\tstrbuf_addstr_urlencode(&sb, urls.v[i], allow_unsanitized);\n+\t\tstrbuf_addstr_urlencode(&sb, p->url, allow_unsanitized);\n \t}\n \n-\tstrvec_clear(&names);\n-\tstrvec_clear(&urls);\n+\tpromisor_info_list_clear(&config_info);\n \n \treturn strbuf_detach(&sb, NULL);\n }\n \n-/*\n- * Find first index of 'nicks' where there is 'nick'. 'nick' is\n- * compared case sensitively to the strings in 'nicks'. If not found\n- * 'nicks->nr' is returned.\n- */\n-static size_t remote_nick_find(struct strvec *nicks, const char *nick)\n-{\n-\tfor (size_t i = 0; i < nicks->nr; i++)\n-\t\tif (!strcmp(nicks->v[i], nick))\n-\t\t\treturn i;\n-\treturn nicks->nr;\n-}\n-\n enum accept_promisor {\n \tACCEPT_NONE = 0,\n \tACCEPT_KNOWN_URL,\n@@ -390,19 +411,23 @@ enum accept_promisor {\n \n static int should_accept_remote(enum accept_promisor accept,\n \t\t\t\tconst char *remote_name, const char *remote_url,\n-\t\t\t\tstruct strvec *names, struct strvec *urls)\n+\t\t\t\tstruct string_list *config_info)\n {\n-\tsize_t i;\n+\tstruct promisor_info *p;\n+\tstruct string_list_item *item;\n \n \tif (accept == ACCEPT_ALL)\n \t\treturn 1;\n \n-\ti = remote_nick_find(names, remote_name);\n+\t/* Get config info for that promisor remote */\n+\titem = string_list_lookup(config_info, remote_name);\n \n-\tif (i >= names->nr)\n+\tif (!item)\n \t\t/* We don't know about that remote */\n \t\treturn 0;\n \n+\tp = item->util;\n+\n \tif (accept == ACCEPT_KNOWN_NAME)\n \t\treturn 1;\n \n@@ -414,11 +439,15 @@ static int should_accept_remote(enum accept_promisor accept,\n \t\treturn 0;\n \t}\n \n-\tif (!strcmp(urls->v[i], remote_url))\n+\tif (!p->url)\n+\t\tBUG(\"bad config_info (invalid URL) for remote '%s'\",\n+\t\t    remote_name);\n+\n+\tif (!strcmp(p->url, remote_url))\n \t\treturn 1;\n \n \twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s'\"),\n-\t\tremote_name, urls->v[i], remote_url);\n+\t\tremote_name, p->url, remote_url);\n \n \treturn 0;\n }\n@@ -430,8 +459,7 @@ static void filter_promisor_remote(struct repository *repo,\n \tstruct strbuf **remotes;\n \tconst char *accept_str;\n \tenum accept_promisor accept = ACCEPT_NONE;\n-\tstruct strvec names = STRVEC_INIT;\n-\tstruct strvec urls = STRVEC_INIT;\n+\tstruct string_list config_info = STRING_LIST_INIT_NODUP;\n \n \tif (!git_config_get_string_tmp(\"promisor.acceptfromserver\", &accept_str)) {\n \t\tif (!*accept_str || !strcasecmp(\"None\", accept_str))\n@@ -450,8 +478,10 @@ static void filter_promisor_remote(struct repository *repo,\n \tif (accept == ACCEPT_NONE)\n \t\treturn;\n \n-\tif (accept != ACCEPT_ALL)\n-\t\tpromisor_info_vecs(repo, &names, &urls);\n+\tif (accept != ACCEPT_ALL) {\n+\t\tpromisor_config_info_list(repo, &config_info);\n+\t\tstring_list_sort(&config_info);\n+\t}\n \n \t/* Parse remote info received */\n \n@@ -482,7 +512,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tif (remote_url)\n \t\t\tdecoded_url = url_percent_decode(remote_url);\n \n-\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, &names, &urls))\n+\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, &config_info))\n \t\t\tstrvec_push(accepted, decoded_name);\n \n \t\tstrbuf_list_free(elems);\n@@ -490,8 +520,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tfree(decoded_url);\n \t}\n \n-\tstrvec_clear(&names);\n-\tstrvec_clear(&urls);\n+\tpromisor_info_list_clear(&config_info);\n \tstrbuf_list_free(remotes);\n }\n \n-- \n2.50.0.rc2.5.ge8efe62b7f\n\n"},{"id":"520092","messageId":"20250611134506.2975856-3-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250611134506.2975856-1-christian.couder@gmail.com","subject":"[PATCH v4 2/5] promisor-remote: allow a server to advertise more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-11T13:45:03Z","receivedAt":"2025-06-11T13:45:27Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"For now the \"promisor-remote\" protocol capability can only pass \"name\"\nand \"url\" information from a server to a client in the form\n\"name=<remote_name>,url=<remote_url>\".\n\nLet's make it possible to pass more information by introducing a new\n\"promisor.sendFields\" configuration variable. This variable should\ncontain a comma or space separated list of field names that will be\nlooked up in the configuration of the remote on the server to find the\nvalues that will be passed to the client.\n\nOnly a set of predefined fields are allowed. The only fields in this\nset are \"partialCloneFilter\" and \"token\". The \"partialCloneFilter\"\nfield specifies the filter definition used by the promisor remote,\nand the \"token\" field can provide an authentication credential for\naccessing it.\n\nFor example, if \"promisor.sendFields\" is set to \"partialCloneFilter\",\nand the server has the \"remote.<name>.partialCloneFilter\" config\nvariable set to a value for a remote, then that value will be passed\nin the form \"partialCloneFilter=<value>\" after the \"name\" and \"url\"\nfields.\n\nA following commit will allow the client to use the information to\ndecide if it accepts the remote or not. For now the client doesn't do\nanything with the additional information it receives.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    |  22 +++++\n Documentation/gitprotocol-v2.adoc     |  59 ++++++++----\n promisor-remote.c                     | 128 ++++++++++++++++++++++++--\n t/t5710-promisor-remote-capability.sh |  31 +++++++\n 4 files changed, 215 insertions(+), 25 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex 2638b01f83..beb8f65518 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -9,6 +9,28 @@ promisor.advertise::\n \t\"false\", which means the \"promisor-remote\" capability is not\n \tadvertised.\n \n+promisor.sendFields::\n+\tA comma or space separated list of additional remote related\n+\tfield names. A server will send these field names and the\n+\tassociated field values from its configuration when\n+\tadvertising its promisor remotes using the \"promisor-remote\"\n+\tcapability, see linkgit:gitprotocol-v2[5]. Currently, only the\n+\t\"partialCloneFilter\" and \"token\" field names are supported.\n++\n+* \"partialCloneFilter\": contains the partial clone filter\n+  used for the remote.\n++\n+* \"token\": contains an authentication token for the remote.\n++\n+When a field name is part of this list and a corresponding\n+\"remote.foo.<field name>\" config variable is set on the server to a\n+non-empty value, then the field name and value will be sent when\n+advertising the promisor remote \"foo\".\n++\n+This list has no effect unless the \"promisor.advertise\" config\n+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\"\ndiff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\nindex 5598c93e67..3c12c158fd 100644\n--- a/Documentation/gitprotocol-v2.adoc\n+++ b/Documentation/gitprotocol-v2.adoc\n@@ -785,33 +785,59 @@ retrieving the header from a bundle at the indicated URI, and thus\n save themselves and the server(s) the request(s) needed to inspect the\n headers of that bundle or bundles.\n \n-promisor-remote=<pr-infos>\n+promisor-remote=<pr-info>\n ~~~~~~~~~~~~~~~~~~~~~~~~~~\n \n The server may advertise some promisor remotes it is using or knows\n about to a client which may want to use them as its promisor remotes,\n-instead of this repository. In this case <pr-infos> should be of the\n+instead of this repository. In this case <pr-info> should be of the\n form:\n \n-\tpr-infos = pr-info | pr-infos \";\" pr-info\n+\tpr-info = pr-fields | pr-info \";\" pr-info\n \n-\tpr-info = \"name=\" pr-name | \"name=\" pr-name \",\" \"url=\" pr-url\n+\tpr-fields = field-name \"=\" field-value | pr-fields \",\" pr-fields\n \n-where `pr-name` is the urlencoded name of a promisor remote, and\n-`pr-url` the urlencoded URL of that promisor remote.\n+where all the `field-name` and `field-value` in a given `pr-fields`\n+are field names and values related to a single promisor remote.\n \n-In this case, if the client decides to use one or more promisor\n-remotes the server advertised, it can reply with\n-\"promisor-remote=<pr-names>\" where <pr-names> should be of the form:\n+The server MUST advertise at least the \"name\" and \"url\" field names\n+along with the associated field values, which are the name of a valid\n+remote and its URL, in each `pr-fields`. The \"name\" and \"url\" fields\n+MUST appear first in each pr-fields, in that order.\n \n-\tpr-names = pr-name | pr-names \";\" pr-name\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+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+\n+- \"token\": An authentication token that clients can use when\n+connecting to the remote. It corresponds to the \"remote.<name>.token\"\n+config setting.\n+\n+No other fields are defined by the protocol at this time. Clients MUST\n+ignore fields they don't recognize to allow for future protocol\n+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+\n+Field values MUST be urlencoded.\n+\n+If the client decides to use one or more promisor remotes the server\n+advertised, it can reply with \"promisor-remote=<pr-names>\" where\n+<pr-names> should be of the form:\n+\n+\tpr-names = pr-name | pr-names \";\" pr-names\n \n where `pr-name` is the urlencoded name of a promisor remote the server\n advertised and the client accepts.\n \n-Note that, everywhere in this document, `pr-name` MUST be a valid\n-remote name, and the ';' and ',' characters MUST be encoded if they\n-appear in `pr-name` or `pr-url`.\n+Note that, everywhere in this document, the ';' and ',' characters\n+MUST be encoded if they appear in `pr-name` or `field-value`.\n \n If the server doesn't know any promisor remote that could be good for\n a client to use, or prefers a client not to use any promisor remote it\n@@ -822,9 +848,10 @@ In this case, or if the client doesn't want to use any promisor remote\n the server advertised, the client shouldn't advertise the\n \"promisor-remote\" capability at all in its reply.\n \n-The \"promisor.advertise\" and \"promisor.acceptFromServer\" configuration\n-options can be used on the server and client side to control what they\n-advertise or accept respectively. See the documentation of these\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 \n Note that in the future it would be nice if the \"promisor-remote\"\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 90a063ea53..ec7f4dd2de 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -314,6 +314,74 @@ static int allow_unsanitized(char ch)\n \treturn ch > 32 && ch < 127;\n }\n \n+static const char promisor_field_filter[] = \"partialCloneFilter\";\n+static const char promisor_field_token[] = \"token\";\n+\n+/*\n+ * List of optional field names that can be used in the\n+ * \"promisor-remote\" protocol capability (others must be\n+ * ignored). Each field should correspond to a configurable property\n+ * of a remote that can be relevant for the client.\n+ */\n+static const char *known_fields[] = {\n+\tpromisor_field_filter, /* Filter used for partial clone */\n+\tpromisor_field_token,  /* Authentication token for the remote */\n+\tNULL\n+};\n+\n+/*\n+ * Check if 'field' is in the list of the known field names for the\n+ * \"promisor-remote\" protocol capability.\n+ */\n+static int is_known_field(const char *field)\n+{\n+\tconst char **p;\n+\n+\tfor (p = known_fields; *p; p++)\n+\t\tif (!strcasecmp(*p, field))\n+\t\t\treturn 1;\n+\treturn 0;\n+}\n+\n+static int is_valid_field(struct string_list_item *item, void *cb_data)\n+{\n+\tconst char *field = item->string;\n+\tconst char *config_key = (const char *)cb_data;\n+\n+\tif (!is_known_field(field)) {\n+\t\twarning(_(\"unsupported field '%s' in '%s' config\"), field, config_key);\n+\t\treturn 0;\n+\t}\n+\treturn 1;\n+}\n+\n+static char *fields_from_config(struct string_list *fields_list, const char *config_key)\n+{\n+\tchar *fields = NULL;\n+\n+\tif (!git_config_get_string(config_key, &fields) && *fields) {\n+\t\tstring_list_split_in_place(fields_list, fields, \", \", -1);\n+\t\tstring_list_remove_empty_items(fields_list, 0);\n+\t\tfilter_string_list(fields_list, 0, is_valid_field, (void *)config_key);\n+\t}\n+\n+\treturn fields;\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 = 0;\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+}\n+\n /*\n  * Struct for promisor remotes involved in the \"promisor-remote\"\n  * protocol capability.\n@@ -326,6 +394,8 @@ static int allow_unsanitized(char ch)\n struct promisor_info {\n \tconst char *name;\n \tconst char *url;\n+\tconst char *filter;\n+\tconst char *token;\n };\n \n static void promisor_info_list_clear(struct string_list *list)\n@@ -334,15 +404,47 @@ static void promisor_info_list_clear(struct string_list *list)\n \t\tstruct promisor_info *p = list->items[i].util;\n \t\tfree((char *)p->name);\n \t\tfree((char *)p->url);\n+\t\tfree((char *)p->filter);\n+\t\tfree((char *)p->token);\n \t}\n \tstring_list_clear(list, 1);\n }\n \n+static void set_one_field(struct promisor_info *p,\n+\t\t\t  const char *field, const char *value)\n+{\n+\tif (!strcasecmp(field, promisor_field_filter))\n+\t\tp->filter = xstrdup(value);\n+\telse if (!strcasecmp(field, promisor_field_token))\n+\t\tp->token = xstrdup(value);\n+\telse\n+\t\tBUG(\"invalid field '%s'\", field);\n+}\n+\n+static void set_fields(struct promisor_info *p,\n+\t\t       struct string_list *field_names)\n+{\n+\tstruct string_list_item *item;\n+\n+\tfor_each_string_list_item(item, field_names) {\n+\t\tchar *key = xstrfmt(\"remote.%s.%s\", p->name, item->string);\n+\t\tconst char *val;\n+\t\tif (!git_config_get_string_tmp(key, &val) && *val)\n+\t\t\tset_one_field(p, item->string, val);\n+\t\tfree(key);\n+\t}\n+}\n+\n /*\n  * Populate 'list' with promisor remote information from the config.\n- * The 'util' pointer of each list item will hold a 'struct promisor_info'.\n+ * The 'util' pointer of each list item will hold a 'struct\n+ * promisor_info'. Except \"name\" and \"url\", only members of that\n+ * struct specified by the 'field_names' list are set (using values\n+ * from the configuration).\n  */\n-static void promisor_config_info_list(struct repository *repo, struct string_list *list)\n+static void promisor_config_info_list(struct repository *repo,\n+\t\t\t\t      struct string_list *list,\n+\t\t\t\t      struct string_list *field_names)\n {\n \tstruct promisor_remote *r;\n \n@@ -360,6 +462,9 @@ static void promisor_config_info_list(struct repository *repo, struct string_lis\n \t\t\tnew_info->name = xstrdup(r->name);\n \t\t\tnew_info->url = xstrdup(url);\n \n+\t\t\tif (field_names)\n+\t\t\t\tset_fields(new_info, field_names);\n+\n \t\t\titem = string_list_append(list, new_info->name);\n \t\t\titem->util = new_info;\n \t\t}\n@@ -380,7 +485,7 @@ char *promisor_remote_info(struct repository *repo)\n \tif (!advertise_promisors)\n \t\treturn NULL;\n \n-\tpromisor_config_info_list(repo, &config_info);\n+\tpromisor_config_info_list(repo, &config_info, fields_sent());\n \n \tif (!config_info.nr)\n \t\treturn NULL;\n@@ -395,6 +500,15 @@ char *promisor_remote_info(struct repository *repo)\n \t\tstrbuf_addstr_urlencode(&sb, p->name, allow_unsanitized);\n \t\tstrbuf_addstr(&sb, \",url=\");\n \t\tstrbuf_addstr_urlencode(&sb, p->url, allow_unsanitized);\n+\n+\t\tif (p->filter) {\n+\t\t\tstrbuf_addf(&sb, \",%s=\", promisor_field_filter);\n+\t\t\tstrbuf_addstr_urlencode(&sb, p->filter, allow_unsanitized);\n+\t\t}\n+\t\tif (p->token) {\n+\t\t\tstrbuf_addf(&sb, \",%s=\", promisor_field_token);\n+\t\t\tstrbuf_addstr_urlencode(&sb, p->token, allow_unsanitized);\n+\t\t}\n \t}\n \n \tpromisor_info_list_clear(&config_info);\n@@ -479,7 +593,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\treturn;\n \n \tif (accept != ACCEPT_ALL) {\n-\t\tpromisor_config_info_list(repo, &config_info);\n+\t\tpromisor_config_info_list(repo, &config_info, NULL);\n \t\tstring_list_sort(&config_info);\n \t}\n \n@@ -498,13 +612,9 @@ static void filter_promisor_remote(struct repository *repo,\n \t\telems = strbuf_split(remotes[i], ',');\n \n \t\tfor (size_t j = 0; elems[j]; j++) {\n-\t\t\tint res;\n \t\t\tstrbuf_strip_suffix(elems[j], \",\");\n-\t\t\tres = skip_prefix(elems[j]->buf, \"name=\", &remote_name) ||\n+\t\t\tif (!skip_prefix(elems[j]->buf, \"name=\", &remote_name))\n \t\t\t\tskip_prefix(elems[j]->buf, \"url=\", &remote_url);\n-\t\t\tif (!res)\n-\t\t\t\twarning(_(\"unknown element '%s' from remote info\"),\n-\t\t\t\t\telems[j]->buf);\n \t\t}\n \n \t\tif (remote_name)\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex cb061b1f35..204528b2e0 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -295,6 +295,37 @@ 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 promisor.sendFields\" '\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+\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 promisor.acceptfromserver=All \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that fields are properly transmitted\n+\tENCODED_URL=$(echo \"file://$(pwd)/lop\" | sed -e \"s/ /%20/g\") &&\n+\tPR1=\"name=lop,url=$ENCODED_URL,partialCloneFilter=blob:none\" &&\n+\tPR2=\"name=otherLop,url=https://invalid.invalid,partialCloneFilter=blob:limit=10k,token=fooBar\" &&\n+\ttest_grep \"clone< promisor-remote=$PR1;$PR2\" trace &&\n+\ttest_grep \"clone> promisor-remote=lop;otherLop\" 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 \n-- \n2.50.0.rc2.5.ge8efe62b7f\n\n"},{"id":"520093","messageId":"20250611134506.2975856-4-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250611134506.2975856-1-christian.couder@gmail.com","subject":"[PATCH v4 3/5] promisor-remote: refactor how we parse advertised fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-11T13:45:04Z","receivedAt":"2025-06-11T13:45:28Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"In a follow up commit we are going to parse more fields, like a filter\nand a token, coming from the server when it advertises promisor remotes\nusing the \"promisor-remote\" capability.\n\nTo prepare for this, let's refactor the code that parses the advertised\nfields coming from the server into a new parse_one_advertised_remote()\nfunction that will populate a `struct promisor_info` with the content\nof the fields it parsed.\n\nWhile at it, let's also pass this `struct promisor_info` to the\nshould_accept_remote() function, instead of passing it the parsed name\nand url.\n\nThese changes will make it simpler to both parse more fields and access\nthe content of these parsed fields in follow up commits.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 91 ++++++++++++++++++++++++++++++++---------------\n 1 file changed, 62 insertions(+), 29 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex ec7f4dd2de..e291a00a73 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -398,16 +398,20 @@ struct promisor_info {\n \tconst char *token;\n };\n \n+static void promisor_info_free(struct promisor_info *p)\n+{\n+\tfree((char *)p->name);\n+\tfree((char *)p->url);\n+\tfree((char *)p->filter);\n+\tfree((char *)p->token);\n+\tfree(p);\n+}\n+\n static void promisor_info_list_clear(struct string_list *list)\n {\n-\tfor (size_t i = 0; i < list->nr; i++) {\n-\t\tstruct promisor_info *p = list->items[i].util;\n-\t\tfree((char *)p->name);\n-\t\tfree((char *)p->url);\n-\t\tfree((char *)p->filter);\n-\t\tfree((char *)p->token);\n-\t}\n-\tstring_list_clear(list, 1);\n+\tfor (size_t i = 0; i < list->nr; i++)\n+\t\tpromisor_info_free(list->items[i].util);\n+\tstring_list_clear(list, 0);\n }\n \n static void set_one_field(struct promisor_info *p,\n@@ -524,11 +528,13 @@ enum accept_promisor {\n };\n \n static int should_accept_remote(enum accept_promisor accept,\n-\t\t\t\tconst char *remote_name, const char *remote_url,\n+\t\t\t\tstruct promisor_info *advertised,\n \t\t\t\tstruct string_list *config_info)\n {\n \tstruct promisor_info *p;\n \tstruct string_list_item *item;\n+\tconst char *remote_name = advertised->name;\n+\tconst char *remote_url = advertised->url;\n \n \tif (accept == ACCEPT_ALL)\n \t\treturn 1;\n@@ -566,6 +572,46 @@ static int should_accept_remote(enum accept_promisor accept,\n \treturn 0;\n }\n \n+static struct promisor_info *parse_one_advertised_remote(struct strbuf *remote_info)\n+{\n+\tstruct promisor_info *info = xcalloc(1, sizeof(*info));\n+\tstruct strbuf **elems = strbuf_split(remote_info, ',');\n+\n+\tfor (size_t i = 0; elems[i]; i++) {\n+\t\tchar *elem = elems[i]->buf;\n+\t\tchar *value;\n+\t\tchar *p = strchr(elem, '=');\n+\n+\t\tstrbuf_strip_suffix(elems[i], \",\");\n+\n+\t\tif (!p) {\n+\t\t\twarning(_(\"invalid element '%s' from remote info\"), elem);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\t*p = '\\0';\n+\t\tvalue = url_percent_decode(p + 1);\n+\n+\t\tif (!strcmp(elem, \"name\"))\n+\t\t\tinfo->name = value;\n+\t\telse if (!strcmp(elem, \"url\"))\n+\t\t\tinfo->url = value;\n+\t\telse\n+\t\t\tfree(value);\n+\t}\n+\n+\tstrbuf_list_free(elems);\n+\n+\tif (!info->name || !info->url) {\n+\t\twarning(_(\"server advertised a promisor remote without a name or URL: %s\"),\n+\t\t\tremote_info->buf);\n+\t\tpromisor_info_free(info);\n+\t\treturn NULL;\n+\t}\n+\n+\treturn info;\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@@ -602,32 +648,19 @@ static void filter_promisor_remote(struct repository *repo,\n \tremotes = strbuf_split_str(info, ';', 0);\n \n \tfor (size_t i = 0; remotes[i]; i++) {\n-\t\tstruct strbuf **elems;\n-\t\tconst char *remote_name = NULL;\n-\t\tconst char *remote_url = NULL;\n-\t\tchar *decoded_name = NULL;\n-\t\tchar *decoded_url = NULL;\n+\t\tstruct promisor_info *advertised;\n \n \t\tstrbuf_strip_suffix(remotes[i], \";\");\n-\t\telems = strbuf_split(remotes[i], ',');\n \n-\t\tfor (size_t j = 0; elems[j]; j++) {\n-\t\t\tstrbuf_strip_suffix(elems[j], \",\");\n-\t\t\tif (!skip_prefix(elems[j]->buf, \"name=\", &remote_name))\n-\t\t\t\tskip_prefix(elems[j]->buf, \"url=\", &remote_url);\n-\t\t}\n+\t\tadvertised = parse_one_advertised_remote(remotes[i]);\n \n-\t\tif (remote_name)\n-\t\t\tdecoded_name = url_percent_decode(remote_name);\n-\t\tif (remote_url)\n-\t\t\tdecoded_url = url_percent_decode(remote_url);\n+\t\tif (!advertised)\n+\t\t\tcontinue;\n \n-\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, &config_info))\n-\t\t\tstrvec_push(accepted, decoded_name);\n+\t\tif (should_accept_remote(accept, advertised, &config_info))\n+\t\t\tstrvec_push(accepted, advertised->name);\n \n-\t\tstrbuf_list_free(elems);\n-\t\tfree(decoded_name);\n-\t\tfree(decoded_url);\n+\t\tpromisor_info_free(advertised);\n \t}\n \n \tpromisor_info_list_clear(&config_info);\n-- \n2.50.0.rc2.5.ge8efe62b7f\n\n"},{"id":"520094","messageId":"20250611134506.2975856-5-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250611134506.2975856-1-christian.couder@gmail.com","subject":"[PATCH v4 4/5] promisor-remote: allow a client to check fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-11T13:45:05Z","receivedAt":"2025-06-11T13:45:30Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"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\nLet's make it possible for a client to check if these fields match\nwhat it expects before accepting a promisor remote.\n\nWe allow this by introducing a new \"promisor.checkFields\"\nconfiguration variable. It should contain a comma or space separated\nlist of fields that will be checked.\n\nBy limiting the protocol to specific well-defined fields, we ensure\nboth server and client have a shared understanding of field\nsemantics and usage.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    | 40 ++++++++++++\n promisor-remote.c                     | 89 ++++++++++++++++++++++++---\n t/t5710-promisor-remote-capability.sh | 34 ++++++++++\n 3 files changed, 155 insertions(+), 8 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex beb8f65518..9682ada23c 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -50,3 +50,43 @@ promisor.acceptFromServer::\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+\n+promisor.checkFields::\n+\tA comma or space separated list of additional remote related\n+\tfield names. A client will check if the values of these fields\n+\ttransmitted by a server correspond to the values of these\n+\tfields in its own configuration before accepting a promisor\n+\tremote. Currently, \"partialCloneFilter\" and \"token\" are the\n+\tonly supported field names.\n++\n+If one of these field names (e.g., \"token\") is being checked for an\n+advertised promisor remote (e.g., \"foo\"), three conditions must be met\n+for the check of this specific field to pass:\n++\n+1. The corresponding local configuration (e.g., `remote.foo.token`)\n+   must be set.\n+2. The server must advertise the \"token\" field for remote \"foo\".\n+3. The value of the locally configured `remote.foo.token` must exactly\n+   match the value advertised by the server for the \"token\" field.\n++\n+If any of these conditions is not met for any field name listed in\n+`promisor.checkFields`, the advertised remote \"foo\" will be rejected.\n++\n+For the \"partialCloneFilter\" field, this allows the client to ensure\n+that the server's filter matches what it expects locally, preventing\n+inconsistencies in filtering behavior. For the \"token\" field, this can\n+be used to verify that authentication credentials match expected\n+values.\n++\n+Field names are compared case-insensitively. Field values are compared\n+case-sensitively.\n++\n+The \"name\" and \"url\" fields are always checked according to the\n+`promisor.acceptFromServer` policy, independently of this setting.\n++\n+The field names and values should be passed by the server through the\n+\"promisor-remote\" capability by using the `promisor.sendFields` config\n+variable. The fields will be checked only if the\n+`promisor.acceptFromServer` config variable is not set to \"None\". If\n+set to \"None\", this config variable will have no effect. See\n+linkgit:gitprotocol-v2[5].\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex e291a00a73..939cc78a7d 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -382,6 +382,20 @@ static struct string_list *fields_sent(void)\n \treturn &fields_list;\n }\n \n+static struct string_list *fields_checked(void)\n+{\n+\tstatic struct string_list fields_list = STRING_LIST_INIT_NODUP;\n+\tstatic int initialized = 0;\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+}\n+\n /*\n  * Struct for promisor remotes involved in the \"promisor-remote\"\n  * protocol capability.\n@@ -527,6 +541,61 @@ enum accept_promisor {\n \tACCEPT_ALL\n };\n \n+static int match_field_against_config(const char *field, const char *value,\n+\t\t\t\t      struct promisor_info *config_info)\n+{\n+\tif (config_info->filter && !strcasecmp(field, promisor_field_filter))\n+\t\treturn !strcmp(config_info->filter, value);\n+\telse if (config_info->token && !strcasecmp(field, promisor_field_token))\n+\t\treturn !strcmp(config_info->token, value);\n+\n+\treturn 0;\n+}\n+\n+static int all_fields_match(struct promisor_info *advertised,\n+\t\t\t    struct string_list *config_info,\n+\t\t\t    int in_list)\n+{\n+\tstruct string_list* fields = fields_checked();\n+\tstruct string_list_item *item_checked;\n+\n+\tfor_each_string_list_item(item_checked, fields) {\n+\t\tint match = 0;\n+\t\tconst char *field = item_checked->string;\n+\t\tconst char *value = NULL;\n+\t\tstruct string_list_item *item;\n+\n+\t\tif (!strcasecmp(field, promisor_field_filter))\n+\t\t\tvalue = advertised->filter;\n+\t\telse if (!strcasecmp(field, promisor_field_token))\n+\t\t\tvalue = advertised->token;\n+\n+\t\tif (!value)\n+\t\t\treturn 0;\n+\n+\t\tif (in_list) {\n+\t\t\tfor_each_string_list_item(item, config_info) {\n+\t\t\t\tstruct promisor_info *p = item->util;\n+\t\t\t\tif (match_field_against_config(field, value, p)) {\n+\t\t\t\t\tmatch = 1;\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t}\n+\t\t} else {\n+\t\t\titem = string_list_lookup(config_info, advertised->name);\n+\t\t\tif (item) {\n+\t\t\t\tstruct promisor_info *p = item->util;\n+\t\t\t\tmatch = match_field_against_config(field, value, p);\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (!match)\n+\t\t\treturn 0;\n+\t}\n+\n+\treturn 1;\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@@ -537,7 +606,7 @@ static int should_accept_remote(enum accept_promisor accept,\n \tconst char *remote_url = advertised->url;\n \n \tif (accept == ACCEPT_ALL)\n-\t\treturn 1;\n+\t\treturn all_fields_match(advertised, config_info, 1);\n \n \t/* Get config info for that promisor remote */\n \titem = string_list_lookup(config_info, remote_name);\n@@ -549,7 +618,7 @@ static int should_accept_remote(enum accept_promisor accept,\n \tp = item->util;\n \n \tif (accept == ACCEPT_KNOWN_NAME)\n-\t\treturn 1;\n+\t\treturn all_fields_match(advertised, config_info, 0);\n \n \tif (accept != ACCEPT_KNOWN_URL)\n \t\tBUG(\"Unhandled 'enum accept_promisor' value '%d'\", accept);\n@@ -564,7 +633,7 @@ static int should_accept_remote(enum accept_promisor accept,\n \t\t    remote_name);\n \n \tif (!strcmp(p->url, remote_url))\n-\t\treturn 1;\n+\t\treturn all_fields_match(advertised, config_info, 0);\n \n \twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s'\"),\n \t\tremote_name, p->url, remote_url);\n@@ -596,6 +665,10 @@ static struct promisor_info *parse_one_advertised_remote(struct strbuf *remote_i\n \t\t\tinfo->name = value;\n \t\telse if (!strcmp(elem, \"url\"))\n \t\t\tinfo->url = value;\n+\t\telse if (!strcasecmp(elem, promisor_field_filter))\n+\t\t\tinfo->filter = value;\n+\t\telse if (!strcasecmp(elem, promisor_field_token))\n+\t\t\tinfo->token = value;\n \t\telse\n \t\t\tfree(value);\n \t}\n@@ -638,11 +711,6 @@ static void filter_promisor_remote(struct repository *repo,\n \tif (accept == ACCEPT_NONE)\n \t\treturn;\n \n-\tif (accept != ACCEPT_ALL) {\n-\t\tpromisor_config_info_list(repo, &config_info, NULL);\n-\t\tstring_list_sort(&config_info);\n-\t}\n-\n \t/* Parse remote info received */\n \n \tremotes = strbuf_split_str(info, ';', 0);\n@@ -657,6 +725,11 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tif (!advertised)\n \t\t\tcontinue;\n \n+\t\tif (!config_info.nr) {\n+\t\t\tpromisor_config_info_list(repo, &config_info, fields_checked());\n+\t\t\tstring_list_sort(&config_info);\n+\t\t}\n+\n \t\tif (should_accept_remote(accept, advertised, &config_info))\n \t\t\tstrvec_push(accepted, advertised->name);\n \ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex 204528b2e0..023735d6a8 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -326,6 +326,40 @@ test_expect_success \"clone with promisor.sendFields\" '\n \tcheck_missing_objects server 1 \"$oid\"\n '\n \n+test_expect_success \"clone with promisor.checkFields\" '\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+\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.partialCloneFilter=\"blob:none\" \\\n+\t\t-c promisor.acceptfromserver=All \\\n+\t\t-c promisor.checkFields=partialcloneFilter \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that fields are properly transmitted\n+\tENCODED_URL=$(echo \"file://$(pwd)/lop\" | sed -e \"s/ /%20/g\") &&\n+\tPR1=\"name=lop,url=$ENCODED_URL,partialCloneFilter=blob:none\" &&\n+\tPR2=\"name=otherLop,url=https://invalid.invalid,partialCloneFilter=blob:limit=10k,token=fooBar\" &&\n+\ttest_grep \"clone< promisor-remote=$PR1;$PR2\" trace &&\n+\ttest_grep \"clone> promisor-remote=lop\" trace &&\n+\ttest_grep ! \"clone> promisor-remote=lop;otherLop\" 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 \n-- \n2.50.0.rc2.5.ge8efe62b7f\n\n"},{"id":"520095","messageId":"20250611134506.2975856-6-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250611134506.2975856-1-christian.couder@gmail.com","subject":"[PATCH v4 5/5] promisor-remote: use string constants for 'name' and 'url' too","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-11T13:45:06Z","receivedAt":"2025-06-11T13:45:31Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"A previous commit started to define `promisor_field_filter` and\n`promisor_field_token`, and used them instead of the\n\"partialCloneFilter\" and \"token\" string literals.\n\nLet's do the same for \"name\" and \"url\" to avoid repeating them\nseveral times and for consistency with the other fields.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 14 ++++++++++----\n 1 file changed, 10 insertions(+), 4 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 939cc78a7d..07fa0158ec 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -314,6 +314,12 @@ static int allow_unsanitized(char ch)\n \treturn ch > 32 && ch < 127;\n }\n \n+/*\n+ * All the fields used in \"promisor-remote\" protocol capability,\n+ * including the mandatory \"name\" and \"url\" ones.\n+ */\n+static const char promisor_field_name[] = \"name\";\n+static const char promisor_field_url[] = \"url\";\n static const char promisor_field_filter[] = \"partialCloneFilter\";\n static const char promisor_field_token[] = \"token\";\n \n@@ -514,9 +520,9 @@ char *promisor_remote_info(struct repository *repo)\n \t\tif (item != config_info.items)\n \t\t\tstrbuf_addch(&sb, ';');\n \n-\t\tstrbuf_addstr(&sb, \"name=\");\n+\t\tstrbuf_addf(&sb, \"%s=\", promisor_field_name);\n \t\tstrbuf_addstr_urlencode(&sb, p->name, allow_unsanitized);\n-\t\tstrbuf_addstr(&sb, \",url=\");\n+\t\tstrbuf_addf(&sb, \",%s=\", promisor_field_url);\n \t\tstrbuf_addstr_urlencode(&sb, p->url, allow_unsanitized);\n \n \t\tif (p->filter) {\n@@ -661,9 +667,9 @@ static struct promisor_info *parse_one_advertised_remote(struct strbuf *remote_i\n \t\t*p = '\\0';\n \t\tvalue = url_percent_decode(p + 1);\n \n-\t\tif (!strcmp(elem, \"name\"))\n+\t\tif (!strcmp(elem, promisor_field_name))\n \t\t\tinfo->name = value;\n-\t\telse if (!strcmp(elem, \"url\"))\n+\t\telse if (!strcmp(elem, promisor_field_url))\n \t\t\tinfo->url = value;\n \t\telse if (!strcasecmp(elem, promisor_field_filter))\n \t\t\tinfo->filter = value;\n-- \n2.50.0.rc2.5.ge8efe62b7f\n\n"},{"id":"520096","messageId":"CAP8UFD1b9GzfTSE0dSef_C8mjMz+PchUoRHBPp5M9yKz44Ndsg@mail.gmail.com","threadId":"63293","inReplyTo":"aDVu6O9MUicmd8y4@pks.im","subject":"Re: [PATCH v3 2/5] promisor-remote: allow a server to advertise more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-11T13:46:11Z","receivedAt":"2025-06-11T13:46:26Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Tue, May 27, 2025 at 9:51 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Mon, May 19, 2025 at 04:12:56PM +0200, Christian Couder wrote:\n> > diff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\n> > index 2638b01f83..71311b70c8 100644\n> > --- a/Documentation/config/promisor.adoc\n> > +++ b/Documentation/config/promisor.adoc\n> > @@ -9,6 +9,24 @@ promisor.advertise::\n> >       \"false\", which means the \"promisor-remote\" capability is not\n> >       advertised.\n> >\n> > +promisor.sendFields::\n> > +     A comma or space separated list of additional remote related\n> > +     fields that a server will send while advertising its promisor\n> > +     remotes using the \"promisor-remote\" capability, see\n> > +     linkgit:gitprotocol-v2[5]. Currently, only the\n> > +     \"partialCloneFilter\" and \"token\" fields are supported. The\n> > +     \"partialCloneFilter\" field contains the partial clone filter\n> > +     used for the remote, and the \"token\" field contains an\n> > +     authentication token for the remote.\n> > ++\n> > +When a field is part of this list and a corresponding\n> > +\"remote.foo.<field>\" config variable is set on the server to a\n> > +non-empty value, then the field and its value will be sent when\n> > +advertising the promisor remote \"foo\". This list has no effect unless\n> > +the \"promisor.advertise\" config variable is set to \"true\", and the\n> > +\"name\" and \"url\" fields are always advertised regardless of this\n> > +setting.\n>\n> I think this documentation should be clarified to explicitly talk about\n> \"field names\". In v2 I misread these paragraphs to mean that the admin\n> is expected to configure name-value pairs because you say \"fields\" here,\n> and that term is specified elsewhere to be such a pair.\n\nYeah, right, in the v4, I have clarified the doc here and in other\nplaces to talk about \"field names\" when relevant.\n\n> > diff --git a/promisor-remote.c b/promisor-remote.c\n> > index 94e87f2f48..cde4079d8c 100644\n> > --- a/promisor-remote.c\n> > +++ b/promisor-remote.c\n> > @@ -314,6 +314,73 @@ static int allow_unsanitized(char ch)\n> >       return ch > 32 && ch < 127;\n> >  }\n> >\n> > +static const char promisor_field_filter[] = \"partialCloneFilter\";\n> > +static const char promisor_field_token[] = \"token\";\n>\n> Curious. Why aren't these declared as mere string constants (static\n> const char *)?\n\nUsing \"static const char *\" is a bit less efficient, as it uses an\nadditional pointer to point to the literal string.\n\n> It might be a bit more idiomatic to have these as\n> all-uppercase defines to make it obvious that those aren't a local\n> variable.\n>\n>     #define PROMISOR_FIELD_FILTER \"partialCloneFilter\"\n>     #define PROMISOR_FIELD_TOKEN  \"token\"\n\nThis has been discussed a few times in the past on the mailing list\nbut I can't find any references to such discussions now.\n\nAs far as I recall the first discussion I had about this was with\nDscho who was in favor of using \"static const char []\" because it's\nmore type safe than \"#define ...\" and more efficient than \"static\nconst char *\".\n\n> > -/* Prepare a 'struct promisor_info' linked list with config information. */\n> > -static struct promisor_info *promisor_config_info_list(struct repository *repo)\n> > +static void set_one_field(struct promisor_info *p,\n> > +                       const char *field, const char *value)\n> > +{\n> > +     if (!strcasecmp(field, promisor_field_filter))\n> > +             p->filter = xstrdup(value);\n> > +     else if (!strcasecmp(field, promisor_field_token))\n> > +             p->token = xstrdup(value);\n> > +     else\n> > +             BUG(\"Invalid field '%s'\", field);\n>\n> s/Invalid/invalid/\n\nI have made this change in the v4.\n\nThanks for your review!\n"},{"id":"520097","messageId":"CAP8UFD3necac4PBYYN1ubss3h0T6fgFx=HO8S0F+o4haFFU6vw@mail.gmail.com","threadId":"63293","inReplyTo":"aDVu4JmTU7Wz32Z7@pks.im","subject":"Re: [PATCH v2 2/3] promisor-remote: allow a server to advertise more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-11T13:46:20Z","receivedAt":"2025-06-11T13:46:34Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Tue, May 27, 2025 at 9:51 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Mon, May 19, 2025 at 04:11:18PM +0200, Christian Couder wrote:\n\n> > I am not sure I understand what you mean. This promisor.sendFields\n> > config variable is for the server side which advertises remotes. The\n> > server advertises its remotes (if it wants to) before receiving\n> > information from the client, so it cannot know what the client\n> > accepts.\n>\n> In the current form you need to reflow this whole paragraph every time a\n> new field is supported, and it's easy to miss the exact supported\n> fields. So my idea was to maybe move the supported fields into a\n> bulleted list. E.g.:\n>\n>     promisor.sendFields::\n>          A comma or space separated list of additional remote related\n>          fields that a server will send while advertising its promisor\n>          remotes using the \"promisor-remote\" capability, see\n>          linkgit:gitprotocol-v2[5]. The following fields are supported:\n>     +\n>     * \"partialCloneFilter\": contains the partial clone filter used for\n>       the remote.\n>     * \"token\": contains the authentication token for the remote.\n\nYeah, the doc uses a bulleted list in v4.\n\n> > > Furthermore, should we maybe refactor this to match the restrictive\n> > > design where valid fields are explicitly specified? In other words,\n> > > should we have separate config keys for each of the accepted fields now?\n> >\n> > Maybe I don't understand what you mean with \"accepted fields\".\n>\n> I think I had a misunderstanding on my side. I didn't get that this is\n> only configuring field _names_ that we'll end up sending to the remote\n> side. So I thought that the user is expected to configure name-value\n> pairs here that are then sent to the client, not only the name.\n>\n> I guess this is mostly because the config documentation talks about\n> \"fields\", but that term is used elsewhere to indicate a name-value pair.\n\nI see. I have tried to use \"field names\" instead of \"fields\"\neverywhere it makes sense in the v4.\n\n> > > Does\n> > > it mean that this promisor remote should only be used in case we do have\n> > > the exact same filter passed to git-clone(1)?\n> >\n> > It's up to the client to decide, but yeah it will likely work better\n> > if the same filter is used. It should still work if a different filter\n> > is used though. In case the promisor remote doesn't have an object,\n> > there should be a fallback to ask the main server for that object.\n> >\n> > Also the filter mechanism already exists for a long time and this\n> > series doesn't change how it works. It's already possible to have\n> > different repos using the same promisor remote with different filters.\n> > So documentation about what happens when they do that should not be\n> > specific to this patch series.\n>\n> That's fair enough, but spelling this out somewhere and drawing the\n> bigger picture helps the reviewer understand the vision that you've got\n> here.\n\nOk, I have added the following to the cover letter of the v4 about this:\n\n\"Note that the filter mechanism already exists for a long time and this\nseries doesn't change how it works. For example, it has already been\npossible for a long time to have different repos using the same\npromisor remote with different filters. See the existing partial clone\ndocumentation (like \"Documentation/technical/partial-clone.adoc\") for\nmore information on partial clone.\"\n"},{"id":"520098","messageId":"CAP8UFD0xQzHptsQJ9cOUNSri1kQkyJk5dUQgtDtF6NPDTvB0Mg@mail.gmail.com","threadId":"63293","inReplyTo":"agvpcmvdxxpfwr34mw34dvatqr3rlc72zntgbrhlzynvn4h5rh@5mdgt4mdocnx","subject":"Re: [PATCH v3 2/5] promisor-remote: allow a server to advertise more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-11T13:46:50Z","receivedAt":"2025-06-11T13:47:04Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Wed, May 21, 2025 at 10:36 PM Justin Tobler <jltobler@gmail.com> wrote:\n>\n> On 25/05/19 04:12PM, Christian Couder wrote:\n\n> > +static int is_valid_field(struct string_list_item *item, void *cb_data)\n> > +{\n> > +     const char *field = item->string;\n> > +     const char *config_key = (const char *)cb_data;\n> > +\n> > +     if (!is_known_field(field)) {\n> > +             warning(_(\"unsupported field '%s' in '%s' config\"), field, config_key);\n> > +             return 0;\n> > +     }\n> > +     return 1;\n> > +}\n>\n> Ok, so if the server has an unknown sendField value configured, the\n> value is ignored by the server and a warning printed.\n\nYes.\n\n> > +static char *fields_from_config(struct string_list *fields_list, const char *config_key)\n> > +{\n> > +     char *fields = NULL;\n> > +\n> > +     if (!git_config_get_string(config_key, &fields) && *fields) {\n>\n> We could use `repo_config_get_string()` here instead and wire the nearby\n> `struct repository`, but as the rest of the file is not doing so it's\n> not really a big deal.\n\nYeah, removing use of the_repository in this file is left for another\nfuture patch series.\n\n> > +             string_list_split_in_place(fields_list, fields, \", \", -1);\n> > +             filter_string_list(fields_list, 0, is_valid_field, (void *)config_key);\n> > +     }\n> > +\n> > +     return fields;\n> > +}\n> > +\n> > +static struct string_list *fields_sent(void)\n> > +{\n> > +     static struct string_list fields_list = STRING_LIST_INIT_NODUP;\n> > +     static int initialized = 0;\n> > +\n> > +     if (!initialized) {\n> > +             fields_list.cmp = strcasecmp;\n> > +             fields_from_config(&fields_list, \"promisor.sendFields\");\n> > +             initialized = 1;\n> > +     }\n> > +\n> > +     return &fields_list;\n> > +}\n>\n> Are there scenarios where `fields_sent()` is getting invoked more than\n> once? My understanding is that this is invoked only when the capability\n> is being advertised. Regardless, I wonder if we really need the static\n> initialization here.\n\nI am not sure if `fields_sent()` could be invoked more than once (by\nthe same process), but I think it's very risky to rely on it being\ncalled just once, and I think it doesn't cost much to be safe.\n\n> > diff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\n> > index cb061b1f35..27c32b2573 100755\n> > --- a/t/t5710-promisor-remote-capability.sh\n> > +++ b/t/t5710-promisor-remote-capability.sh\n> > @@ -295,6 +295,38 @@ 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 promisor.sendFields\" '\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> > +     git -C server config promisor.sendFields \"partialCloneFilter, token\" &&\n>\n> This configuration results in the unsupported field warning message\n> being printed. This is because of the SP following the comma resulting\n> in an empty value in the middle after the string split.\n\nYeah, right, in the v4 I have added a call to\nstring_list_remove_empty_items() to avoid spurious empty values and\nrelated warnings.\n\n> > +     test_when_finished \"git -C server config unset promisor.sendFields\" &&\n>\n> I think we could use `test_config` which would automatically unset the\n> configuration after the test.\n\nYeah, `test_config` is used in v4 to avoid this test_when_finished.\n\nThanks for your review!\n"},{"id":"520099","messageId":"CAP8UFD1mOitL5Um0cnMYKAVLKamj_Xzhp6ECwQ6_BsqZ0QUy2Q@mail.gmail.com","threadId":"63293","inReplyTo":"xmqqldqqc8kq.fsf@gitster.g","subject":"Re: [PATCH v3 1/5] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-11T13:47:11Z","receivedAt":"2025-06-11T13:47:25Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Wed, May 21, 2025 at 5:00 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Christian Couder <christian.couder@gmail.com> writes:\n>\n> > I don't think it would bring a lot of benefits. Using an strmap or a\n> > sorted string list might make things faster if there are a lot of\n> > promisor remotes configured on the clients, but I don't think we are\n> > at a point where such an optimisation is worth it.\n>\n> What I was getting at using common collection types instead of\n> rolling your own linked list was not primarily about performance.\n> They are more battle-tested and much easier to readers who are\n> familiar with these existing types.\n\nOK, I have implemented the string_list approach you suggested that\nuses the nickname as the key (item->string) with a pointer to a\npromisor_info structure as the data (item->util) in the v4. The\nstring_list is sorted when it can be useful.\n\nAlso remote_nick_find() has been renamed promisor_info_find().\n"},{"id":"520402","messageId":"CAOLa=ZQm+Hm_rMNJY7B4dGAuV=nFecsiPZ9N8uChhEwzb8n+Hw@mail.gmail.com","threadId":"63293","inReplyTo":"20250611134506.2975856-2-christian.couder@gmail.com","subject":"Re: [PATCH v4 1/5] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-06-19T11:53:22Z","receivedAt":"2025-06-19T11:53:24Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> In a following commit, we will use the new 'promisor-remote' protocol\n> capability introduced by d460267613 (Add 'promisor-remote' capability\n> to protocol v2, 2025-02-18) to pass and process more information\n> about promisor remotes than just their name and url.\n>\n> For that purpose, we will need to store information about other\n> fields, especially information that might or might not be available\n> for different promisor remotes. Unfortunately using 'struct strvec',\n> as we currently do, to store information about the promisor remotes\n> with one 'struct strvec' for each field like \"name\" or \"url\" does not\n> scale easily in that case.\n>\n\nNit: It would be nice to mention _why_ it doesn't scale easily here.\n\n> Let's refactor this and introduce a new 'struct promisor_info'.\n>\n> It will only store promisor remote information in its members. For now\n> it has only a 'name' member for the promisor remote name and an 'url'\n> member for its URL. We will use use a 'struct string_list' to store\n> the instances of 'struct promisor_info'. For each 'item' in the\n> string_list, 'item->string' will point to the promisor remote name and\n> 'item->util' will point to the corresponding 'struct promisor_info'\n> instance.\n>\n> Explicit members are used within 'struct promisor_info' for type\n> safety and clarity regarding the specific information being handled,\n> rather than a generic key-value store. We want to specify and document\n> each field and its content, so adding new members to the struct as\n> more fields are supported is fine.\n>\n\nThe rest of the patch looks good to me.\n\n[snip]\n"},{"id":"520403","messageId":"CAOLa=ZSAU4vfNNvcmpVrEavYWss_txVwfutY5ZZLzvVmv5wPdA@mail.gmail.com","threadId":"63293","inReplyTo":"20250611134506.2975856-3-christian.couder@gmail.com","subject":"Re: [PATCH v4 2/5] promisor-remote: allow a server to advertise more fields","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-06-19T12:15:14Z","receivedAt":"2025-06-19T12:15:17Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n[snip]\n\n> diff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\n> index cb061b1f35..204528b2e0 100755\n> --- a/t/t5710-promisor-remote-capability.sh\n> +++ b/t/t5710-promisor-remote-capability.sh\n> @@ -295,6 +295,37 @@ 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 promisor.sendFields\" '\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> +\ttest_config -C server promisor.sendFields \"partialCloneFilter, token\" &&\n\nWhat about testing only 'comma' separated and only 'space' separated\nfields, since we support those too.\n\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 promisor.acceptfromserver=All \\\n> +\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n> +\n> +\t# Check that fields are properly transmitted\n> +\tENCODED_URL=$(echo \"file://$(pwd)/lop\" | sed -e \"s/ /%20/g\") &&\n> +\tPR1=\"name=lop,url=$ENCODED_URL,partialCloneFilter=blob:none\" &&\n> +\tPR2=\"name=otherLop,url=https://invalid.invalid,partialCloneFilter=blob:limit=10k,token=fooBar\" &&\n> +\ttest_grep \"clone< promisor-remote=$PR1;$PR2\" trace &&\n> +\ttest_grep \"clone> promisor-remote=lop;otherLop\" 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>\n> --\n> 2.50.0.rc2.5.ge8efe62b7f\n"},{"id":"520404","messageId":"CAOLa=ZT9c-4Wb3H8D_7TcUteyuyDLgZZAWFkg-CWHLOLoYOBvQ@mail.gmail.com","threadId":"63293","inReplyTo":"20250611134506.2975856-1-christian.couder@gmail.com","subject":"Re: [PATCH v4 0/5] Make the \"promisor-remote\" capability support more fields","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-06-19T12:18:07Z","receivedAt":"2025-06-19T12:18:09Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> The \"promisor-remote\" capability can only be used to pass the names\n> and URLs of the promisor remotes from the server to the client. After\n> that the client can use this information to decide if it accepts the\n> remotes or not.\n>\n> It would be nice if the server could pass more fields about its\n> remotes and if the client could use that additional information to\n> decide about the remotes by comparing it with its local information\n> about the remotes.\n>\n> This patch series implements this by adding the \"promisor.sendFields\"\n> on the server side and the \"promisor.checkFields\" on the client side.\n>\n> For example, if \"promisor.sendFields\" is set to \"partialCloneFilter\",\n> and the server has the remote \"foo\" configured like this:\n>\n> [remote \"foo\"]\n>         url = file:///tmp/foo.git\n> \tpartialCloneFilter = blob:none\n>\n> then \"name=foo,url=file:///tmp/foo.git,partialCloneFilter=blob:none\"\n> will be sent by the server for this remote.\n>\n> All the information passed through the \"promisor-remote\" capability is\n> still only used to decide if the remotes are accepted or not. The\n> client doesn't store it and doesn't use it for any other purpose.\n>\n> Note that the filter mechanism already exists for a long time and this\n> series doesn't change how it works. For example, it has already been\n> possible for a long time to have different repos using the same\n> promisor remote with different filters. See the existing partial clone\n> documentation (like \"Documentation/technical/partial-clone.adoc\") for\n> more information on partial clone.\n>\n> The fields that can be passed are limited to \"partialCloneFilter\" and\n> \"token\".\n>\n> On the technical side, we get rid of 'struct strvec' and we use\n> 'struct promisor_info' to store the data and 'struct string_list' to\n> store the 'struct promisor_info' instances instead. This matches the\n> latest suggestion from Junio.\n>\n> This work is part of the \"LOP\" effort documented in:\n>\n>   Documentation/technical/large-object-promisors.adoc\n>\n> See that doc for more information on the broader context.\n>\n\nI've left some small nits, but mostly this version looks good to me.\nI don't specifically see a need for re-roll, but will leave it up to\nyou!\n\n[snip]\n\nThanks,\n- Karthik\n"},{"id":"520586","messageId":"2z3uanklsfnrgxmiqjt2innv2diownbpwtgcj67bi7udwjdqpg@ackusinuz7c6","threadId":"63293","inReplyTo":"20250611134506.2975856-2-christian.couder@gmail.com","subject":"Re: [PATCH v4 1/5] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-06-23T19:38:15Z","receivedAt":"2025-06-23T19:43:32Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/06/11 03:45PM, Christian Couder wrote:\n> In a following commit, we will use the new 'promisor-remote' protocol\n> capability introduced by d460267613 (Add 'promisor-remote' capability\n> to protocol v2, 2025-02-18) to pass and process more information\n> about promisor remotes than just their name and url.\n> \n> For that purpose, we will need to store information about other\n> fields, especially information that might or might not be available\n> for different promisor remotes. Unfortunately using 'struct strvec',\n> as we currently do, to store information about the promisor remotes\n> with one 'struct strvec' for each field like \"name\" or \"url\" does not\n> scale easily in that case.\n> \n> Let's refactor this and introduce a new 'struct promisor_info'.\n> \n> It will only store promisor remote information in its members. For now\n> it has only a 'name' member for the promisor remote name and an 'url'\n> member for its URL. We will use use a 'struct string_list' to store\n\ns/use use/use/\n\n> the instances of 'struct promisor_info'. For each 'item' in the\n> string_list, 'item->string' will point to the promisor remote name and\n> 'item->util' will point to the corresponding 'struct promisor_info'\n> instance.\n> \n> Explicit members are used within 'struct promisor_info' for type\n> safety and clarity regarding the specific information being handled,\n> rather than a generic key-value store. We want to specify and document\n> each field and its content, so adding new members to the struct as\n> more fields are supported is fine.\n> \n> Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n> ---\n>  promisor-remote.c | 111 +++++++++++++++++++++++++++++-----------------\n>  1 file changed, 70 insertions(+), 41 deletions(-)\n> \n> diff --git a/promisor-remote.c b/promisor-remote.c\n> index 9d058586df..90a063ea53 100644\n> --- a/promisor-remote.c\n> +++ b/promisor-remote.c\n> @@ -314,9 +314,35 @@ static int allow_unsanitized(char ch)\n>  \treturn ch > 32 && ch < 127;\n>  }\n>  \n> -static void promisor_info_vecs(struct repository *repo,\n> -\t\t\t       struct strvec *names,\n> -\t\t\t       struct strvec *urls)\n> +/*\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> + */\n> +struct promisor_info {\n> +\tconst char *name;\n> +\tconst char *url;\n> +};\n\nOk so now all promisor info for a given remote is stored in its own\nstruct. This will enable easier extension in the future to add\nadditional fields.\n\n> +\n> +static void promisor_info_list_clear(struct string_list *list)\n> +{\n> +\tfor (size_t i = 0; i < list->nr; i++) {\n> +\t\tstruct promisor_info *p = list->items[i].util;\n> +\t\tfree((char *)p->name);\n> +\t\tfree((char *)p->url);\n> +\t}\n> +\tstring_list_clear(list, 1);\n> +}\n> +\n> +/*\n> + * Populate 'list' with promisor remote information from the config.\n> + * The 'util' pointer of each list item will hold a 'struct promisor_info'.\n> + */\n> +static void promisor_config_info_list(struct repository *repo, struct string_list *list)\n>  {\n>  \tstruct promisor_remote *r;\n>  \n> @@ -328,8 +354,14 @@ static void promisor_info_vecs(struct repository *repo,\n>  \n>  \t\t/* Only add remotes with a non empty URL */\n>  \t\tif (!git_config_get_string_tmp(url_key, &url) && *url) {\n> -\t\t\tstrvec_push(names, r->name);\n> -\t\t\tstrvec_push(urls, url);\n> +\t\t\tstruct promisor_info *new_info = xcalloc(1, sizeof(*new_info));\n> +\t\t\tstruct string_list_item *item;\n> +\n> +\t\t\tnew_info->name = xstrdup(r->name);\n> +\t\t\tnew_info->url = xstrdup(url);\n> +\n> +\t\t\titem = string_list_append(list, new_info->name);\n> +\t\t\titem->util = new_info;\n>  \t\t}\n\nIn this version, each remote is now stored as a member of a `struct\nstring_list` instead of a custom collection type specific to promisor\nremote info. Nice\n\n>  \t\tfree(url_key);\n> @@ -340,47 +372,36 @@ char *promisor_remote_info(struct repository *repo)\n>  {\n>  \tstruct strbuf sb = STRBUF_INIT;\n>  \tint advertise_promisors = 0;\n> -\tstruct strvec names = STRVEC_INIT;\n> -\tstruct strvec urls = STRVEC_INIT;\n> +\tstruct string_list config_info = STRING_LIST_INIT_NODUP;\n\nnit: Ok in this context, \"config_info\" is specific to the list of\npromisor_info not just generic git configuration. Something like\n\"promisor_info_list\" would be a bit more explicit, but I don't feel\nsuper strongly.\n\n> +\tstruct string_list_item *item;\n>  \n>  \tgit_config_get_bool(\"promisor.advertise\", &advertise_promisors);\n>  \n>  \tif (!advertise_promisors)\n>  \t\treturn NULL;\n>  \n> -\tpromisor_info_vecs(repo, &names, &urls);\n> +\tpromisor_config_info_list(repo, &config_info);\n>  \n> -\tif (!names.nr)\n> +\tif (!config_info.nr)\n>  \t\treturn NULL;\n>  \n> -\tfor (size_t i = 0; i < names.nr; i++) {\n> -\t\tif (i)\n> +\tfor_each_string_list_item(item, &config_info) {\n> +\t\tstruct promisor_info *p = item->util;\n> +\n> +\t\tif (item != config_info.items)\n>  \t\t\tstrbuf_addch(&sb, ';');\n\nOut of curiousity, is it invalid for the trailing promisor remote entry\nto end with a ';'? It would be simpler if each entry could just end with\na semi-colon.\n\n> +\n>  \t\tstrbuf_addstr(&sb, \"name=\");\n> -\t\tstrbuf_addstr_urlencode(&sb, names.v[i], allow_unsanitized);\n> +\t\tstrbuf_addstr_urlencode(&sb, p->name, allow_unsanitized);\n>  \t\tstrbuf_addstr(&sb, \",url=\");\n> -\t\tstrbuf_addstr_urlencode(&sb, urls.v[i], allow_unsanitized);\n> +\t\tstrbuf_addstr_urlencode(&sb, p->url, allow_unsanitized);\n>  \t}\n>  \n> -\tstrvec_clear(&names);\n> -\tstrvec_clear(&urls);\n> +\tpromisor_info_list_clear(&config_info);\n>  \n>  \treturn strbuf_detach(&sb, NULL);\n>  }\n>  \n> -/*\n> - * Find first index of 'nicks' where there is 'nick'. 'nick' is\n> - * compared case sensitively to the strings in 'nicks'. If not found\n> - * 'nicks->nr' is returned.\n> - */\n> -static size_t remote_nick_find(struct strvec *nicks, const char *nick)\n> -{\n> -\tfor (size_t i = 0; i < nicks->nr; i++)\n> -\t\tif (!strcmp(nicks->v[i], nick))\n> -\t\t\treturn i;\n> -\treturn nicks->nr;\n> -}\n> -\n>  enum accept_promisor {\n>  \tACCEPT_NONE = 0,\n>  \tACCEPT_KNOWN_URL,\n> @@ -390,19 +411,23 @@ enum accept_promisor {\n>  \n>  static int should_accept_remote(enum accept_promisor accept,\n>  \t\t\t\tconst char *remote_name, const char *remote_url,\n> -\t\t\t\tstruct strvec *names, struct strvec *urls)\n> +\t\t\t\tstruct string_list *config_info)\n>  {\n> -\tsize_t i;\n> +\tstruct promisor_info *p;\n> +\tstruct string_list_item *item;\n>  \n>  \tif (accept == ACCEPT_ALL)\n>  \t\treturn 1;\n>  \n> -\ti = remote_nick_find(names, remote_name);\n> +\t/* Get config info for that promisor remote */\n> +\titem = string_list_lookup(config_info, remote_name);\n>  \n> -\tif (i >= names->nr)\n> +\tif (!item)\n>  \t\t/* We don't know about that remote */\n>  \t\treturn 0;\n>  \n> +\tp = item->util;\n> +\n>  \tif (accept == ACCEPT_KNOWN_NAME)\n>  \t\treturn 1;\n>  \n> @@ -414,11 +439,15 @@ static int should_accept_remote(enum accept_promisor accept,\n>  \t\treturn 0;\n>  \t}\n>  \n> -\tif (!strcmp(urls->v[i], remote_url))\n> +\tif (!p->url)\n> +\t\tBUG(\"bad config_info (invalid URL) for remote '%s'\",\n> +\t\t    remote_name);\n\nOk just to clarify, it is invalid for a promisor remote to not have a\nURL specified. If so, it might be better to say \"empty URL\" or something\nalong those lines.\n\n> +\n> +\tif (!strcmp(p->url, remote_url))\n>  \t\treturn 1;\n>  \n>  \twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s'\"),\n> -\t\tremote_name, urls->v[i], remote_url);\n> +\t\tremote_name, p->url, remote_url);\n>  \n>  \treturn 0;\n>  }\n> @@ -430,8 +459,7 @@ static void filter_promisor_remote(struct repository *repo,\n>  \tstruct strbuf **remotes;\n>  \tconst char *accept_str;\n>  \tenum accept_promisor accept = ACCEPT_NONE;\n> -\tstruct strvec names = STRVEC_INIT;\n> -\tstruct strvec urls = STRVEC_INIT;\n> +\tstruct string_list config_info = STRING_LIST_INIT_NODUP;\n>  \n>  \tif (!git_config_get_string_tmp(\"promisor.acceptfromserver\", &accept_str)) {\n>  \t\tif (!*accept_str || !strcasecmp(\"None\", accept_str))\n> @@ -450,8 +478,10 @@ static void filter_promisor_remote(struct repository *repo,\n>  \tif (accept == ACCEPT_NONE)\n>  \t\treturn;\n>  \n> -\tif (accept != ACCEPT_ALL)\n> -\t\tpromisor_info_vecs(repo, &names, &urls);\n> +\tif (accept != ACCEPT_ALL) {\n> +\t\tpromisor_config_info_list(repo, &config_info);\n> +\t\tstring_list_sort(&config_info);\n> +\t}\n>  \n>  \t/* Parse remote info received */\n>  \n> @@ -482,7 +512,7 @@ static void filter_promisor_remote(struct repository *repo,\n>  \t\tif (remote_url)\n>  \t\t\tdecoded_url = url_percent_decode(remote_url);\n>  \n> -\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, &names, &urls))\n> +\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, &config_info))\n>  \t\t\tstrvec_push(accepted, decoded_name);\n>  \n>  \t\tstrbuf_list_free(elems);\n> @@ -490,8 +520,7 @@ static void filter_promisor_remote(struct repository *repo,\n>  \t\tfree(decoded_url);\n>  \t}\n>  \n> -\tstrvec_clear(&names);\n> -\tstrvec_clear(&urls);\n> +\tpromisor_info_list_clear(&config_info);\n>  \tstrbuf_list_free(remotes);\n>  }\n>  \n> -- \n> 2.50.0.rc2.5.ge8efe62b7f\n> \n"},{"id":"520587","messageId":"ojee3heqvvlk3v4ftuzaulm5vu7tvmc5idvqbda2ioiuurbmdw@sevh7jtsbbad","threadId":"63293","inReplyTo":"20250611134506.2975856-3-christian.couder@gmail.com","subject":"Re: [PATCH v4 2/5] promisor-remote: allow a server to advertise more fields","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-06-23T19:59:17Z","receivedAt":"2025-06-23T20:04:34Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/06/11 03:45PM, Christian Couder wrote:\n> For now the \"promisor-remote\" protocol capability can only pass \"name\"\n> and \"url\" information from a server to a client in the form\n> \"name=<remote_name>,url=<remote_url>\".\n> \n> Let's make it possible to pass more information by introducing a new\n> \"promisor.sendFields\" configuration variable. This variable should\n> contain a comma or space separated list of field names that will be\n> looked up in the configuration of the remote on the server to find the\n> values that will be passed to the client.\n> \n> Only a set of predefined fields are allowed. The only fields in this\n> set are \"partialCloneFilter\" and \"token\". The \"partialCloneFilter\"\n> field specifies the filter definition used by the promisor remote,\n> and the \"token\" field can provide an authentication credential for\n> accessing it.\n> \n> For example, if \"promisor.sendFields\" is set to \"partialCloneFilter\",\n> and the server has the \"remote.<name>.partialCloneFilter\" config\n> variable set to a value for a remote, then that value will be passed\n> in the form \"partialCloneFilter=<value>\" after the \"name\" and \"url\"\n> fields.\n> \n> A following commit will allow the client to use the information to\n> decide if it accepts the remote or not. For now the client doesn't do\n> anything with the additional information it receives.\n> \n> Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n> ---\n[snip]\n> +static char *fields_from_config(struct string_list *fields_list, const char *config_key)\n> +{\n> +\tchar *fields = NULL;\n> +\n> +\tif (!git_config_get_string(config_key, &fields) && *fields) {\n> +\t\tstring_list_split_in_place(fields_list, fields, \", \", -1);\n> +\t\tstring_list_remove_empty_items(fields_list, 0);\n\nOk, in this version we now filter out empty entries from the\nstring_list. Previously if fields were specified with both a comma and\nSP character (i.e. \"partialCloneFilter, token\"), an empty entry would be\nparsed in the middle and lead to a warning message.\n\nThis change is good because it would be pretty natural for a user to\nspecify the config with both. It might be nice to leave a comment\nexplaining why we do this though as it may be confusing without context.\n\n-Justin\n\n> +\t\tfilter_string_list(fields_list, 0, is_valid_field, (void *)config_key);\n> +\t}\n> +\n> +\treturn fields;\n> +}\n> +\n"},{"id":"520683","messageId":"20250625125055.1375596-1-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250611134506.2975856-1-christian.couder@gmail.com","subject":"[PATCH v5 0/5] Make the \"promisor-remote\" capability support more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-25T12:50:50Z","receivedAt":"2025-06-25T12:51:15Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"The \"promisor-remote\" capability can only be used to pass the names\nand URLs of the promisor remotes from the server to the client. After\nthat the client can use this information to decide if it accepts the\nremotes or not.\n\nIt would be nice if the server could pass more fields about its\nremotes and if the client could use that additional information to\ndecide about the remotes by comparing it with its local information\nabout the remotes.\n\nThis patch series implements this by adding the \"promisor.sendFields\"\non the server side and the \"promisor.checkFields\" on the client side.\n\nFor example, if \"promisor.sendFields\" is set to \"partialCloneFilter\",\nand the server has the remote \"foo\" configured like this:\n\n[remote \"foo\"]\n        url = file:///tmp/foo.git\n\tpartialCloneFilter = blob:none\n\nthen \"name=foo,url=file:///tmp/foo.git,partialCloneFilter=blob:none\"\nwill be sent by the server for this remote.\n\nAll the information passed through the \"promisor-remote\" capability is\nstill only used to decide if the remotes are accepted or not. The\nclient doesn't store it and doesn't use it for any other purpose.\n\nNote that the filter mechanism already exists for a long time and this\nseries doesn't change how it works. For example, it has already been\npossible for a long time to have different repos using the same\npromisor remote with different filters. See the existing partial clone\ndocumentation (like \"Documentation/technical/partial-clone.adoc\") for\nmore information on partial clone.\n\nThe fields that can be passed are limited to \"partialCloneFilter\" and\n\"token\".\n\nOn the technical side, we get rid of 'struct strvec' and we use\n'struct promisor_info' to store the data and 'struct string_list' to\nstore the 'struct promisor_info' instances instead.\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\nChanges since v4\n----------------\n\nThanks to Patrick, Junio, Karthik and Justin for their comments on the\nprevious versions.\n\nThere are very few changes compared to v4 and they are quite small.\n\nIn patch 1/5, in the commit message:\n\n  - a few sentences were added to explain why using 'struct strvec'\n    for the new fields wouldn't scale well,\n\n  - a typo \"use use\" was fixed.\n\nIn patch 1/5, in the code, a BUG() message was improved.\n\nIn patch 2/5, in the code, some code comments were added in\nfields_from_config().\n\nCI tests\n--------\n\nThey all passed:\n\nhttps://github.com/chriscool/git/actions/runs/15846478103\n\nRange diff compared to v4\n-------------------------\n\n1:  8a4df71d2c ! 1:  3700939f67 promisor-remote: refactor to get rid of 'struct strvec'\n    @@ Commit message\n         for different promisor remotes. Unfortunately using 'struct strvec',\n         as we currently do, to store information about the promisor remotes\n         with one 'struct strvec' for each field like \"name\" or \"url\" does not\n    -    scale easily in that case.\n    +    scale easily in that case. We would need one 'struct strvec' for each\n    +    new field, and then we would have to pass all these 'struct strvec'\n    +    around.\n     \n         Let's refactor this and introduce a new 'struct promisor_info'.\n     \n         It will only store promisor remote information in its members. For now\n         it has only a 'name' member for the promisor remote name and an 'url'\n    -    member for its URL. We will use use a 'struct string_list' to store\n    -    the instances of 'struct promisor_info'. For each 'item' in the\n    +    member for its URL. We will use a 'struct string_list' to store the\n    +    instances of 'struct promisor_info'. For each 'item' in the\n         string_list, 'item->string' will point to the promisor remote name and\n         'item->util' will point to the corresponding 'struct promisor_info'\n         instance.\n    @@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n      \n     -  if (!strcmp(urls->v[i], remote_url))\n     +  if (!p->url)\n    -+          BUG(\"bad config_info (invalid URL) for remote '%s'\",\n    ++          BUG(\"bad config_info (URL is NULL) for remote '%s'\",\n     +              remote_name);\n     +\n     +  if (!strcmp(p->url, remote_url))\n2:  8f3111b4f2 ! 2:  f546756705 promisor-remote: allow a server to advertise more fields\n    @@ promisor-remote.c: static int allow_unsanitized(char ch)\n     +  char *fields = NULL;\n     +\n     +  if (!git_config_get_string(config_key, &fields) && *fields) {\n    ++          /* Split on any comma or space character */\n     +          string_list_split_in_place(fields_list, fields, \", \", -1);\n    ++          /*\n    ++           * Remove empty items that might result from trailing\n    ++           * commas, or from items being separated by both\n    ++           * commas and spaces.\n    ++           */\n     +          string_list_remove_empty_items(fields_list, 0);\n     +          filter_string_list(fields_list, 0, is_valid_field, (void *)config_key);\n     +  }\n3:  c91a1ba04a = 3:  3ac73b14eb promisor-remote: refactor how we parse advertised fields\n4:  ad68202057 = 4:  b75577eff2 promisor-remote: allow a client to check fields\n5:  e8efe62b7f = 5:  149b275087 promisor-remote: use string constants for 'name' and 'url' too\n\n\nChristian Couder (5):\n  promisor-remote: refactor to get rid of 'struct strvec'\n  promisor-remote: allow a server to advertise more fields\n  promisor-remote: refactor how we parse advertised fields\n  promisor-remote: allow a client to check fields\n  promisor-remote: use string constants for 'name' and 'url' too\n\n Documentation/config/promisor.adoc    |  62 ++++\n Documentation/gitprotocol-v2.adoc     |  59 +++-\n promisor-remote.c                     | 401 +++++++++++++++++++++-----\n t/t5710-promisor-remote-capability.sh |  65 +++++\n 4 files changed, 499 insertions(+), 88 deletions(-)\n\n-- \n2.50.0.2.g875523421d\n\n"},{"id":"520684","messageId":"20250625125055.1375596-2-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250625125055.1375596-1-christian.couder@gmail.com","subject":"[PATCH v5 1/5] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-25T12:50:51Z","receivedAt":"2025-06-25T12:51:16Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"In a following commit, we will use the new 'promisor-remote' protocol\ncapability introduced by d460267613 (Add 'promisor-remote' capability\nto protocol v2, 2025-02-18) to pass and process more information\nabout promisor remotes than just their name and url.\n\nFor that purpose, we will need to store information about other\nfields, especially information that might or might not be available\nfor different promisor remotes. Unfortunately using 'struct strvec',\nas we currently do, to store information about the promisor remotes\nwith one 'struct strvec' for each field like \"name\" or \"url\" does not\nscale easily in that case. We would need one 'struct strvec' for each\nnew field, and then we would have to pass all these 'struct strvec'\naround.\n\nLet's refactor this and introduce a new 'struct promisor_info'.\n\nIt will only store promisor remote information in its members. For now\nit has only a 'name' member for the promisor remote name and an 'url'\nmember for its URL. We will use a 'struct string_list' to store the\ninstances of 'struct promisor_info'. For each 'item' in the\nstring_list, 'item->string' will point to the promisor remote name and\n'item->util' will point to the corresponding 'struct promisor_info'\ninstance.\n\nExplicit members are used within 'struct promisor_info' for type\nsafety and clarity regarding the specific information being handled,\nrather than a generic key-value store. We want to specify and document\neach field and its content, so adding new members to the struct as\nmore fields are supported is fine.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 111 +++++++++++++++++++++++++++++-----------------\n 1 file changed, 70 insertions(+), 41 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 9d058586df..1cc614701b 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -314,9 +314,35 @@ static int allow_unsanitized(char ch)\n \treturn ch > 32 && ch < 127;\n }\n \n-static void promisor_info_vecs(struct repository *repo,\n-\t\t\t       struct strvec *names,\n-\t\t\t       struct strvec *urls)\n+/*\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+ */\n+struct promisor_info {\n+\tconst char *name;\n+\tconst char *url;\n+};\n+\n+static void promisor_info_list_clear(struct string_list *list)\n+{\n+\tfor (size_t i = 0; i < list->nr; i++) {\n+\t\tstruct promisor_info *p = list->items[i].util;\n+\t\tfree((char *)p->name);\n+\t\tfree((char *)p->url);\n+\t}\n+\tstring_list_clear(list, 1);\n+}\n+\n+/*\n+ * Populate 'list' with promisor remote information from the config.\n+ * The 'util' pointer of each list item will hold a 'struct promisor_info'.\n+ */\n+static void promisor_config_info_list(struct repository *repo, struct string_list *list)\n {\n \tstruct promisor_remote *r;\n \n@@ -328,8 +354,14 @@ static void promisor_info_vecs(struct repository *repo,\n \n \t\t/* Only add remotes with a non empty URL */\n \t\tif (!git_config_get_string_tmp(url_key, &url) && *url) {\n-\t\t\tstrvec_push(names, r->name);\n-\t\t\tstrvec_push(urls, url);\n+\t\t\tstruct promisor_info *new_info = xcalloc(1, sizeof(*new_info));\n+\t\t\tstruct string_list_item *item;\n+\n+\t\t\tnew_info->name = xstrdup(r->name);\n+\t\t\tnew_info->url = xstrdup(url);\n+\n+\t\t\titem = string_list_append(list, new_info->name);\n+\t\t\titem->util = new_info;\n \t\t}\n \n \t\tfree(url_key);\n@@ -340,47 +372,36 @@ char *promisor_remote_info(struct repository *repo)\n {\n \tstruct strbuf sb = STRBUF_INIT;\n \tint advertise_promisors = 0;\n-\tstruct strvec names = STRVEC_INIT;\n-\tstruct strvec urls = STRVEC_INIT;\n+\tstruct string_list config_info = STRING_LIST_INIT_NODUP;\n+\tstruct string_list_item *item;\n \n \tgit_config_get_bool(\"promisor.advertise\", &advertise_promisors);\n \n \tif (!advertise_promisors)\n \t\treturn NULL;\n \n-\tpromisor_info_vecs(repo, &names, &urls);\n+\tpromisor_config_info_list(repo, &config_info);\n \n-\tif (!names.nr)\n+\tif (!config_info.nr)\n \t\treturn NULL;\n \n-\tfor (size_t i = 0; i < names.nr; i++) {\n-\t\tif (i)\n+\tfor_each_string_list_item(item, &config_info) {\n+\t\tstruct promisor_info *p = item->util;\n+\n+\t\tif (item != config_info.items)\n \t\t\tstrbuf_addch(&sb, ';');\n+\n \t\tstrbuf_addstr(&sb, \"name=\");\n-\t\tstrbuf_addstr_urlencode(&sb, names.v[i], allow_unsanitized);\n+\t\tstrbuf_addstr_urlencode(&sb, p->name, allow_unsanitized);\n \t\tstrbuf_addstr(&sb, \",url=\");\n-\t\tstrbuf_addstr_urlencode(&sb, urls.v[i], allow_unsanitized);\n+\t\tstrbuf_addstr_urlencode(&sb, p->url, allow_unsanitized);\n \t}\n \n-\tstrvec_clear(&names);\n-\tstrvec_clear(&urls);\n+\tpromisor_info_list_clear(&config_info);\n \n \treturn strbuf_detach(&sb, NULL);\n }\n \n-/*\n- * Find first index of 'nicks' where there is 'nick'. 'nick' is\n- * compared case sensitively to the strings in 'nicks'. If not found\n- * 'nicks->nr' is returned.\n- */\n-static size_t remote_nick_find(struct strvec *nicks, const char *nick)\n-{\n-\tfor (size_t i = 0; i < nicks->nr; i++)\n-\t\tif (!strcmp(nicks->v[i], nick))\n-\t\t\treturn i;\n-\treturn nicks->nr;\n-}\n-\n enum accept_promisor {\n \tACCEPT_NONE = 0,\n \tACCEPT_KNOWN_URL,\n@@ -390,19 +411,23 @@ enum accept_promisor {\n \n static int should_accept_remote(enum accept_promisor accept,\n \t\t\t\tconst char *remote_name, const char *remote_url,\n-\t\t\t\tstruct strvec *names, struct strvec *urls)\n+\t\t\t\tstruct string_list *config_info)\n {\n-\tsize_t i;\n+\tstruct promisor_info *p;\n+\tstruct string_list_item *item;\n \n \tif (accept == ACCEPT_ALL)\n \t\treturn 1;\n \n-\ti = remote_nick_find(names, remote_name);\n+\t/* Get config info for that promisor remote */\n+\titem = string_list_lookup(config_info, remote_name);\n \n-\tif (i >= names->nr)\n+\tif (!item)\n \t\t/* We don't know about that remote */\n \t\treturn 0;\n \n+\tp = item->util;\n+\n \tif (accept == ACCEPT_KNOWN_NAME)\n \t\treturn 1;\n \n@@ -414,11 +439,15 @@ static int should_accept_remote(enum accept_promisor accept,\n \t\treturn 0;\n \t}\n \n-\tif (!strcmp(urls->v[i], remote_url))\n+\tif (!p->url)\n+\t\tBUG(\"bad config_info (URL is NULL) for remote '%s'\",\n+\t\t    remote_name);\n+\n+\tif (!strcmp(p->url, remote_url))\n \t\treturn 1;\n \n \twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s'\"),\n-\t\tremote_name, urls->v[i], remote_url);\n+\t\tremote_name, p->url, remote_url);\n \n \treturn 0;\n }\n@@ -430,8 +459,7 @@ static void filter_promisor_remote(struct repository *repo,\n \tstruct strbuf **remotes;\n \tconst char *accept_str;\n \tenum accept_promisor accept = ACCEPT_NONE;\n-\tstruct strvec names = STRVEC_INIT;\n-\tstruct strvec urls = STRVEC_INIT;\n+\tstruct string_list config_info = STRING_LIST_INIT_NODUP;\n \n \tif (!git_config_get_string_tmp(\"promisor.acceptfromserver\", &accept_str)) {\n \t\tif (!*accept_str || !strcasecmp(\"None\", accept_str))\n@@ -450,8 +478,10 @@ static void filter_promisor_remote(struct repository *repo,\n \tif (accept == ACCEPT_NONE)\n \t\treturn;\n \n-\tif (accept != ACCEPT_ALL)\n-\t\tpromisor_info_vecs(repo, &names, &urls);\n+\tif (accept != ACCEPT_ALL) {\n+\t\tpromisor_config_info_list(repo, &config_info);\n+\t\tstring_list_sort(&config_info);\n+\t}\n \n \t/* Parse remote info received */\n \n@@ -482,7 +512,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tif (remote_url)\n \t\t\tdecoded_url = url_percent_decode(remote_url);\n \n-\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, &names, &urls))\n+\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, &config_info))\n \t\t\tstrvec_push(accepted, decoded_name);\n \n \t\tstrbuf_list_free(elems);\n@@ -490,8 +520,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tfree(decoded_url);\n \t}\n \n-\tstrvec_clear(&names);\n-\tstrvec_clear(&urls);\n+\tpromisor_info_list_clear(&config_info);\n \tstrbuf_list_free(remotes);\n }\n \n-- \n2.50.0.2.g875523421d\n\n"},{"id":"520685","messageId":"20250625125055.1375596-4-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250625125055.1375596-1-christian.couder@gmail.com","subject":"[PATCH v5 3/5] promisor-remote: refactor how we parse advertised fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-25T12:50:53Z","receivedAt":"2025-06-25T12:51:18Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"In a follow up commit we are going to parse more fields, like a filter\nand a token, coming from the server when it advertises promisor remotes\nusing the \"promisor-remote\" capability.\n\nTo prepare for this, let's refactor the code that parses the advertised\nfields coming from the server into a new parse_one_advertised_remote()\nfunction that will populate a `struct promisor_info` with the content\nof the fields it parsed.\n\nWhile at it, let's also pass this `struct promisor_info` to the\nshould_accept_remote() function, instead of passing it the parsed name\nand url.\n\nThese changes will make it simpler to both parse more fields and access\nthe content of these parsed fields in follow up commits.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 91 ++++++++++++++++++++++++++++++++---------------\n 1 file changed, 62 insertions(+), 29 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex ddb9514de0..b68772d573 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -404,16 +404,20 @@ struct promisor_info {\n \tconst char *token;\n };\n \n+static void promisor_info_free(struct promisor_info *p)\n+{\n+\tfree((char *)p->name);\n+\tfree((char *)p->url);\n+\tfree((char *)p->filter);\n+\tfree((char *)p->token);\n+\tfree(p);\n+}\n+\n static void promisor_info_list_clear(struct string_list *list)\n {\n-\tfor (size_t i = 0; i < list->nr; i++) {\n-\t\tstruct promisor_info *p = list->items[i].util;\n-\t\tfree((char *)p->name);\n-\t\tfree((char *)p->url);\n-\t\tfree((char *)p->filter);\n-\t\tfree((char *)p->token);\n-\t}\n-\tstring_list_clear(list, 1);\n+\tfor (size_t i = 0; i < list->nr; i++)\n+\t\tpromisor_info_free(list->items[i].util);\n+\tstring_list_clear(list, 0);\n }\n \n static void set_one_field(struct promisor_info *p,\n@@ -530,11 +534,13 @@ enum accept_promisor {\n };\n \n static int should_accept_remote(enum accept_promisor accept,\n-\t\t\t\tconst char *remote_name, const char *remote_url,\n+\t\t\t\tstruct promisor_info *advertised,\n \t\t\t\tstruct string_list *config_info)\n {\n \tstruct promisor_info *p;\n \tstruct string_list_item *item;\n+\tconst char *remote_name = advertised->name;\n+\tconst char *remote_url = advertised->url;\n \n \tif (accept == ACCEPT_ALL)\n \t\treturn 1;\n@@ -572,6 +578,46 @@ static int should_accept_remote(enum accept_promisor accept,\n \treturn 0;\n }\n \n+static struct promisor_info *parse_one_advertised_remote(struct strbuf *remote_info)\n+{\n+\tstruct promisor_info *info = xcalloc(1, sizeof(*info));\n+\tstruct strbuf **elems = strbuf_split(remote_info, ',');\n+\n+\tfor (size_t i = 0; elems[i]; i++) {\n+\t\tchar *elem = elems[i]->buf;\n+\t\tchar *value;\n+\t\tchar *p = strchr(elem, '=');\n+\n+\t\tstrbuf_strip_suffix(elems[i], \",\");\n+\n+\t\tif (!p) {\n+\t\t\twarning(_(\"invalid element '%s' from remote info\"), elem);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\t*p = '\\0';\n+\t\tvalue = url_percent_decode(p + 1);\n+\n+\t\tif (!strcmp(elem, \"name\"))\n+\t\t\tinfo->name = value;\n+\t\telse if (!strcmp(elem, \"url\"))\n+\t\t\tinfo->url = value;\n+\t\telse\n+\t\t\tfree(value);\n+\t}\n+\n+\tstrbuf_list_free(elems);\n+\n+\tif (!info->name || !info->url) {\n+\t\twarning(_(\"server advertised a promisor remote without a name or URL: %s\"),\n+\t\t\tremote_info->buf);\n+\t\tpromisor_info_free(info);\n+\t\treturn NULL;\n+\t}\n+\n+\treturn info;\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@@ -608,32 +654,19 @@ static void filter_promisor_remote(struct repository *repo,\n \tremotes = strbuf_split_str(info, ';', 0);\n \n \tfor (size_t i = 0; remotes[i]; i++) {\n-\t\tstruct strbuf **elems;\n-\t\tconst char *remote_name = NULL;\n-\t\tconst char *remote_url = NULL;\n-\t\tchar *decoded_name = NULL;\n-\t\tchar *decoded_url = NULL;\n+\t\tstruct promisor_info *advertised;\n \n \t\tstrbuf_strip_suffix(remotes[i], \";\");\n-\t\telems = strbuf_split(remotes[i], ',');\n \n-\t\tfor (size_t j = 0; elems[j]; j++) {\n-\t\t\tstrbuf_strip_suffix(elems[j], \",\");\n-\t\t\tif (!skip_prefix(elems[j]->buf, \"name=\", &remote_name))\n-\t\t\t\tskip_prefix(elems[j]->buf, \"url=\", &remote_url);\n-\t\t}\n+\t\tadvertised = parse_one_advertised_remote(remotes[i]);\n \n-\t\tif (remote_name)\n-\t\t\tdecoded_name = url_percent_decode(remote_name);\n-\t\tif (remote_url)\n-\t\t\tdecoded_url = url_percent_decode(remote_url);\n+\t\tif (!advertised)\n+\t\t\tcontinue;\n \n-\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, &config_info))\n-\t\t\tstrvec_push(accepted, decoded_name);\n+\t\tif (should_accept_remote(accept, advertised, &config_info))\n+\t\t\tstrvec_push(accepted, advertised->name);\n \n-\t\tstrbuf_list_free(elems);\n-\t\tfree(decoded_name);\n-\t\tfree(decoded_url);\n+\t\tpromisor_info_free(advertised);\n \t}\n \n \tpromisor_info_list_clear(&config_info);\n-- \n2.50.0.2.g875523421d\n\n"},{"id":"520686","messageId":"20250625125055.1375596-3-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250625125055.1375596-1-christian.couder@gmail.com","subject":"[PATCH v5 2/5] promisor-remote: allow a server to advertise more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-25T12:50:52Z","receivedAt":"2025-06-25T12:51:18Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"For now the \"promisor-remote\" protocol capability can only pass \"name\"\nand \"url\" information from a server to a client in the form\n\"name=<remote_name>,url=<remote_url>\".\n\nLet's make it possible to pass more information by introducing a new\n\"promisor.sendFields\" configuration variable. This variable should\ncontain a comma or space separated list of field names that will be\nlooked up in the configuration of the remote on the server to find the\nvalues that will be passed to the client.\n\nOnly a set of predefined fields are allowed. The only fields in this\nset are \"partialCloneFilter\" and \"token\". The \"partialCloneFilter\"\nfield specifies the filter definition used by the promisor remote,\nand the \"token\" field can provide an authentication credential for\naccessing it.\n\nFor example, if \"promisor.sendFields\" is set to \"partialCloneFilter\",\nand the server has the \"remote.<name>.partialCloneFilter\" config\nvariable set to a value for a remote, then that value will be passed\nin the form \"partialCloneFilter=<value>\" after the \"name\" and \"url\"\nfields.\n\nA following commit will allow the client to use the information to\ndecide if it accepts the remote or not. For now the client doesn't do\nanything with the additional information it receives.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    |  22 +++++\n Documentation/gitprotocol-v2.adoc     |  59 +++++++++---\n promisor-remote.c                     | 134 ++++++++++++++++++++++++--\n t/t5710-promisor-remote-capability.sh |  31 ++++++\n 4 files changed, 221 insertions(+), 25 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex 2638b01f83..beb8f65518 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -9,6 +9,28 @@ promisor.advertise::\n \t\"false\", which means the \"promisor-remote\" capability is not\n \tadvertised.\n \n+promisor.sendFields::\n+\tA comma or space separated list of additional remote related\n+\tfield names. A server will send these field names and the\n+\tassociated field values from its configuration when\n+\tadvertising its promisor remotes using the \"promisor-remote\"\n+\tcapability, see linkgit:gitprotocol-v2[5]. Currently, only the\n+\t\"partialCloneFilter\" and \"token\" field names are supported.\n++\n+* \"partialCloneFilter\": contains the partial clone filter\n+  used for the remote.\n++\n+* \"token\": contains an authentication token for the remote.\n++\n+When a field name is part of this list and a corresponding\n+\"remote.foo.<field name>\" config variable is set on the server to a\n+non-empty value, then the field name and value will be sent when\n+advertising the promisor remote \"foo\".\n++\n+This list has no effect unless the \"promisor.advertise\" config\n+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\"\ndiff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\nindex 9a57005d77..0583fafa09 100644\n--- a/Documentation/gitprotocol-v2.adoc\n+++ b/Documentation/gitprotocol-v2.adoc\n@@ -785,33 +785,59 @@ retrieving the header from a bundle at the indicated URI, and thus\n save themselves and the server(s) the request(s) needed to inspect the\n headers of that bundle or bundles.\n \n-promisor-remote=<pr-infos>\n+promisor-remote=<pr-info>\n ~~~~~~~~~~~~~~~~~~~~~~~~~~\n \n The server may advertise some promisor remotes it is using or knows\n about to a client which may want to use them as its promisor remotes,\n-instead of this repository. In this case <pr-infos> should be of the\n+instead of this repository. In this case <pr-info> should be of the\n form:\n \n-\tpr-infos = pr-info | pr-infos \";\" pr-info\n+\tpr-info = pr-fields | pr-info \";\" pr-info\n \n-\tpr-info = \"name=\" pr-name | \"name=\" pr-name \",\" \"url=\" pr-url\n+\tpr-fields = field-name \"=\" field-value | pr-fields \",\" pr-fields\n \n-where `pr-name` is the urlencoded name of a promisor remote, and\n-`pr-url` the urlencoded URL of that promisor remote.\n+where all the `field-name` and `field-value` in a given `pr-fields`\n+are field names and values related to a single promisor remote.\n \n-In this case, if the client decides to use one or more promisor\n-remotes the server advertised, it can reply with\n-\"promisor-remote=<pr-names>\" where <pr-names> should be of the form:\n+The server MUST advertise at least the \"name\" and \"url\" field names\n+along with the associated field values, which are the name of a valid\n+remote and its URL, in each `pr-fields`. The \"name\" and \"url\" fields\n+MUST appear first in each pr-fields, in that order.\n \n-\tpr-names = pr-name | pr-names \";\" pr-name\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+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+\n+- \"token\": An authentication token that clients can use when\n+connecting to the remote. It corresponds to the \"remote.<name>.token\"\n+config setting.\n+\n+No other fields are defined by the protocol at this time. Clients MUST\n+ignore fields they don't recognize to allow for future protocol\n+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+\n+Field values MUST be urlencoded.\n+\n+If the client decides to use one or more promisor remotes the server\n+advertised, it can reply with \"promisor-remote=<pr-names>\" where\n+<pr-names> should be of the form:\n+\n+\tpr-names = pr-name | pr-names \";\" pr-names\n \n where `pr-name` is the urlencoded name of a promisor remote the server\n advertised and the client accepts.\n \n-Note that, everywhere in this document, `pr-name` MUST be a valid\n-remote name, and the ';' and ',' characters MUST be encoded if they\n-appear in `pr-name` or `pr-url`.\n+Note that, everywhere in this document, the ';' and ',' characters\n+MUST be encoded if they appear in `pr-name` or `field-value`.\n \n If the server doesn't know any promisor remote that could be good for\n a client to use, or prefers a client not to use any promisor remote it\n@@ -822,9 +848,10 @@ In this case, or if the client doesn't want to use any promisor remote\n the server advertised, the client shouldn't advertise the\n \"promisor-remote\" capability at all in its reply.\n \n-The \"promisor.advertise\" and \"promisor.acceptFromServer\" configuration\n-options can be used on the server and client side to control what they\n-advertise or accept respectively. See the documentation of these\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 \n Note that in the future it would be nice if the \"promisor-remote\"\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 1cc614701b..ddb9514de0 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -314,6 +314,80 @@ static int allow_unsanitized(char ch)\n \treturn ch > 32 && ch < 127;\n }\n \n+static const char promisor_field_filter[] = \"partialCloneFilter\";\n+static const char promisor_field_token[] = \"token\";\n+\n+/*\n+ * List of optional field names that can be used in the\n+ * \"promisor-remote\" protocol capability (others must be\n+ * ignored). Each field should correspond to a configurable property\n+ * of a remote that can be relevant for the client.\n+ */\n+static const char *known_fields[] = {\n+\tpromisor_field_filter, /* Filter used for partial clone */\n+\tpromisor_field_token,  /* Authentication token for the remote */\n+\tNULL\n+};\n+\n+/*\n+ * Check if 'field' is in the list of the known field names for the\n+ * \"promisor-remote\" protocol capability.\n+ */\n+static int is_known_field(const char *field)\n+{\n+\tconst char **p;\n+\n+\tfor (p = known_fields; *p; p++)\n+\t\tif (!strcasecmp(*p, field))\n+\t\t\treturn 1;\n+\treturn 0;\n+}\n+\n+static int is_valid_field(struct string_list_item *item, void *cb_data)\n+{\n+\tconst char *field = item->string;\n+\tconst char *config_key = (const char *)cb_data;\n+\n+\tif (!is_known_field(field)) {\n+\t\twarning(_(\"unsupported field '%s' in '%s' config\"), field, config_key);\n+\t\treturn 0;\n+\t}\n+\treturn 1;\n+}\n+\n+static char *fields_from_config(struct string_list *fields_list, const char *config_key)\n+{\n+\tchar *fields = NULL;\n+\n+\tif (!git_config_get_string(config_key, &fields) && *fields) {\n+\t\t/* Split on any comma or space character */\n+\t\tstring_list_split_in_place(fields_list, fields, \", \", -1);\n+\t\t/*\n+\t\t * Remove empty items that might result from trailing\n+\t\t * commas, or from items being separated by both\n+\t\t * commas and spaces.\n+\t\t */\n+\t\tstring_list_remove_empty_items(fields_list, 0);\n+\t\tfilter_string_list(fields_list, 0, is_valid_field, (void *)config_key);\n+\t}\n+\n+\treturn fields;\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 = 0;\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+}\n+\n /*\n  * Struct for promisor remotes involved in the \"promisor-remote\"\n  * protocol capability.\n@@ -326,6 +400,8 @@ static int allow_unsanitized(char ch)\n struct promisor_info {\n \tconst char *name;\n \tconst char *url;\n+\tconst char *filter;\n+\tconst char *token;\n };\n \n static void promisor_info_list_clear(struct string_list *list)\n@@ -334,15 +410,47 @@ static void promisor_info_list_clear(struct string_list *list)\n \t\tstruct promisor_info *p = list->items[i].util;\n \t\tfree((char *)p->name);\n \t\tfree((char *)p->url);\n+\t\tfree((char *)p->filter);\n+\t\tfree((char *)p->token);\n \t}\n \tstring_list_clear(list, 1);\n }\n \n+static void set_one_field(struct promisor_info *p,\n+\t\t\t  const char *field, const char *value)\n+{\n+\tif (!strcasecmp(field, promisor_field_filter))\n+\t\tp->filter = xstrdup(value);\n+\telse if (!strcasecmp(field, promisor_field_token))\n+\t\tp->token = xstrdup(value);\n+\telse\n+\t\tBUG(\"invalid field '%s'\", field);\n+}\n+\n+static void set_fields(struct promisor_info *p,\n+\t\t       struct string_list *field_names)\n+{\n+\tstruct string_list_item *item;\n+\n+\tfor_each_string_list_item(item, field_names) {\n+\t\tchar *key = xstrfmt(\"remote.%s.%s\", p->name, item->string);\n+\t\tconst char *val;\n+\t\tif (!git_config_get_string_tmp(key, &val) && *val)\n+\t\t\tset_one_field(p, item->string, val);\n+\t\tfree(key);\n+\t}\n+}\n+\n /*\n  * Populate 'list' with promisor remote information from the config.\n- * The 'util' pointer of each list item will hold a 'struct promisor_info'.\n+ * The 'util' pointer of each list item will hold a 'struct\n+ * promisor_info'. Except \"name\" and \"url\", only members of that\n+ * struct specified by the 'field_names' list are set (using values\n+ * from the configuration).\n  */\n-static void promisor_config_info_list(struct repository *repo, struct string_list *list)\n+static void promisor_config_info_list(struct repository *repo,\n+\t\t\t\t      struct string_list *list,\n+\t\t\t\t      struct string_list *field_names)\n {\n \tstruct promisor_remote *r;\n \n@@ -360,6 +468,9 @@ static void promisor_config_info_list(struct repository *repo, struct string_lis\n \t\t\tnew_info->name = xstrdup(r->name);\n \t\t\tnew_info->url = xstrdup(url);\n \n+\t\t\tif (field_names)\n+\t\t\t\tset_fields(new_info, field_names);\n+\n \t\t\titem = string_list_append(list, new_info->name);\n \t\t\titem->util = new_info;\n \t\t}\n@@ -380,7 +491,7 @@ char *promisor_remote_info(struct repository *repo)\n \tif (!advertise_promisors)\n \t\treturn NULL;\n \n-\tpromisor_config_info_list(repo, &config_info);\n+\tpromisor_config_info_list(repo, &config_info, fields_sent());\n \n \tif (!config_info.nr)\n \t\treturn NULL;\n@@ -395,6 +506,15 @@ char *promisor_remote_info(struct repository *repo)\n \t\tstrbuf_addstr_urlencode(&sb, p->name, allow_unsanitized);\n \t\tstrbuf_addstr(&sb, \",url=\");\n \t\tstrbuf_addstr_urlencode(&sb, p->url, allow_unsanitized);\n+\n+\t\tif (p->filter) {\n+\t\t\tstrbuf_addf(&sb, \",%s=\", promisor_field_filter);\n+\t\t\tstrbuf_addstr_urlencode(&sb, p->filter, allow_unsanitized);\n+\t\t}\n+\t\tif (p->token) {\n+\t\t\tstrbuf_addf(&sb, \",%s=\", promisor_field_token);\n+\t\t\tstrbuf_addstr_urlencode(&sb, p->token, allow_unsanitized);\n+\t\t}\n \t}\n \n \tpromisor_info_list_clear(&config_info);\n@@ -479,7 +599,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\treturn;\n \n \tif (accept != ACCEPT_ALL) {\n-\t\tpromisor_config_info_list(repo, &config_info);\n+\t\tpromisor_config_info_list(repo, &config_info, NULL);\n \t\tstring_list_sort(&config_info);\n \t}\n \n@@ -498,13 +618,9 @@ static void filter_promisor_remote(struct repository *repo,\n \t\telems = strbuf_split(remotes[i], ',');\n \n \t\tfor (size_t j = 0; elems[j]; j++) {\n-\t\t\tint res;\n \t\t\tstrbuf_strip_suffix(elems[j], \",\");\n-\t\t\tres = skip_prefix(elems[j]->buf, \"name=\", &remote_name) ||\n+\t\t\tif (!skip_prefix(elems[j]->buf, \"name=\", &remote_name))\n \t\t\t\tskip_prefix(elems[j]->buf, \"url=\", &remote_url);\n-\t\t\tif (!res)\n-\t\t\t\twarning(_(\"unknown element '%s' from remote info\"),\n-\t\t\t\t\telems[j]->buf);\n \t\t}\n \n \t\tif (remote_name)\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex cb061b1f35..204528b2e0 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -295,6 +295,37 @@ 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 promisor.sendFields\" '\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+\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 promisor.acceptfromserver=All \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that fields are properly transmitted\n+\tENCODED_URL=$(echo \"file://$(pwd)/lop\" | sed -e \"s/ /%20/g\") &&\n+\tPR1=\"name=lop,url=$ENCODED_URL,partialCloneFilter=blob:none\" &&\n+\tPR2=\"name=otherLop,url=https://invalid.invalid,partialCloneFilter=blob:limit=10k,token=fooBar\" &&\n+\ttest_grep \"clone< promisor-remote=$PR1;$PR2\" trace &&\n+\ttest_grep \"clone> promisor-remote=lop;otherLop\" 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 \n-- \n2.50.0.2.g875523421d\n\n"},{"id":"520687","messageId":"20250625125055.1375596-5-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250625125055.1375596-1-christian.couder@gmail.com","subject":"[PATCH v5 4/5] promisor-remote: allow a client to check fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-25T12:50:54Z","receivedAt":"2025-06-25T12:51:20Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"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\nLet's make it possible for a client to check if these fields match\nwhat it expects before accepting a promisor remote.\n\nWe allow this by introducing a new \"promisor.checkFields\"\nconfiguration variable. It should contain a comma or space separated\nlist of fields that will be checked.\n\nBy limiting the protocol to specific well-defined fields, we ensure\nboth server and client have a shared understanding of field\nsemantics and usage.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    | 40 ++++++++++++\n promisor-remote.c                     | 89 ++++++++++++++++++++++++---\n t/t5710-promisor-remote-capability.sh | 34 ++++++++++\n 3 files changed, 155 insertions(+), 8 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex beb8f65518..9682ada23c 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -50,3 +50,43 @@ promisor.acceptFromServer::\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+\n+promisor.checkFields::\n+\tA comma or space separated list of additional remote related\n+\tfield names. A client will check if the values of these fields\n+\ttransmitted by a server correspond to the values of these\n+\tfields in its own configuration before accepting a promisor\n+\tremote. Currently, \"partialCloneFilter\" and \"token\" are the\n+\tonly supported field names.\n++\n+If one of these field names (e.g., \"token\") is being checked for an\n+advertised promisor remote (e.g., \"foo\"), three conditions must be met\n+for the check of this specific field to pass:\n++\n+1. The corresponding local configuration (e.g., `remote.foo.token`)\n+   must be set.\n+2. The server must advertise the \"token\" field for remote \"foo\".\n+3. The value of the locally configured `remote.foo.token` must exactly\n+   match the value advertised by the server for the \"token\" field.\n++\n+If any of these conditions is not met for any field name listed in\n+`promisor.checkFields`, the advertised remote \"foo\" will be rejected.\n++\n+For the \"partialCloneFilter\" field, this allows the client to ensure\n+that the server's filter matches what it expects locally, preventing\n+inconsistencies in filtering behavior. For the \"token\" field, this can\n+be used to verify that authentication credentials match expected\n+values.\n++\n+Field names are compared case-insensitively. Field values are compared\n+case-sensitively.\n++\n+The \"name\" and \"url\" fields are always checked according to the\n+`promisor.acceptFromServer` policy, independently of this setting.\n++\n+The field names and values should be passed by the server through the\n+\"promisor-remote\" capability by using the `promisor.sendFields` config\n+variable. The fields will be checked only if the\n+`promisor.acceptFromServer` config variable is not set to \"None\". If\n+set to \"None\", this config variable will have no effect. See\n+linkgit:gitprotocol-v2[5].\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex b68772d573..049406c882 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -388,6 +388,20 @@ static struct string_list *fields_sent(void)\n \treturn &fields_list;\n }\n \n+static struct string_list *fields_checked(void)\n+{\n+\tstatic struct string_list fields_list = STRING_LIST_INIT_NODUP;\n+\tstatic int initialized = 0;\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+}\n+\n /*\n  * Struct for promisor remotes involved in the \"promisor-remote\"\n  * protocol capability.\n@@ -533,6 +547,61 @@ enum accept_promisor {\n \tACCEPT_ALL\n };\n \n+static int match_field_against_config(const char *field, const char *value,\n+\t\t\t\t      struct promisor_info *config_info)\n+{\n+\tif (config_info->filter && !strcasecmp(field, promisor_field_filter))\n+\t\treturn !strcmp(config_info->filter, value);\n+\telse if (config_info->token && !strcasecmp(field, promisor_field_token))\n+\t\treturn !strcmp(config_info->token, value);\n+\n+\treturn 0;\n+}\n+\n+static int all_fields_match(struct promisor_info *advertised,\n+\t\t\t    struct string_list *config_info,\n+\t\t\t    int in_list)\n+{\n+\tstruct string_list* fields = fields_checked();\n+\tstruct string_list_item *item_checked;\n+\n+\tfor_each_string_list_item(item_checked, fields) {\n+\t\tint match = 0;\n+\t\tconst char *field = item_checked->string;\n+\t\tconst char *value = NULL;\n+\t\tstruct string_list_item *item;\n+\n+\t\tif (!strcasecmp(field, promisor_field_filter))\n+\t\t\tvalue = advertised->filter;\n+\t\telse if (!strcasecmp(field, promisor_field_token))\n+\t\t\tvalue = advertised->token;\n+\n+\t\tif (!value)\n+\t\t\treturn 0;\n+\n+\t\tif (in_list) {\n+\t\t\tfor_each_string_list_item(item, config_info) {\n+\t\t\t\tstruct promisor_info *p = item->util;\n+\t\t\t\tif (match_field_against_config(field, value, p)) {\n+\t\t\t\t\tmatch = 1;\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t}\n+\t\t} else {\n+\t\t\titem = string_list_lookup(config_info, advertised->name);\n+\t\t\tif (item) {\n+\t\t\t\tstruct promisor_info *p = item->util;\n+\t\t\t\tmatch = match_field_against_config(field, value, p);\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (!match)\n+\t\t\treturn 0;\n+\t}\n+\n+\treturn 1;\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@@ -543,7 +612,7 @@ static int should_accept_remote(enum accept_promisor accept,\n \tconst char *remote_url = advertised->url;\n \n \tif (accept == ACCEPT_ALL)\n-\t\treturn 1;\n+\t\treturn all_fields_match(advertised, config_info, 1);\n \n \t/* Get config info for that promisor remote */\n \titem = string_list_lookup(config_info, remote_name);\n@@ -555,7 +624,7 @@ static int should_accept_remote(enum accept_promisor accept,\n \tp = item->util;\n \n \tif (accept == ACCEPT_KNOWN_NAME)\n-\t\treturn 1;\n+\t\treturn all_fields_match(advertised, config_info, 0);\n \n \tif (accept != ACCEPT_KNOWN_URL)\n \t\tBUG(\"Unhandled 'enum accept_promisor' value '%d'\", accept);\n@@ -570,7 +639,7 @@ static int should_accept_remote(enum accept_promisor accept,\n \t\t    remote_name);\n \n \tif (!strcmp(p->url, remote_url))\n-\t\treturn 1;\n+\t\treturn all_fields_match(advertised, config_info, 0);\n \n \twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s'\"),\n \t\tremote_name, p->url, remote_url);\n@@ -602,6 +671,10 @@ static struct promisor_info *parse_one_advertised_remote(struct strbuf *remote_i\n \t\t\tinfo->name = value;\n \t\telse if (!strcmp(elem, \"url\"))\n \t\t\tinfo->url = value;\n+\t\telse if (!strcasecmp(elem, promisor_field_filter))\n+\t\t\tinfo->filter = value;\n+\t\telse if (!strcasecmp(elem, promisor_field_token))\n+\t\t\tinfo->token = value;\n \t\telse\n \t\t\tfree(value);\n \t}\n@@ -644,11 +717,6 @@ static void filter_promisor_remote(struct repository *repo,\n \tif (accept == ACCEPT_NONE)\n \t\treturn;\n \n-\tif (accept != ACCEPT_ALL) {\n-\t\tpromisor_config_info_list(repo, &config_info, NULL);\n-\t\tstring_list_sort(&config_info);\n-\t}\n-\n \t/* Parse remote info received */\n \n \tremotes = strbuf_split_str(info, ';', 0);\n@@ -663,6 +731,11 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tif (!advertised)\n \t\t\tcontinue;\n \n+\t\tif (!config_info.nr) {\n+\t\t\tpromisor_config_info_list(repo, &config_info, fields_checked());\n+\t\t\tstring_list_sort(&config_info);\n+\t\t}\n+\n \t\tif (should_accept_remote(accept, advertised, &config_info))\n \t\t\tstrvec_push(accepted, advertised->name);\n \ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex 204528b2e0..023735d6a8 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -326,6 +326,40 @@ test_expect_success \"clone with promisor.sendFields\" '\n \tcheck_missing_objects server 1 \"$oid\"\n '\n \n+test_expect_success \"clone with promisor.checkFields\" '\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+\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.partialCloneFilter=\"blob:none\" \\\n+\t\t-c promisor.acceptfromserver=All \\\n+\t\t-c promisor.checkFields=partialcloneFilter \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that fields are properly transmitted\n+\tENCODED_URL=$(echo \"file://$(pwd)/lop\" | sed -e \"s/ /%20/g\") &&\n+\tPR1=\"name=lop,url=$ENCODED_URL,partialCloneFilter=blob:none\" &&\n+\tPR2=\"name=otherLop,url=https://invalid.invalid,partialCloneFilter=blob:limit=10k,token=fooBar\" &&\n+\ttest_grep \"clone< promisor-remote=$PR1;$PR2\" trace &&\n+\ttest_grep \"clone> promisor-remote=lop\" trace &&\n+\ttest_grep ! \"clone> promisor-remote=lop;otherLop\" 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 \n-- \n2.50.0.2.g875523421d\n\n"},{"id":"520688","messageId":"20250625125055.1375596-6-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250625125055.1375596-1-christian.couder@gmail.com","subject":"[PATCH v5 5/5] promisor-remote: use string constants for 'name' and 'url' too","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-25T12:50:55Z","receivedAt":"2025-06-25T12:51:21Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"A previous commit started to define `promisor_field_filter` and\n`promisor_field_token`, and used them instead of the\n\"partialCloneFilter\" and \"token\" string literals.\n\nLet's do the same for \"name\" and \"url\" to avoid repeating them\nseveral times and for consistency with the other fields.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 14 ++++++++++----\n 1 file changed, 10 insertions(+), 4 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 049406c882..1dc2f525ce 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -314,6 +314,12 @@ static int allow_unsanitized(char ch)\n \treturn ch > 32 && ch < 127;\n }\n \n+/*\n+ * All the fields used in \"promisor-remote\" protocol capability,\n+ * including the mandatory \"name\" and \"url\" ones.\n+ */\n+static const char promisor_field_name[] = \"name\";\n+static const char promisor_field_url[] = \"url\";\n static const char promisor_field_filter[] = \"partialCloneFilter\";\n static const char promisor_field_token[] = \"token\";\n \n@@ -520,9 +526,9 @@ char *promisor_remote_info(struct repository *repo)\n \t\tif (item != config_info.items)\n \t\t\tstrbuf_addch(&sb, ';');\n \n-\t\tstrbuf_addstr(&sb, \"name=\");\n+\t\tstrbuf_addf(&sb, \"%s=\", promisor_field_name);\n \t\tstrbuf_addstr_urlencode(&sb, p->name, allow_unsanitized);\n-\t\tstrbuf_addstr(&sb, \",url=\");\n+\t\tstrbuf_addf(&sb, \",%s=\", promisor_field_url);\n \t\tstrbuf_addstr_urlencode(&sb, p->url, allow_unsanitized);\n \n \t\tif (p->filter) {\n@@ -667,9 +673,9 @@ static struct promisor_info *parse_one_advertised_remote(struct strbuf *remote_i\n \t\t*p = '\\0';\n \t\tvalue = url_percent_decode(p + 1);\n \n-\t\tif (!strcmp(elem, \"name\"))\n+\t\tif (!strcmp(elem, promisor_field_name))\n \t\t\tinfo->name = value;\n-\t\telse if (!strcmp(elem, \"url\"))\n+\t\telse if (!strcmp(elem, promisor_field_url))\n \t\t\tinfo->url = value;\n \t\telse if (!strcasecmp(elem, promisor_field_filter))\n \t\t\tinfo->filter = value;\n-- \n2.50.0.2.g875523421d\n\n"},{"id":"520689","messageId":"CAP8UFD3-ayeLMiU+kF_4bF-XnBDT1xuWHzq4eLts9by0WikEhA@mail.gmail.com","threadId":"63293","inReplyTo":"CAOLa=ZSAU4vfNNvcmpVrEavYWss_txVwfutY5ZZLzvVmv5wPdA@mail.gmail.com","subject":"Re: [PATCH v4 2/5] promisor-remote: allow a server to advertise more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-25T12:51:32Z","receivedAt":"2025-06-25T12:51:46Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Thu, Jun 19, 2025 at 2:15 PM Karthik Nayak <karthik.188@gmail.com> wrote:\n>\n> Christian Couder <christian.couder@gmail.com> writes:\n\n[...]\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> > +     test_config -C server promisor.sendFields \"partialCloneFilter, token\" &&\n>\n> What about testing only 'comma' separated and only 'space' separated\n> fields, since we support those too.\n\nI think it would only test the following 2 lines from fields_from_config():\n\nstring_list_split_in_place(fields_list, fields, \", \", -1);\nstring_list_remove_empty_items(fields_list, 0);\n\nwhich should be already tested separately in some string_list C tests.\n\nSo I don't think it's worth it.\n\nThanks.\n"},{"id":"520690","messageId":"CAP8UFD2TYwXPm7djcpkuCjmh4h2FBPF5eD7NAQHt1GGOn63OgA@mail.gmail.com","threadId":"63293","inReplyTo":"ojee3heqvvlk3v4ftuzaulm5vu7tvmc5idvqbda2ioiuurbmdw@sevh7jtsbbad","subject":"Re: [PATCH v4 2/5] promisor-remote: allow a server to advertise more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-25T12:51:38Z","receivedAt":"2025-06-25T12:51:52Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Mon, Jun 23, 2025 at 10:04 PM Justin Tobler <jltobler@gmail.com> wrote:\n>\n> On 25/06/11 03:45PM, Christian Couder wrote:\n\n> > +static char *fields_from_config(struct string_list *fields_list, const char *config_key)\n> > +{\n> > +     char *fields = NULL;\n> > +\n> > +     if (!git_config_get_string(config_key, &fields) && *fields) {\n> > +             string_list_split_in_place(fields_list, fields, \", \", -1);\n> > +             string_list_remove_empty_items(fields_list, 0);\n>\n> Ok, in this version we now filter out empty entries from the\n> string_list. Previously if fields were specified with both a comma and\n> SP character (i.e. \"partialCloneFilter, token\"), an empty entry would be\n> parsed in the middle and lead to a warning message.\n>\n> This change is good because it would be pretty natural for a user to\n> specify the config with both. It might be nice to leave a comment\n> explaining why we do this though as it may be confusing without context.\n\nYeah, I have added some comments, so it looks like this in v5:\n\n        /* Split on any comma or space character */\n        string_list_split_in_place(fields_list, fields, \", \", -1);\n        /*\n         * Remove empty items that might result from trailing\n         * commas, or from items being separated by both\n         * commas and spaces.\n         */\n        string_list_remove_empty_items(fields_list, 0);\n\nThanks.\n"},{"id":"520691","messageId":"CAP8UFD1LxX966Cd__AUg=trOWPc6-2K55-diarEU4Z69zLNtgg@mail.gmail.com","threadId":"63293","inReplyTo":"2z3uanklsfnrgxmiqjt2innv2diownbpwtgcj67bi7udwjdqpg@ackusinuz7c6","subject":"Re: [PATCH v4 1/5] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-25T12:52:40Z","receivedAt":"2025-06-25T12:52:54Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Mon, Jun 23, 2025 at 9:43 PM Justin Tobler <jltobler@gmail.com> wrote:\n>\n> On 25/06/11 03:45PM, Christian Couder wrote:\n\n> > It will only store promisor remote information in its members. For now\n> > it has only a 'name' member for the promisor remote name and an 'url'\n> > member for its URL. We will use use a 'struct string_list' to store\n>\n> s/use use/use/\n\nFixed in v5.\n\n[...]\n\n> > @@ -340,47 +372,36 @@ char *promisor_remote_info(struct repository *repo)\n> >  {\n> >       struct strbuf sb = STRBUF_INIT;\n> >       int advertise_promisors = 0;\n> > -     struct strvec names = STRVEC_INIT;\n> > -     struct strvec urls = STRVEC_INIT;\n> > +     struct string_list config_info = STRING_LIST_INIT_NODUP;\n>\n> nit: Ok in this context, \"config_info\" is specific to the list of\n> promisor_info not just generic git configuration. Something like\n> \"promisor_info_list\" would be a bit more explicit, but I don't feel\n> super strongly.\n\nYeah, it's not generic config, but it's still config, and I think\nthat's important to help understand what the code does when it uses\nit, so we should keep \"config\" somehow in this variable name. For now\nI haven't changed it.\n\n> > +     struct string_list_item *item;\n> >\n> >       git_config_get_bool(\"promisor.advertise\", &advertise_promisors);\n> >\n> >       if (!advertise_promisors)\n> >               return NULL;\n> >\n> > -     promisor_info_vecs(repo, &names, &urls);\n> > +     promisor_config_info_list(repo, &config_info);\n> >\n> > -     if (!names.nr)\n> > +     if (!config_info.nr)\n> >               return NULL;\n> >\n> > -     for (size_t i = 0; i < names.nr; i++) {\n> > -             if (i)\n> > +     for_each_string_list_item(item, &config_info) {\n> > +             struct promisor_info *p = item->util;\n> > +\n> > +             if (item != config_info.items)\n> >                       strbuf_addch(&sb, ';');\n>\n> Out of curiousity, is it invalid for the trailing promisor remote entry\n> to end with a ';'? It would be simpler if each entry could just end with\n> a semi-colon.\n\nIt would work but it's not really valid.\n\nIn \"Documentation/gitprotocol-v2.adoc\" which specifies the protocol,\nmost of the time when different items are transmitted together they\nare separated by SP like:\n\nitem1 SP item2 SP item3 LF\n\nand there is no SP before the LF.\n\nFor the \"promisor-remote\", we need something more complex but for\nconsistency I think it makes sense to not repeat separators at the\nend, in the same way as SP are not repeated before LF.\n\n> > -     if (!strcmp(urls->v[i], remote_url))\n> > +     if (!p->url)\n> > +             BUG(\"bad config_info (invalid URL) for remote '%s'\",\n> > +                 remote_name);\n>\n> Ok just to clarify, it is invalid for a promisor remote to not have a\n> URL specified. If so, it might be better to say \"empty URL\" or something\n> along those lines.\n\nYeah, in v5 it's now \"URL is NULL\" instead of \"invalid URL\".\n\nThanks.\n"},{"id":"520692","messageId":"CAP8UFD07pK0iAY4cqu4gCAASeaJRtu1+FhPtgcbJBP0Th72sKg@mail.gmail.com","threadId":"63293","inReplyTo":"CAOLa=ZQm+Hm_rMNJY7B4dGAuV=nFecsiPZ9N8uChhEwzb8n+Hw@mail.gmail.com","subject":"Re: [PATCH v4 1/5] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-25T12:53:00Z","receivedAt":"2025-06-25T12:53:14Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Thu, Jun 19, 2025 at 1:53 PM Karthik Nayak <karthik.188@gmail.com> wrote:\n>\n> Christian Couder <christian.couder@gmail.com> writes:\n>\n> > In a following commit, we will use the new 'promisor-remote' protocol\n> > capability introduced by d460267613 (Add 'promisor-remote' capability\n> > to protocol v2, 2025-02-18) to pass and process more information\n> > about promisor remotes than just their name and url.\n> >\n> > For that purpose, we will need to store information about other\n> > fields, especially information that might or might not be available\n> > for different promisor remotes. Unfortunately using 'struct strvec',\n> > as we currently do, to store information about the promisor remotes\n> > with one 'struct strvec' for each field like \"name\" or \"url\" does not\n> > scale easily in that case.\n> >\n>\n> Nit: It would be nice to mention _why_ it doesn't scale easily here.\n\nIn v5, I have added the following to better explain it:\n\n\"We would need one 'struct strvec' for each new field, and then we\nwould have to pass all these 'struct strvec' around.\"\n\n[...]\n\n> The rest of the patch looks good to me.\n\nThanks!\n"},{"id":"520723","messageId":"xmqqv7ojvjjs.fsf@gitster.g","threadId":"63293","inReplyTo":"20250625125055.1375596-2-christian.couder@gmail.com","subject":"Re: [PATCH v5 1/5] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-06-25T17:05:11Z","receivedAt":"2025-06-25T17:05:14Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> @@ -414,11 +439,15 @@ static int should_accept_remote(enum accept_promisor accept,\n>  \t\treturn 0;\n>  \t}\n>  \n> -\tif (!strcmp(urls->v[i], remote_url))\n> +\tif (!p->url)\n> +\t\tBUG(\"bad config_info (URL is NULL) for remote '%s'\",\n> +\t\t    remote_name);\n> +\n> +\tif (!strcmp(p->url, remote_url))\n>  \t\treturn 1;\n\nThe code seems to trust string_list enough that once an earlier call\nto string_list_lookup() in this code path finds the entry for the\nremote_name, item->util is assumed to be non-NULL and points at a\nvalid promisor_info instance.  But it does not seem to be defensive\nagainst p being NULL, but it does so for p->url being NULL, which\nsmells fishy.  Once promisor_config_info_list() reads the data from\nthe configuration, do we ever update the contents and the risk of us\ncorrupting p->url while we are running is great enough to warrant\nbeing defensive?\n\nMy inclination is to suggest removing the check and BUG() here, but\nI may be missing something obvious (like \"p->url gets NULL'ed out in\nthis function, but then such a remote is never checked with this\nfunction because such and such logic in that function\").\n\nThanks.\n"},{"id":"520735","messageId":"xmqqbjqbsbe0.fsf@gitster.g","threadId":"63293","inReplyTo":"20250625125055.1375596-3-christian.couder@gmail.com","subject":"Re: [PATCH v5 2/5] promisor-remote: allow a server to advertise more fields","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-06-25T22:29:43Z","receivedAt":"2025-06-25T22:29:46Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> For now the \"promisor-remote\" protocol capability can only pass \"name\"\n> and \"url\" information from a server to a client in the form\n> \"name=<remote_name>,url=<remote_url>\".\n>\n> Let's make it possible to pass more information by introducing\n\nMotivation, like \"in order to allow the client more efficient\ntransfer\", \"in order to give more control on what can be fetched\nfrom the server\", etc., needs to be given before we say \"let's do\nX\".  We want to do X not because we can do X; we want to do X as it\ncurrently seems to us the best way to achieve Y.  What is our Y in\nthis case for the proposed feature?\n\n> a new\n> \"promisor.sendFields\" configuration variable. This variable should\n> contain a comma or space separated list\n\nBy making it easier for casual humans who manually write the\nconfiguration variable (presumably while testing) and allowing both\ncomma and space as separator, this design decision is forcing one\nmore rule to worry about for those who are writing the parser for\nthe value.  There may be some existing configuration variables with\nsuch a \"leninent\" syntax, but I'd rather see us not make the mess\neven worse.\n\n> of field names that will be\n> looked up in the configuration of the remote on the server to find the\n> values that will be passed to the client.\n\nI know the name \"field\" was discussed in earlier iterations, but\nthese three lines together with \"For example\" in a later paragraph,\nit hints to me that this mechanism is to choose variable-value pairs\nfor which among remotes.<name>.* variables to send after name=<name>\nand url=<url>; is my understanding correct?  If so, can we clarify\nthe paragraphs around here even more so that I do not even have to\nask this question?\n\nWhat do we call the third-level of a variable name in the\nconfiguration file?  The description on the \"--regexp\" option in\n\"git config --help\" hints one:\n\n    With `get`, interpret the name as a regular expression. Regular\n    expression matching is currently case-sensitive and done against\n    a canonicalized version of the key in which section and variable\n    names are lowercased, but subsection names are not.\n\nSo a for remote.origin.partialCloneFilter, \"remote\" is the section\nname, \"origin\" is the subsection name, and \"partialCloneFilter\" is\nthe variable name.\n\nArmed with that knowledge, perhaps something like:\n\n    <<the motivation comes here, and then ...>> In order to give\n    additional information to the client, the server side can set a\n    new 'promisor.sendAdditionalVariables' configuration variable,\n    whose value is a comma separated list of variable names.  \n\n    The values of the configuration variables specified by this\n    variable for the given remote [*] are sent as comma separated\n    list of variable=<value>, after name=<name>,url=<url> in the\n    promisor-remote capability.\n\n    [*] Concatenating each of these variable names listed as the\n    value of promisor.sendAdditionalVariables after remote.<name>.\n    results in the configuration variables exposed to the client.\n\nto replace all of the above, and then it ...\n\n> Only a set of predefined fields are allowed. The only fields in this\n> set are \"partialCloneFilter\" and \"token\". The \"partialCloneFilter\"\n> field specifies the filter definition used by the promisor remote,\n> and the \"token\" field can provide an authentication credential for\n> accessing it.\n\n... is a good thing to describe that we have these two supported.\n\nAnd this one ...\n\n> For example, if \"promisor.sendFields\" is set to \"partialCloneFilter\",\n> and the server has the \"remote.<name>.partialCloneFilter\" config\n> variable set to a value for a remote, then that value will be passed\n> in the form \"partialCloneFilter=<value>\" after the \"name\" and \"url\"\n> fields.\n\n... has already been covered.  My main point is that we should\nclarify what a 'field' is and how it relates to the variables in the\nconfiguration file of which side (the server, not the client) a lot\nearlier than we say \"these two are the only ones that are\nsupported\".  Before understanding what a field is, \"only these two\nare valid\" does not give readers much useful information.\n\n> A following commit will allow the client to use the information to\n> decide if it accepts the remote or not. For now the client doesn't do\n> anything with the additional information it receives.\n>\n> Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n> ---\n>  Documentation/config/promisor.adoc    |  22 +++++\n>  Documentation/gitprotocol-v2.adoc     |  59 +++++++++---\n>  promisor-remote.c                     | 134 ++++++++++++++++++++++++--\n>  t/t5710-promisor-remote-capability.sh |  31 ++++++\n>  4 files changed, 221 insertions(+), 25 deletions(-)\n>\n> diff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\n> index 2638b01f83..beb8f65518 100644\n> --- a/Documentation/config/promisor.adoc\n> +++ b/Documentation/config/promisor.adoc\n> @@ -9,6 +9,28 @@ promisor.advertise::\n>  \t\"false\", which means the \"promisor-remote\" capability is not\n>  \tadvertised.\n>  \n> +promisor.sendFields::\n> +\tA comma or space separated list of additional remote related\n> +\tfield names. A server will send these field names and the\n> +\tassociated field values from its configuration when\n> +\tadvertising its promisor remotes using the \"promisor-remote\"\n> +\tcapability, see linkgit:gitprotocol-v2[5]. Currently, only the\n> +\t\"partialCloneFilter\" and \"token\" field names are supported.\n> ++\n> +* \"partialCloneFilter\": contains the partial clone filter\n> +  used for the remote.\n> ++\n> +* \"token\": contains an authentication token for the remote.\n> ++\n> +When a field name is part of this list and a corresponding\n> +\"remote.foo.<field name>\" config variable is set on the server to a\n> +non-empty value, then the field name and value will be sent when\n> +advertising the promisor remote \"foo\".\n> ++\n> +This list has no effect unless the \"promisor.advertise\" config\n> +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> diff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\n> index 9a57005d77..0583fafa09 100644\n> --- a/Documentation/gitprotocol-v2.adoc\n> +++ b/Documentation/gitprotocol-v2.adoc\n> @@ -785,33 +785,59 @@ retrieving the header from a bundle at the indicated URI, and thus\n>  save themselves and the server(s) the request(s) needed to inspect the\n>  headers of that bundle or bundles.\n>  \n> -promisor-remote=<pr-infos>\n> +promisor-remote=<pr-info>\n>  ~~~~~~~~~~~~~~~~~~~~~~~~~~\n>  \n>  The server may advertise some promisor remotes it is using or knows\n>  about to a client which may want to use them as its promisor remotes,\n> +instead of this repository. In this case <pr-info> should be of the\n>  form:\n>  \n> +\tpr-info = pr-fields | pr-info \";\" pr-info\n>  \n> +\tpr-fields = field-name \"=\" field-value | pr-fields \",\" pr-fields\n\nTypically a name is at most a few words (concatenated with some\npunctuation), and url would probably be a few hundred bytes at most?\ncan we afford to cram even more var=value on the same line?\n\nThe syntax allows you to send more than one name/url pair on the\nsame promisor-remote.  Length aside, does this pr-fields mechanism\ngive the values of these fields for multiple remotes?\n\nSuppose you want to advertise promisor-remote \"A\" and \"B\", and your\nsendFields says \"token\".  What does your promisor-remote=<pr-info> look\nlike in such a case?\n\n    name=A,url=https://git.git/A,token=foo;name=B,url=https://git.git/B,token=bar\n\nDo we explicitly say that the set of var=val on promisor-remote are\ngrouped into distinct sets with ';' and each set talks about only\none remote?  I.e.\n\n    name=A,name=B,url=https://git.git/A,url=https://git.git/B,token=foo,token=bar\n\nmay be syntactically allowed in the above BNF, but it needs _some_\nrule to match which URL and which token go with which name.  Do we\nalso need to say that within a single ';' separated group, a variable\ncan only appear once?  IOW, this is invalid?\n\n    name=A,url=https://git.git/A,token=foo,token=bar\n\n> +where all the `field-name` and `field-value` in a given `pr-fields`\n> +are field names and values related to a single promisor remote.\n>  \n> +The server MUST advertise at least the \"name\" and \"url\" field names\n> +along with the associated field values, which are the name of a valid\n> +remote and its URL, in each `pr-fields`. The \"name\" and \"url\" fields\n> +MUST appear first in each pr-fields, in that order.\n\nWith\n\n    pr-fields = pr-fields \",\" pr-fields\n\nthe rule in the last sentence does not make sense.  You'd need a\ndistinct BNF non-terminal to name the one you want the rule to apply\nto.  Perhaps something like\n\n\tpr-info = pr-one-remote-info | pr-info ';' pr-one-remote-info \n\tpr-one-remote-info = pr-fields\n        pr-fields = pr-field | pr-fields ',' pr-field\n\tpr-field = variable '=' value\n\nThen you can safely say 'name' and 'url', must be the first two that\nappear in each pr-one-remote-info.\n\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> +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> +\n> +- \"token\": An authentication token that clients can use when\n> +connecting to the remote. It corresponds to the \"remote.<name>.token\"\n> +config setting.\n> +\n> +No other fields are defined by the protocol at this time. Clients MUST\n> +ignore fields they don't recognize to allow for future protocol\n> +extensions.\n\nThis forces us to never add support for a field that MUST be\nunderstood for the protocol to operate correctly.  Is it sensible?\n\nJust like the index file format defines optional extensions that can\nbe ignored (implication of which is that you MUST die if you do not\nunderstand any non-optional ones), shouldn't we have some mechanism\nto tell \"if you do not understand this, do not use the remote, or\nyour repository will be broken in a horrible way\"?\n\nI dunno.\n"},{"id":"520819","messageId":"c49d73de-568a-4584-aa8f-9a9ffd68e4ce@free.fr","threadId":"63293","inReplyTo":"20250625125055.1375596-3-christian.couder@gmail.com","subject":"Re: [PATCH v5 2/5] promisor-remote: allow a server to advertise more fields","fromName":"Jean-Noël Avila","fromEmail":"jn.avila@free.fr","sentAt":"2025-06-27T18:47:53Z","receivedAt":"2025-06-27T18:53:54Z","isPatch":true,"sender":{"key":"jn.avila@free.fr","avatar":"https://avatars.githubusercontent.com/u/156172?v=4"},"body":"Le 25/06/2025 à 14:50, Christian Couder a écrit :\n> For now the \"promisor-remote\" protocol capability can only pass \"name\"\n> and \"url\" information from a server to a client in the form\n> \"name=<remote_name>,url=<remote_url>\".\n> \n> Let's make it possible to pass more information by introducing a new\n> \"promisor.sendFields\" configuration variable. This variable should\n> contain a comma or space separated list of field names that will be\n> looked up in the configuration of the remote on the server to find the\n> values that will be passed to the client.\n> \n> Only a set of predefined fields are allowed. The only fields in this\n> set are \"partialCloneFilter\" and \"token\". The \"partialCloneFilter\"\n> field specifies the filter definition used by the promisor remote,\n> and the \"token\" field can provide an authentication credential for\n> accessing it.\n> \n> For example, if \"promisor.sendFields\" is set to \"partialCloneFilter\",\n> and the server has the \"remote.<name>.partialCloneFilter\" config\n> variable set to a value for a remote, then that value will be passed\n> in the form \"partialCloneFilter=<value>\" after the \"name\" and \"url\"\n> fields.\n> \n> A following commit will allow the client to use the information to\n> decide if it accepts the remote or not. For now the client doesn't do\n> anything with the additional information it receives.\n> \n> Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n> ---\n>  Documentation/config/promisor.adoc    |  22 +++++\n>  Documentation/gitprotocol-v2.adoc     |  59 +++++++++---\n>  promisor-remote.c                     | 134 ++++++++++++++++++++++++--\n>  t/t5710-promisor-remote-capability.sh |  31 ++++++\n>  4 files changed, 221 insertions(+), 25 deletions(-)\n> \n> diff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\n> index 2638b01f83..beb8f65518 100644\n> --- a/Documentation/config/promisor.adoc\n> +++ b/Documentation/config/promisor.adoc\n> @@ -9,6 +9,28 @@ promisor.advertise::\n>  \t\"false\", which means the \"promisor-remote\" capability is not\n>  \tadvertised.\n>  \n> +promisor.sendFields::\n> +\tA comma or space separated list of additional remote related\n> +\tfield names. A server will send these field names and the\n> +\tassociated field values from its configuration when\n> +\tadvertising its promisor remotes using the \"promisor-remote\"\n> +\tcapability, see linkgit:gitprotocol-v2[5]. Currently, only the\n> +\t\"partialCloneFilter\" and \"token\" field names are supported.\n> ++\n> +* \"partialCloneFilter\": contains the partial clone filter\n> +  used for the remote.\n> ++\n> +* \"token\": contains an authentication token for the remote.\n> ++\n\nThis kind of text structure calls a description list instead and you can\nalready use backquotes:\n\n`partialCloneFilter`:: contains the partial clone filter\n> +  used for the remote.\n> ++\n> +`token`:: contains an authentication token for the remote.\n\n> +When a field name is part of this list and a corresponding\n> +\"remote.foo.<field name>\" config variable is set on the server to a\n\nPlease no space in placeholders: <field-name>\n\n> +non-empty value, then the field name and value will be sent when\n> +advertising the promisor remote \"foo\".\n> ++\n> +This list has no effect unless the \"promisor.advertise\" config\n> +variable is set to \"true\", and the \"name\" and \"url\" fields are always\n> +advertised regardless of this setting.\n> +\n\nMore generally, I am a bit annoyed by the usage of the \"will\" auxiliary\nwhen not expressing a true future. For an international audience, this\ncan be misleading. The plain language[1] philosophy mandates to not use\nauxiliaries other than where they are required (no convoluted sentences).\n\nWould it make sense to start a style guide to help writing consistent\ndocumentation that targets people whose first language is not English?\nBeing an non native speaker, I often find our docs too literate, with\nlengthy sentences.\n\n[1] https://en.wikipedia.org/wiki/Plain_language\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> diff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\n> index 9a57005d77..0583fafa09 100644\n> --- a/Documentation/gitprotocol-v2.adoc\n> +++ b/Documentation/gitprotocol-v2.adoc\n> @@ -785,33 +785,59 @@ retrieving the header from a bundle at the indicated URI, and thus\n>  save themselves and the server(s) the request(s) needed to inspect the\n>  headers of that bundle or bundles.\n>  \n> -promisor-remote=<pr-infos>\n> +promisor-remote=<pr-info>\n>  ~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nBe careful to adjust the length of the underline to the one of the title\n\n>  \n>  The server may advertise some promisor remotes it is using or knows\n >  about to a client which may want to use them as its promisor\nremotes,> -instead of this repository. In this case <pr-infos> should be\nof the\n> +instead of this repository. In this case <pr-info> should be of the\n>  form:\n>  \n> -\tpr-infos = pr-info | pr-infos \";\" pr-info\n> +\tpr-info = pr-fields | pr-info \";\" pr-info\n>  \n> -\tpr-info = \"name=\" pr-name | \"name=\" pr-name \",\" \"url=\" pr-url\n> +\tpr-fields = field-name \"=\" field-value | pr-fields \",\" pr-fields\n>  \n> -where `pr-name` is the urlencoded name of a promisor remote, and\n> -`pr-url` the urlencoded URL of that promisor remote.\n> +where all the `field-name` and `field-value` in a given `pr-fields`\n> +are field names and values related to a single promisor remote.\n>  \n> -In this case, if the client decides to use one or more promisor\n> -remotes the server advertised, it can reply with\n> -\"promisor-remote=<pr-names>\" where <pr-names> should be of the form:\n> +The server MUST advertise at least the \"name\" and \"url\" field names\n> +along with the associated field values, which are the name of a valid\n> +remote and its URL, in each `pr-fields`. The \"name\" and \"url\" fields\n> +MUST appear first in each pr-fields, in that order.\n>  \n> -\tpr-names = pr-name | pr-names \";\" pr-name\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> +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> +\n> +- \"token\": An authentication token that clients can use when\n> +connecting to the remote. It corresponds to the \"remote.<name>.token\"\n> +config setting.\n> +\n\nThis list can be turned into a description list.\n\n> +No other fields are defined by the protocol at this time. Clients MUST\n> +ignore fields they don't recognize to allow for future protocol\n> +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> +\n> +Field values MUST be urlencoded.\n> +\n> +If the client decides to use one or more promisor remotes the server\n> +advertised, it can reply with \"promisor-remote=<pr-names>\" where\n> +<pr-names> should be of the form:\n> +\n> +\tpr-names = pr-name | pr-names \";\" pr-names\n\nHere the syntax used is not compatible with synopsis. Would it make\nsense to uniformize it, or is BNF ok?\n\n>  \n>  where `pr-name` is the urlencoded name of a promisor remote the server\n>  advertised and the client accepts.\n>  \n> -Note that, everywhere in this document, `pr-name` MUST be a valid\n> -remote name, and the ';' and ',' characters MUST be encoded if they\n> -appear in `pr-name` or `pr-url`.\n> +Note that, everywhere in this document, the ';' and ',' characters\n> +MUST be encoded if they appear in `pr-name` or `field-value`.\n>  \n>  If the server doesn't know any promisor remote that could be good for\n>  a client to use, or prefers a client not to use any promisor remote it\n> @@ -822,9 +848,10 @@ In this case, or if the client doesn't want to use any promisor remote\n>  the server advertised, the client shouldn't advertise the\n>  \"promisor-remote\" capability at all in its reply.\n>  \n> -The \"promisor.advertise\" and \"promisor.acceptFromServer\" configuration\n> -options can be used on the server and client side to control what they\n> -advertise or accept respectively. See the documentation of these\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>  \nThanks\n\nJean-Noël\n\n"},{"id":"521480","messageId":"xmqqzfdf1ve9.fsf@gitster.g","threadId":"63293","inReplyTo":"20250625125055.1375596-1-christian.couder@gmail.com","subject":"Re: [PATCH v5 0/5] Make the \"promisor-remote\" capability support more fields","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-07T22:35:58Z","receivedAt":"2025-07-07T22:36:00Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> Changes since v4\n> ----------------\n>\n> Thanks to Patrick, Junio, Karthik and Justin for their comments on the\n> previous versions.\n>\n> There are very few changes compared to v4 and they are quite small.\n>\n> In patch 1/5, in the commit message:\n>\n>   - a few sentences were added to explain why using 'struct strvec'\n>     for the new fields wouldn't scale well,\n>\n>   - a typo \"use use\" was fixed.\n>\n> In patch 1/5, in the code, a BUG() message was improved.\n>\n> In patch 2/5, in the code, some code comments were added in\n> fields_from_config().\n\nThe topic saw only a few comments in this last iteration.  Would we\nbe seeing a hopefully small and final update to tie the loose ends\nbefore we declare that the topic is ready for 'next'?\n\nThanks.\n"},{"id":"521497","messageId":"CAP8UFD1GKjcV=-4SdWSPSe737ghJ9G3=CyTLX1=UodxqWY-USQ@mail.gmail.com","threadId":"63293","inReplyTo":"xmqqzfdf1ve9.fsf@gitster.g","subject":"Re: [PATCH v5 0/5] Make the \"promisor-remote\" capability support more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-08T03:34:26Z","receivedAt":"2025-07-08T03:34:40Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Tue, Jul 8, 2025 at 12:35 AM Junio C Hamano <gitster@pobox.com> wrote:\n\n> The topic saw only a few comments in this last iteration.  Would we\n> be seeing a hopefully small and final update to tie the loose ends\n> before we declare that the topic is ready for 'next'?\n\nYeah, I am planning to work on a small update soon.\n\nThanks.\n"},{"id":"522350","messageId":"CAP8UFD0tHnYKh5QeVq_YUYrnk4yZW04_cggbGG-NC_EksHXELA@mail.gmail.com","threadId":"63293","inReplyTo":"xmqqv7ojvjjs.fsf@gitster.g","subject":"Re: [PATCH v5 1/5] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-21T14:08:24Z","receivedAt":"2025-07-21T14:08:40Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Wed, Jun 25, 2025 at 7:05 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Christian Couder <christian.couder@gmail.com> writes:\n>\n> > @@ -414,11 +439,15 @@ static int should_accept_remote(enum accept_promisor accept,\n> >               return 0;\n> >       }\n> >\n> > -     if (!strcmp(urls->v[i], remote_url))\n> > +     if (!p->url)\n> > +             BUG(\"bad config_info (URL is NULL) for remote '%s'\",\n> > +                 remote_name);\n> > +\n> > +     if (!strcmp(p->url, remote_url))\n> >               return 1;\n>\n> The code seems to trust string_list enough that once an earlier call\n> to string_list_lookup() in this code path finds the entry for the\n> remote_name, item->util is assumed to be non-NULL and points at a\n> valid promisor_info instance.  But it does not seem to be defensive\n> against p being NULL, but it does so for p->url being NULL, which\n> smells fishy.  Once promisor_config_info_list() reads the data from\n> the configuration, do we ever update the contents and the risk of us\n> corrupting p->url while we are running is great enough to warrant\n> being defensive?\n>\n> My inclination is to suggest removing the check and BUG() here, but\n> I may be missing something obvious (like \"p->url gets NULL'ed out in\n> this function, but then such a remote is never checked with this\n> function because such and such logic in that function\").\n\nYeah, right, if we trust promisor_config_info_list() to properly\npopulate the `config_info` string list (which is not modified\nanywhere), let's trust it fully. So I have removed the `if (!p->url)`\ncheck and the associated `BUG(...)`, in the v6.\n\nThanks.\n"},{"id":"522351","messageId":"CAP8UFD1wYXrEC2VYBHTL7NS1ksSa0oiBgB3Ua=6V1SnP=+RMsQ@mail.gmail.com","threadId":"63293","inReplyTo":"xmqqbjqbsbe0.fsf@gitster.g","subject":"Re: [PATCH v5 2/5] promisor-remote: allow a server to advertise more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-21T14:09:10Z","receivedAt":"2025-07-21T14:09:25Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Thu, Jun 26, 2025 at 12:29 AM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Christian Couder <christian.couder@gmail.com> writes:\n>\n> > For now the \"promisor-remote\" protocol capability can only pass \"name\"\n> > and \"url\" information from a server to a client in the form\n> > \"name=<remote_name>,url=<remote_url>\".\n> >\n> > Let's make it possible to pass more information by introducing\n>\n> Motivation, like \"in order to allow the client more efficient\n> transfer\", \"in order to give more control on what can be fetched\n> from the server\", etc., needs to be given before we say \"let's do\n> X\".  We want to do X not because we can do X; we want to do X as it\n> currently seems to us the best way to achieve Y.  What is our Y in\n> this case for the proposed feature?\n\nOk, I have changed it to the following in v6:\n\n   To allow clients to make more informed decisions about which promisor\n   remotes they accept, let's make it possible to pass more information\n   by introducing a new \"promisor.sendFields\" configuration variable.\n\n> > a new\n> > \"promisor.sendFields\" configuration variable. This variable should\n> > contain a comma or space separated list\n>\n> By making it easier for casual humans who manually write the\n> configuration variable (presumably while testing) and allowing both\n> comma and space as separator, this design decision is forcing one\n> more rule to worry about for those who are writing the parser for\n> the value.  There may be some existing configuration variables with\n> such a \"leninent\" syntax, but I'd rather see us not make the mess\n> even worse.\n\nWe indeed have a number of configuration variables accepting lists of\nitems separated by both comma and space. As we cannot fix those easily\nfor backward compatibility and they still make things a bit simpler\nfor users, my opinion is that we'd better bite the bullet and make\nsure we have a simple and hard-to-misuse standard way to parse such\nlists. Then we can allow such comma and space separated lists in many\nplaces, and use this standard way to parse those lists wherever they\nare allowed. Users would have a consistent experience when dealing\nwith different configuration variables.\n\nIt also seems to me that parsing such lists is not so difficult right\nnow. It basically only requires a call to\n`string_list_split_in_place()`, followed by a call to\n`string_list_remove_empty_items()`. Maybe we could refactor that into\na dedicated (possibly inline) function (maybe called\n`string_list_split_config_list()`)\nand make sure it is used in all the places where such lists are\naccepted.\n\nI would prefer adding that kind of refactoring in the form of a\npreparatory patch (or maybe a separate patch altogether) rather than\nfor example allowing only spaces as separators. For now I have made no\nchanges in v6 regarding this.\n\n> > of field names that will be\n> > looked up in the configuration of the remote on the server to find the\n> > values that will be passed to the client.\n>\n> I know the name \"field\" was discussed in earlier iterations, but\n> these three lines together with \"For example\" in a later paragraph,\n> it hints to me that this mechanism is to choose variable-value pairs\n> for which among remotes.<name>.* variables to send after name=<name>\n> and url=<url>; is my understanding correct?  If so, can we clarify\n> the paragraphs around here even more so that I do not even have to\n> ask this question?\n\nYeah, it seems to me that there was previously a sentence about what\nfields are, but it looks like I removed it by mistake. Anyway, before\nthe paragraph about what the \"promisor.sendFields\" configuration\nvariable contains, I have added the following in v6:\n\n   On the server side, information about a remote `foo` is stored in\n   configuration variables named `remote.foo.<variable-name>`. To make\n   it clearer and simpler, we use `field` and `field name` like this:\n\n     * `field name` refers to the <variable-name> part of such a\n       configuration variable, and\n\n     * `field` refers to both the `field name` and the value of such a\n       configuration variable.\n\n> What do we call the third-level of a variable name in the\n> configuration file?  The description on the \"--regexp\" option in\n> \"git config --help\" hints one:\n>\n>     With `get`, interpret the name as a regular expression. Regular\n>     expression matching is currently case-sensitive and done against\n>     a canonicalized version of the key in which section and variable\n>     names are lowercased, but subsection names are not.\n>\n> So a for remote.origin.partialCloneFilter, \"remote\" is the section\n> name, \"origin\" is the subsection name, and \"partialCloneFilter\" is\n> the variable name.\n\nYeah, I thought that \"variable name\" could be confusing, so I prefered\nto use another word, \"field\", to talk about this.\n\n> Armed with that knowledge, perhaps something like:\n>\n>     <<the motivation comes here, and then ...>> In order to give\n>     additional information to the client, the server side can set a\n>     new 'promisor.sendAdditionalVariables' configuration variable,\n>     whose value is a comma separated list of variable names.\n>\n>     The values of the configuration variables specified by this\n>     variable for the given remote [*] are sent as comma separated\n>     list of variable=<value>, after name=<name>,url=<url> in the\n>     promisor-remote capability.\n>\n>     [*] Concatenating each of these variable names listed as the\n>     value of promisor.sendAdditionalVariables after remote.<name>.\n>     results in the configuration variables exposed to the client.\n\nI'd rather avoid talking about \"variable name\" other than to explain\nwhat \"field name\" and \"field\" are. I think it would be too confusing,\nespecially if the name of the new config options are still\n\"promisor.sendFields\" and \"promisor.checkFields\".\n\n> to replace all of the above, and then it ...\n>\n> > Only a set of predefined fields are allowed. The only fields in this\n> > set are \"partialCloneFilter\" and \"token\". The \"partialCloneFilter\"\n> > field specifies the filter definition used by the promisor remote,\n> > and the \"token\" field can provide an authentication credential for\n> > accessing it.\n>\n> ... is a good thing to describe that we have these two supported.\n>\n> And this one ...\n>\n> > For example, if \"promisor.sendFields\" is set to \"partialCloneFilter\",\n> > and the server has the \"remote.<name>.partialCloneFilter\" config\n> > variable set to a value for a remote, then that value will be passed\n> > in the form \"partialCloneFilter=<value>\" after the \"name\" and \"url\"\n> > fields.\n>\n> ... has already been covered.  My main point is that we should\n> clarify what a 'field' is and how it relates to the variables in the\n> configuration file of which side (the server, not the client) a lot\n> earlier than we say \"these two are the only ones that are\n> supported\".  Before understanding what a field is, \"only these two\n> are valid\" does not give readers much useful information.\n\nYeah, I hope that the definitions I have added are good enough.\n\n> > diff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\n> > index 9a57005d77..0583fafa09 100644\n> > --- a/Documentation/gitprotocol-v2.adoc\n> > +++ b/Documentation/gitprotocol-v2.adoc\n> > @@ -785,33 +785,59 @@ retrieving the header from a bundle at the indicated URI, and thus\n> >  save themselves and the server(s) the request(s) needed to inspect the\n> >  headers of that bundle or bundles.\n> >\n> > -promisor-remote=<pr-infos>\n> > +promisor-remote=<pr-info>\n> >  ~~~~~~~~~~~~~~~~~~~~~~~~~~\n> >\n> >  The server may advertise some promisor remotes it is using or knows\n> >  about to a client which may want to use them as its promisor remotes,\n> > +instead of this repository. In this case <pr-info> should be of the\n> >  form:\n> >\n> > +     pr-info = pr-fields | pr-info \";\" pr-info\n> >\n> > +     pr-fields = field-name \"=\" field-value | pr-fields \",\" pr-fields\n>\n> Typically a name is at most a few words (concatenated with some\n> punctuation), and url would probably be a few hundred bytes at most?\n> can we afford to cram even more var=value on the same line?\n\nWe are adding only 2 relatively short optional \"var=value\" per remote,\nso a few hundred bytes at most per remote. This means the whole\nadvertisement for one remote is at most around 500 bytes. As the max\npkt-line size is around 16^4, so 65536, this still allows for more\nthan 130 remotes to be advertised which should be enough for now.\n\n> The syntax allows you to send more than one name/url pair on the\n> same promisor-remote.  Length aside, does this pr-fields mechanism\n> give the values of these fields for multiple remotes?\n>\n> Suppose you want to advertise promisor-remote \"A\" and \"B\", and your\n> sendFields says \"token\".  What does your promisor-remote=<pr-info> look\n> like in such a case?\n>\n>     name=A,url=https://git.git/A,token=foo;name=B,url=https://git.git/B,token=bar\n\nYes, it should look like this, and each \"field-name\" should appear at most once.\n\n> Do we explicitly say that the set of var=val on promisor-remote are\n> grouped into distinct sets with ';' and each set talks about only\n> one remote?  I.e.\n>\n>     name=A,name=B,url=https://git.git/A,url=https://git.git/B,token=foo,token=bar\n>\n> may be syntactically allowed in the above BNF, but it needs _some_\n> rule to match which URL and which token go with which name.\n\nThere is already the following:\n\n\"where all the `field-name` and `field-value` in a given `pr-fields`\nare field names and values related to a single promisor remote.\"\n\nThis disallows having fields related to different promisor remotes\ngrouped together by ';'.\n\n> Do we\n> also need to say that within a single ';' separated group, a variable\n> can only appear once?  IOW, this is invalid?\n>\n>     name=A,url=https://git.git/A,token=foo,token=bar\n\nThis should be invalid, but it wasn't explicitly said that it is\ninvalid. In v6 though I have added \"A given `field-name` MUST NOT\nappear more than once in given `pr-fields`.\"\n\n> > +where all the `field-name` and `field-value` in a given `pr-fields`\n> > +are field names and values related to a single promisor remote.\n> >\n> > +The server MUST advertise at least the \"name\" and \"url\" field names\n> > +along with the associated field values, which are the name of a valid\n> > +remote and its URL, in each `pr-fields`. The \"name\" and \"url\" fields\n> > +MUST appear first in each pr-fields, in that order.\n>\n> With\n>\n>     pr-fields = pr-fields \",\" pr-fields\n>\n> the rule in the last sentence does not make sense.  You'd need a\n> distinct BNF non-terminal to name the one you want the rule to apply\n> to.  Perhaps something like\n>\n>         pr-info = pr-one-remote-info | pr-info ';' pr-one-remote-info\n>         pr-one-remote-info = pr-fields\n>         pr-fields = pr-field | pr-fields ',' pr-field\n>         pr-field = variable '=' value\n\nI think the following, which I have put in v6, should be enough but\nmaybe I am missing something:\n\n    pr-info = pr-fields | pr-info \";\" pr-fields\n    pr-fields = pr-field | pr-fields \",\" pr-field\n    pr-field = field-name \"=\" field-value\n\nAlso in the 'object-info' section of the same file, \"attrs = attr |\nattrs SP attrs\" has the same problem and might want to be changed to\n\"attrs = attr | attrs SP attr\".\n\n> Then you can safely say 'name' and 'url', must be the first two that\n> appear in each pr-one-remote-info.\n\nIt seems to me that this sentence which is already in the doc should be enough:\n\n\"The \"name\" and \"url\" fields MUST appear first in each pr-fields, in\nthat order.\"\n\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> > +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> > +\n> > +- \"token\": An authentication token that clients can use when\n> > +connecting to the remote. It corresponds to the \"remote.<name>.token\"\n> > +config setting.\n> > +\n> > +No other fields are defined by the protocol at this time. Clients MUST\n> > +ignore fields they don't recognize to allow for future protocol\n> > +extensions.\n>\n> This forces us to never add support for a field that MUST be\n> understood for the protocol to operate correctly.  Is it sensible?\n\nThe protocol for now allows clients to only reply with the names of\nthe promisor remote they accept. Clients can also set\n\"promisor.acceptFromServer\" to \"All\". This means that servers should\nnot trust anyway that the client understood any specific field they\nsent based on the client's reply.\n\nIf the server controls the promisor remotes it advertises, it can send\nfor example a token in the \"token\" field and then use other means than\nthis protocol to make sure that the client understood the token (and\nmaybe other fields) when the client actually accesses the promisor\nremote.\n\nIf a client wants to make sure that the server passed a specific field\nwith a specific value, they can list this field's name in their\n\"promisor.checkFields\" configuration variable which is introduced in\npatch 4/5 and have their corresponding config option set to the\nspecific value.\n\n> Just like the index file format defines optional extensions that can\n> be ignored (implication of which is that you MUST die if you do not\n> understand any non-optional ones), shouldn't we have some mechanism\n> to tell \"if you do not understand this, do not use the remote, or\n> your repository will be broken in a horrible way\"?\n>\n> I dunno.\n\nIf we want to make sure that clients don't access some remotes unless\nthey understand something, we'd better rely at least partially on\nanother mechanism because clients can already be configured to access\nany remote. They don't need this protocol to be able to access a\nremote.\n\nIn fact the information transmitted by the server is not used to\naccess the remote. It's only used to decide if the remote is accepted.\nSo the remote has to be already configured to access the remote.\n\nThanks.\n"},{"id":"522352","messageId":"CAP8UFD1vZoSVnPK+wF2w0pb47JzWp8=hk9D4T_OcwGRR+m=qPg@mail.gmail.com","threadId":"63293","inReplyTo":"c49d73de-568a-4584-aa8f-9a9ffd68e4ce@free.fr","subject":"Re: [PATCH v5 2/5] promisor-remote: allow a server to advertise more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-21T14:09:19Z","receivedAt":"2025-07-21T14:09:34Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Fri, Jun 27, 2025 at 8:48 PM Jean-Noël Avila <jn.avila@free.fr> wrote:\n>\n> Le 25/06/2025 à 14:50, Christian Couder a écrit :\n\n> > +promisor.sendFields::\n> > +     A comma or space separated list of additional remote related\n> > +     field names. A server will send these field names and the\n> > +     associated field values from its configuration when\n> > +     advertising its promisor remotes using the \"promisor-remote\"\n> > +     capability, see linkgit:gitprotocol-v2[5]. Currently, only the\n> > +     \"partialCloneFilter\" and \"token\" field names are supported.\n> > ++\n> > +* \"partialCloneFilter\": contains the partial clone filter\n> > +  used for the remote.\n> > ++\n> > +* \"token\": contains an authentication token for the remote.\n> > ++\n>\n> This kind of text structure calls a description list instead and you can\n> already use backquotes:\n>\n> `partialCloneFilter`:: contains the partial clone filter\n> > +  used for the remote.\n> > ++\n> > +`token`:: contains an authentication token for the remote.\n\nThanks for the suggestion. I have used that in v6.\n\n> > +When a field name is part of this list and a corresponding\n> > +\"remote.foo.<field name>\" config variable is set on the server to a\n>\n> Please no space in placeholders: <field-name>\n>\n> > +non-empty value, then the field name and value will be sent when\n> > +advertising the promisor remote \"foo\".\n> > ++\n> > +This list has no effect unless the \"promisor.advertise\" config\n> > +variable is set to \"true\", and the \"name\" and \"url\" fields are always\n> > +advertised regardless of this setting.\n> > +\n>\n> More generally, I am a bit annoyed by the usage of the \"will\" auxiliary\n> when not expressing a true future. For an international audience, this\n> can be misleading. The plain language[1] philosophy mandates to not use\n> auxiliaries other than where they are required (no convoluted sentences).\n\nI have removed some usage of \"will\" in the patches in v6. I haven't\nchanged the existing documentation in this file though.\n\n> Would it make sense to start a style guide to help writing consistent\n> documentation that targets people whose first language is not English?\n> Being an non native speaker, I often find our docs too literate, with\n> lengthy sentences.\n>\n> [1] https://en.wikipedia.org/wiki/Plain_language\n\nI think it's a separate discussion, and I am not sure I want to\nparticipate in it.\n\n> >  promisor.acceptFromServer::\n> >       If set to \"all\", a client will accept all the promisor remotes\n> >       a server might advertise using the \"promisor-remote\"\n> > diff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\n> > index 9a57005d77..0583fafa09 100644\n> > --- a/Documentation/gitprotocol-v2.adoc\n> > +++ b/Documentation/gitprotocol-v2.adoc\n> > @@ -785,33 +785,59 @@ retrieving the header from a bundle at the indicated URI, and thus\n> >  save themselves and the server(s) the request(s) needed to inspect the\n> >  headers of that bundle or bundles.\n> >\n> > -promisor-remote=<pr-infos>\n> > +promisor-remote=<pr-info>\n> >  ~~~~~~~~~~~~~~~~~~~~~~~~~~\n>\n> Be careful to adjust the length of the underline to the one of the title\n\nI have adjusted it in v6. Thanks.\n\n[...]\n\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> > +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> > +\n> > +- \"token\": An authentication token that clients can use when\n> > +connecting to the remote. It corresponds to the \"remote.<name>.token\"\n> > +config setting.\n> > +\n>\n> This list can be turned into a description list.\n\nDone.\n\n> > +No other fields are defined by the protocol at this time. Clients MUST\n> > +ignore fields they don't recognize to allow for future protocol\n> > +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> > +\n> > +Field values MUST be urlencoded.\n> > +\n> > +If the client decides to use one or more promisor remotes the server\n> > +advertised, it can reply with \"promisor-remote=<pr-names>\" where\n> > +<pr-names> should be of the form:\n> > +\n> > +     pr-names = pr-name | pr-names \";\" pr-names\n>\n> Here the syntax used is not compatible with synopsis. Would it make\n> sense to uniformize it, or is BNF ok?\n\nI am not sure what you call the \"synopsis\". Is it \"promisor-remote=<pr-info>\"?\n\nIf yes, then I think  \"promisor-remote=<pr-names>\" where\n\n    pr-names = pr-name | pr-names \";\" pr-name\n\n(which is what I have currently) should be considered compatible. Or\ndo you have another suggestion?\n\nThanks for your review,\nChristian.\n"},{"id":"522353","messageId":"20250721141056.2283349-1-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250625125055.1375596-1-christian.couder@gmail.com","subject":"[PATCH v6 0/5] Make the \"promisor-remote\" capability support more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-21T14:10:18Z","receivedAt":"2025-07-21T14:11:14Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"The \"promisor-remote\" capability can only be used to pass the names\nand URLs of the promisor remotes from the server to the client. After\nthat the client can use this information to decide if it accepts the\nremotes or not.\n\nIt would be nice if the server could pass more fields about its\nremotes and if the client could use that additional information to\ndecide about the remotes by comparing it with its local information\nabout the remotes.\n\nThis patch series implements this by adding the \"promisor.sendFields\"\non the server side and the \"promisor.checkFields\" on the client side.\n\nFor example, if \"promisor.sendFields\" is set to \"partialCloneFilter\",\nand the server has the remote \"foo\" configured like this:\n\n[remote \"foo\"]\n        url = file:///tmp/foo.git\n\tpartialCloneFilter = blob:none\n\nthen \"name=foo,url=file:///tmp/foo.git,partialCloneFilter=blob:none\"\nwill be sent by the server for this remote.\n\nAll the information passed through the \"promisor-remote\" capability is\nstill only used to decide if the remotes are accepted or not. The\nclient doesn't store it and doesn't use it for any other purpose.\n\nNote that the filter mechanism already exists for a long time and this\nseries doesn't change how it works. For example, it has already been\npossible for a long time to have different repos using the same\npromisor remote with different filters. See the existing partial clone\ndocumentation (like \"Documentation/technical/partial-clone.adoc\") for\nmore information on partial clone.\n\nThe fields that can be passed are limited to \"partialCloneFilter\" and\n\"token\".\n\nOn the technical side, we get rid of 'struct strvec' and we use\n'struct promisor_info' to store the data and 'struct string_list' to\nstore the 'struct promisor_info' instances instead.\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\nChanges since v5\n----------------\n\nThanks to Patrick, Junio, Karthik, Jean-Noël and Justin for their\ncomments on the previous versions.\n\nThere are a number of changes compared to v5 but they are mostly on\nthe documentation and commit messages.\n\n  - In patch 1/5 (\"promisor-remote: refactor to get rid of 'struct\n    strvec'\"), we don't check p->url and BUG() if it's NULL anymore.\n\n  - In patch 2/5 (\"promisor-remote: allow a server to advertise more\n    fields\") the commit message has been improved. Especially there\n    are now explanations about what we call fields and field names.\n\n  - In patch 2/5 there are the following changes to the documentation:\n\n    - use of \"will\" has been reduced,\n\n    - \"`partialCloneFilter`::\" and \"`token`::\" are used to describe\n      these field names,\n\n    - \"<field-name>\" is used instead of \"<field name>\",\n\n    - the length of the underline to the \"promisor-remote=<pr-info>\"\n      title has been adjusted\n\n    - the BNF definition of \"pr-info\" has been improved,\n\n    - it's stated that a given field name should not appear more than\n      once for each promisor remote,\n\n    - the BNF definition of \"pr-names\" has been improved.\n\n  - In patch 4/5 (\"promisor-remote: allow a client to check fields\")\n    the use of \"will\" has alse been reduced.\n\nCI tests\n--------\n\nThey have all passed, see:\n\nhttps://github.com/chriscool/git/actions/runs/16417265102\n\nRange diff compared to v5\n-------------------------\n\n1:  3700939f67 ! 1:  87a6ba5c48 promisor-remote: refactor to get rid of 'struct strvec'\n    @@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n      \t}\n      \n     -\tif (!strcmp(urls->v[i], remote_url))\n    -+\tif (!p->url)\n    -+\t\tBUG(\"bad config_info (URL is NULL) for remote '%s'\",\n    -+\t\t    remote_name);\n    -+\n     +\tif (!strcmp(p->url, remote_url))\n      \t\treturn 1;\n      \n2:  f546756705 ! 2:  0543a42858 promisor-remote: allow a server to advertise more fields\n    @@ Commit message\n         and \"url\" information from a server to a client in the form\n         \"name=<remote_name>,url=<remote_url>\".\n     \n    -    Let's make it possible to pass more information by introducing a new\n    -    \"promisor.sendFields\" configuration variable. This variable should\n    -    contain a comma or space separated list of field names that will be\n    -    looked up in the configuration of the remote on the server to find the\n    -    values that will be passed to the client.\n    +    To allow clients to make more informed decisions about which promisor\n    +    remotes they accept, let's make it possible to pass more information\n    +    by introducing a new \"promisor.sendFields\" configuration variable.\n     \n    -    Only a set of predefined fields are allowed. The only fields in this\n    -    set are \"partialCloneFilter\" and \"token\". The \"partialCloneFilter\"\n    -    field specifies the filter definition used by the promisor remote,\n    -    and the \"token\" field can provide an authentication credential for\n    -    accessing it.\n    +    On the server side, information about a remote `foo` is stored in\n    +    configuration variables named `remote.foo.<variable-name>`. To make\n    +    it clearer and simpler, we use `field` and `field name` like this:\n    +\n    +      * `field name` refers to the <variable-name> part of such a\n    +        configuration variable, and\n    +\n    +      * `field` refers to both the `field name` and the value of such a\n    +        configuration variable.\n    +\n    +    The \"promisor.sendFields\" configuration variable should contain a\n    +    comma or space separated list of field names that will be looked up\n    +    in the configuration of the remote on the server to find the values\n    +    that will be passed to the client.\n    +\n    +    Only a set of predefined field names are allowed. The only field\n    +    names in this set are \"partialCloneFilter\" and \"token\". The\n    +    \"partialCloneFilter\" field name specifies the filter definition used\n    +    by the promisor remote, and the \"token\" field name can provide an\n    +    authentication credential for accessing it.\n     \n         For example, if \"promisor.sendFields\" is set to \"partialCloneFilter\",\n    -    and the server has the \"remote.<name>.partialCloneFilter\" config\n    -    variable set to a value for a remote, then that value will be passed\n    -    in the form \"partialCloneFilter=<value>\" after the \"name\" and \"url\"\n    -    fields.\n    +    and the server has the \"remote.foo.partialCloneFilter\" config\n    +    variable set to a value, then that value will be passed in the\n    +    \"partialCloneFilter\" field in the form \"partialCloneFilter=<value>\"\n    +    after the \"name\" and \"url\" fields.\n     \n         A following commit will allow the client to use the information to\n         decide if it accepts the remote or not. For now the client doesn't do\n    @@ Documentation/config/promisor.adoc: promisor.advertise::\n      \n     +promisor.sendFields::\n     +\tA comma or space separated list of additional remote related\n    -+\tfield names. A server will send these field names and the\n    ++\tfield names. A server sends these field names and the\n     +\tassociated field values from its configuration when\n     +\tadvertising its promisor remotes using the \"promisor-remote\"\n     +\tcapability, see linkgit:gitprotocol-v2[5]. Currently, only the\n     +\t\"partialCloneFilter\" and \"token\" field names are supported.\n     ++\n    -+* \"partialCloneFilter\": contains the partial clone filter\n    -+  used for the remote.\n    ++`partialCloneFilter`:: contains the partial clone filter\n    ++used for the remote.\n     ++\n    -+* \"token\": contains an authentication token for the remote.\n    ++`token`:: contains an authentication token for the remote.\n     ++\n     +When a field name is part of this list and a corresponding\n    -+\"remote.foo.<field name>\" config variable is set on the server to a\n    -+non-empty value, then the field name and value will be sent when\n    ++\"remote.foo.<field-name>\" config variable is set on the server to a\n    ++non-empty value, then the field name and value are sent when\n     +advertising the promisor remote \"foo\".\n     ++\n     +This list has no effect unless the \"promisor.advertise\" config\n    @@ Documentation/gitprotocol-v2.adoc: retrieving the header from a bundle at the in\n      headers of that bundle or bundles.\n      \n     -promisor-remote=<pr-infos>\n    +-~~~~~~~~~~~~~~~~~~~~~~~~~~\n     +promisor-remote=<pr-info>\n    - ~~~~~~~~~~~~~~~~~~~~~~~~~~\n    ++~~~~~~~~~~~~~~~~~~~~~~~~~\n      \n      The server may advertise some promisor remotes it is using or knows\n      about to a client which may want to use them as its promisor remotes,\n    @@ Documentation/gitprotocol-v2.adoc: retrieving the header from a bundle at the in\n      form:\n      \n     -\tpr-infos = pr-info | pr-infos \";\" pr-info\n    -+\tpr-info = pr-fields | pr-info \";\" pr-info\n    ++\tpr-info = pr-fields | pr-info \";\" pr-fields\n      \n     -\tpr-info = \"name=\" pr-name | \"name=\" pr-name \",\" \"url=\" pr-url\n    -+\tpr-fields = field-name \"=\" field-value | pr-fields \",\" pr-fields\n    ++\tpr-fields = pr-field | pr-fields \",\" pr-field\n      \n     -where `pr-name` is the urlencoded name of a promisor remote, and\n     -`pr-url` the urlencoded URL of that promisor remote.\n    -+where all the `field-name` and `field-value` in a given `pr-fields`\n    -+are field names and values related to a single promisor remote.\n    ++\tpr-field = field-name \"=\" field-value\n      \n     -In this case, if the client decides to use one or more promisor\n     -remotes the server advertised, it can reply with\n     -\"promisor-remote=<pr-names>\" where <pr-names> should be of the form:\n    ++where all the `field-name` and `field-value` in a given `pr-fields`\n    ++are field names and values related to a single promisor remote. A\n    ++given `field-name` MUST NOT appear more than once in given\n    ++`pr-fields`.\n    ++\n     +The server MUST advertise at least the \"name\" and \"url\" field names\n     +along with the associated field values, which are the name of a valid\n     +remote and its URL, in each `pr-fields`. The \"name\" and \"url\" fields\n     +MUST appear first in each pr-fields, in that order.\n    - \n    --\tpr-names = pr-name | pr-names \";\" pr-name\n    ++\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 used by the remote.\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     +\n    -+- \"token\": An authentication token that clients can use when\n    ++`token`:: An authentication token that clients can use when\n     +connecting to the remote. It corresponds to the \"remote.<name>.token\"\n     +config setting.\n     +\n    @@ Documentation/gitprotocol-v2.adoc: retrieving the header from a bundle at the in\n     +If the client decides to use one or more promisor remotes the server\n     +advertised, it can reply with \"promisor-remote=<pr-names>\" where\n     +<pr-names> should be of the form:\n    -+\n    -+\tpr-names = pr-name | pr-names \";\" pr-names\n    + \n    + \tpr-names = pr-name | pr-names \";\" pr-name\n      \n      where `pr-name` is the urlencoded name of a promisor remote the server\n      advertised and the client accepts.\n3:  3ac73b14eb = 3:  d566719807 promisor-remote: refactor how we parse advertised fields\n4:  b75577eff2 ! 4:  d2a13eabc0 promisor-remote: allow a client to check fields\n    @@ Documentation/config/promisor.adoc: promisor.acceptFromServer::\n     +\n     +promisor.checkFields::\n     +\tA comma or space separated list of additional remote related\n    -+\tfield names. A client will check if the values of these fields\n    ++\tfield names. A client checks if the values of these fields\n     +\ttransmitted by a server correspond to the values of these\n     +\tfields in its own configuration before accepting a promisor\n     +\tremote. Currently, \"partialCloneFilter\" and \"token\" are the\n    @@ Documentation/config/promisor.adoc: promisor.acceptFromServer::\n     +   match the value advertised by the server for the \"token\" field.\n     ++\n     +If any of these conditions is not met for any field name listed in\n    -+`promisor.checkFields`, the advertised remote \"foo\" will be rejected.\n    ++`promisor.checkFields`, the advertised remote \"foo\" is rejected.\n     ++\n     +For the \"partialCloneFilter\" field, this allows the client to ensure\n     +that the server's filter matches what it expects locally, preventing\n    @@ Documentation/config/promisor.adoc: promisor.acceptFromServer::\n     ++\n     +The field names and values should be passed by the server through the\n     +\"promisor-remote\" capability by using the `promisor.sendFields` config\n    -+variable. The fields will be checked only if the\n    ++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 will have no effect. See\n    ++set to \"None\", this config variable has no effect. See\n     +linkgit:gitprotocol-v2[5].\n     \n      ## promisor-remote.c ##\n    @@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n      \tif (accept != ACCEPT_KNOWN_URL)\n      \t\tBUG(\"Unhandled 'enum accept_promisor' value '%d'\", accept);\n     @@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n    - \t\t    remote_name);\n    + \t}\n      \n      \tif (!strcmp(p->url, remote_url))\n     -\t\treturn 1;\n5:  149b275087 = 5:  c7d7c83534 promisor-remote: use string constants for 'name' and 'url' too\n\n\nChristian Couder (5):\n  promisor-remote: refactor to get rid of 'struct strvec'\n  promisor-remote: allow a server to advertise more fields\n  promisor-remote: refactor how we parse advertised fields\n  promisor-remote: allow a client to check fields\n  promisor-remote: use string constants for 'name' and 'url' too\n\n Documentation/config/promisor.adoc    |  62 ++++\n Documentation/gitprotocol-v2.adoc     |  63 ++--\n promisor-remote.c                     | 397 +++++++++++++++++++++-----\n t/t5710-promisor-remote-capability.sh |  65 +++++\n 4 files changed, 499 insertions(+), 88 deletions(-)\n\n-- \n2.50.1.324.gc7d7c83534\n\n"},{"id":"522354","messageId":"20250721141056.2283349-2-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250721141056.2283349-1-christian.couder@gmail.com","subject":"[PATCH v6 1/5] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-21T14:10:19Z","receivedAt":"2025-07-21T14:11:15Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"In a following commit, we will use the new 'promisor-remote' protocol\ncapability introduced by d460267613 (Add 'promisor-remote' capability\nto protocol v2, 2025-02-18) to pass and process more information\nabout promisor remotes than just their name and url.\n\nFor that purpose, we will need to store information about other\nfields, especially information that might or might not be available\nfor different promisor remotes. Unfortunately using 'struct strvec',\nas we currently do, to store information about the promisor remotes\nwith one 'struct strvec' for each field like \"name\" or \"url\" does not\nscale easily in that case. We would need one 'struct strvec' for each\nnew field, and then we would have to pass all these 'struct strvec'\naround.\n\nLet's refactor this and introduce a new 'struct promisor_info'.\n\nIt will only store promisor remote information in its members. For now\nit has only a 'name' member for the promisor remote name and an 'url'\nmember for its URL. We will use a 'struct string_list' to store the\ninstances of 'struct promisor_info'. For each 'item' in the\nstring_list, 'item->string' will point to the promisor remote name and\n'item->util' will point to the corresponding 'struct promisor_info'\ninstance.\n\nExplicit members are used within 'struct promisor_info' for type\nsafety and clarity regarding the specific information being handled,\nrather than a generic key-value store. We want to specify and document\neach field and its content, so adding new members to the struct as\nmore fields are supported is fine.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 107 ++++++++++++++++++++++++++++------------------\n 1 file changed, 66 insertions(+), 41 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex be6f82d12f..0213b8768f 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -314,9 +314,35 @@ static int allow_unsanitized(char ch)\n \treturn ch > 32 && ch < 127;\n }\n \n-static void promisor_info_vecs(struct repository *repo,\n-\t\t\t       struct strvec *names,\n-\t\t\t       struct strvec *urls)\n+/*\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+ */\n+struct promisor_info {\n+\tconst char *name;\n+\tconst char *url;\n+};\n+\n+static void promisor_info_list_clear(struct string_list *list)\n+{\n+\tfor (size_t i = 0; i < list->nr; i++) {\n+\t\tstruct promisor_info *p = list->items[i].util;\n+\t\tfree((char *)p->name);\n+\t\tfree((char *)p->url);\n+\t}\n+\tstring_list_clear(list, 1);\n+}\n+\n+/*\n+ * Populate 'list' with promisor remote information from the config.\n+ * The 'util' pointer of each list item will hold a 'struct promisor_info'.\n+ */\n+static void promisor_config_info_list(struct repository *repo, struct string_list *list)\n {\n \tstruct promisor_remote *r;\n \n@@ -328,8 +354,14 @@ static void promisor_info_vecs(struct repository *repo,\n \n \t\t/* Only add remotes with a non empty URL */\n \t\tif (!git_config_get_string_tmp(url_key, &url) && *url) {\n-\t\t\tstrvec_push(names, r->name);\n-\t\t\tstrvec_push(urls, url);\n+\t\t\tstruct promisor_info *new_info = xcalloc(1, sizeof(*new_info));\n+\t\t\tstruct string_list_item *item;\n+\n+\t\t\tnew_info->name = xstrdup(r->name);\n+\t\t\tnew_info->url = xstrdup(url);\n+\n+\t\t\titem = string_list_append(list, new_info->name);\n+\t\t\titem->util = new_info;\n \t\t}\n \n \t\tfree(url_key);\n@@ -340,47 +372,36 @@ char *promisor_remote_info(struct repository *repo)\n {\n \tstruct strbuf sb = STRBUF_INIT;\n \tint advertise_promisors = 0;\n-\tstruct strvec names = STRVEC_INIT;\n-\tstruct strvec urls = STRVEC_INIT;\n+\tstruct string_list config_info = STRING_LIST_INIT_NODUP;\n+\tstruct string_list_item *item;\n \n \tgit_config_get_bool(\"promisor.advertise\", &advertise_promisors);\n \n \tif (!advertise_promisors)\n \t\treturn NULL;\n \n-\tpromisor_info_vecs(repo, &names, &urls);\n+\tpromisor_config_info_list(repo, &config_info);\n \n-\tif (!names.nr)\n+\tif (!config_info.nr)\n \t\treturn NULL;\n \n-\tfor (size_t i = 0; i < names.nr; i++) {\n-\t\tif (i)\n+\tfor_each_string_list_item(item, &config_info) {\n+\t\tstruct promisor_info *p = item->util;\n+\n+\t\tif (item != config_info.items)\n \t\t\tstrbuf_addch(&sb, ';');\n+\n \t\tstrbuf_addstr(&sb, \"name=\");\n-\t\tstrbuf_addstr_urlencode(&sb, names.v[i], allow_unsanitized);\n+\t\tstrbuf_addstr_urlencode(&sb, p->name, allow_unsanitized);\n \t\tstrbuf_addstr(&sb, \",url=\");\n-\t\tstrbuf_addstr_urlencode(&sb, urls.v[i], allow_unsanitized);\n+\t\tstrbuf_addstr_urlencode(&sb, p->url, allow_unsanitized);\n \t}\n \n-\tstrvec_clear(&names);\n-\tstrvec_clear(&urls);\n+\tpromisor_info_list_clear(&config_info);\n \n \treturn strbuf_detach(&sb, NULL);\n }\n \n-/*\n- * Find first index of 'nicks' where there is 'nick'. 'nick' is\n- * compared case sensitively to the strings in 'nicks'. If not found\n- * 'nicks->nr' is returned.\n- */\n-static size_t remote_nick_find(struct strvec *nicks, const char *nick)\n-{\n-\tfor (size_t i = 0; i < nicks->nr; i++)\n-\t\tif (!strcmp(nicks->v[i], nick))\n-\t\t\treturn i;\n-\treturn nicks->nr;\n-}\n-\n enum accept_promisor {\n \tACCEPT_NONE = 0,\n \tACCEPT_KNOWN_URL,\n@@ -390,19 +411,23 @@ enum accept_promisor {\n \n static int should_accept_remote(enum accept_promisor accept,\n \t\t\t\tconst char *remote_name, const char *remote_url,\n-\t\t\t\tstruct strvec *names, struct strvec *urls)\n+\t\t\t\tstruct string_list *config_info)\n {\n-\tsize_t i;\n+\tstruct promisor_info *p;\n+\tstruct string_list_item *item;\n \n \tif (accept == ACCEPT_ALL)\n \t\treturn 1;\n \n-\ti = remote_nick_find(names, remote_name);\n+\t/* Get config info for that promisor remote */\n+\titem = string_list_lookup(config_info, remote_name);\n \n-\tif (i >= names->nr)\n+\tif (!item)\n \t\t/* We don't know about that remote */\n \t\treturn 0;\n \n+\tp = item->util;\n+\n \tif (accept == ACCEPT_KNOWN_NAME)\n \t\treturn 1;\n \n@@ -414,11 +439,11 @@ static int should_accept_remote(enum accept_promisor accept,\n \t\treturn 0;\n \t}\n \n-\tif (!strcmp(urls->v[i], remote_url))\n+\tif (!strcmp(p->url, remote_url))\n \t\treturn 1;\n \n \twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s'\"),\n-\t\tremote_name, urls->v[i], remote_url);\n+\t\tremote_name, p->url, remote_url);\n \n \treturn 0;\n }\n@@ -430,8 +455,7 @@ static void filter_promisor_remote(struct repository *repo,\n \tstruct strbuf **remotes;\n \tconst char *accept_str;\n \tenum accept_promisor accept = ACCEPT_NONE;\n-\tstruct strvec names = STRVEC_INIT;\n-\tstruct strvec urls = STRVEC_INIT;\n+\tstruct string_list config_info = STRING_LIST_INIT_NODUP;\n \n \tif (!git_config_get_string_tmp(\"promisor.acceptfromserver\", &accept_str)) {\n \t\tif (!*accept_str || !strcasecmp(\"None\", accept_str))\n@@ -450,8 +474,10 @@ static void filter_promisor_remote(struct repository *repo,\n \tif (accept == ACCEPT_NONE)\n \t\treturn;\n \n-\tif (accept != ACCEPT_ALL)\n-\t\tpromisor_info_vecs(repo, &names, &urls);\n+\tif (accept != ACCEPT_ALL) {\n+\t\tpromisor_config_info_list(repo, &config_info);\n+\t\tstring_list_sort(&config_info);\n+\t}\n \n \t/* Parse remote info received */\n \n@@ -482,7 +508,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tif (remote_url)\n \t\t\tdecoded_url = url_percent_decode(remote_url);\n \n-\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, &names, &urls))\n+\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, &config_info))\n \t\t\tstrvec_push(accepted, decoded_name);\n \n \t\tstrbuf_list_free(elems);\n@@ -490,8 +516,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tfree(decoded_url);\n \t}\n \n-\tstrvec_clear(&names);\n-\tstrvec_clear(&urls);\n+\tpromisor_info_list_clear(&config_info);\n \tstrbuf_list_free(remotes);\n }\n \n-- \n2.50.1.324.gc7d7c83534\n\n"},{"id":"522355","messageId":"20250721141056.2283349-3-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250721141056.2283349-1-christian.couder@gmail.com","subject":"[PATCH v6 2/5] promisor-remote: allow a server to advertise more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-21T14:10:20Z","receivedAt":"2025-07-21T14:11:16Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"For now the \"promisor-remote\" protocol capability can only pass \"name\"\nand \"url\" information from a server to a client in the form\n\"name=<remote_name>,url=<remote_url>\".\n\nTo allow clients to make more informed decisions about which promisor\nremotes they accept, let's make it possible to pass more information\nby introducing a new \"promisor.sendFields\" configuration variable.\n\nOn the server side, information about a remote `foo` is stored in\nconfiguration variables named `remote.foo.<variable-name>`. To make\nit clearer and simpler, we use `field` and `field name` like this:\n\n  * `field name` refers to the <variable-name> part of such a\n    configuration variable, and\n\n  * `field` refers to both the `field name` and the value of such a\n    configuration variable.\n\nThe \"promisor.sendFields\" configuration variable should contain a\ncomma or space separated list of field names that will be looked up\nin the configuration of the remote on the server to find the values\nthat will be passed to the client.\n\nOnly a set of predefined field names are allowed. The only field\nnames in this set are \"partialCloneFilter\" and \"token\". The\n\"partialCloneFilter\" field name specifies the filter definition used\nby the promisor remote, and the \"token\" field name can provide an\nauthentication credential for accessing it.\n\nFor example, if \"promisor.sendFields\" is set to \"partialCloneFilter\",\nand the server has the \"remote.foo.partialCloneFilter\" config\nvariable set to a value, then that value will be passed in the\n\"partialCloneFilter\" field in the form \"partialCloneFilter=<value>\"\nafter the \"name\" and \"url\" fields.\n\nA following commit will allow the client to use the information to\ndecide if it accepts the remote or not. For now the client doesn't do\nanything with the additional information it receives.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    |  22 +++++\n Documentation/gitprotocol-v2.adoc     |  63 +++++++++---\n promisor-remote.c                     | 134 ++++++++++++++++++++++++--\n t/t5710-promisor-remote-capability.sh |  31 ++++++\n 4 files changed, 225 insertions(+), 25 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex 2638b01f83..b4a72c2152 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -9,6 +9,28 @@ promisor.advertise::\n \t\"false\", which means the \"promisor-remote\" capability is not\n \tadvertised.\n \n+promisor.sendFields::\n+\tA comma or space separated list of additional remote related\n+\tfield names. A server sends these field names and the\n+\tassociated field values from its configuration when\n+\tadvertising its promisor remotes using the \"promisor-remote\"\n+\tcapability, see linkgit:gitprotocol-v2[5]. Currently, only the\n+\t\"partialCloneFilter\" and \"token\" field names are supported.\n++\n+`partialCloneFilter`:: contains the partial clone filter\n+used for the remote.\n++\n+`token`:: contains an authentication token for the remote.\n++\n+When a field name is part of this list and a corresponding\n+\"remote.foo.<field-name>\" config variable is set on the server to a\n+non-empty value, then the field name and value are sent when\n+advertising the promisor remote \"foo\".\n++\n+This list has no effect unless the \"promisor.advertise\" config\n+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\"\ndiff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\nindex 9a57005d77..b4ec77d89d 100644\n--- a/Documentation/gitprotocol-v2.adoc\n+++ b/Documentation/gitprotocol-v2.adoc\n@@ -785,33 +785,63 @@ retrieving the header from a bundle at the indicated URI, and thus\n save themselves and the server(s) the request(s) needed to inspect the\n headers of that bundle or bundles.\n \n-promisor-remote=<pr-infos>\n-~~~~~~~~~~~~~~~~~~~~~~~~~~\n+promisor-remote=<pr-info>\n+~~~~~~~~~~~~~~~~~~~~~~~~~\n \n The server may advertise some promisor remotes it is using or knows\n about to a client which may want to use them as its promisor remotes,\n-instead of this repository. In this case <pr-infos> should be of the\n+instead of this repository. In this case <pr-info> should be of the\n form:\n \n-\tpr-infos = pr-info | pr-infos \";\" pr-info\n+\tpr-info = pr-fields | pr-info \";\" pr-fields\n \n-\tpr-info = \"name=\" pr-name | \"name=\" pr-name \",\" \"url=\" pr-url\n+\tpr-fields = pr-field | pr-fields \",\" pr-field\n \n-where `pr-name` is the urlencoded name of a promisor remote, and\n-`pr-url` the urlencoded URL of that promisor remote.\n+\tpr-field = field-name \"=\" field-value\n \n-In this case, if the client decides to use one or more promisor\n-remotes the server advertised, it can reply with\n-\"promisor-remote=<pr-names>\" where <pr-names> should be of the form:\n+where all the `field-name` and `field-value` in a given `pr-fields`\n+are field names and values related to a single promisor remote. A\n+given `field-name` MUST NOT appear more than once in given\n+`pr-fields`.\n+\n+The server MUST advertise at least the \"name\" and \"url\" field names\n+along with the associated field values, which are the name of a valid\n+remote and its URL, in each `pr-fields`. The \"name\" and \"url\" fields\n+MUST appear first in each pr-fields, in that order.\n+\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+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+\n+`token`:: An authentication token that clients can use when\n+connecting to the remote. It corresponds to the \"remote.<name>.token\"\n+config setting.\n+\n+No other fields are defined by the protocol at this time. Clients MUST\n+ignore fields they don't recognize to allow for future protocol\n+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+\n+Field values MUST be urlencoded.\n+\n+If the client decides to use one or more promisor remotes the server\n+advertised, it can reply with \"promisor-remote=<pr-names>\" where\n+<pr-names> should be of the form:\n \n \tpr-names = pr-name | pr-names \";\" pr-name\n \n where `pr-name` is the urlencoded name of a promisor remote the server\n advertised and the client accepts.\n \n-Note that, everywhere in this document, `pr-name` MUST be a valid\n-remote name, and the ';' and ',' characters MUST be encoded if they\n-appear in `pr-name` or `pr-url`.\n+Note that, everywhere in this document, the ';' and ',' characters\n+MUST be encoded if they appear in `pr-name` or `field-value`.\n \n If the server doesn't know any promisor remote that could be good for\n a client to use, or prefers a client not to use any promisor remote it\n@@ -822,9 +852,10 @@ In this case, or if the client doesn't want to use any promisor remote\n the server advertised, the client shouldn't advertise the\n \"promisor-remote\" capability at all in its reply.\n \n-The \"promisor.advertise\" and \"promisor.acceptFromServer\" configuration\n-options can be used on the server and client side to control what they\n-advertise or accept respectively. See the documentation of these\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 \n Note that in the future it would be nice if the \"promisor-remote\"\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 0213b8768f..3420ccae63 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -314,6 +314,80 @@ static int allow_unsanitized(char ch)\n \treturn ch > 32 && ch < 127;\n }\n \n+static const char promisor_field_filter[] = \"partialCloneFilter\";\n+static const char promisor_field_token[] = \"token\";\n+\n+/*\n+ * List of optional field names that can be used in the\n+ * \"promisor-remote\" protocol capability (others must be\n+ * ignored). Each field should correspond to a configurable property\n+ * of a remote that can be relevant for the client.\n+ */\n+static const char *known_fields[] = {\n+\tpromisor_field_filter, /* Filter used for partial clone */\n+\tpromisor_field_token,  /* Authentication token for the remote */\n+\tNULL\n+};\n+\n+/*\n+ * Check if 'field' is in the list of the known field names for the\n+ * \"promisor-remote\" protocol capability.\n+ */\n+static int is_known_field(const char *field)\n+{\n+\tconst char **p;\n+\n+\tfor (p = known_fields; *p; p++)\n+\t\tif (!strcasecmp(*p, field))\n+\t\t\treturn 1;\n+\treturn 0;\n+}\n+\n+static int is_valid_field(struct string_list_item *item, void *cb_data)\n+{\n+\tconst char *field = item->string;\n+\tconst char *config_key = (const char *)cb_data;\n+\n+\tif (!is_known_field(field)) {\n+\t\twarning(_(\"unsupported field '%s' in '%s' config\"), field, config_key);\n+\t\treturn 0;\n+\t}\n+\treturn 1;\n+}\n+\n+static char *fields_from_config(struct string_list *fields_list, const char *config_key)\n+{\n+\tchar *fields = NULL;\n+\n+\tif (!git_config_get_string(config_key, &fields) && *fields) {\n+\t\t/* Split on any comma or space character */\n+\t\tstring_list_split_in_place(fields_list, fields, \", \", -1);\n+\t\t/*\n+\t\t * Remove empty items that might result from trailing\n+\t\t * commas, or from items being separated by both\n+\t\t * commas and spaces.\n+\t\t */\n+\t\tstring_list_remove_empty_items(fields_list, 0);\n+\t\tfilter_string_list(fields_list, 0, is_valid_field, (void *)config_key);\n+\t}\n+\n+\treturn fields;\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 = 0;\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+}\n+\n /*\n  * Struct for promisor remotes involved in the \"promisor-remote\"\n  * protocol capability.\n@@ -326,6 +400,8 @@ static int allow_unsanitized(char ch)\n struct promisor_info {\n \tconst char *name;\n \tconst char *url;\n+\tconst char *filter;\n+\tconst char *token;\n };\n \n static void promisor_info_list_clear(struct string_list *list)\n@@ -334,15 +410,47 @@ static void promisor_info_list_clear(struct string_list *list)\n \t\tstruct promisor_info *p = list->items[i].util;\n \t\tfree((char *)p->name);\n \t\tfree((char *)p->url);\n+\t\tfree((char *)p->filter);\n+\t\tfree((char *)p->token);\n \t}\n \tstring_list_clear(list, 1);\n }\n \n+static void set_one_field(struct promisor_info *p,\n+\t\t\t  const char *field, const char *value)\n+{\n+\tif (!strcasecmp(field, promisor_field_filter))\n+\t\tp->filter = xstrdup(value);\n+\telse if (!strcasecmp(field, promisor_field_token))\n+\t\tp->token = xstrdup(value);\n+\telse\n+\t\tBUG(\"invalid field '%s'\", field);\n+}\n+\n+static void set_fields(struct promisor_info *p,\n+\t\t       struct string_list *field_names)\n+{\n+\tstruct string_list_item *item;\n+\n+\tfor_each_string_list_item(item, field_names) {\n+\t\tchar *key = xstrfmt(\"remote.%s.%s\", p->name, item->string);\n+\t\tconst char *val;\n+\t\tif (!git_config_get_string_tmp(key, &val) && *val)\n+\t\t\tset_one_field(p, item->string, val);\n+\t\tfree(key);\n+\t}\n+}\n+\n /*\n  * Populate 'list' with promisor remote information from the config.\n- * The 'util' pointer of each list item will hold a 'struct promisor_info'.\n+ * The 'util' pointer of each list item will hold a 'struct\n+ * promisor_info'. Except \"name\" and \"url\", only members of that\n+ * struct specified by the 'field_names' list are set (using values\n+ * from the configuration).\n  */\n-static void promisor_config_info_list(struct repository *repo, struct string_list *list)\n+static void promisor_config_info_list(struct repository *repo,\n+\t\t\t\t      struct string_list *list,\n+\t\t\t\t      struct string_list *field_names)\n {\n \tstruct promisor_remote *r;\n \n@@ -360,6 +468,9 @@ static void promisor_config_info_list(struct repository *repo, struct string_lis\n \t\t\tnew_info->name = xstrdup(r->name);\n \t\t\tnew_info->url = xstrdup(url);\n \n+\t\t\tif (field_names)\n+\t\t\t\tset_fields(new_info, field_names);\n+\n \t\t\titem = string_list_append(list, new_info->name);\n \t\t\titem->util = new_info;\n \t\t}\n@@ -380,7 +491,7 @@ char *promisor_remote_info(struct repository *repo)\n \tif (!advertise_promisors)\n \t\treturn NULL;\n \n-\tpromisor_config_info_list(repo, &config_info);\n+\tpromisor_config_info_list(repo, &config_info, fields_sent());\n \n \tif (!config_info.nr)\n \t\treturn NULL;\n@@ -395,6 +506,15 @@ char *promisor_remote_info(struct repository *repo)\n \t\tstrbuf_addstr_urlencode(&sb, p->name, allow_unsanitized);\n \t\tstrbuf_addstr(&sb, \",url=\");\n \t\tstrbuf_addstr_urlencode(&sb, p->url, allow_unsanitized);\n+\n+\t\tif (p->filter) {\n+\t\t\tstrbuf_addf(&sb, \",%s=\", promisor_field_filter);\n+\t\t\tstrbuf_addstr_urlencode(&sb, p->filter, allow_unsanitized);\n+\t\t}\n+\t\tif (p->token) {\n+\t\t\tstrbuf_addf(&sb, \",%s=\", promisor_field_token);\n+\t\t\tstrbuf_addstr_urlencode(&sb, p->token, allow_unsanitized);\n+\t\t}\n \t}\n \n \tpromisor_info_list_clear(&config_info);\n@@ -475,7 +595,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\treturn;\n \n \tif (accept != ACCEPT_ALL) {\n-\t\tpromisor_config_info_list(repo, &config_info);\n+\t\tpromisor_config_info_list(repo, &config_info, NULL);\n \t\tstring_list_sort(&config_info);\n \t}\n \n@@ -494,13 +614,9 @@ static void filter_promisor_remote(struct repository *repo,\n \t\telems = strbuf_split(remotes[i], ',');\n \n \t\tfor (size_t j = 0; elems[j]; j++) {\n-\t\t\tint res;\n \t\t\tstrbuf_strip_suffix(elems[j], \",\");\n-\t\t\tres = skip_prefix(elems[j]->buf, \"name=\", &remote_name) ||\n+\t\t\tif (!skip_prefix(elems[j]->buf, \"name=\", &remote_name))\n \t\t\t\tskip_prefix(elems[j]->buf, \"url=\", &remote_url);\n-\t\t\tif (!res)\n-\t\t\t\twarning(_(\"unknown element '%s' from remote info\"),\n-\t\t\t\t\telems[j]->buf);\n \t\t}\n \n \t\tif (remote_name)\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex cb061b1f35..204528b2e0 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -295,6 +295,37 @@ 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 promisor.sendFields\" '\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+\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 promisor.acceptfromserver=All \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that fields are properly transmitted\n+\tENCODED_URL=$(echo \"file://$(pwd)/lop\" | sed -e \"s/ /%20/g\") &&\n+\tPR1=\"name=lop,url=$ENCODED_URL,partialCloneFilter=blob:none\" &&\n+\tPR2=\"name=otherLop,url=https://invalid.invalid,partialCloneFilter=blob:limit=10k,token=fooBar\" &&\n+\ttest_grep \"clone< promisor-remote=$PR1;$PR2\" trace &&\n+\ttest_grep \"clone> promisor-remote=lop;otherLop\" 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 \n-- \n2.50.1.324.gc7d7c83534\n\n"},{"id":"522356","messageId":"20250721141056.2283349-4-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250721141056.2283349-1-christian.couder@gmail.com","subject":"[PATCH v6 3/5] promisor-remote: refactor how we parse advertised fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-21T14:10:21Z","receivedAt":"2025-07-21T14:11:17Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"In a follow up commit we are going to parse more fields, like a filter\nand a token, coming from the server when it advertises promisor remotes\nusing the \"promisor-remote\" capability.\n\nTo prepare for this, let's refactor the code that parses the advertised\nfields coming from the server into a new parse_one_advertised_remote()\nfunction that will populate a `struct promisor_info` with the content\nof the fields it parsed.\n\nWhile at it, let's also pass this `struct promisor_info` to the\nshould_accept_remote() function, instead of passing it the parsed name\nand url.\n\nThese changes will make it simpler to both parse more fields and access\nthe content of these parsed fields in follow up commits.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 91 ++++++++++++++++++++++++++++++++---------------\n 1 file changed, 62 insertions(+), 29 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 3420ccae63..ae2c49a0a0 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -404,16 +404,20 @@ struct promisor_info {\n \tconst char *token;\n };\n \n+static void promisor_info_free(struct promisor_info *p)\n+{\n+\tfree((char *)p->name);\n+\tfree((char *)p->url);\n+\tfree((char *)p->filter);\n+\tfree((char *)p->token);\n+\tfree(p);\n+}\n+\n static void promisor_info_list_clear(struct string_list *list)\n {\n-\tfor (size_t i = 0; i < list->nr; i++) {\n-\t\tstruct promisor_info *p = list->items[i].util;\n-\t\tfree((char *)p->name);\n-\t\tfree((char *)p->url);\n-\t\tfree((char *)p->filter);\n-\t\tfree((char *)p->token);\n-\t}\n-\tstring_list_clear(list, 1);\n+\tfor (size_t i = 0; i < list->nr; i++)\n+\t\tpromisor_info_free(list->items[i].util);\n+\tstring_list_clear(list, 0);\n }\n \n static void set_one_field(struct promisor_info *p,\n@@ -530,11 +534,13 @@ enum accept_promisor {\n };\n \n static int should_accept_remote(enum accept_promisor accept,\n-\t\t\t\tconst char *remote_name, const char *remote_url,\n+\t\t\t\tstruct promisor_info *advertised,\n \t\t\t\tstruct string_list *config_info)\n {\n \tstruct promisor_info *p;\n \tstruct string_list_item *item;\n+\tconst char *remote_name = advertised->name;\n+\tconst char *remote_url = advertised->url;\n \n \tif (accept == ACCEPT_ALL)\n \t\treturn 1;\n@@ -568,6 +574,46 @@ static int should_accept_remote(enum accept_promisor accept,\n \treturn 0;\n }\n \n+static struct promisor_info *parse_one_advertised_remote(struct strbuf *remote_info)\n+{\n+\tstruct promisor_info *info = xcalloc(1, sizeof(*info));\n+\tstruct strbuf **elems = strbuf_split(remote_info, ',');\n+\n+\tfor (size_t i = 0; elems[i]; i++) {\n+\t\tchar *elem = elems[i]->buf;\n+\t\tchar *value;\n+\t\tchar *p = strchr(elem, '=');\n+\n+\t\tstrbuf_strip_suffix(elems[i], \",\");\n+\n+\t\tif (!p) {\n+\t\t\twarning(_(\"invalid element '%s' from remote info\"), elem);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\t*p = '\\0';\n+\t\tvalue = url_percent_decode(p + 1);\n+\n+\t\tif (!strcmp(elem, \"name\"))\n+\t\t\tinfo->name = value;\n+\t\telse if (!strcmp(elem, \"url\"))\n+\t\t\tinfo->url = value;\n+\t\telse\n+\t\t\tfree(value);\n+\t}\n+\n+\tstrbuf_list_free(elems);\n+\n+\tif (!info->name || !info->url) {\n+\t\twarning(_(\"server advertised a promisor remote without a name or URL: %s\"),\n+\t\t\tremote_info->buf);\n+\t\tpromisor_info_free(info);\n+\t\treturn NULL;\n+\t}\n+\n+\treturn info;\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@@ -604,32 +650,19 @@ static void filter_promisor_remote(struct repository *repo,\n \tremotes = strbuf_split_str(info, ';', 0);\n \n \tfor (size_t i = 0; remotes[i]; i++) {\n-\t\tstruct strbuf **elems;\n-\t\tconst char *remote_name = NULL;\n-\t\tconst char *remote_url = NULL;\n-\t\tchar *decoded_name = NULL;\n-\t\tchar *decoded_url = NULL;\n+\t\tstruct promisor_info *advertised;\n \n \t\tstrbuf_strip_suffix(remotes[i], \";\");\n-\t\telems = strbuf_split(remotes[i], ',');\n \n-\t\tfor (size_t j = 0; elems[j]; j++) {\n-\t\t\tstrbuf_strip_suffix(elems[j], \",\");\n-\t\t\tif (!skip_prefix(elems[j]->buf, \"name=\", &remote_name))\n-\t\t\t\tskip_prefix(elems[j]->buf, \"url=\", &remote_url);\n-\t\t}\n+\t\tadvertised = parse_one_advertised_remote(remotes[i]);\n \n-\t\tif (remote_name)\n-\t\t\tdecoded_name = url_percent_decode(remote_name);\n-\t\tif (remote_url)\n-\t\t\tdecoded_url = url_percent_decode(remote_url);\n+\t\tif (!advertised)\n+\t\t\tcontinue;\n \n-\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, &config_info))\n-\t\t\tstrvec_push(accepted, decoded_name);\n+\t\tif (should_accept_remote(accept, advertised, &config_info))\n+\t\t\tstrvec_push(accepted, advertised->name);\n \n-\t\tstrbuf_list_free(elems);\n-\t\tfree(decoded_name);\n-\t\tfree(decoded_url);\n+\t\tpromisor_info_free(advertised);\n \t}\n \n \tpromisor_info_list_clear(&config_info);\n-- \n2.50.1.324.gc7d7c83534\n\n"},{"id":"522358","messageId":"20250721141056.2283349-5-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250721141056.2283349-1-christian.couder@gmail.com","subject":"[PATCH v6 4/5] promisor-remote: allow a client to check fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-21T14:10:22Z","receivedAt":"2025-07-21T14:11:18Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"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\nLet's make it possible for a client to check if these fields match\nwhat it expects before accepting a promisor remote.\n\nWe allow this by introducing a new \"promisor.checkFields\"\nconfiguration variable. It should contain a comma or space separated\nlist of fields that will be checked.\n\nBy limiting the protocol to specific well-defined fields, we ensure\nboth server and client have a shared understanding of field\nsemantics and usage.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    | 40 ++++++++++++\n promisor-remote.c                     | 89 ++++++++++++++++++++++++---\n t/t5710-promisor-remote-capability.sh | 34 ++++++++++\n 3 files changed, 155 insertions(+), 8 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex b4a72c2152..1cfea2fa07 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -50,3 +50,43 @@ promisor.acceptFromServer::\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+\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+\ttransmitted by a server correspond to the values of these\n+\tfields in its own configuration before accepting a promisor\n+\tremote. Currently, \"partialCloneFilter\" and \"token\" are the\n+\tonly supported field names.\n++\n+If one of these field names (e.g., \"token\") is being checked for an\n+advertised promisor remote (e.g., \"foo\"), three conditions must be met\n+for the check of this specific field to pass:\n++\n+1. The corresponding local configuration (e.g., `remote.foo.token`)\n+   must be set.\n+2. The server must advertise the \"token\" field for remote \"foo\".\n+3. The value of the locally configured `remote.foo.token` must exactly\n+   match the value advertised by the server for the \"token\" field.\n++\n+If any of these conditions is not met for any field name listed in\n+`promisor.checkFields`, the advertised remote \"foo\" is rejected.\n++\n+For the \"partialCloneFilter\" field, this allows the client to ensure\n+that the server's filter matches what it expects locally, preventing\n+inconsistencies in filtering behavior. For the \"token\" field, this can\n+be used to verify that authentication credentials match expected\n+values.\n++\n+Field names are compared case-insensitively. Field values are compared\n+case-sensitively.\n++\n+The \"name\" and \"url\" fields are always checked according to the\n+`promisor.acceptFromServer` policy, independently of this setting.\n++\n+The field names and values should be passed by the server through the\n+\"promisor-remote\" capability by using the `promisor.sendFields` config\n+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].\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex ae2c49a0a0..501cb92391 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -388,6 +388,20 @@ static struct string_list *fields_sent(void)\n \treturn &fields_list;\n }\n \n+static struct string_list *fields_checked(void)\n+{\n+\tstatic struct string_list fields_list = STRING_LIST_INIT_NODUP;\n+\tstatic int initialized = 0;\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+}\n+\n /*\n  * Struct for promisor remotes involved in the \"promisor-remote\"\n  * protocol capability.\n@@ -533,6 +547,61 @@ enum accept_promisor {\n \tACCEPT_ALL\n };\n \n+static int match_field_against_config(const char *field, const char *value,\n+\t\t\t\t      struct promisor_info *config_info)\n+{\n+\tif (config_info->filter && !strcasecmp(field, promisor_field_filter))\n+\t\treturn !strcmp(config_info->filter, value);\n+\telse if (config_info->token && !strcasecmp(field, promisor_field_token))\n+\t\treturn !strcmp(config_info->token, value);\n+\n+\treturn 0;\n+}\n+\n+static int all_fields_match(struct promisor_info *advertised,\n+\t\t\t    struct string_list *config_info,\n+\t\t\t    int in_list)\n+{\n+\tstruct string_list* fields = fields_checked();\n+\tstruct string_list_item *item_checked;\n+\n+\tfor_each_string_list_item(item_checked, fields) {\n+\t\tint match = 0;\n+\t\tconst char *field = item_checked->string;\n+\t\tconst char *value = NULL;\n+\t\tstruct string_list_item *item;\n+\n+\t\tif (!strcasecmp(field, promisor_field_filter))\n+\t\t\tvalue = advertised->filter;\n+\t\telse if (!strcasecmp(field, promisor_field_token))\n+\t\t\tvalue = advertised->token;\n+\n+\t\tif (!value)\n+\t\t\treturn 0;\n+\n+\t\tif (in_list) {\n+\t\t\tfor_each_string_list_item(item, config_info) {\n+\t\t\t\tstruct promisor_info *p = item->util;\n+\t\t\t\tif (match_field_against_config(field, value, p)) {\n+\t\t\t\t\tmatch = 1;\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t}\n+\t\t} else {\n+\t\t\titem = string_list_lookup(config_info, advertised->name);\n+\t\t\tif (item) {\n+\t\t\t\tstruct promisor_info *p = item->util;\n+\t\t\t\tmatch = match_field_against_config(field, value, p);\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (!match)\n+\t\t\treturn 0;\n+\t}\n+\n+\treturn 1;\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@@ -543,7 +612,7 @@ static int should_accept_remote(enum accept_promisor accept,\n \tconst char *remote_url = advertised->url;\n \n \tif (accept == ACCEPT_ALL)\n-\t\treturn 1;\n+\t\treturn all_fields_match(advertised, config_info, 1);\n \n \t/* Get config info for that promisor remote */\n \titem = string_list_lookup(config_info, remote_name);\n@@ -555,7 +624,7 @@ static int should_accept_remote(enum accept_promisor accept,\n \tp = item->util;\n \n \tif (accept == ACCEPT_KNOWN_NAME)\n-\t\treturn 1;\n+\t\treturn all_fields_match(advertised, config_info, 0);\n \n \tif (accept != ACCEPT_KNOWN_URL)\n \t\tBUG(\"Unhandled 'enum accept_promisor' value '%d'\", accept);\n@@ -566,7 +635,7 @@ static int should_accept_remote(enum accept_promisor accept,\n \t}\n \n \tif (!strcmp(p->url, remote_url))\n-\t\treturn 1;\n+\t\treturn all_fields_match(advertised, config_info, 0);\n \n \twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s'\"),\n \t\tremote_name, p->url, remote_url);\n@@ -598,6 +667,10 @@ static struct promisor_info *parse_one_advertised_remote(struct strbuf *remote_i\n \t\t\tinfo->name = value;\n \t\telse if (!strcmp(elem, \"url\"))\n \t\t\tinfo->url = value;\n+\t\telse if (!strcasecmp(elem, promisor_field_filter))\n+\t\t\tinfo->filter = value;\n+\t\telse if (!strcasecmp(elem, promisor_field_token))\n+\t\t\tinfo->token = value;\n \t\telse\n \t\t\tfree(value);\n \t}\n@@ -640,11 +713,6 @@ static void filter_promisor_remote(struct repository *repo,\n \tif (accept == ACCEPT_NONE)\n \t\treturn;\n \n-\tif (accept != ACCEPT_ALL) {\n-\t\tpromisor_config_info_list(repo, &config_info, NULL);\n-\t\tstring_list_sort(&config_info);\n-\t}\n-\n \t/* Parse remote info received */\n \n \tremotes = strbuf_split_str(info, ';', 0);\n@@ -659,6 +727,11 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tif (!advertised)\n \t\t\tcontinue;\n \n+\t\tif (!config_info.nr) {\n+\t\t\tpromisor_config_info_list(repo, &config_info, fields_checked());\n+\t\t\tstring_list_sort(&config_info);\n+\t\t}\n+\n \t\tif (should_accept_remote(accept, advertised, &config_info))\n \t\t\tstrvec_push(accepted, advertised->name);\n \ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex 204528b2e0..023735d6a8 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -326,6 +326,40 @@ test_expect_success \"clone with promisor.sendFields\" '\n \tcheck_missing_objects server 1 \"$oid\"\n '\n \n+test_expect_success \"clone with promisor.checkFields\" '\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+\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.partialCloneFilter=\"blob:none\" \\\n+\t\t-c promisor.acceptfromserver=All \\\n+\t\t-c promisor.checkFields=partialcloneFilter \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that fields are properly transmitted\n+\tENCODED_URL=$(echo \"file://$(pwd)/lop\" | sed -e \"s/ /%20/g\") &&\n+\tPR1=\"name=lop,url=$ENCODED_URL,partialCloneFilter=blob:none\" &&\n+\tPR2=\"name=otherLop,url=https://invalid.invalid,partialCloneFilter=blob:limit=10k,token=fooBar\" &&\n+\ttest_grep \"clone< promisor-remote=$PR1;$PR2\" trace &&\n+\ttest_grep \"clone> promisor-remote=lop\" trace &&\n+\ttest_grep ! \"clone> promisor-remote=lop;otherLop\" 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 \n-- \n2.50.1.324.gc7d7c83534\n\n"},{"id":"522357","messageId":"20250721141056.2283349-6-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250721141056.2283349-1-christian.couder@gmail.com","subject":"[PATCH v6 5/5] promisor-remote: use string constants for 'name' and 'url' too","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-21T14:10:23Z","receivedAt":"2025-07-21T14:11:20Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"A previous commit started to define `promisor_field_filter` and\n`promisor_field_token`, and used them instead of the\n\"partialCloneFilter\" and \"token\" string literals.\n\nLet's do the same for \"name\" and \"url\" to avoid repeating them\nseveral times and for consistency with the other fields.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 14 ++++++++++----\n 1 file changed, 10 insertions(+), 4 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 501cb92391..2f86c68397 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -314,6 +314,12 @@ static int allow_unsanitized(char ch)\n \treturn ch > 32 && ch < 127;\n }\n \n+/*\n+ * All the fields used in \"promisor-remote\" protocol capability,\n+ * including the mandatory \"name\" and \"url\" ones.\n+ */\n+static const char promisor_field_name[] = \"name\";\n+static const char promisor_field_url[] = \"url\";\n static const char promisor_field_filter[] = \"partialCloneFilter\";\n static const char promisor_field_token[] = \"token\";\n \n@@ -520,9 +526,9 @@ char *promisor_remote_info(struct repository *repo)\n \t\tif (item != config_info.items)\n \t\t\tstrbuf_addch(&sb, ';');\n \n-\t\tstrbuf_addstr(&sb, \"name=\");\n+\t\tstrbuf_addf(&sb, \"%s=\", promisor_field_name);\n \t\tstrbuf_addstr_urlencode(&sb, p->name, allow_unsanitized);\n-\t\tstrbuf_addstr(&sb, \",url=\");\n+\t\tstrbuf_addf(&sb, \",%s=\", promisor_field_url);\n \t\tstrbuf_addstr_urlencode(&sb, p->url, allow_unsanitized);\n \n \t\tif (p->filter) {\n@@ -663,9 +669,9 @@ static struct promisor_info *parse_one_advertised_remote(struct strbuf *remote_i\n \t\t*p = '\\0';\n \t\tvalue = url_percent_decode(p + 1);\n \n-\t\tif (!strcmp(elem, \"name\"))\n+\t\tif (!strcmp(elem, promisor_field_name))\n \t\t\tinfo->name = value;\n-\t\telse if (!strcmp(elem, \"url\"))\n+\t\telse if (!strcmp(elem, promisor_field_url))\n \t\t\tinfo->url = value;\n \t\telse if (!strcasecmp(elem, promisor_field_filter))\n \t\t\tinfo->filter = value;\n-- \n2.50.1.324.gc7d7c83534\n\n"},{"id":"522366","messageId":"xmqqtt35xtng.fsf@gitster.g","threadId":"63293","inReplyTo":"CAP8UFD1wYXrEC2VYBHTL7NS1ksSa0oiBgB3Ua=6V1SnP=+RMsQ@mail.gmail.com","subject":"Re: [PATCH v5 2/5] promisor-remote: allow a server to advertise more fields","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-21T18:53:23Z","receivedAt":"2025-07-21T18:53:26Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> Ok, I have changed it to the following in v6:\n>\n>    To allow clients to make more informed decisions about which promisor\n>    remotes they accept, let's make it possible to pass more information\n>    by introducing a new \"promisor.sendFields\" configuration variable.\n\nA meta observation, as I haven't even thought if the above proposal\nmakes sense or not, but a reponse for v5 like the above that comes\nnearly at the same minite as v6 highly discourages any response to\nthis message that may want to say \"oh, that is not a good idea\nbecause ...\".\n\n>> By making it easier for casual humans who manually write the\n>> configuration variable (presumably while testing) and allowing both\n>> comma and space as separator, this design decision is forcing one\n>> more rule to worry about for those who are writing the parser for\n>> the value.  There may be some existing configuration variables with\n>> such a \"leninent\" syntax, but I'd rather see us not make the mess\n>> even worse.\n>\n> We indeed have a number of configuration variables accepting lists of\n> items separated by both comma and space. As we cannot fix those easily\n> for backward compatibility and they still make things a bit simpler\n> for users, my opinion is that we'd better bite the bullet and make\n> sure we have a simple and hard-to-misuse standard way to parse such\n> lists.\n\nThe position is understandable (I am not saying agreeable), but if\none takes such a position, this series will get even longer, by\nrequiring such a refactoring and new set of helper functions in the\nfront of the series, to avoid making things even worse than they\ncurrently are.  If you want to punt on that \"simple standard way\"\nand leave it outside the series, then please do not add such syntax\nto the variables in this series.\n\n>> I know the name \"field\" was discussed in earlier iterations, but\n>> these three lines together with \"For example\" in a later paragraph,\n>> it hints to me that this mechanism is to choose variable-value pairs\n>> for which among remotes.<name>.* variables to send after name=<name>\n>> and url=<url>; is my understanding correct?  If so, can we clarify\n>> the paragraphs around here even more so that I do not even have to\n>> ask this question?\n>\n> Yeah, it seems to me that there was previously a sentence about what\n> fields are, but it looks like I removed it by mistake. Anyway, before\n> the paragraph about what the \"promisor.sendFields\" configuration\n> variable contains, I have added the following in v6:\n>\n>    On the server side, information about a remote `foo` is stored in\n>    configuration variables named `remote.foo.<variable-name>`. To make\n>    it clearer and simpler, we use `field` and `field name` like this:\n>\n>      * `field name` refers to the <variable-name> part of such a\n>        configuration variable, and\n>\n>      * `field` refers to both the `field name` and the value of such a\n>        configuration variable.\n>\n>> What do we call the third-level of a variable name in the\n>> configuration file?  The description on the \"--regexp\" option in\n>> \"git config --help\" hints one:\n>>\n>>     With `get`, interpret the name as a regular expression. Regular\n>>     expression matching is currently case-sensitive and done against\n>>     a canonicalized version of the key in which section and variable\n>>     names are lowercased, but subsection names are not.\n>>\n>> So a for remote.origin.partialCloneFilter, \"remote\" is the section\n>> name, \"origin\" is the subsection name, and \"partialCloneFilter\" is\n>> the variable name.\n>\n> Yeah, I thought that \"variable name\" could be confusing, so I prefered\n> to use another word, \"field\", to talk about this.\n> ...\n> I'd rather avoid talking about \"variable name\" other than to explain\n> what \"field name\" and \"field\" are. I think it would be too confusing,\n> especially if the name of the new config options are still\n> \"promisor.sendFields\" and \"promisor.checkFields\".\n\nSorry, the argument does not make much sense to me.\n\nAfter all, the end-users who follow the documentation needs to know\nwhich *VARIABLES* (in remotes.<name><VARIABLE>) to set to affect the\nvalue that this mechanism lets them send out.  And the configuration\nvariable to control which of remotes.<name>.<VARIABLE> are sent out\nis a new thing, and it itself calls it a FIELD.  Whatever name that\nnew thing chooses to call itself, the fact is that it is about what\nthe users have long known as configuration variables.\n\nSo, what makes this whole thing more confusing than necessary, to\nme, looks like your use of the word \"field\" in the first place.  The\nuse of word \"field\" in \"sendFields\" and \"checkFields\" are much much\nmore recent than the concept of \"configuration variables\" that has\nlong been established in users' minds (as far as I know, these\n\"fields\" are not even in any released versions), so I do not see why\nwe want to keep it and force users learn yet another word.  Just fix\nthe name of these new configuration variables, explain that they are\nused to name other configuration variables, and be done it without\nuttering \"field\" even once, and we would be good and less confusing,\nwouldn't we?  Or am I being too naive and forgetting some already\nestablished use of sendFields and checkFields?\n"},{"id":"522369","messageId":"xmqqldohxpp5.fsf@gitster.g","threadId":"63293","inReplyTo":"20250721141056.2283349-6-christian.couder@gmail.com","subject":"Re: [PATCH v6 5/5] promisor-remote: use string constants for 'name' and 'url' too","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-21T20:18:46Z","receivedAt":"2025-07-21T20:18:50Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> A previous commit started to define `promisor_field_filter` and\n> `promisor_field_token`, and used them instead of the\n> \"partialCloneFilter\" and \"token\" string literals.\n>\n> Let's do the same for \"name\" and \"url\" to avoid repeating them\n> several times and for consistency with the other fields.\n>\n> Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n> ---\n>  promisor-remote.c | 14 ++++++++++----\n>  1 file changed, 10 insertions(+), 4 deletions(-)\n\nMakes the code much cleaner.  Nice.\n\n> diff --git a/promisor-remote.c b/promisor-remote.c\n> index 501cb92391..2f86c68397 100644\n> --- a/promisor-remote.c\n> +++ b/promisor-remote.c\n> @@ -314,6 +314,12 @@ static int allow_unsanitized(char ch)\n>  \treturn ch > 32 && ch < 127;\n>  }\n>  \n> +/*\n> + * All the fields used in \"promisor-remote\" protocol capability,\n> + * including the mandatory \"name\" and \"url\" ones.\n> + */\n> +static const char promisor_field_name[] = \"name\";\n> +static const char promisor_field_url[] = \"url\";\n>  static const char promisor_field_filter[] = \"partialCloneFilter\";\n>  static const char promisor_field_token[] = \"token\";\n>  \n> @@ -520,9 +526,9 @@ char *promisor_remote_info(struct repository *repo)\n>  \t\tif (item != config_info.items)\n>  \t\t\tstrbuf_addch(&sb, ';');\n>  \n> -\t\tstrbuf_addstr(&sb, \"name=\");\n> +\t\tstrbuf_addf(&sb, \"%s=\", promisor_field_name);\n>  \t\tstrbuf_addstr_urlencode(&sb, p->name, allow_unsanitized);\n> -\t\tstrbuf_addstr(&sb, \",url=\");\n> +\t\tstrbuf_addf(&sb, \",%s=\", promisor_field_url);\n>  \t\tstrbuf_addstr_urlencode(&sb, p->url, allow_unsanitized);\n>  \n>  \t\tif (p->filter) {\n> @@ -663,9 +669,9 @@ static struct promisor_info *parse_one_advertised_remote(struct strbuf *remote_i\n>  \t\t*p = '\\0';\n>  \t\tvalue = url_percent_decode(p + 1);\n>  \n> -\t\tif (!strcmp(elem, \"name\"))\n> +\t\tif (!strcmp(elem, promisor_field_name))\n>  \t\t\tinfo->name = value;\n> -\t\telse if (!strcmp(elem, \"url\"))\n> +\t\telse if (!strcmp(elem, promisor_field_url))\n>  \t\t\tinfo->url = value;\n>  \t\telse if (!strcasecmp(elem, promisor_field_filter))\n>  \t\t\tinfo->filter = value;\n"},{"id":"522372","messageId":"xmqq1pq9xoqe.fsf@gitster.g","threadId":"63293","inReplyTo":"20250721141056.2283349-4-christian.couder@gmail.com","subject":"Re: [PATCH v6 3/5] promisor-remote: refactor how we parse advertised fields","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-21T20:39:37Z","receivedAt":"2025-07-21T20:39:40Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> +static struct promisor_info *parse_one_advertised_remote(struct strbuf *remote_info)\n> +{\n> +\tstruct promisor_info *info = xcalloc(1, sizeof(*info));\n> +\tstruct strbuf **elems = strbuf_split(remote_info, ',');\n\nUnless the primary use of an array is about passing it around as a\nwhole \"set\", name such a variable singular, so that element[4] can\nbe naturally read as \"fourth element\"---\"fourth elements\" is not as\nnatural.\n\nAlso, can't we do this without strbuf_split(), which is a wrong API\nto use in general [*]?  strbuf is a very good data structure to work\nwith when editing string data, but an array of strbuf is not---you\nwould not be editing many pieces of string data in parallel.\n\n    [*] often string_list_split_in_place() is a better alternative,\n    especially when you do not have to heavily edit the substrings.\n\n> +\tfor (size_t i = 0; elems[i]; i++) {\n> +\t\tchar *elem = elems[i]->buf;\n> +\t\tchar *value;\n> +\t\tchar *p = strchr(elem, '=');\n\n\tThe pointer elem points at the name, and the pointer p\n\tpoints at the beginning of value, which could contain '='.\n\n> +\t\tstrbuf_strip_suffix(elems[i], \",\");\n\nThis does not even count as \"editing\"; split_in_place() would have\nremoved the trailing comma (and replaced it with NUL to terminate\nthe string).\n\n> +\t\tif (!p) {\n> +\t\t\twarning(_(\"invalid element '%s' from remote info\"), elem);\n> +\t\t\tcontinue;\n> +\t\t}\n\n\telem pointed at \"foo\" or \"foo,\"; we may have stripped the\n\ttrailing comma, but we didn't see the equal sign to start\n\tthe value at all.  Bad input.\n\n> +\t\t*p = '\\0';\n\n\tTerminate the name by replacing '=' with NUL.\n\n> +\t\tvalue = url_percent_decode(p + 1);\n\n\tCan this helper function fail and signal that it saw a\n\tmalformed data?  If not already, shouldn't it be taught to\n\tdo so?\n\nWe are inventing the syntax for this data in this series, so if this\nhelper takes garbage data silently, and if we are not willing to fix\nit, then we can even consider changing the syntax to something with\na helper we can use that already has a good error checking.\n\n> +\t\tif (!strcmp(elem, \"name\"))\n> +\t\t\tinfo->name = value;\n> +\t\telse if (!strcmp(elem, \"url\"))\n> +\t\t\tinfo->url = value;\n> +\t\telse\n> +\t\t\tfree(value);\n\nAs url_percent_decode() always allocate a new copy of string even\nwhen there is nothing to decode, value will always be an allocated\nstring, and if we are not storing it away, it will leak.  The copies\nwe kept in info->{name,url} are ours to own.  Makes sense.\n\n> +\tstrbuf_list_free(elems);\n\nAnd because [elem..p] (name) we only peeked, we can safely release\nthe whole thing.  If you used string_list_split_in_place(), you\nwould only free the string_list shell without having to free the\nunderlying string.\n\n> +\tif (!info->name || !info->url) {\n> +\t\twarning(_(\"server advertised a promisor remote without a name or URL: %s\"),\n> +\t\t\tremote_info->buf);\n> +\t\tpromisor_info_free(info);\n> +\t\treturn NULL;\n\nNicely done.\n\n> +\t}\n> +\n> +\treturn info;\n> +}\n"},{"id":"522373","messageId":"xmqqo6tdw996.fsf@gitster.g","threadId":"63293","inReplyTo":"20250721141056.2283349-5-christian.couder@gmail.com","subject":"Re: [PATCH v6 4/5] promisor-remote: allow a client to check fields","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-21T20:59:17Z","receivedAt":"2025-07-21T20:59:20Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> diff --git a/promisor-remote.c b/promisor-remote.c\n> index ae2c49a0a0..501cb92391 100644\n> --- a/promisor-remote.c\n> +++ b/promisor-remote.c\n> @@ -388,6 +388,20 @@ static struct string_list *fields_sent(void)\n>  \treturn &fields_list;\n>  }\n>  \n> +static struct string_list *fields_checked(void)\n> +{\n> +\tstatic struct string_list fields_list = STRING_LIST_INIT_NODUP;\n> +\tstatic int initialized = 0;\n\nNo need to explicitly 0 initialize \"static int\"; let BSS take care\nof it.  Perhaps we should add an entry to CodingGuidelines if we do\nnot have one (#leftoverbits).\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> +}\n\nOK.\n\n> @@ -533,6 +547,61 @@ enum accept_promisor {\n>  \tACCEPT_ALL\n>  };\n>  \n> +static int match_field_against_config(const char *field, const char *value,\n> +\t\t\t\t      struct promisor_info *config_info)\n> +{\n> +\tif (config_info->filter && !strcasecmp(field, promisor_field_filter))\n> +\t\treturn !strcmp(config_info->filter, value);\n> +\telse if (config_info->token && !strcasecmp(field, promisor_field_token))\n> +\t\treturn !strcmp(config_info->token, value);\n> +\n> +\treturn 0;\n> +}\n> +\n> +static int all_fields_match(struct promisor_info *advertised,\n> +\t\t\t    struct string_list *config_info,\n> +\t\t\t    int in_list)\n> +{\n> +\tstruct string_list* fields = fields_checked();\n\nAsterisk sticks to a variable, not type.  I.e.\n\n\tstruct string_list *fields = ...;\n\n\n> +\tstruct string_list_item *item_checked;\n> +\n> +\tfor_each_string_list_item(item_checked, fields) {\n> +\t\tint match = 0;\n> +\t\tconst char *field = item_checked->string;\n> +\t\tconst char *value = NULL;\n> +\t\tstruct string_list_item *item;\n> +\n> +\t\tif (!strcasecmp(field, promisor_field_filter))\n> +\t\t\tvalue = advertised->filter;\n> +\t\telse if (!strcasecmp(field, promisor_field_token))\n> +\t\t\tvalue = advertised->token;\n\nHmph, together with match_field_against_config(), do we really need\nto have this case insensitive to begin with?  I would suggest making\nit a habit to design the interface minimally, and not making case\ninsensitive comparison as default counts one of them.  If the\ncomparison were not case insensitive, we do not even need to have\nthis loop; rather we can just look up from the list of the fields\nfor an exact string (i.e. promisor_field_filter).  I do not know\noffhand what other code will become easier to read and simpler by\nsuch a change.\n\nThanks.\n"},{"id":"523055","messageId":"CAP8UFD0scEUQ1X6gX+ZTKE_z05RESyJi2pq8Qa0MhS0CxjMQVg@mail.gmail.com","threadId":"63293","inReplyTo":"xmqqtt35xtng.fsf@gitster.g","subject":"Re: [PATCH v5 2/5] promisor-remote: allow a server to advertise more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-31T07:20:55Z","receivedAt":"2025-07-31T07:21:09Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Mon, Jul 21, 2025 at 8:53 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Christian Couder <christian.couder@gmail.com> writes:\n>\n> > Ok, I have changed it to the following in v6:\n> >\n> >    To allow clients to make more informed decisions about which promisor\n> >    remotes they accept, let's make it possible to pass more information\n> >    by introducing a new \"promisor.sendFields\" configuration variable.\n>\n> A meta observation, as I haven't even thought if the above proposal\n> makes sense or not, but a reponse for v5 like the above that comes\n> nearly at the same minite as v6 highly discourages any response to\n> this message that may want to say \"oh, that is not a good idea\n> because ...\".\n\nI am not sure why it discourages reponses. People can still reply to\nthe v6 patch or to the discussion that started from the v5 patch or\nboth. The discussion from the v5 patch is not obsolete if some\ncomments have not been properly addressed.\n\nAlso the fact that the replies come at the same time as a new version,\nmeans that reviewers don't need to context switch as much as if there\nwere for example some replies at one time, then a few days later\nreplies to the rest of the reviews, then a few days later a new\nversion. So for reviewers who don't like to context switch much it\nshould be better if everything is sent at once.\n\n> >> By making it easier for casual humans who manually write the\n> >> configuration variable (presumably while testing) and allowing both\n> >> comma and space as separator, this design decision is forcing one\n> >> more rule to worry about for those who are writing the parser for\n> >> the value.  There may be some existing configuration variables with\n> >> such a \"leninent\" syntax, but I'd rather see us not make the mess\n> >> even worse.\n> >\n> > We indeed have a number of configuration variables accepting lists of\n> > items separated by both comma and space. As we cannot fix those easily\n> > for backward compatibility and they still make things a bit simpler\n> > for users, my opinion is that we'd better bite the bullet and make\n> > sure we have a simple and hard-to-misuse standard way to parse such\n> > lists.\n>\n> The position is understandable (I am not saying agreeable), but if\n> one takes such a position, this series will get even longer, by\n> requiring such a refactoring and new set of helper functions in the\n> front of the series, to avoid making things even worse than they\n> currently are.  If you want to punt on that \"simple standard way\"\n> and leave it outside the series, then please do not add such syntax\n> to the variables in this series.\n\nI'd like it if there was a consensus on doing such a refactoring\nbefore starting to work on it. So if you are fine with that\nrefactoring, I am willing to either add it as part of this series, or\nwork on it after this series is merged (like for the recent\ncc/t9350-cleanup patch), as you prefer.\n\nI couldn't know your and others' opinion on it (and I am still not\nsure about it) before we started discussing it. Sometimes for example\nyou are also fine with just leaving over those kinds of bits. So for\nnow in v7, I haven't changed this, but let me know and I will make the\nnecessary changes in v8 or later in separate patches.\n\n> > Yeah, it seems to me that there was previously a sentence about what\n> > fields are, but it looks like I removed it by mistake. Anyway, before\n> > the paragraph about what the \"promisor.sendFields\" configuration\n> > variable contains, I have added the following in v6:\n> >\n> >    On the server side, information about a remote `foo` is stored in\n> >    configuration variables named `remote.foo.<variable-name>`. To make\n> >    it clearer and simpler, we use `field` and `field name` like this:\n> >\n> >      * `field name` refers to the <variable-name> part of such a\n> >        configuration variable, and\n> >\n> >      * `field` refers to both the `field name` and the value of such a\n> >        configuration variable.\n> >\n> >> What do we call the third-level of a variable name in the\n> >> configuration file?  The description on the \"--regexp\" option in\n> >> \"git config --help\" hints one:\n> >>\n> >>     With `get`, interpret the name as a regular expression. Regular\n> >>     expression matching is currently case-sensitive and done against\n> >>     a canonicalized version of the key in which section and variable\n> >>     names are lowercased, but subsection names are not.\n> >>\n> >> So a for remote.origin.partialCloneFilter, \"remote\" is the section\n> >> name, \"origin\" is the subsection name, and \"partialCloneFilter\" is\n> >> the variable name.\n> >\n> > Yeah, I thought that \"variable name\" could be confusing, so I prefered\n> > to use another word, \"field\", to talk about this.\n> > ...\n> > I'd rather avoid talking about \"variable name\" other than to explain\n> > what \"field name\" and \"field\" are. I think it would be too confusing,\n> > especially if the name of the new config options are still\n> > \"promisor.sendFields\" and \"promisor.checkFields\".\n>\n> Sorry, the argument does not make much sense to me.\n>\n> After all, the end-users who follow the documentation needs to know\n> which *VARIABLES* (in remotes.<name><VARIABLE>) to set to affect the\n> value that this mechanism lets them send out.  And the configuration\n> variable to control which of remotes.<name>.<VARIABLE> are sent out\n> is a new thing, and it itself calls it a FIELD.  Whatever name that\n> new thing chooses to call itself, the fact is that it is about what\n> the users have long known as configuration variables.\n>\n> So, what makes this whole thing more confusing than necessary, to\n> me, looks like your use of the word \"field\" in the first place.  The\n> use of word \"field\" in \"sendFields\" and \"checkFields\" are much much\n> more recent than the concept of \"configuration variables\" that has\n> long been established in users' minds (as far as I know, these\n> \"fields\" are not even in any released versions), so I do not see why\n> we want to keep it and force users learn yet another word.  Just fix\n> the name of these new configuration variables,\n\nYou mean using \"promisor.sendVariables\" and \"promisor.checkVariables\"\ninstead of \"promisor.sendFields\" and \"promisor.checkFields\", and using\n\"variable\" instead of \"field\" everywhere in the documentation and the\ncode?\n\n> explain that they are\n> used to name other configuration variables, and be done it without\n> uttering \"field\" even once, and we would be good and less confusing,\n> wouldn't we?  Or am I being too naive and forgetting some already\n> established use of sendFields and checkFields?\n\nI just think it will be confusing because after being sent over\nthrough the protocol, they are no longer variable in the sense that\nthe client can't (and shouldn't) change them. They could also be\nconfused with environment variables, so perhaps\n\"promisor.sendConfigVariables\" and \"promisor.checkConfigVariables\",\nbut the names will start to be long especially if we have to say that\nthey are from the server when processed on the client side.\n\nAlso I think it's better to use a different name because they are used\ndifferently, serve different purposes and have different rules applied\nto them in the context of the promisor-remote protocol capability.\n\nAnyway if that's what is prefered, I will make the change. For now in\nthe v7, I haven't changed this.\n"},{"id":"523056","messageId":"CAP8UFD0a5xJuSm4683J_wrZQic9Lzj3imjf7MgGHmwudbDhbtA@mail.gmail.com","threadId":"63293","inReplyTo":"xmqqo6tdw996.fsf@gitster.g","subject":"Re: [PATCH v6 4/5] promisor-remote: allow a client to check fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-31T07:21:46Z","receivedAt":"2025-07-31T07:22:01Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Mon, Jul 21, 2025 at 10:59 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Christian Couder <christian.couder@gmail.com> writes:\n>\n> > diff --git a/promisor-remote.c b/promisor-remote.c\n> > index ae2c49a0a0..501cb92391 100644\n> > --- a/promisor-remote.c\n> > +++ b/promisor-remote.c\n> > @@ -388,6 +388,20 @@ static struct string_list *fields_sent(void)\n> >       return &fields_list;\n> >  }\n> >\n> > +static struct string_list *fields_checked(void)\n> > +{\n> > +     static struct string_list fields_list = STRING_LIST_INIT_NODUP;\n> > +     static int initialized = 0;\n>\n> No need to explicitly 0 initialize \"static int\"; let BSS take care\n> of it.  Perhaps we should add an entry to CodingGuidelines if we do\n> not have one (#leftoverbits).\n\nYeah, right. This is fixed in v7 in both fields_checked() and fields_sent().\n\n> > @@ -533,6 +547,61 @@ enum accept_promisor {\n> >       ACCEPT_ALL\n> >  };\n> >\n> > +static int match_field_against_config(const char *field, const char *value,\n> > +                                   struct promisor_info *config_info)\n> > +{\n> > +     if (config_info->filter && !strcasecmp(field, promisor_field_filter))\n> > +             return !strcmp(config_info->filter, value);\n> > +     else if (config_info->token && !strcasecmp(field, promisor_field_token))\n> > +             return !strcmp(config_info->token, value);\n> > +\n> > +     return 0;\n> > +}\n> > +\n> > +static int all_fields_match(struct promisor_info *advertised,\n> > +                         struct string_list *config_info,\n> > +                         int in_list)\n> > +{\n> > +     struct string_list* fields = fields_checked();\n>\n> Asterisk sticks to a variable, not type.  I.e.\n>\n>         struct string_list *fields = ...;\n\nRight, fixed in v7.\n\n> > +     struct string_list_item *item_checked;\n> > +\n> > +     for_each_string_list_item(item_checked, fields) {\n> > +             int match = 0;\n> > +             const char *field = item_checked->string;\n> > +             const char *value = NULL;\n> > +             struct string_list_item *item;\n> > +\n> > +             if (!strcasecmp(field, promisor_field_filter))\n> > +                     value = advertised->filter;\n> > +             else if (!strcasecmp(field, promisor_field_token))\n> > +                     value = advertised->token;\n>\n> Hmph, together with match_field_against_config(), do we really need\n> to have this case insensitive to begin with?\n\nI think so. When comparing config option keys, we use case insensitive\ncomparisons, so I think it makes sense to use that when comparing\nfield names too.\n\nWhen we call fields_checked(), we get a list of field names as they\nhave been configured in the \"promisor.checkFields\" config variable. It\nis very likely that for \"partialCloneFilter\" some users may not\ncamelcase it properly. So if we use strcmp() to check if `field` is\n\"partialCloneFilter\", it might result in things not working as they\nexpect because of subtle camelcase issues.\n\n> I would suggest making\n> it a habit to design the interface minimally, and not making case\n> insensitive comparison as default counts one of them.\n\nThere is a reason. They are not used by default here.\n\n> If the\n> comparison were not case insensitive, we do not even need to have\n> this loop; rather we can just look up from the list of the fields\n> for an exact string (i.e. promisor_field_filter).  I do not know\n> offhand what other code will become easier to read and simpler by\n> such a change.\n\nI checked other places in the code and I think it's right that in the\n\"promisor-remote\" protocol the field names (\"partialCloneFilter\" and\n\"token\") should be checked case-sensitively, so in v7 I have made this\nchange in parse_one_advertised_remote():\n\n   @@ promisor-remote.c: static struct promisor_info\n*parse_one_advertised_remote(stru\n                       info->name = value;\n               else if (!strcmp(elem, \"url\"))\n                       info->url = value;\n   -+          else if (!strcasecmp(elem, promisor_field_filter))\n   ++          else if (!strcmp(elem, promisor_field_filter))\n    +                  info->filter = value;\n   -+          else if (!strcasecmp(elem, promisor_field_token))\n   ++          else if (!strcmp(elem, promisor_field_token))\n    +                  info->token = value;\n               else\n                       free(value);\n\nand I have clarified that in the doc too:\n\n   @@ Documentation/gitprotocol-v2.adoc: retrieving the header from a\nbundle at the in\n    +connecting to the remote. It corresponds to the \"remote.<name>.token\"\n    +config setting.\n    +\n   -+No other fields are defined by the protocol at this time. Clients MUST\n   -+ignore fields they don't recognize to allow for future protocol\n   -+extensions.\n   ++No other fields are defined by the protocol at this time. Field names\n   ++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\nBut when we deal with field names from the config, I think it's much\nmore user friendly to use case insensitive comparisons.\n\nAn alternative implementation might be to canonize the field names in\nfields_checked() and fields_sent(), so that for example\n\"partialclonefilter\" would be changed to \"partialCloneFilter\" there.\nWe could also use for example an `enum field_id` instead of field\nnames and convert the field names to those field IDs in\nfields_checked(), fields_sent() and perhaps other places. Then yeah we\nmight use case sensitive comparisons in some places like here and\nperhaps simplify other parts of the code. I am open to going in this\ndirection if that's what you prefer. I am not sure overall the code\nwill be much simpler though. We might just move complexity around.\nAlso for now I think using strcasecmp() is not likely to be a\nperformance issue.\n\nThanks.\n"},{"id":"523057","messageId":"CAP8UFD0urQ_0dbHjyF16w9HeTVxwo_r3Yi62hbwcC49m=aPZHw@mail.gmail.com","threadId":"63293","inReplyTo":"xmqq1pq9xoqe.fsf@gitster.g","subject":"Re: [PATCH v6 3/5] promisor-remote: refactor how we parse advertised fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-31T07:22:47Z","receivedAt":"2025-07-31T07:23:02Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Mon, Jul 21, 2025 at 10:39 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Christian Couder <christian.couder@gmail.com> writes:\n>\n> > +static struct promisor_info *parse_one_advertised_remote(struct strbuf *remote_info)\n> > +{\n> > +     struct promisor_info *info = xcalloc(1, sizeof(*info));\n> > +     struct strbuf **elems = strbuf_split(remote_info, ',');\n>\n> Unless the primary use of an array is about passing it around as a\n> whole \"set\", name such a variable singular, so that element[4] can\n> be naturally read as \"fourth element\"---\"fourth elements\" is not as\n> natural.\n>\n> Also, can't we do this without strbuf_split(), which is a wrong API\n> to use in general [*]?  strbuf is a very good data structure to work\n> with when editing string data, but an array of strbuf is not---you\n> would not be editing many pieces of string data in parallel.\n>\n>     [*] often string_list_split_in_place() is a better alternative,\n>     especially when you do not have to heavily edit the substrings.\n\nYeah, string_list_split_in_place() is used in v7 and it looks better.\n\n> > +     for (size_t i = 0; elems[i]; i++) {\n> > +             char *elem = elems[i]->buf;\n> > +             char *value;\n> > +             char *p = strchr(elem, '=');\n>\n>         The pointer elem points at the name, and the pointer p\n>         points at the beginning of value, which could contain '='.\n>\n> > +             strbuf_strip_suffix(elems[i], \",\");\n>\n> This does not even count as \"editing\"; split_in_place() would have\n> removed the trailing comma (and replaced it with NUL to terminate\n> the string).\n\nYeah, right. This is fixed in v7.\n\n> > +             if (!p) {\n> > +                     warning(_(\"invalid element '%s' from remote info\"), elem);\n> > +                     continue;\n> > +             }\n>\n>         elem pointed at \"foo\" or \"foo,\"; we may have stripped the\n>         trailing comma, but we didn't see the equal sign to start\n>         the value at all.  Bad input.\n>\n> > +             *p = '\\0';\n>\n>         Terminate the name by replacing '=' with NUL.\n>\n> > +             value = url_percent_decode(p + 1);\n>\n>         Can this helper function fail and signal that it saw a\n>         malformed data?  If not already, shouldn't it be taught to\n>         do so?\n>\n> We are inventing the syntax for this data in this series, so if this\n> helper takes garbage data silently, and if we are not willing to fix\n> it, then we can even consider changing the syntax to something with\n> a helper we can use that already has a good error checking.\n\nI am not sure what the interface would look like. Should a version of\nurl_percent_decode() that returns NULL in case of error would be\nenough, or should it use `return error(\"...\");`in case of error?\n\nAnd it seems to me that an error can only happen when the string\npassed to url_percent_decode() contains a '%' which is not followed by\n2 hexadecimal characters, or do you see other possible errors?\n\nFor now I haven't changed this in v7, but I am open to suggestions\nabout the best way to implement it in a v8 or maybe a separate series.\n\n> > +             if (!strcmp(elem, \"name\"))\n> > +                     info->name = value;\n> > +             else if (!strcmp(elem, \"url\"))\n> > +                     info->url = value;\n> > +             else\n> > +                     free(value);\n>\n> As url_percent_decode() always allocate a new copy of string even\n> when there is nothing to decode, value will always be an allocated\n> string, and if we are not storing it away, it will leak.  The copies\n> we kept in info->{name,url} are ours to own.  Makes sense.\n>\n> > +     strbuf_list_free(elems);\n>\n> And because [elem..p] (name) we only peeked, we can safely release\n> the whole thing.  If you used string_list_split_in_place(), you\n> would only free the string_list shell without having to free the\n> underlying string.\n\nYeah, it's better with string_list_split_in_place().\n\nThanks.\n"},{"id":"523058","messageId":"20250731072401.3817074-1-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250721141056.2283349-1-christian.couder@gmail.com","subject":"[PATCH v7 0/5] Make the \"promisor-remote\" capability support more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-31T07:23:52Z","receivedAt":"2025-07-31T07:24:20Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"The \"promisor-remote\" capability can only be used to pass the names\nand URLs of the promisor remotes from the server to the client. After\nthat the client can use this information to decide if it accepts the\nremotes or not.\n\nIt would be nice if the server could pass more fields about its\nremotes and if the client could use that additional information to\ndecide about the remotes by comparing it with its local information\nabout the remotes.\n\nThis patch series implements this by adding the \"promisor.sendFields\"\non the server side and the \"promisor.checkFields\" on the client side.\n\nFor example, if \"promisor.sendFields\" is set to \"partialCloneFilter\",\nand the server has the remote \"foo\" configured like this:\n\n[remote \"foo\"]\n        url = file:///tmp/foo.git\n\tpartialCloneFilter = blob:none\n\nthen \"name=foo,url=file:///tmp/foo.git,partialCloneFilter=blob:none\"\nwill be sent by the server for this remote.\n\nAll the information passed through the \"promisor-remote\" capability is\nstill only used to decide if the remotes are accepted or not. The\nclient doesn't store it and doesn't use it for any other purpose.\n\nNote that the filter mechanism already exists for a long time and this\nseries doesn't change how it works. For example, it has already been\npossible for a long time to have different repos using the same\npromisor remote with different filters. See the existing partial clone\ndocumentation (like \"Documentation/technical/partial-clone.adoc\") for\nmore information on partial clone.\n\nThe fields that can be passed are limited to \"partialCloneFilter\" and\n\"token\".\n\nOn the technical side, we get rid of 'struct strvec' and we use\n'struct promisor_info' to store the data and 'struct string_list' to\nstore the 'struct promisor_info' instances instead.\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\nChanges since v6\n----------------\n\nThanks to Patrick, Junio, Karthik, Jean-Noël and Justin for their\ncomments on the previous versions.\n\nHere are the changes compared to v6:\n\n- In patch 2/5 (\"promisor-remote: allow a server to advertise more\n  fields\") in \"Documentation/gitprotocol-v2.adoc\" we now clarify that\n  field names in the protocol are case-sensitive and MUST be\n  transmitted exactly as specified.\n\n- In patch 2/5 and 4/5 we don't initialize the `static int\n  initialized` to `0`, we just let the BSS do it.\n\n- In patch 3/5 (\"promisor-remote: refactor how we parse advertised\n  fields\"), we now use string_list_split_in_place() instead of\n  strbuf_split() and `struct string_list elem_list` instead of `struct\n  strbuf **elems`. The useless call to strbuf_strip_suffix() has also\n  been removed.\n\n- In patch 4/5 (\"promisor-remote: allow a client to check fields\") in\n  \"Documentation/config/promisor.adoc\" we removed \"Field names are\n  compared case-insensitively.\" as it's not true when field names are\n  part of the protocol.\n\n- Also in patch 4/5:\n\n  - in all_fields_match((), we use `struct string_list *fields`\n    instead of `struct string_list* fields` according to our style,\n    and\n\n  - in parse_one_advertised_remote() we use strcmp() instead of\n    strcasecmp() as we decided that field names in the protocol are\n    case-sensitive.\n\nCI tests\n--------\n\nThey have all passed, see:\n\nhttps://github.com/chriscool/git/actions/runs/16632158419\n\nRange diff compared to v6\n-------------------------\n\n1:  87a6ba5c48 = 1:  87a6ba5c48 promisor-remote: refactor to get rid of 'struct strvec'\n2:  0543a42858 ! 2:  c729c110d0 promisor-remote: allow a server to advertise more fields\n    @@ Documentation/gitprotocol-v2.adoc: retrieving the header from a bundle at the in\n     +connecting to the remote. It corresponds to the \"remote.<name>.token\"\n     +config setting.\n     +\n    -+No other fields are defined by the protocol at this time. Clients MUST\n    -+ignore fields they don't recognize to allow for future protocol\n    -+extensions.\n    ++No other fields are defined by the protocol at this time. Field names\n    ++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    @@ promisor-remote.c: static int allow_unsanitized(char ch)\n     +static struct string_list *fields_sent(void)\n     +{\n     +  static struct string_list fields_list = STRING_LIST_INIT_NODUP;\n    -+  static int initialized = 0;\n    ++  static int initialized;\n     +\n     +  if (!initialized) {\n     +          fields_list.cmp = strcasecmp;\n3:  d566719807 ! 3:  9e0eccae21 promisor-remote: refactor how we parse advertised fields\n    @@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n     +static struct promisor_info *parse_one_advertised_remote(struct strbuf *remote_info)\n     +{\n     +  struct promisor_info *info = xcalloc(1, sizeof(*info));\n    -+  struct strbuf **elems = strbuf_split(remote_info, ',');\n    ++  struct string_list elem_list = STRING_LIST_INIT_NODUP;\n    ++  struct string_list_item *item;\n     +\n    -+  for (size_t i = 0; elems[i]; i++) {\n    -+          char *elem = elems[i]->buf;\n    ++  string_list_split_in_place(&elem_list, remote_info->buf, \",\", -1);\n    ++\n    ++  for_each_string_list_item(item, &elem_list) {\n    ++          char *elem = item->string;\n     +          char *value;\n     +          char *p = strchr(elem, '=');\n     +\n    -+          strbuf_strip_suffix(elems[i], \",\");\n    -+\n     +          if (!p) {\n     +                  warning(_(\"invalid element '%s' from remote info\"), elem);\n     +                  continue;\n    @@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n     +                  free(value);\n     +  }\n     +\n    -+  strbuf_list_free(elems);\n    ++  string_list_clear(&elem_list, 0);\n     +\n     +  if (!info->name || !info->url) {\n     +          warning(_(\"server advertised a promisor remote without a name or URL: %s\"),\n4:  d2a13eabc0 ! 4:  b1a3384ddc promisor-remote: allow a client to check fields\n    @@ Documentation/config/promisor.adoc: promisor.acceptFromServer::\n     +be used to verify that authentication credentials match expected\n     +values.\n     ++\n    -+Field names are compared case-insensitively. Field values are compared\n    -+case-sensitively.\n    ++Field values are compared case-sensitively.\n     ++\n     +The \"name\" and \"url\" fields are always checked according to the\n     +`promisor.acceptFromServer` policy, independently of this setting.\n    @@ promisor-remote.c: static struct string_list *fields_sent(void)\n     +static struct string_list *fields_checked(void)\n     +{\n     +  static struct string_list fields_list = STRING_LIST_INIT_NODUP;\n    -+  static int initialized = 0;\n    ++  static int initialized;\n     +\n     +  if (!initialized) {\n     +          fields_list.cmp = strcasecmp;\n    @@ promisor-remote.c: enum accept_promisor {\n     +                      struct string_list *config_info,\n     +                      int in_list)\n     +{\n    -+  struct string_list* fields = fields_checked();\n    ++  struct string_list *fields = fields_checked();\n     +  struct string_list_item *item_checked;\n     +\n     +  for_each_string_list_item(item_checked, fields) {\n    @@ promisor-remote.c: static struct promisor_info *parse_one_advertised_remote(stru\n                        info->name = value;\n                else if (!strcmp(elem, \"url\"))\n                        info->url = value;\n    -+          else if (!strcasecmp(elem, promisor_field_filter))\n    ++          else if (!strcmp(elem, promisor_field_filter))\n     +                  info->filter = value;\n    -+          else if (!strcasecmp(elem, promisor_field_token))\n    ++          else if (!strcmp(elem, promisor_field_token))\n     +                  info->token = value;\n                else\n                        free(value);\n5:  c7d7c83534 ! 5:  d0f7fda912 promisor-remote: use string constants for 'name' and 'url' too\n    @@ promisor-remote.c: static struct promisor_info *parse_one_advertised_remote(stru\n     -          else if (!strcmp(elem, \"url\"))\n     +          else if (!strcmp(elem, promisor_field_url))\n                        info->url = value;\n    -           else if (!strcasecmp(elem, promisor_field_filter))\n    +           else if (!strcmp(elem, promisor_field_filter))\n                        info->filter = value;\n\n\nChristian Couder (5):\n  promisor-remote: refactor to get rid of 'struct strvec'\n  promisor-remote: allow a server to advertise more fields\n  promisor-remote: refactor how we parse advertised fields\n  promisor-remote: allow a client to check fields\n  promisor-remote: use string constants for 'name' and 'url' too\n\n Documentation/config/promisor.adoc    |  61 ++++\n Documentation/gitprotocol-v2.adoc     |  64 +++--\n promisor-remote.c                     | 398 +++++++++++++++++++++-----\n t/t5710-promisor-remote-capability.sh |  65 +++++\n 4 files changed, 500 insertions(+), 88 deletions(-)\n\n-- \n2.50.1.323.g4e0625aa69.dirty\n\n"},{"id":"523059","messageId":"20250731072401.3817074-2-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250731072401.3817074-1-christian.couder@gmail.com","subject":"[PATCH v7 1/5] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-31T07:23:53Z","receivedAt":"2025-07-31T07:24:21Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"In a following commit, we will use the new 'promisor-remote' protocol\ncapability introduced by d460267613 (Add 'promisor-remote' capability\nto protocol v2, 2025-02-18) to pass and process more information\nabout promisor remotes than just their name and url.\n\nFor that purpose, we will need to store information about other\nfields, especially information that might or might not be available\nfor different promisor remotes. Unfortunately using 'struct strvec',\nas we currently do, to store information about the promisor remotes\nwith one 'struct strvec' for each field like \"name\" or \"url\" does not\nscale easily in that case. We would need one 'struct strvec' for each\nnew field, and then we would have to pass all these 'struct strvec'\naround.\n\nLet's refactor this and introduce a new 'struct promisor_info'.\n\nIt will only store promisor remote information in its members. For now\nit has only a 'name' member for the promisor remote name and an 'url'\nmember for its URL. We will use a 'struct string_list' to store the\ninstances of 'struct promisor_info'. For each 'item' in the\nstring_list, 'item->string' will point to the promisor remote name and\n'item->util' will point to the corresponding 'struct promisor_info'\ninstance.\n\nExplicit members are used within 'struct promisor_info' for type\nsafety and clarity regarding the specific information being handled,\nrather than a generic key-value store. We want to specify and document\neach field and its content, so adding new members to the struct as\nmore fields are supported is fine.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 107 ++++++++++++++++++++++++++++------------------\n 1 file changed, 66 insertions(+), 41 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex be6f82d12f..0213b8768f 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -314,9 +314,35 @@ static int allow_unsanitized(char ch)\n \treturn ch > 32 && ch < 127;\n }\n \n-static void promisor_info_vecs(struct repository *repo,\n-\t\t\t       struct strvec *names,\n-\t\t\t       struct strvec *urls)\n+/*\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+ */\n+struct promisor_info {\n+\tconst char *name;\n+\tconst char *url;\n+};\n+\n+static void promisor_info_list_clear(struct string_list *list)\n+{\n+\tfor (size_t i = 0; i < list->nr; i++) {\n+\t\tstruct promisor_info *p = list->items[i].util;\n+\t\tfree((char *)p->name);\n+\t\tfree((char *)p->url);\n+\t}\n+\tstring_list_clear(list, 1);\n+}\n+\n+/*\n+ * Populate 'list' with promisor remote information from the config.\n+ * The 'util' pointer of each list item will hold a 'struct promisor_info'.\n+ */\n+static void promisor_config_info_list(struct repository *repo, struct string_list *list)\n {\n \tstruct promisor_remote *r;\n \n@@ -328,8 +354,14 @@ static void promisor_info_vecs(struct repository *repo,\n \n \t\t/* Only add remotes with a non empty URL */\n \t\tif (!git_config_get_string_tmp(url_key, &url) && *url) {\n-\t\t\tstrvec_push(names, r->name);\n-\t\t\tstrvec_push(urls, url);\n+\t\t\tstruct promisor_info *new_info = xcalloc(1, sizeof(*new_info));\n+\t\t\tstruct string_list_item *item;\n+\n+\t\t\tnew_info->name = xstrdup(r->name);\n+\t\t\tnew_info->url = xstrdup(url);\n+\n+\t\t\titem = string_list_append(list, new_info->name);\n+\t\t\titem->util = new_info;\n \t\t}\n \n \t\tfree(url_key);\n@@ -340,47 +372,36 @@ char *promisor_remote_info(struct repository *repo)\n {\n \tstruct strbuf sb = STRBUF_INIT;\n \tint advertise_promisors = 0;\n-\tstruct strvec names = STRVEC_INIT;\n-\tstruct strvec urls = STRVEC_INIT;\n+\tstruct string_list config_info = STRING_LIST_INIT_NODUP;\n+\tstruct string_list_item *item;\n \n \tgit_config_get_bool(\"promisor.advertise\", &advertise_promisors);\n \n \tif (!advertise_promisors)\n \t\treturn NULL;\n \n-\tpromisor_info_vecs(repo, &names, &urls);\n+\tpromisor_config_info_list(repo, &config_info);\n \n-\tif (!names.nr)\n+\tif (!config_info.nr)\n \t\treturn NULL;\n \n-\tfor (size_t i = 0; i < names.nr; i++) {\n-\t\tif (i)\n+\tfor_each_string_list_item(item, &config_info) {\n+\t\tstruct promisor_info *p = item->util;\n+\n+\t\tif (item != config_info.items)\n \t\t\tstrbuf_addch(&sb, ';');\n+\n \t\tstrbuf_addstr(&sb, \"name=\");\n-\t\tstrbuf_addstr_urlencode(&sb, names.v[i], allow_unsanitized);\n+\t\tstrbuf_addstr_urlencode(&sb, p->name, allow_unsanitized);\n \t\tstrbuf_addstr(&sb, \",url=\");\n-\t\tstrbuf_addstr_urlencode(&sb, urls.v[i], allow_unsanitized);\n+\t\tstrbuf_addstr_urlencode(&sb, p->url, allow_unsanitized);\n \t}\n \n-\tstrvec_clear(&names);\n-\tstrvec_clear(&urls);\n+\tpromisor_info_list_clear(&config_info);\n \n \treturn strbuf_detach(&sb, NULL);\n }\n \n-/*\n- * Find first index of 'nicks' where there is 'nick'. 'nick' is\n- * compared case sensitively to the strings in 'nicks'. If not found\n- * 'nicks->nr' is returned.\n- */\n-static size_t remote_nick_find(struct strvec *nicks, const char *nick)\n-{\n-\tfor (size_t i = 0; i < nicks->nr; i++)\n-\t\tif (!strcmp(nicks->v[i], nick))\n-\t\t\treturn i;\n-\treturn nicks->nr;\n-}\n-\n enum accept_promisor {\n \tACCEPT_NONE = 0,\n \tACCEPT_KNOWN_URL,\n@@ -390,19 +411,23 @@ enum accept_promisor {\n \n static int should_accept_remote(enum accept_promisor accept,\n \t\t\t\tconst char *remote_name, const char *remote_url,\n-\t\t\t\tstruct strvec *names, struct strvec *urls)\n+\t\t\t\tstruct string_list *config_info)\n {\n-\tsize_t i;\n+\tstruct promisor_info *p;\n+\tstruct string_list_item *item;\n \n \tif (accept == ACCEPT_ALL)\n \t\treturn 1;\n \n-\ti = remote_nick_find(names, remote_name);\n+\t/* Get config info for that promisor remote */\n+\titem = string_list_lookup(config_info, remote_name);\n \n-\tif (i >= names->nr)\n+\tif (!item)\n \t\t/* We don't know about that remote */\n \t\treturn 0;\n \n+\tp = item->util;\n+\n \tif (accept == ACCEPT_KNOWN_NAME)\n \t\treturn 1;\n \n@@ -414,11 +439,11 @@ static int should_accept_remote(enum accept_promisor accept,\n \t\treturn 0;\n \t}\n \n-\tif (!strcmp(urls->v[i], remote_url))\n+\tif (!strcmp(p->url, remote_url))\n \t\treturn 1;\n \n \twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s'\"),\n-\t\tremote_name, urls->v[i], remote_url);\n+\t\tremote_name, p->url, remote_url);\n \n \treturn 0;\n }\n@@ -430,8 +455,7 @@ static void filter_promisor_remote(struct repository *repo,\n \tstruct strbuf **remotes;\n \tconst char *accept_str;\n \tenum accept_promisor accept = ACCEPT_NONE;\n-\tstruct strvec names = STRVEC_INIT;\n-\tstruct strvec urls = STRVEC_INIT;\n+\tstruct string_list config_info = STRING_LIST_INIT_NODUP;\n \n \tif (!git_config_get_string_tmp(\"promisor.acceptfromserver\", &accept_str)) {\n \t\tif (!*accept_str || !strcasecmp(\"None\", accept_str))\n@@ -450,8 +474,10 @@ static void filter_promisor_remote(struct repository *repo,\n \tif (accept == ACCEPT_NONE)\n \t\treturn;\n \n-\tif (accept != ACCEPT_ALL)\n-\t\tpromisor_info_vecs(repo, &names, &urls);\n+\tif (accept != ACCEPT_ALL) {\n+\t\tpromisor_config_info_list(repo, &config_info);\n+\t\tstring_list_sort(&config_info);\n+\t}\n \n \t/* Parse remote info received */\n \n@@ -482,7 +508,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tif (remote_url)\n \t\t\tdecoded_url = url_percent_decode(remote_url);\n \n-\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, &names, &urls))\n+\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, &config_info))\n \t\t\tstrvec_push(accepted, decoded_name);\n \n \t\tstrbuf_list_free(elems);\n@@ -490,8 +516,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tfree(decoded_url);\n \t}\n \n-\tstrvec_clear(&names);\n-\tstrvec_clear(&urls);\n+\tpromisor_info_list_clear(&config_info);\n \tstrbuf_list_free(remotes);\n }\n \n-- \n2.50.1.323.g4e0625aa69.dirty\n\n"},{"id":"523060","messageId":"20250731072401.3817074-3-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250731072401.3817074-1-christian.couder@gmail.com","subject":"[PATCH v7 2/5] promisor-remote: allow a server to advertise more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-31T07:23:54Z","receivedAt":"2025-07-31T07:24:23Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"For now the \"promisor-remote\" protocol capability can only pass \"name\"\nand \"url\" information from a server to a client in the form\n\"name=<remote_name>,url=<remote_url>\".\n\nTo allow clients to make more informed decisions about which promisor\nremotes they accept, let's make it possible to pass more information\nby introducing a new \"promisor.sendFields\" configuration variable.\n\nOn the server side, information about a remote `foo` is stored in\nconfiguration variables named `remote.foo.<variable-name>`. To make\nit clearer and simpler, we use `field` and `field name` like this:\n\n  * `field name` refers to the <variable-name> part of such a\n    configuration variable, and\n\n  * `field` refers to both the `field name` and the value of such a\n    configuration variable.\n\nThe \"promisor.sendFields\" configuration variable should contain a\ncomma or space separated list of field names that will be looked up\nin the configuration of the remote on the server to find the values\nthat will be passed to the client.\n\nOnly a set of predefined field names are allowed. The only field\nnames in this set are \"partialCloneFilter\" and \"token\". The\n\"partialCloneFilter\" field name specifies the filter definition used\nby the promisor remote, and the \"token\" field name can provide an\nauthentication credential for accessing it.\n\nFor example, if \"promisor.sendFields\" is set to \"partialCloneFilter\",\nand the server has the \"remote.foo.partialCloneFilter\" config\nvariable set to a value, then that value will be passed in the\n\"partialCloneFilter\" field in the form \"partialCloneFilter=<value>\"\nafter the \"name\" and \"url\" fields.\n\nA following commit will allow the client to use the information to\ndecide if it accepts the remote or not. For now the client doesn't do\nanything with the additional information it receives.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    |  22 +++++\n Documentation/gitprotocol-v2.adoc     |  64 +++++++++---\n promisor-remote.c                     | 134 ++++++++++++++++++++++++--\n t/t5710-promisor-remote-capability.sh |  31 ++++++\n 4 files changed, 226 insertions(+), 25 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex 2638b01f83..b4a72c2152 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -9,6 +9,28 @@ promisor.advertise::\n \t\"false\", which means the \"promisor-remote\" capability is not\n \tadvertised.\n \n+promisor.sendFields::\n+\tA comma or space separated list of additional remote related\n+\tfield names. A server sends these field names and the\n+\tassociated field values from its configuration when\n+\tadvertising its promisor remotes using the \"promisor-remote\"\n+\tcapability, see linkgit:gitprotocol-v2[5]. Currently, only the\n+\t\"partialCloneFilter\" and \"token\" field names are supported.\n++\n+`partialCloneFilter`:: contains the partial clone filter\n+used for the remote.\n++\n+`token`:: contains an authentication token for the remote.\n++\n+When a field name is part of this list and a corresponding\n+\"remote.foo.<field-name>\" config variable is set on the server to a\n+non-empty value, then the field name and value are sent when\n+advertising the promisor remote \"foo\".\n++\n+This list has no effect unless the \"promisor.advertise\" config\n+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\"\ndiff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\nindex 9a57005d77..c7db103299 100644\n--- a/Documentation/gitprotocol-v2.adoc\n+++ b/Documentation/gitprotocol-v2.adoc\n@@ -785,33 +785,64 @@ retrieving the header from a bundle at the indicated URI, and thus\n save themselves and the server(s) the request(s) needed to inspect the\n headers of that bundle or bundles.\n \n-promisor-remote=<pr-infos>\n-~~~~~~~~~~~~~~~~~~~~~~~~~~\n+promisor-remote=<pr-info>\n+~~~~~~~~~~~~~~~~~~~~~~~~~\n \n The server may advertise some promisor remotes it is using or knows\n about to a client which may want to use them as its promisor remotes,\n-instead of this repository. In this case <pr-infos> should be of the\n+instead of this repository. In this case <pr-info> should be of the\n form:\n \n-\tpr-infos = pr-info | pr-infos \";\" pr-info\n+\tpr-info = pr-fields | pr-info \";\" pr-fields\n \n-\tpr-info = \"name=\" pr-name | \"name=\" pr-name \",\" \"url=\" pr-url\n+\tpr-fields = pr-field | pr-fields \",\" pr-field\n \n-where `pr-name` is the urlencoded name of a promisor remote, and\n-`pr-url` the urlencoded URL of that promisor remote.\n+\tpr-field = field-name \"=\" field-value\n \n-In this case, if the client decides to use one or more promisor\n-remotes the server advertised, it can reply with\n-\"promisor-remote=<pr-names>\" where <pr-names> should be of the form:\n+where all the `field-name` and `field-value` in a given `pr-fields`\n+are field names and values related to a single promisor remote. A\n+given `field-name` MUST NOT appear more than once in given\n+`pr-fields`.\n+\n+The server MUST advertise at least the \"name\" and \"url\" field names\n+along with the associated field values, which are the name of a valid\n+remote and its URL, in each `pr-fields`. The \"name\" and \"url\" fields\n+MUST appear first in each pr-fields, in that order.\n+\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+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+\n+`token`:: An authentication token that clients can use when\n+connecting to the remote. It corresponds to the \"remote.<name>.token\"\n+config setting.\n+\n+No other fields are defined by the protocol at this time. Field names\n+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+\n+Field values MUST be urlencoded.\n+\n+If the client decides to use one or more promisor remotes the server\n+advertised, it can reply with \"promisor-remote=<pr-names>\" where\n+<pr-names> should be of the form:\n \n \tpr-names = pr-name | pr-names \";\" pr-name\n \n where `pr-name` is the urlencoded name of a promisor remote the server\n advertised and the client accepts.\n \n-Note that, everywhere in this document, `pr-name` MUST be a valid\n-remote name, and the ';' and ',' characters MUST be encoded if they\n-appear in `pr-name` or `pr-url`.\n+Note that, everywhere in this document, the ';' and ',' characters\n+MUST be encoded if they appear in `pr-name` or `field-value`.\n \n If the server doesn't know any promisor remote that could be good for\n a client to use, or prefers a client not to use any promisor remote it\n@@ -822,9 +853,10 @@ In this case, or if the client doesn't want to use any promisor remote\n the server advertised, the client shouldn't advertise the\n \"promisor-remote\" capability at all in its reply.\n \n-The \"promisor.advertise\" and \"promisor.acceptFromServer\" configuration\n-options can be used on the server and client side to control what they\n-advertise or accept respectively. See the documentation of these\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 \n Note that in the future it would be nice if the \"promisor-remote\"\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 0213b8768f..620133c2ec 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -314,6 +314,80 @@ static int allow_unsanitized(char ch)\n \treturn ch > 32 && ch < 127;\n }\n \n+static const char promisor_field_filter[] = \"partialCloneFilter\";\n+static const char promisor_field_token[] = \"token\";\n+\n+/*\n+ * List of optional field names that can be used in the\n+ * \"promisor-remote\" protocol capability (others must be\n+ * ignored). Each field should correspond to a configurable property\n+ * of a remote that can be relevant for the client.\n+ */\n+static const char *known_fields[] = {\n+\tpromisor_field_filter, /* Filter used for partial clone */\n+\tpromisor_field_token,  /* Authentication token for the remote */\n+\tNULL\n+};\n+\n+/*\n+ * Check if 'field' is in the list of the known field names for the\n+ * \"promisor-remote\" protocol capability.\n+ */\n+static int is_known_field(const char *field)\n+{\n+\tconst char **p;\n+\n+\tfor (p = known_fields; *p; p++)\n+\t\tif (!strcasecmp(*p, field))\n+\t\t\treturn 1;\n+\treturn 0;\n+}\n+\n+static int is_valid_field(struct string_list_item *item, void *cb_data)\n+{\n+\tconst char *field = item->string;\n+\tconst char *config_key = (const char *)cb_data;\n+\n+\tif (!is_known_field(field)) {\n+\t\twarning(_(\"unsupported field '%s' in '%s' config\"), field, config_key);\n+\t\treturn 0;\n+\t}\n+\treturn 1;\n+}\n+\n+static char *fields_from_config(struct string_list *fields_list, const char *config_key)\n+{\n+\tchar *fields = NULL;\n+\n+\tif (!git_config_get_string(config_key, &fields) && *fields) {\n+\t\t/* Split on any comma or space character */\n+\t\tstring_list_split_in_place(fields_list, fields, \", \", -1);\n+\t\t/*\n+\t\t * Remove empty items that might result from trailing\n+\t\t * commas, or from items being separated by both\n+\t\t * commas and spaces.\n+\t\t */\n+\t\tstring_list_remove_empty_items(fields_list, 0);\n+\t\tfilter_string_list(fields_list, 0, is_valid_field, (void *)config_key);\n+\t}\n+\n+\treturn fields;\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+}\n+\n /*\n  * Struct for promisor remotes involved in the \"promisor-remote\"\n  * protocol capability.\n@@ -326,6 +400,8 @@ static int allow_unsanitized(char ch)\n struct promisor_info {\n \tconst char *name;\n \tconst char *url;\n+\tconst char *filter;\n+\tconst char *token;\n };\n \n static void promisor_info_list_clear(struct string_list *list)\n@@ -334,15 +410,47 @@ static void promisor_info_list_clear(struct string_list *list)\n \t\tstruct promisor_info *p = list->items[i].util;\n \t\tfree((char *)p->name);\n \t\tfree((char *)p->url);\n+\t\tfree((char *)p->filter);\n+\t\tfree((char *)p->token);\n \t}\n \tstring_list_clear(list, 1);\n }\n \n+static void set_one_field(struct promisor_info *p,\n+\t\t\t  const char *field, const char *value)\n+{\n+\tif (!strcasecmp(field, promisor_field_filter))\n+\t\tp->filter = xstrdup(value);\n+\telse if (!strcasecmp(field, promisor_field_token))\n+\t\tp->token = xstrdup(value);\n+\telse\n+\t\tBUG(\"invalid field '%s'\", field);\n+}\n+\n+static void set_fields(struct promisor_info *p,\n+\t\t       struct string_list *field_names)\n+{\n+\tstruct string_list_item *item;\n+\n+\tfor_each_string_list_item(item, field_names) {\n+\t\tchar *key = xstrfmt(\"remote.%s.%s\", p->name, item->string);\n+\t\tconst char *val;\n+\t\tif (!git_config_get_string_tmp(key, &val) && *val)\n+\t\t\tset_one_field(p, item->string, val);\n+\t\tfree(key);\n+\t}\n+}\n+\n /*\n  * Populate 'list' with promisor remote information from the config.\n- * The 'util' pointer of each list item will hold a 'struct promisor_info'.\n+ * The 'util' pointer of each list item will hold a 'struct\n+ * promisor_info'. Except \"name\" and \"url\", only members of that\n+ * struct specified by the 'field_names' list are set (using values\n+ * from the configuration).\n  */\n-static void promisor_config_info_list(struct repository *repo, struct string_list *list)\n+static void promisor_config_info_list(struct repository *repo,\n+\t\t\t\t      struct string_list *list,\n+\t\t\t\t      struct string_list *field_names)\n {\n \tstruct promisor_remote *r;\n \n@@ -360,6 +468,9 @@ static void promisor_config_info_list(struct repository *repo, struct string_lis\n \t\t\tnew_info->name = xstrdup(r->name);\n \t\t\tnew_info->url = xstrdup(url);\n \n+\t\t\tif (field_names)\n+\t\t\t\tset_fields(new_info, field_names);\n+\n \t\t\titem = string_list_append(list, new_info->name);\n \t\t\titem->util = new_info;\n \t\t}\n@@ -380,7 +491,7 @@ char *promisor_remote_info(struct repository *repo)\n \tif (!advertise_promisors)\n \t\treturn NULL;\n \n-\tpromisor_config_info_list(repo, &config_info);\n+\tpromisor_config_info_list(repo, &config_info, fields_sent());\n \n \tif (!config_info.nr)\n \t\treturn NULL;\n@@ -395,6 +506,15 @@ char *promisor_remote_info(struct repository *repo)\n \t\tstrbuf_addstr_urlencode(&sb, p->name, allow_unsanitized);\n \t\tstrbuf_addstr(&sb, \",url=\");\n \t\tstrbuf_addstr_urlencode(&sb, p->url, allow_unsanitized);\n+\n+\t\tif (p->filter) {\n+\t\t\tstrbuf_addf(&sb, \",%s=\", promisor_field_filter);\n+\t\t\tstrbuf_addstr_urlencode(&sb, p->filter, allow_unsanitized);\n+\t\t}\n+\t\tif (p->token) {\n+\t\t\tstrbuf_addf(&sb, \",%s=\", promisor_field_token);\n+\t\t\tstrbuf_addstr_urlencode(&sb, p->token, allow_unsanitized);\n+\t\t}\n \t}\n \n \tpromisor_info_list_clear(&config_info);\n@@ -475,7 +595,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\treturn;\n \n \tif (accept != ACCEPT_ALL) {\n-\t\tpromisor_config_info_list(repo, &config_info);\n+\t\tpromisor_config_info_list(repo, &config_info, NULL);\n \t\tstring_list_sort(&config_info);\n \t}\n \n@@ -494,13 +614,9 @@ static void filter_promisor_remote(struct repository *repo,\n \t\telems = strbuf_split(remotes[i], ',');\n \n \t\tfor (size_t j = 0; elems[j]; j++) {\n-\t\t\tint res;\n \t\t\tstrbuf_strip_suffix(elems[j], \",\");\n-\t\t\tres = skip_prefix(elems[j]->buf, \"name=\", &remote_name) ||\n+\t\t\tif (!skip_prefix(elems[j]->buf, \"name=\", &remote_name))\n \t\t\t\tskip_prefix(elems[j]->buf, \"url=\", &remote_url);\n-\t\t\tif (!res)\n-\t\t\t\twarning(_(\"unknown element '%s' from remote info\"),\n-\t\t\t\t\telems[j]->buf);\n \t\t}\n \n \t\tif (remote_name)\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex cb061b1f35..204528b2e0 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -295,6 +295,37 @@ 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 promisor.sendFields\" '\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+\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 promisor.acceptfromserver=All \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that fields are properly transmitted\n+\tENCODED_URL=$(echo \"file://$(pwd)/lop\" | sed -e \"s/ /%20/g\") &&\n+\tPR1=\"name=lop,url=$ENCODED_URL,partialCloneFilter=blob:none\" &&\n+\tPR2=\"name=otherLop,url=https://invalid.invalid,partialCloneFilter=blob:limit=10k,token=fooBar\" &&\n+\ttest_grep \"clone< promisor-remote=$PR1;$PR2\" trace &&\n+\ttest_grep \"clone> promisor-remote=lop;otherLop\" 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 \n-- \n2.50.1.323.g4e0625aa69.dirty\n\n"},{"id":"523061","messageId":"20250731072401.3817074-4-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250731072401.3817074-1-christian.couder@gmail.com","subject":"[PATCH v7 3/5] promisor-remote: refactor how we parse advertised fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-31T07:23:55Z","receivedAt":"2025-07-31T07:24:24Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"In a follow up commit we are going to parse more fields, like a filter\nand a token, coming from the server when it advertises promisor remotes\nusing the \"promisor-remote\" capability.\n\nTo prepare for this, let's refactor the code that parses the advertised\nfields coming from the server into a new parse_one_advertised_remote()\nfunction that will populate a `struct promisor_info` with the content\nof the fields it parsed.\n\nWhile at it, let's also pass this `struct promisor_info` to the\nshould_accept_remote() function, instead of passing it the parsed name\nand url.\n\nThese changes will make it simpler to both parse more fields and access\nthe content of these parsed fields in follow up commits.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 92 ++++++++++++++++++++++++++++++++---------------\n 1 file changed, 63 insertions(+), 29 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 620133c2ec..2ea069ff6b 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -404,16 +404,20 @@ struct promisor_info {\n \tconst char *token;\n };\n \n+static void promisor_info_free(struct promisor_info *p)\n+{\n+\tfree((char *)p->name);\n+\tfree((char *)p->url);\n+\tfree((char *)p->filter);\n+\tfree((char *)p->token);\n+\tfree(p);\n+}\n+\n static void promisor_info_list_clear(struct string_list *list)\n {\n-\tfor (size_t i = 0; i < list->nr; i++) {\n-\t\tstruct promisor_info *p = list->items[i].util;\n-\t\tfree((char *)p->name);\n-\t\tfree((char *)p->url);\n-\t\tfree((char *)p->filter);\n-\t\tfree((char *)p->token);\n-\t}\n-\tstring_list_clear(list, 1);\n+\tfor (size_t i = 0; i < list->nr; i++)\n+\t\tpromisor_info_free(list->items[i].util);\n+\tstring_list_clear(list, 0);\n }\n \n static void set_one_field(struct promisor_info *p,\n@@ -530,11 +534,13 @@ enum accept_promisor {\n };\n \n static int should_accept_remote(enum accept_promisor accept,\n-\t\t\t\tconst char *remote_name, const char *remote_url,\n+\t\t\t\tstruct promisor_info *advertised,\n \t\t\t\tstruct string_list *config_info)\n {\n \tstruct promisor_info *p;\n \tstruct string_list_item *item;\n+\tconst char *remote_name = advertised->name;\n+\tconst char *remote_url = advertised->url;\n \n \tif (accept == ACCEPT_ALL)\n \t\treturn 1;\n@@ -568,6 +574,47 @@ static int should_accept_remote(enum accept_promisor accept,\n \treturn 0;\n }\n \n+static struct promisor_info *parse_one_advertised_remote(struct strbuf *remote_info)\n+{\n+\tstruct promisor_info *info = xcalloc(1, sizeof(*info));\n+\tstruct string_list elem_list = STRING_LIST_INIT_NODUP;\n+\tstruct string_list_item *item;\n+\n+\tstring_list_split_in_place(&elem_list, remote_info->buf, \",\", -1);\n+\n+\tfor_each_string_list_item(item, &elem_list) {\n+\t\tchar *elem = item->string;\n+\t\tchar *value;\n+\t\tchar *p = strchr(elem, '=');\n+\n+\t\tif (!p) {\n+\t\t\twarning(_(\"invalid element '%s' from remote info\"), elem);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\t*p = '\\0';\n+\t\tvalue = url_percent_decode(p + 1);\n+\n+\t\tif (!strcmp(elem, \"name\"))\n+\t\t\tinfo->name = value;\n+\t\telse if (!strcmp(elem, \"url\"))\n+\t\t\tinfo->url = value;\n+\t\telse\n+\t\t\tfree(value);\n+\t}\n+\n+\tstring_list_clear(&elem_list, 0);\n+\n+\tif (!info->name || !info->url) {\n+\t\twarning(_(\"server advertised a promisor remote without a name or URL: %s\"),\n+\t\t\tremote_info->buf);\n+\t\tpromisor_info_free(info);\n+\t\treturn NULL;\n+\t}\n+\n+\treturn info;\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@@ -604,32 +651,19 @@ static void filter_promisor_remote(struct repository *repo,\n \tremotes = strbuf_split_str(info, ';', 0);\n \n \tfor (size_t i = 0; remotes[i]; i++) {\n-\t\tstruct strbuf **elems;\n-\t\tconst char *remote_name = NULL;\n-\t\tconst char *remote_url = NULL;\n-\t\tchar *decoded_name = NULL;\n-\t\tchar *decoded_url = NULL;\n+\t\tstruct promisor_info *advertised;\n \n \t\tstrbuf_strip_suffix(remotes[i], \";\");\n-\t\telems = strbuf_split(remotes[i], ',');\n \n-\t\tfor (size_t j = 0; elems[j]; j++) {\n-\t\t\tstrbuf_strip_suffix(elems[j], \",\");\n-\t\t\tif (!skip_prefix(elems[j]->buf, \"name=\", &remote_name))\n-\t\t\t\tskip_prefix(elems[j]->buf, \"url=\", &remote_url);\n-\t\t}\n+\t\tadvertised = parse_one_advertised_remote(remotes[i]);\n \n-\t\tif (remote_name)\n-\t\t\tdecoded_name = url_percent_decode(remote_name);\n-\t\tif (remote_url)\n-\t\t\tdecoded_url = url_percent_decode(remote_url);\n+\t\tif (!advertised)\n+\t\t\tcontinue;\n \n-\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, &config_info))\n-\t\t\tstrvec_push(accepted, decoded_name);\n+\t\tif (should_accept_remote(accept, advertised, &config_info))\n+\t\t\tstrvec_push(accepted, advertised->name);\n \n-\t\tstrbuf_list_free(elems);\n-\t\tfree(decoded_name);\n-\t\tfree(decoded_url);\n+\t\tpromisor_info_free(advertised);\n \t}\n \n \tpromisor_info_list_clear(&config_info);\n-- \n2.50.1.323.g4e0625aa69.dirty\n\n"},{"id":"523062","messageId":"20250731072401.3817074-5-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250731072401.3817074-1-christian.couder@gmail.com","subject":"[PATCH v7 4/5] promisor-remote: allow a client to check fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-31T07:23:56Z","receivedAt":"2025-07-31T07:24:26Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"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\nLet's make it possible for a client to check if these fields match\nwhat it expects before accepting a promisor remote.\n\nWe allow this by introducing a new \"promisor.checkFields\"\nconfiguration variable. It should contain a comma or space separated\nlist of fields that will be checked.\n\nBy limiting the protocol to specific well-defined fields, we ensure\nboth server and client have a shared understanding of field\nsemantics and usage.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    | 39 ++++++++++++\n promisor-remote.c                     | 89 ++++++++++++++++++++++++---\n t/t5710-promisor-remote-capability.sh | 34 ++++++++++\n 3 files changed, 154 insertions(+), 8 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex b4a72c2152..93e5e0d9b5 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -50,3 +50,42 @@ promisor.acceptFromServer::\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+\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+\ttransmitted by a server correspond to the values of these\n+\tfields in its own configuration before accepting a promisor\n+\tremote. Currently, \"partialCloneFilter\" and \"token\" are the\n+\tonly supported field names.\n++\n+If one of these field names (e.g., \"token\") is being checked for an\n+advertised promisor remote (e.g., \"foo\"), three conditions must be met\n+for the check of this specific field to pass:\n++\n+1. The corresponding local configuration (e.g., `remote.foo.token`)\n+   must be set.\n+2. The server must advertise the \"token\" field for remote \"foo\".\n+3. The value of the locally configured `remote.foo.token` must exactly\n+   match the value advertised by the server for the \"token\" field.\n++\n+If any of these conditions is not met for any field name listed in\n+`promisor.checkFields`, the advertised remote \"foo\" is rejected.\n++\n+For the \"partialCloneFilter\" field, this allows the client to ensure\n+that the server's filter matches what it expects locally, preventing\n+inconsistencies in filtering behavior. For the \"token\" field, this can\n+be used to verify that authentication credentials match expected\n+values.\n++\n+Field values are compared case-sensitively.\n++\n+The \"name\" and \"url\" fields are always checked according to the\n+`promisor.acceptFromServer` policy, independently of this setting.\n++\n+The field names and values should be passed by the server through the\n+\"promisor-remote\" capability by using the `promisor.sendFields` config\n+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].\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 2ea069ff6b..46b2cb0bc9 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -388,6 +388,20 @@ static struct string_list *fields_sent(void)\n \treturn &fields_list;\n }\n \n+static struct string_list *fields_checked(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.checkFields\");\n+\t\tinitialized = 1;\n+\t}\n+\n+\treturn &fields_list;\n+}\n+\n /*\n  * Struct for promisor remotes involved in the \"promisor-remote\"\n  * protocol capability.\n@@ -533,6 +547,61 @@ enum accept_promisor {\n \tACCEPT_ALL\n };\n \n+static int match_field_against_config(const char *field, const char *value,\n+\t\t\t\t      struct promisor_info *config_info)\n+{\n+\tif (config_info->filter && !strcasecmp(field, promisor_field_filter))\n+\t\treturn !strcmp(config_info->filter, value);\n+\telse if (config_info->token && !strcasecmp(field, promisor_field_token))\n+\t\treturn !strcmp(config_info->token, value);\n+\n+\treturn 0;\n+}\n+\n+static int all_fields_match(struct promisor_info *advertised,\n+\t\t\t    struct string_list *config_info,\n+\t\t\t    int in_list)\n+{\n+\tstruct string_list *fields = fields_checked();\n+\tstruct string_list_item *item_checked;\n+\n+\tfor_each_string_list_item(item_checked, fields) {\n+\t\tint match = 0;\n+\t\tconst char *field = item_checked->string;\n+\t\tconst char *value = NULL;\n+\t\tstruct string_list_item *item;\n+\n+\t\tif (!strcasecmp(field, promisor_field_filter))\n+\t\t\tvalue = advertised->filter;\n+\t\telse if (!strcasecmp(field, promisor_field_token))\n+\t\t\tvalue = advertised->token;\n+\n+\t\tif (!value)\n+\t\t\treturn 0;\n+\n+\t\tif (in_list) {\n+\t\t\tfor_each_string_list_item(item, config_info) {\n+\t\t\t\tstruct promisor_info *p = item->util;\n+\t\t\t\tif (match_field_against_config(field, value, p)) {\n+\t\t\t\t\tmatch = 1;\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t}\n+\t\t} else {\n+\t\t\titem = string_list_lookup(config_info, advertised->name);\n+\t\t\tif (item) {\n+\t\t\t\tstruct promisor_info *p = item->util;\n+\t\t\t\tmatch = match_field_against_config(field, value, p);\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (!match)\n+\t\t\treturn 0;\n+\t}\n+\n+\treturn 1;\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@@ -543,7 +612,7 @@ static int should_accept_remote(enum accept_promisor accept,\n \tconst char *remote_url = advertised->url;\n \n \tif (accept == ACCEPT_ALL)\n-\t\treturn 1;\n+\t\treturn all_fields_match(advertised, config_info, 1);\n \n \t/* Get config info for that promisor remote */\n \titem = string_list_lookup(config_info, remote_name);\n@@ -555,7 +624,7 @@ static int should_accept_remote(enum accept_promisor accept,\n \tp = item->util;\n \n \tif (accept == ACCEPT_KNOWN_NAME)\n-\t\treturn 1;\n+\t\treturn all_fields_match(advertised, config_info, 0);\n \n \tif (accept != ACCEPT_KNOWN_URL)\n \t\tBUG(\"Unhandled 'enum accept_promisor' value '%d'\", accept);\n@@ -566,7 +635,7 @@ static int should_accept_remote(enum accept_promisor accept,\n \t}\n \n \tif (!strcmp(p->url, remote_url))\n-\t\treturn 1;\n+\t\treturn all_fields_match(advertised, config_info, 0);\n \n \twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s'\"),\n \t\tremote_name, p->url, remote_url);\n@@ -599,6 +668,10 @@ static struct promisor_info *parse_one_advertised_remote(struct strbuf *remote_i\n \t\t\tinfo->name = value;\n \t\telse if (!strcmp(elem, \"url\"))\n \t\t\tinfo->url = value;\n+\t\telse if (!strcmp(elem, promisor_field_filter))\n+\t\t\tinfo->filter = value;\n+\t\telse if (!strcmp(elem, promisor_field_token))\n+\t\t\tinfo->token = value;\n \t\telse\n \t\t\tfree(value);\n \t}\n@@ -641,11 +714,6 @@ static void filter_promisor_remote(struct repository *repo,\n \tif (accept == ACCEPT_NONE)\n \t\treturn;\n \n-\tif (accept != ACCEPT_ALL) {\n-\t\tpromisor_config_info_list(repo, &config_info, NULL);\n-\t\tstring_list_sort(&config_info);\n-\t}\n-\n \t/* Parse remote info received */\n \n \tremotes = strbuf_split_str(info, ';', 0);\n@@ -660,6 +728,11 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tif (!advertised)\n \t\t\tcontinue;\n \n+\t\tif (!config_info.nr) {\n+\t\t\tpromisor_config_info_list(repo, &config_info, fields_checked());\n+\t\t\tstring_list_sort(&config_info);\n+\t\t}\n+\n \t\tif (should_accept_remote(accept, advertised, &config_info))\n \t\t\tstrvec_push(accepted, advertised->name);\n \ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex 204528b2e0..023735d6a8 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -326,6 +326,40 @@ test_expect_success \"clone with promisor.sendFields\" '\n \tcheck_missing_objects server 1 \"$oid\"\n '\n \n+test_expect_success \"clone with promisor.checkFields\" '\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+\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.partialCloneFilter=\"blob:none\" \\\n+\t\t-c promisor.acceptfromserver=All \\\n+\t\t-c promisor.checkFields=partialcloneFilter \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that fields are properly transmitted\n+\tENCODED_URL=$(echo \"file://$(pwd)/lop\" | sed -e \"s/ /%20/g\") &&\n+\tPR1=\"name=lop,url=$ENCODED_URL,partialCloneFilter=blob:none\" &&\n+\tPR2=\"name=otherLop,url=https://invalid.invalid,partialCloneFilter=blob:limit=10k,token=fooBar\" &&\n+\ttest_grep \"clone< promisor-remote=$PR1;$PR2\" trace &&\n+\ttest_grep \"clone> promisor-remote=lop\" trace &&\n+\ttest_grep ! \"clone> promisor-remote=lop;otherLop\" 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 \n-- \n2.50.1.323.g4e0625aa69.dirty\n\n"},{"id":"523063","messageId":"20250731072401.3817074-6-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250731072401.3817074-1-christian.couder@gmail.com","subject":"[PATCH v7 5/5] promisor-remote: use string constants for 'name' and 'url' too","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-31T07:23:57Z","receivedAt":"2025-07-31T07:24:27Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"A previous commit started to define `promisor_field_filter` and\n`promisor_field_token`, and used them instead of the\n\"partialCloneFilter\" and \"token\" string literals.\n\nLet's do the same for \"name\" and \"url\" to avoid repeating them\nseveral times and for consistency with the other fields.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 14 ++++++++++----\n 1 file changed, 10 insertions(+), 4 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 46b2cb0bc9..88ff4e658c 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -314,6 +314,12 @@ static int allow_unsanitized(char ch)\n \treturn ch > 32 && ch < 127;\n }\n \n+/*\n+ * All the fields used in \"promisor-remote\" protocol capability,\n+ * including the mandatory \"name\" and \"url\" ones.\n+ */\n+static const char promisor_field_name[] = \"name\";\n+static const char promisor_field_url[] = \"url\";\n static const char promisor_field_filter[] = \"partialCloneFilter\";\n static const char promisor_field_token[] = \"token\";\n \n@@ -520,9 +526,9 @@ char *promisor_remote_info(struct repository *repo)\n \t\tif (item != config_info.items)\n \t\t\tstrbuf_addch(&sb, ';');\n \n-\t\tstrbuf_addstr(&sb, \"name=\");\n+\t\tstrbuf_addf(&sb, \"%s=\", promisor_field_name);\n \t\tstrbuf_addstr_urlencode(&sb, p->name, allow_unsanitized);\n-\t\tstrbuf_addstr(&sb, \",url=\");\n+\t\tstrbuf_addf(&sb, \",%s=\", promisor_field_url);\n \t\tstrbuf_addstr_urlencode(&sb, p->url, allow_unsanitized);\n \n \t\tif (p->filter) {\n@@ -664,9 +670,9 @@ static struct promisor_info *parse_one_advertised_remote(struct strbuf *remote_i\n \t\t*p = '\\0';\n \t\tvalue = url_percent_decode(p + 1);\n \n-\t\tif (!strcmp(elem, \"name\"))\n+\t\tif (!strcmp(elem, promisor_field_name))\n \t\t\tinfo->name = value;\n-\t\telse if (!strcmp(elem, \"url\"))\n+\t\telse if (!strcmp(elem, promisor_field_url))\n \t\t\tinfo->url = value;\n \t\telse if (!strcmp(elem, promisor_field_filter))\n \t\t\tinfo->filter = value;\n-- \n2.50.1.323.g4e0625aa69.dirty\n\n"},{"id":"523108","messageId":"xmqqbjp0tl8q.fsf@gitster.g","threadId":"63293","inReplyTo":"20250731072401.3817074-1-christian.couder@gmail.com","subject":"Re: [PATCH v7 0/5] Make the \"promisor-remote\" capability support more fields","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-31T15:48:05Z","receivedAt":"2025-07-31T15:48:08Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> Here are the changes compared to v6:\n\nAll looked sensible.  Especially ...\n\n>     -+  struct strbuf **elems = strbuf_split(remote_info, ',');\n>     ++  struct string_list elem_list = STRING_LIST_INIT_NODUP;\n>     ++  struct string_list_item *item;\n>      +\n>     -+  for (size_t i = 0; elems[i]; i++) {\n>     -+          char *elem = elems[i]->buf;\n>     ++  string_list_split_in_place(&elem_list, remote_info->buf, \",\", -1);\n\n... because of this change, we do not have to do ...\n\n>     -+          strbuf_strip_suffix(elems[i], \",\");\n\n... this, which is very nice.  Unlike string_list_split_in_place()\nthat can take multiple delimiter bytes, strbuf_split() can take only\na single byte as the delimiter, so leaving it at the end of each\nsplit pieces does not make much sense (at least, that is done not\nbecause we do not want to lose information), and having to strip the\nsuffix after splitting always felt like papering over a wrong\nbehaviour of the helper function.\n\n"},{"id":"523109","messageId":"xmqqh5yss5xx.fsf@gitster.g","threadId":"63293","inReplyTo":"20250731072401.3817074-4-christian.couder@gmail.com","subject":"Re: [PATCH v7 3/5] promisor-remote: refactor how we parse advertised fields","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-31T16:03:54Z","receivedAt":"2025-07-31T16:03:56Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> +static struct promisor_info *parse_one_advertised_remote(struct strbuf *remote_info)\n> +{\n> +\tstruct promisor_info *info = xcalloc(1, sizeof(*info));\n> +\tstruct string_list elem_list = STRING_LIST_INIT_NODUP;\n> +\tstruct string_list_item *item;\n> +\n> +\tstring_list_split_in_place(&elem_list, remote_info->buf, \",\", -1);\n\nThis munges a strbuf pointed by a structure that was supplied by the\ncaller as a parameter to this function.  Most notably, all commas in\nremote_info->buf are replaced with NULs.\n\nA quick read of the sole caller of this function reveals that the\ncaller passes one element of an array of strbuf it obtained by\ncalling strbuf_split_str() for the only purpose of calling this\nfunction, and the caller never looks at the .buf member itself, so I\nthink this munging with _in_place() would not hurt the caller.\n\n> +\tfor_each_string_list_item(item, &elem_list) {\n> +\t\tchar *elem = item->string;\n> +\t\tchar *value;\n> +\t\tchar *p = strchr(elem, '=');\n> +\n> +\t\tif (!p) {\n> +\t\t\twarning(_(\"invalid element '%s' from remote info\"), elem);\n> +\t\t\tcontinue;\n> +\t\t}\n\nWe find the first '=' and ...\n\n> +\t\t*p = '\\0';\n\n... replace it with NUL; the item->string here is now split into elem & value.\n\n> +\t\tvalue = url_percent_decode(p + 1);\n\nAnd the value gets decoded.\n\n> +\t\tif (!strcmp(elem, \"name\"))\n> +\t\t\tinfo->name = value;\n> +\t\telse if (!strcmp(elem, \"url\"))\n> +\t\t\tinfo->url = value;\n> +\t\telse\n> +\t\t\tfree(value);\n> +\t}\n> +\n> +\tstring_list_clear(&elem_list, 0);\n\nAnd we are done with the string list that holds the pieces of remote\ninfo split out.\n\n> +\tif (!info->name || !info->url) {\n> +\t\twarning(_(\"server advertised a promisor remote without a name or URL: %s\"),\n> +\t\t\tremote_info->buf);\n\nBut this use of remote_info->buf will no longer give us much useful\ninformation.  It might say something like \"name\", and the warning\nwould not let us see what the rest of the remote_info->buf used to\nhave before _in_place splitting.\n\nI think initializing elem_list with INIT_DUP and using string_list_split()\nwithout _in_place should be sufficient to fix this.\n\n> @@ -604,32 +651,19 @@ static void filter_promisor_remote(struct repository *repo,\n>  \tremotes = strbuf_split_str(info, ';', 0);\n\nIf we splitted this into a string list, then each item in it\nwill not have the terminating ';' at its end, therefore ...\n\n>  \tfor (size_t i = 0; remotes[i]; i++) {\n> -\t\tstruct strbuf **elems;\n> -\t\tconst char *remote_name = NULL;\n> -\t\tconst char *remote_url = NULL;\n> -\t\tchar *decoded_name = NULL;\n> -\t\tchar *decoded_url = NULL;\n> +\t\tstruct promisor_info *advertised;\n>  \n>  \t\tstrbuf_strip_suffix(remotes[i], \";\");\n\n... this strip_suffix() will become unnecessary.\n\n> +\t\tadvertised = parse_one_advertised_remote(remotes[i]);\n\nSuch a change would require this call to be made with const char *\nnot struct strbuf * as a parameter, but the callee we just saw above\nuses its parameter (i.e. remote_info) only to look at the .buf member,\nand does not take any advantage of it being a strbuf (like it can be\ntrimmed or its length is known without running strlen(.buf) on it),\nso that should be an easy conversion.\n\n> +\t\tif (!advertised)\n> +\t\t\tcontinue;\n>  \n> +\t\tif (should_accept_remote(accept, advertised, &config_info))\n> +\t\t\tstrvec_push(accepted, advertised->name);\n>  \n> -\t\tstrbuf_list_free(elems);\n> -\t\tfree(decoded_name);\n> -\t\tfree(decoded_url);\n> +\t\tpromisor_info_free(advertised);\n>  \t}\n>  \n>  \tpromisor_info_list_clear(&config_info);\n\nOverall looking very promising.  Thanks for working on the topic.\n"},{"id":"525169","messageId":"xmqqqzwvqay9.fsf@gitster.g","threadId":"63293","inReplyTo":"20250731072401.3817074-1-christian.couder@gmail.com","subject":"Re: [PATCH v7 0/5] Make the \"promisor-remote\" capability support more fields","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-08-28T23:32:30Z","receivedAt":"2025-08-28T23:32:32Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> Changes since v6\n> ----------------\n> ...\n> Christian Couder (5):\n>   promisor-remote: refactor to get rid of 'struct strvec'\n>   promisor-remote: allow a server to advertise more fields\n>   promisor-remote: refactor how we parse advertised fields\n>   promisor-remote: allow a client to check fields\n>   promisor-remote: use string constants for 'name' and 'url' too\n>\n>  Documentation/config/promisor.adoc    |  61 ++++\n>  Documentation/gitprotocol-v2.adoc     |  64 +++--\n>  promisor-remote.c                     | 398 +++++++++++++++++++++-----\n>  t/t5710-promisor-remote-capability.sh |  65 +++++\n>  4 files changed, 500 insertions(+), 88 deletions(-)\n\nAs I do not want to keep an inactive topic in 'seen' for more than a\nmonth, I was doing my usual \"sweep\" of the topics, and found this\none.\n\nI think I gave a review on one step that pointed out a few problems\nwith an outline for a possible solution, but I did not see anybody\nelse reviewing, and nothing happened since the end of last month.\n\nSince the summer is a slow season, I do not mind keeping it for a\nfew more weeks in 'seen', but I can simply discard the one I have,\nand requeue a new version in 'seen' when it materializes.\n\nThanks.\n"},{"id":"525754","messageId":"20250908053056.956907-1-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250731072401.3817074-1-christian.couder@gmail.com","subject":"[PATCH v8 0/7] Make the \"promisor-remote\" capability support more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-09-08T05:30:46Z","receivedAt":"2025-09-08T05:31:17Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"The \"promisor-remote\" capability can only be used to pass the names\nand URLs of the promisor remotes from the server to the client. After\nthat the client can use this information to decide if it accepts the\nremotes or not.\n\nIt would be nice if the server could pass more fields about its\nremotes and if the client could use that additional information to\ndecide about the remotes by comparing it with its local information\nabout the remotes.\n\nThis patch series implements this by adding the \"promisor.sendFields\"\non the server side and the \"promisor.checkFields\" on the client side.\n\nFor example, if \"promisor.sendFields\" is set to \"partialCloneFilter\",\nand the server has the remote \"foo\" configured like this:\n\n[remote \"foo\"]\n        url = file:///tmp/foo.git\n\tpartialCloneFilter = blob:none\n\nthen \"name=foo,url=file:///tmp/foo.git,partialCloneFilter=blob:none\"\nwill be sent by the server for this remote.\n\nAll the information passed through the \"promisor-remote\" capability is\nstill only used to decide if the remotes are accepted or not. The\nclient doesn't store it and doesn't use it for any other purpose.\n\nNote that the filter mechanism already exists for a long time and this\nseries doesn't change how it works. For example, it has already been\npossible for a long time to have different repos using the same\npromisor remote with different filters. See the existing partial clone\ndocumentation (like \"Documentation/technical/partial-clone.adoc\") for\nmore information on partial clone.\n\nThe fields that can be passed are limited to \"partialCloneFilter\" and\n\"token\".\n\nOn the technical side, we get rid of 'struct strvec' and we use\n'struct promisor_info' to store the data and 'struct string_list' to\nstore the 'struct promisor_info' instances instead. We also replace\nall the calls to strbuf_split*() functions with calls to\nstring_list_split*() functions.\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\nChanges since v7\n----------------\n\nThanks to Patrick, Junio, Karthik, Jean-Noël and Justin for their\ncomments on the previous versions.\n\nHere are the changes compared to v7:\n\n  - Rebased on top of 6ad8021821 (The fifth batch, 2025-08-29) to\n    avoid some conflicts with relatively new topics and to get\n    functions that can simplify parts of the patches like\n    string_list_split_in_place_f().\n\n  - In all the patches, some calls to git_config_get_string*()\n    functions have been replaced with calls to\n    repo_config_get_string*() as git_config_get_string*() functions\n    have been removed.\n\n  - In patch 2/7 (\"promisor-remote: allow a server to advertise more\n    fields\") a call to string_list_split_in_place() and a following\n    call to string_list_remove_empty_items() have been replaced by a\n    single call to string_list_split_in_place_f() which is passed both\n    the STRING_LIST_SPLIT_TRIM and the STRING_LIST_SPLIT_NONEMPTY\n    flags.\n\n  - Patch 3/7 (\"promisor-remote: use string constants for 'name' and\n    'url' too\") was moved earlier in the series. It used to be patch\n    5/5 in v7. This is because it now introduces a new\n    skip_field_name_prefix() helper function which should start to be\n    used early in the series to avoid some churn.\n\n  - In patch 4/7 (\"promisor-remote: refactor how we parse advertised\n    fields\"), the parse_one_advertised_remote() function is improved\n    in the following ways:\n\n    - It is passed a `const char *` instead of a `struct strbuf *` as\n      argument, because it doesn't really need a `struct strbuf *`.\n\n    - It uses string_list_split() instead of\n      string_list_split_in_place() to avoid munging the argument it is\n      passed as this argument is used in a warning() in case we didn't\n      find both a name and a url in the remote advertisement.\n\n    - The parsing is simplified using the new skip_field_name_prefix()\n      function.\n\n  - Patch 5/7 is new. It replaces a call to strbuf_split_str() with a\n    call to string_list_split() as we don't really needed the `struct\n    strbuf` that strbuf_split_str() created. This is allowed by the\n    previous patch which made parse_one_advertised_remote() accept a\n    `const char *` instead of a `struct strbuf *`.\n\n  - In patch 6/7 (\"promisor-remote: allow a client to check fields\")\n    the new skip_field_name_prefix() function is used simplify parsing\n    a filter and a token in remote advertisements.\n\n  - Patch 7/7 (\"promisor-remote: use string_list_split() in\n    mark_remotes_as_accepted()\") is also new. In the same way as patch\n    5/7, a call to strbuf_split_str() is replaced with a call to\n    string_list_split() as we don't need the `struct strbuf` that\n    strbuf_split_str() created. This removes the last call to a\n    strbuf_split*() function in \"promisor-remote.c\".\n\nCI tests\n--------\n\nThey have all passed, see:\n\nhttps://github.com/chriscool/git/actions/runs/17408565173\n\nRange diff compared to v7\n-------------------------\n\n1:  87a6ba5c48 ! 1:  c8716bf361 promisor-remote: refactor to get rid of 'struct strvec'\n    @@ promisor-remote.c: static int allow_unsanitized(char ch)\n     @@ promisor-remote.c: static void promisor_info_vecs(struct repository *repo,\n      \n      \t\t/* Only add remotes with a non empty URL */\n    - \t\tif (!git_config_get_string_tmp(url_key, &url) && *url) {\n    + \t\tif (!repo_config_get_string_tmp(the_repository, url_key, &url) && *url) {\n     -\t\t\tstrvec_push(names, r->name);\n     -\t\t\tstrvec_push(urls, url);\n     +\t\t\tstruct promisor_info *new_info = xcalloc(1, sizeof(*new_info));\n    @@ promisor-remote.c: char *promisor_remote_info(struct repository *repo)\n     +\tstruct string_list config_info = STRING_LIST_INIT_NODUP;\n     +\tstruct string_list_item *item;\n      \n    - \tgit_config_get_bool(\"promisor.advertise\", &advertise_promisors);\n    + \trepo_config_get_bool(the_repository, \"promisor.advertise\", &advertise_promisors);\n      \n      \tif (!advertise_promisors)\n      \t\treturn NULL;\n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n     -\tstruct strvec urls = STRVEC_INIT;\n     +\tstruct string_list config_info = STRING_LIST_INIT_NODUP;\n      \n    - \tif (!git_config_get_string_tmp(\"promisor.acceptfromserver\", &accept_str)) {\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      \tif (accept == ACCEPT_NONE)\n2:  c729c110d0 ! 2:  a07976245a promisor-remote: allow a server to advertise more fields\n    @@ promisor-remote.c: static int allow_unsanitized(char ch)\n     +{\n     +\tchar *fields = NULL;\n     +\n    -+\tif (!git_config_get_string(config_key, &fields) && *fields) {\n    -+\t\t/* Split on any comma or space character */\n    -+\t\tstring_list_split_in_place(fields_list, fields, \", \", -1);\n    -+\t\t/*\n    -+\t\t * Remove empty items that might result from trailing\n    -+\t\t * commas, or from items being separated by both\n    -+\t\t * commas and spaces.\n    -+\t\t */\n    -+\t\tstring_list_remove_empty_items(fields_list, 0);\n    ++\tif (!repo_config_get_string(the_repository, config_key, &fields) && *fields) {\n    ++\t\tstring_list_split_in_place_f(fields_list, fields, \",\", -1,\n    ++\t\t\t\t\t     STRING_LIST_SPLIT_TRIM |\n    ++\t\t\t\t\t     STRING_LIST_SPLIT_NONEMPTY);\n     +\t\tfilter_string_list(fields_list, 0, is_valid_field, (void *)config_key);\n     +\t}\n     +\n    @@ promisor-remote.c: static void promisor_info_list_clear(struct string_list *list\n     +\tfor_each_string_list_item(item, field_names) {\n     +\t\tchar *key = xstrfmt(\"remote.%s.%s\", p->name, item->string);\n     +\t\tconst char *val;\n    -+\t\tif (!git_config_get_string_tmp(key, &val) && *val)\n    ++\t\tif (!repo_config_get_string_tmp(the_repository, key, &val) && *val)\n     +\t\t\tset_one_field(p, item->string, val);\n     +\t\tfree(key);\n     +\t}\n5:  d0f7fda912 ! 3:  e38f2711f5 promisor-remote: use string constants for 'name' and 'url' too\n    @@ Commit message\n         Let's do the same for \"name\" and \"url\" to avoid repeating them\n         several times and for consistency with the other fields.\n     \n    +    For skipping \"name=\" or \"url=\" in advertisements, let's introduce\n    +    a skip_field_name_prefix() helper function to keep parsing clean\n    +    and easy to understand.\n    +\n         Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n     \n      ## promisor-remote.c ##\n    @@ promisor-remote.c: char *promisor_remote_info(struct repository *repo)\n      \t\tstrbuf_addstr_urlencode(&sb, p->url, allow_unsanitized);\n      \n      \t\tif (p->filter) {\n    -@@ promisor-remote.c: static struct promisor_info *parse_one_advertised_remote(struct strbuf *remote_i\n    - \t\t*p = '\\0';\n    - \t\tvalue = url_percent_decode(p + 1);\n    +@@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n    + \treturn 0;\n    + }\n    + \n    ++static int skip_field_name_prefix(const char *elem, const char *field_name, const char **value)\n    ++{\n    ++\tconst char *p;\n    ++\tif (!skip_prefix(elem, field_name, &p) || *p != '=')\n    ++\t\treturn 0;\n    ++\t*value = p + 1;\n    ++\treturn 1;\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    +@@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n    + \n    + \t\tfor (size_t j = 0; elems[j]; j++) {\n    + \t\t\tstrbuf_strip_suffix(elems[j], \",\");\n    +-\t\t\tif (!skip_prefix(elems[j]->buf, \"name=\", &remote_name))\n    +-\t\t\t\tskip_prefix(elems[j]->buf, \"url=\", &remote_url);\n    ++\t\t\tif (!skip_field_name_prefix(elems[j]->buf, promisor_field_name, &remote_name))\n    ++\t\t\t\tskip_field_name_prefix(elems[j]->buf, promisor_field_url, &remote_url);\n    + \t\t}\n      \n    --\t\tif (!strcmp(elem, \"name\"))\n    -+\t\tif (!strcmp(elem, promisor_field_name))\n    - \t\t\tinfo->name = value;\n    --\t\telse if (!strcmp(elem, \"url\"))\n    -+\t\telse if (!strcmp(elem, promisor_field_url))\n    - \t\t\tinfo->url = value;\n    - \t\telse if (!strcmp(elem, promisor_field_filter))\n    - \t\t\tinfo->filter = value;\n    + \t\tif (remote_name)\n3:  9e0eccae21 ! 4:  4263918802 promisor-remote: refactor how we parse advertised fields\n    @@ promisor-remote.c: enum accept_promisor {\n      \n      \tif (accept == ACCEPT_ALL)\n      \t\treturn 1;\n    -@@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n    - \treturn 0;\n    +@@ promisor-remote.c: static int skip_field_name_prefix(const char *elem, const char *field_name, cons\n    + \treturn 1;\n      }\n      \n    -+static struct promisor_info *parse_one_advertised_remote(struct strbuf *remote_info)\n    ++static struct promisor_info *parse_one_advertised_remote(const char *remote_info)\n     +{\n     +\tstruct promisor_info *info = xcalloc(1, sizeof(*info));\n    -+\tstruct string_list elem_list = STRING_LIST_INIT_NODUP;\n    ++\tstruct string_list elem_list = STRING_LIST_INIT_DUP;\n     +\tstruct string_list_item *item;\n     +\n    -+\tstring_list_split_in_place(&elem_list, remote_info->buf, \",\", -1);\n    ++\tstring_list_split(&elem_list, remote_info, \",\", -1);\n     +\n     +\tfor_each_string_list_item(item, &elem_list) {\n    -+\t\tchar *elem = item->string;\n    -+\t\tchar *value;\n    -+\t\tchar *p = strchr(elem, '=');\n    ++\t\tconst char *elem = item->string;\n    ++\t\tconst char *p = strchr(elem, '=');\n     +\n     +\t\tif (!p) {\n     +\t\t\twarning(_(\"invalid element '%s' from remote info\"), elem);\n     +\t\t\tcontinue;\n     +\t\t}\n     +\n    -+\t\t*p = '\\0';\n    -+\t\tvalue = url_percent_decode(p + 1);\n    -+\n    -+\t\tif (!strcmp(elem, \"name\"))\n    -+\t\t\tinfo->name = value;\n    -+\t\telse if (!strcmp(elem, \"url\"))\n    -+\t\t\tinfo->url = value;\n    -+\t\telse\n    -+\t\t\tfree(value);\n    ++\t\tif (skip_field_name_prefix(elem, promisor_field_name, &p))\n    ++\t\t\tinfo->name = url_percent_decode(p);\n    ++\t\telse if (skip_field_name_prefix(elem, promisor_field_url, &p))\n    ++\t\t\tinfo->url = url_percent_decode(p);\n     +\t}\n     +\n     +\tstring_list_clear(&elem_list, 0);\n     +\n     +\tif (!info->name || !info->url) {\n     +\t\twarning(_(\"server advertised a promisor remote without a name or URL: %s\"),\n    -+\t\t\tremote_info->buf);\n    ++\t\t\tremote_info);\n     +\t\tpromisor_info_free(info);\n     +\t\treturn NULL;\n     +\t}\n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n      \n     -\t\tfor (size_t j = 0; elems[j]; j++) {\n     -\t\t\tstrbuf_strip_suffix(elems[j], \",\");\n    --\t\t\tif (!skip_prefix(elems[j]->buf, \"name=\", &remote_name))\n    --\t\t\t\tskip_prefix(elems[j]->buf, \"url=\", &remote_url);\n    +-\t\t\tif (!skip_field_name_prefix(elems[j]->buf, promisor_field_name, &remote_name))\n    +-\t\t\t\tskip_field_name_prefix(elems[j]->buf, promisor_field_url, &remote_url);\n     -\t\t}\n    -+\t\tadvertised = parse_one_advertised_remote(remotes[i]);\n    ++\t\tadvertised = parse_one_advertised_remote(remotes[i]->buf);\n      \n     -\t\tif (remote_name)\n     -\t\t\tdecoded_name = url_percent_decode(remote_name);\n-:  ---------- > 5:  c1f55b84f0 promisor-remote: use string_list_split() in filter_promisor_remote()\n4:  b1a3384ddc ! 6:  0e37b7149f promisor-remote: allow a client to check fields\n    @@ promisor-remote.c: static int should_accept_remote(enum accept_promisor accept,\n      \n      \twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s'\"),\n      \t\tremote_name, p->url, remote_url);\n    -@@ promisor-remote.c: static struct promisor_info *parse_one_advertised_remote(struct strbuf *remote_i\n    - \t\t\tinfo->name = value;\n    - \t\telse if (!strcmp(elem, \"url\"))\n    - \t\t\tinfo->url = value;\n    -+\t\telse if (!strcmp(elem, promisor_field_filter))\n    -+\t\t\tinfo->filter = value;\n    -+\t\telse if (!strcmp(elem, promisor_field_token))\n    -+\t\t\tinfo->token = value;\n    - \t\telse\n    - \t\t\tfree(value);\n    +@@ promisor-remote.c: static struct promisor_info *parse_one_advertised_remote(const char *remote_info\n    + \t\t\tinfo->name = url_percent_decode(p);\n    + \t\telse if (skip_field_name_prefix(elem, promisor_field_url, &p))\n    + \t\t\tinfo->url = url_percent_decode(p);\n    ++\t\telse if (skip_field_name_prefix(elem, promisor_field_filter, &p))\n    ++\t\t\tinfo->filter = url_percent_decode(p);\n    ++\t\telse if (skip_field_name_prefix(elem, promisor_field_token, &p))\n    ++\t\t\tinfo->token = url_percent_decode(p);\n      \t}\n    + \n    + \tstring_list_clear(&elem_list, 0);\n     @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n      \tif (accept == ACCEPT_NONE)\n      \t\treturn;\n    @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n     -\n      \t/* Parse remote info received */\n      \n    - \tremotes = strbuf_split_str(info, ';', 0);\n    + \tstring_list_split(&remote_info, info, \";\", -1);\n     @@ promisor-remote.c: static void filter_promisor_remote(struct repository *repo,\n      \t\tif (!advertised)\n      \t\t\tcontinue;\n-:  ---------- > 7:  123d41a7fc promisor-remote: use string_list_split() in mark_remotes_as_accepted()\n\nChristian Couder (7):\n  promisor-remote: refactor to get rid of 'struct strvec'\n  promisor-remote: allow a server to advertise more fields\n  promisor-remote: use string constants for 'name' and 'url' too\n  promisor-remote: refactor how we parse advertised fields\n  promisor-remote: use string_list_split() in filter_promisor_remote()\n  promisor-remote: allow a client to check fields\n  promisor-remote: use string_list_split() in mark_remotes_as_accepted()\n\n Documentation/config/promisor.adoc    |  61 ++++\n Documentation/gitprotocol-v2.adoc     |  64 +++-\n promisor-remote.c                     | 424 ++++++++++++++++++++------\n t/t5710-promisor-remote-capability.sh |  65 ++++\n 4 files changed, 511 insertions(+), 103 deletions(-)\n\n-- \n2.51.0.168.gc8716bf361\n\n"},{"id":"525755","messageId":"20250908053056.956907-2-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250908053056.956907-1-christian.couder@gmail.com","subject":"[PATCH v8 1/7] promisor-remote: refactor to get rid of 'struct strvec'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-09-08T05:30:47Z","receivedAt":"2025-09-08T05:31:17Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"In a following commit, we will use the new 'promisor-remote' protocol\ncapability introduced by d460267613 (Add 'promisor-remote' capability\nto protocol v2, 2025-02-18) to pass and process more information\nabout promisor remotes than just their name and url.\n\nFor that purpose, we will need to store information about other\nfields, especially information that might or might not be available\nfor different promisor remotes. Unfortunately using 'struct strvec',\nas we currently do, to store information about the promisor remotes\nwith one 'struct strvec' for each field like \"name\" or \"url\" does not\nscale easily in that case. We would need one 'struct strvec' for each\nnew field, and then we would have to pass all these 'struct strvec'\naround.\n\nLet's refactor this and introduce a new 'struct promisor_info'.\n\nIt will only store promisor remote information in its members. For now\nit has only a 'name' member for the promisor remote name and an 'url'\nmember for its URL. We will use a 'struct string_list' to store the\ninstances of 'struct promisor_info'. For each 'item' in the\nstring_list, 'item->string' will point to the promisor remote name and\n'item->util' will point to the corresponding 'struct promisor_info'\ninstance.\n\nExplicit members are used within 'struct promisor_info' for type\nsafety and clarity regarding the specific information being handled,\nrather than a generic key-value store. We want to specify and document\neach field and its content, so adding new members to the struct as\nmore fields are supported is fine.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 107 ++++++++++++++++++++++++++++------------------\n 1 file changed, 66 insertions(+), 41 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 08b0da8962..c3df8f071e 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -314,9 +314,35 @@ static int allow_unsanitized(char ch)\n \treturn ch > 32 && ch < 127;\n }\n \n-static void promisor_info_vecs(struct repository *repo,\n-\t\t\t       struct strvec *names,\n-\t\t\t       struct strvec *urls)\n+/*\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+ */\n+struct promisor_info {\n+\tconst char *name;\n+\tconst char *url;\n+};\n+\n+static void promisor_info_list_clear(struct string_list *list)\n+{\n+\tfor (size_t i = 0; i < list->nr; i++) {\n+\t\tstruct promisor_info *p = list->items[i].util;\n+\t\tfree((char *)p->name);\n+\t\tfree((char *)p->url);\n+\t}\n+\tstring_list_clear(list, 1);\n+}\n+\n+/*\n+ * Populate 'list' with promisor remote information from the config.\n+ * The 'util' pointer of each list item will hold a 'struct promisor_info'.\n+ */\n+static void promisor_config_info_list(struct repository *repo, struct string_list *list)\n {\n \tstruct promisor_remote *r;\n \n@@ -328,8 +354,14 @@ static void promisor_info_vecs(struct repository *repo,\n \n \t\t/* Only add remotes with a non empty URL */\n \t\tif (!repo_config_get_string_tmp(the_repository, url_key, &url) && *url) {\n-\t\t\tstrvec_push(names, r->name);\n-\t\t\tstrvec_push(urls, url);\n+\t\t\tstruct promisor_info *new_info = xcalloc(1, sizeof(*new_info));\n+\t\t\tstruct string_list_item *item;\n+\n+\t\t\tnew_info->name = xstrdup(r->name);\n+\t\t\tnew_info->url = xstrdup(url);\n+\n+\t\t\titem = string_list_append(list, new_info->name);\n+\t\t\titem->util = new_info;\n \t\t}\n \n \t\tfree(url_key);\n@@ -340,47 +372,36 @@ char *promisor_remote_info(struct repository *repo)\n {\n \tstruct strbuf sb = STRBUF_INIT;\n \tint advertise_promisors = 0;\n-\tstruct strvec names = STRVEC_INIT;\n-\tstruct strvec urls = STRVEC_INIT;\n+\tstruct string_list config_info = STRING_LIST_INIT_NODUP;\n+\tstruct string_list_item *item;\n \n \trepo_config_get_bool(the_repository, \"promisor.advertise\", &advertise_promisors);\n \n \tif (!advertise_promisors)\n \t\treturn NULL;\n \n-\tpromisor_info_vecs(repo, &names, &urls);\n+\tpromisor_config_info_list(repo, &config_info);\n \n-\tif (!names.nr)\n+\tif (!config_info.nr)\n \t\treturn NULL;\n \n-\tfor (size_t i = 0; i < names.nr; i++) {\n-\t\tif (i)\n+\tfor_each_string_list_item(item, &config_info) {\n+\t\tstruct promisor_info *p = item->util;\n+\n+\t\tif (item != config_info.items)\n \t\t\tstrbuf_addch(&sb, ';');\n+\n \t\tstrbuf_addstr(&sb, \"name=\");\n-\t\tstrbuf_addstr_urlencode(&sb, names.v[i], allow_unsanitized);\n+\t\tstrbuf_addstr_urlencode(&sb, p->name, allow_unsanitized);\n \t\tstrbuf_addstr(&sb, \",url=\");\n-\t\tstrbuf_addstr_urlencode(&sb, urls.v[i], allow_unsanitized);\n+\t\tstrbuf_addstr_urlencode(&sb, p->url, allow_unsanitized);\n \t}\n \n-\tstrvec_clear(&names);\n-\tstrvec_clear(&urls);\n+\tpromisor_info_list_clear(&config_info);\n \n \treturn strbuf_detach(&sb, NULL);\n }\n \n-/*\n- * Find first index of 'nicks' where there is 'nick'. 'nick' is\n- * compared case sensitively to the strings in 'nicks'. If not found\n- * 'nicks->nr' is returned.\n- */\n-static size_t remote_nick_find(struct strvec *nicks, const char *nick)\n-{\n-\tfor (size_t i = 0; i < nicks->nr; i++)\n-\t\tif (!strcmp(nicks->v[i], nick))\n-\t\t\treturn i;\n-\treturn nicks->nr;\n-}\n-\n enum accept_promisor {\n \tACCEPT_NONE = 0,\n \tACCEPT_KNOWN_URL,\n@@ -390,19 +411,23 @@ enum accept_promisor {\n \n static int should_accept_remote(enum accept_promisor accept,\n \t\t\t\tconst char *remote_name, const char *remote_url,\n-\t\t\t\tstruct strvec *names, struct strvec *urls)\n+\t\t\t\tstruct string_list *config_info)\n {\n-\tsize_t i;\n+\tstruct promisor_info *p;\n+\tstruct string_list_item *item;\n \n \tif (accept == ACCEPT_ALL)\n \t\treturn 1;\n \n-\ti = remote_nick_find(names, remote_name);\n+\t/* Get config info for that promisor remote */\n+\titem = string_list_lookup(config_info, remote_name);\n \n-\tif (i >= names->nr)\n+\tif (!item)\n \t\t/* We don't know about that remote */\n \t\treturn 0;\n \n+\tp = item->util;\n+\n \tif (accept == ACCEPT_KNOWN_NAME)\n \t\treturn 1;\n \n@@ -414,11 +439,11 @@ static int should_accept_remote(enum accept_promisor accept,\n \t\treturn 0;\n \t}\n \n-\tif (!strcmp(urls->v[i], remote_url))\n+\tif (!strcmp(p->url, remote_url))\n \t\treturn 1;\n \n \twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s'\"),\n-\t\tremote_name, urls->v[i], remote_url);\n+\t\tremote_name, p->url, remote_url);\n \n \treturn 0;\n }\n@@ -430,8 +455,7 @@ static void filter_promisor_remote(struct repository *repo,\n \tstruct strbuf **remotes;\n \tconst char *accept_str;\n \tenum accept_promisor accept = ACCEPT_NONE;\n-\tstruct strvec names = STRVEC_INIT;\n-\tstruct strvec urls = STRVEC_INIT;\n+\tstruct string_list config_info = STRING_LIST_INIT_NODUP;\n \n \tif (!repo_config_get_string_tmp(the_repository, \"promisor.acceptfromserver\", &accept_str)) {\n \t\tif (!*accept_str || !strcasecmp(\"None\", accept_str))\n@@ -450,8 +474,10 @@ static void filter_promisor_remote(struct repository *repo,\n \tif (accept == ACCEPT_NONE)\n \t\treturn;\n \n-\tif (accept != ACCEPT_ALL)\n-\t\tpromisor_info_vecs(repo, &names, &urls);\n+\tif (accept != ACCEPT_ALL) {\n+\t\tpromisor_config_info_list(repo, &config_info);\n+\t\tstring_list_sort(&config_info);\n+\t}\n \n \t/* Parse remote info received */\n \n@@ -482,7 +508,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tif (remote_url)\n \t\t\tdecoded_url = url_percent_decode(remote_url);\n \n-\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, &names, &urls))\n+\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, &config_info))\n \t\t\tstrvec_push(accepted, decoded_name);\n \n \t\tstrbuf_list_free(elems);\n@@ -490,8 +516,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tfree(decoded_url);\n \t}\n \n-\tstrvec_clear(&names);\n-\tstrvec_clear(&urls);\n+\tpromisor_info_list_clear(&config_info);\n \tstrbuf_list_free(remotes);\n }\n \n-- \n2.51.0.168.gc8716bf361\n\n"},{"id":"525756","messageId":"20250908053056.956907-3-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250908053056.956907-1-christian.couder@gmail.com","subject":"[PATCH v8 2/7] promisor-remote: allow a server to advertise more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-09-08T05:30:48Z","receivedAt":"2025-09-08T05:31:19Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"For now the \"promisor-remote\" protocol capability can only pass \"name\"\nand \"url\" information from a server to a client in the form\n\"name=<remote_name>,url=<remote_url>\".\n\nTo allow clients to make more informed decisions about which promisor\nremotes they accept, let's make it possible to pass more information\nby introducing a new \"promisor.sendFields\" configuration variable.\n\nOn the server side, information about a remote `foo` is stored in\nconfiguration variables named `remote.foo.<variable-name>`. To make\nit clearer and simpler, we use `field` and `field name` like this:\n\n  * `field name` refers to the <variable-name> part of such a\n    configuration variable, and\n\n  * `field` refers to both the `field name` and the value of such a\n    configuration variable.\n\nThe \"promisor.sendFields\" configuration variable should contain a\ncomma or space separated list of field names that will be looked up\nin the configuration of the remote on the server to find the values\nthat will be passed to the client.\n\nOnly a set of predefined field names are allowed. The only field\nnames in this set are \"partialCloneFilter\" and \"token\". The\n\"partialCloneFilter\" field name specifies the filter definition used\nby the promisor remote, and the \"token\" field name can provide an\nauthentication credential for accessing it.\n\nFor example, if \"promisor.sendFields\" is set to \"partialCloneFilter\",\nand the server has the \"remote.foo.partialCloneFilter\" config\nvariable set to a value, then that value will be passed in the\n\"partialCloneFilter\" field in the form \"partialCloneFilter=<value>\"\nafter the \"name\" and \"url\" fields.\n\nA following commit will allow the client to use the information to\ndecide if it accepts the remote or not. For now the client doesn't do\nanything with the additional information it receives.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    |  22 +++++\n Documentation/gitprotocol-v2.adoc     |  64 +++++++++----\n promisor-remote.c                     | 129 ++++++++++++++++++++++++--\n t/t5710-promisor-remote-capability.sh |  31 +++++++\n 4 files changed, 221 insertions(+), 25 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex 2638b01f83..b4a72c2152 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -9,6 +9,28 @@ promisor.advertise::\n \t\"false\", which means the \"promisor-remote\" capability is not\n \tadvertised.\n \n+promisor.sendFields::\n+\tA comma or space separated list of additional remote related\n+\tfield names. A server sends these field names and the\n+\tassociated field values from its configuration when\n+\tadvertising its promisor remotes using the \"promisor-remote\"\n+\tcapability, see linkgit:gitprotocol-v2[5]. Currently, only the\n+\t\"partialCloneFilter\" and \"token\" field names are supported.\n++\n+`partialCloneFilter`:: contains the partial clone filter\n+used for the remote.\n++\n+`token`:: contains an authentication token for the remote.\n++\n+When a field name is part of this list and a corresponding\n+\"remote.foo.<field-name>\" config variable is set on the server to a\n+non-empty value, then the field name and value are sent when\n+advertising the promisor remote \"foo\".\n++\n+This list has no effect unless the \"promisor.advertise\" config\n+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\"\ndiff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc\nindex 9a57005d77..c7db103299 100644\n--- a/Documentation/gitprotocol-v2.adoc\n+++ b/Documentation/gitprotocol-v2.adoc\n@@ -785,33 +785,64 @@ retrieving the header from a bundle at the indicated URI, and thus\n save themselves and the server(s) the request(s) needed to inspect the\n headers of that bundle or bundles.\n \n-promisor-remote=<pr-infos>\n-~~~~~~~~~~~~~~~~~~~~~~~~~~\n+promisor-remote=<pr-info>\n+~~~~~~~~~~~~~~~~~~~~~~~~~\n \n The server may advertise some promisor remotes it is using or knows\n about to a client which may want to use them as its promisor remotes,\n-instead of this repository. In this case <pr-infos> should be of the\n+instead of this repository. In this case <pr-info> should be of the\n form:\n \n-\tpr-infos = pr-info | pr-infos \";\" pr-info\n+\tpr-info = pr-fields | pr-info \";\" pr-fields\n \n-\tpr-info = \"name=\" pr-name | \"name=\" pr-name \",\" \"url=\" pr-url\n+\tpr-fields = pr-field | pr-fields \",\" pr-field\n \n-where `pr-name` is the urlencoded name of a promisor remote, and\n-`pr-url` the urlencoded URL of that promisor remote.\n+\tpr-field = field-name \"=\" field-value\n \n-In this case, if the client decides to use one or more promisor\n-remotes the server advertised, it can reply with\n-\"promisor-remote=<pr-names>\" where <pr-names> should be of the form:\n+where all the `field-name` and `field-value` in a given `pr-fields`\n+are field names and values related to a single promisor remote. A\n+given `field-name` MUST NOT appear more than once in given\n+`pr-fields`.\n+\n+The server MUST advertise at least the \"name\" and \"url\" field names\n+along with the associated field values, which are the name of a valid\n+remote and its URL, in each `pr-fields`. The \"name\" and \"url\" fields\n+MUST appear first in each pr-fields, in that order.\n+\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+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+\n+`token`:: An authentication token that clients can use when\n+connecting to the remote. It corresponds to the \"remote.<name>.token\"\n+config setting.\n+\n+No other fields are defined by the protocol at this time. Field names\n+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+\n+Field values MUST be urlencoded.\n+\n+If the client decides to use one or more promisor remotes the server\n+advertised, it can reply with \"promisor-remote=<pr-names>\" where\n+<pr-names> should be of the form:\n \n \tpr-names = pr-name | pr-names \";\" pr-name\n \n where `pr-name` is the urlencoded name of a promisor remote the server\n advertised and the client accepts.\n \n-Note that, everywhere in this document, `pr-name` MUST be a valid\n-remote name, and the ';' and ',' characters MUST be encoded if they\n-appear in `pr-name` or `pr-url`.\n+Note that, everywhere in this document, the ';' and ',' characters\n+MUST be encoded if they appear in `pr-name` or `field-value`.\n \n If the server doesn't know any promisor remote that could be good for\n a client to use, or prefers a client not to use any promisor remote it\n@@ -822,9 +853,10 @@ In this case, or if the client doesn't want to use any promisor remote\n the server advertised, the client shouldn't advertise the\n \"promisor-remote\" capability at all in its reply.\n \n-The \"promisor.advertise\" and \"promisor.acceptFromServer\" configuration\n-options can be used on the server and client side to control what they\n-advertise or accept respectively. See the documentation of these\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 \n Note that in the future it would be nice if the \"promisor-remote\"\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex c3df8f071e..98ba59e952 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -314,6 +314,75 @@ static int allow_unsanitized(char ch)\n \treturn ch > 32 && ch < 127;\n }\n \n+static const char promisor_field_filter[] = \"partialCloneFilter\";\n+static const char promisor_field_token[] = \"token\";\n+\n+/*\n+ * List of optional field names that can be used in the\n+ * \"promisor-remote\" protocol capability (others must be\n+ * ignored). Each field should correspond to a configurable property\n+ * of a remote that can be relevant for the client.\n+ */\n+static const char *known_fields[] = {\n+\tpromisor_field_filter, /* Filter used for partial clone */\n+\tpromisor_field_token,  /* Authentication token for the remote */\n+\tNULL\n+};\n+\n+/*\n+ * Check if 'field' is in the list of the known field names for the\n+ * \"promisor-remote\" protocol capability.\n+ */\n+static int is_known_field(const char *field)\n+{\n+\tconst char **p;\n+\n+\tfor (p = known_fields; *p; p++)\n+\t\tif (!strcasecmp(*p, field))\n+\t\t\treturn 1;\n+\treturn 0;\n+}\n+\n+static int is_valid_field(struct string_list_item *item, void *cb_data)\n+{\n+\tconst char *field = item->string;\n+\tconst char *config_key = (const char *)cb_data;\n+\n+\tif (!is_known_field(field)) {\n+\t\twarning(_(\"unsupported field '%s' in '%s' config\"), field, config_key);\n+\t\treturn 0;\n+\t}\n+\treturn 1;\n+}\n+\n+static char *fields_from_config(struct string_list *fields_list, const char *config_key)\n+{\n+\tchar *fields = NULL;\n+\n+\tif (!repo_config_get_string(the_repository, config_key, &fields) && *fields) {\n+\t\tstring_list_split_in_place_f(fields_list, fields, \",\", -1,\n+\t\t\t\t\t     STRING_LIST_SPLIT_TRIM |\n+\t\t\t\t\t     STRING_LIST_SPLIT_NONEMPTY);\n+\t\tfilter_string_list(fields_list, 0, is_valid_field, (void *)config_key);\n+\t}\n+\n+\treturn fields;\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+}\n+\n /*\n  * Struct for promisor remotes involved in the \"promisor-remote\"\n  * protocol capability.\n@@ -326,6 +395,8 @@ static int allow_unsanitized(char ch)\n struct promisor_info {\n \tconst char *name;\n \tconst char *url;\n+\tconst char *filter;\n+\tconst char *token;\n };\n \n static void promisor_info_list_clear(struct string_list *list)\n@@ -334,15 +405,47 @@ static void promisor_info_list_clear(struct string_list *list)\n \t\tstruct promisor_info *p = list->items[i].util;\n \t\tfree((char *)p->name);\n \t\tfree((char *)p->url);\n+\t\tfree((char *)p->filter);\n+\t\tfree((char *)p->token);\n \t}\n \tstring_list_clear(list, 1);\n }\n \n+static void set_one_field(struct promisor_info *p,\n+\t\t\t  const char *field, const char *value)\n+{\n+\tif (!strcasecmp(field, promisor_field_filter))\n+\t\tp->filter = xstrdup(value);\n+\telse if (!strcasecmp(field, promisor_field_token))\n+\t\tp->token = xstrdup(value);\n+\telse\n+\t\tBUG(\"invalid field '%s'\", field);\n+}\n+\n+static void set_fields(struct promisor_info *p,\n+\t\t       struct string_list *field_names)\n+{\n+\tstruct string_list_item *item;\n+\n+\tfor_each_string_list_item(item, field_names) {\n+\t\tchar *key = xstrfmt(\"remote.%s.%s\", p->name, item->string);\n+\t\tconst char *val;\n+\t\tif (!repo_config_get_string_tmp(the_repository, key, &val) && *val)\n+\t\t\tset_one_field(p, item->string, val);\n+\t\tfree(key);\n+\t}\n+}\n+\n /*\n  * Populate 'list' with promisor remote information from the config.\n- * The 'util' pointer of each list item will hold a 'struct promisor_info'.\n+ * The 'util' pointer of each list item will hold a 'struct\n+ * promisor_info'. Except \"name\" and \"url\", only members of that\n+ * struct specified by the 'field_names' list are set (using values\n+ * from the configuration).\n  */\n-static void promisor_config_info_list(struct repository *repo, struct string_list *list)\n+static void promisor_config_info_list(struct repository *repo,\n+\t\t\t\t      struct string_list *list,\n+\t\t\t\t      struct string_list *field_names)\n {\n \tstruct promisor_remote *r;\n \n@@ -360,6 +463,9 @@ static void promisor_config_info_list(struct repository *repo, struct string_lis\n \t\t\tnew_info->name = xstrdup(r->name);\n \t\t\tnew_info->url = xstrdup(url);\n \n+\t\t\tif (field_names)\n+\t\t\t\tset_fields(new_info, field_names);\n+\n \t\t\titem = string_list_append(list, new_info->name);\n \t\t\titem->util = new_info;\n \t\t}\n@@ -380,7 +486,7 @@ char *promisor_remote_info(struct repository *repo)\n \tif (!advertise_promisors)\n \t\treturn NULL;\n \n-\tpromisor_config_info_list(repo, &config_info);\n+\tpromisor_config_info_list(repo, &config_info, fields_sent());\n \n \tif (!config_info.nr)\n \t\treturn NULL;\n@@ -395,6 +501,15 @@ char *promisor_remote_info(struct repository *repo)\n \t\tstrbuf_addstr_urlencode(&sb, p->name, allow_unsanitized);\n \t\tstrbuf_addstr(&sb, \",url=\");\n \t\tstrbuf_addstr_urlencode(&sb, p->url, allow_unsanitized);\n+\n+\t\tif (p->filter) {\n+\t\t\tstrbuf_addf(&sb, \",%s=\", promisor_field_filter);\n+\t\t\tstrbuf_addstr_urlencode(&sb, p->filter, allow_unsanitized);\n+\t\t}\n+\t\tif (p->token) {\n+\t\t\tstrbuf_addf(&sb, \",%s=\", promisor_field_token);\n+\t\t\tstrbuf_addstr_urlencode(&sb, p->token, allow_unsanitized);\n+\t\t}\n \t}\n \n \tpromisor_info_list_clear(&config_info);\n@@ -475,7 +590,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t\treturn;\n \n \tif (accept != ACCEPT_ALL) {\n-\t\tpromisor_config_info_list(repo, &config_info);\n+\t\tpromisor_config_info_list(repo, &config_info, NULL);\n \t\tstring_list_sort(&config_info);\n \t}\n \n@@ -494,13 +609,9 @@ static void filter_promisor_remote(struct repository *repo,\n \t\telems = strbuf_split(remotes[i], ',');\n \n \t\tfor (size_t j = 0; elems[j]; j++) {\n-\t\t\tint res;\n \t\t\tstrbuf_strip_suffix(elems[j], \",\");\n-\t\t\tres = skip_prefix(elems[j]->buf, \"name=\", &remote_name) ||\n+\t\t\tif (!skip_prefix(elems[j]->buf, \"name=\", &remote_name))\n \t\t\t\tskip_prefix(elems[j]->buf, \"url=\", &remote_url);\n-\t\t\tif (!res)\n-\t\t\t\twarning(_(\"unknown element '%s' from remote info\"),\n-\t\t\t\t\telems[j]->buf);\n \t\t}\n \n \t\tif (remote_name)\ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex cb061b1f35..204528b2e0 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -295,6 +295,37 @@ 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 promisor.sendFields\" '\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+\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 promisor.acceptfromserver=All \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that fields are properly transmitted\n+\tENCODED_URL=$(echo \"file://$(pwd)/lop\" | sed -e \"s/ /%20/g\") &&\n+\tPR1=\"name=lop,url=$ENCODED_URL,partialCloneFilter=blob:none\" &&\n+\tPR2=\"name=otherLop,url=https://invalid.invalid,partialCloneFilter=blob:limit=10k,token=fooBar\" &&\n+\ttest_grep \"clone< promisor-remote=$PR1;$PR2\" trace &&\n+\ttest_grep \"clone> promisor-remote=lop;otherLop\" 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 \n-- \n2.51.0.168.gc8716bf361\n\n"},{"id":"525757","messageId":"20250908053056.956907-4-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250908053056.956907-1-christian.couder@gmail.com","subject":"[PATCH v8 3/7] promisor-remote: use string constants for 'name' and 'url' too","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-09-08T05:30:49Z","receivedAt":"2025-09-08T05:31:20Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"A previous commit started to define `promisor_field_filter` and\n`promisor_field_token`, and used them instead of the\n\"partialCloneFilter\" and \"token\" string literals.\n\nLet's do the same for \"name\" and \"url\" to avoid repeating them\nseveral times and for consistency with the other fields.\n\nFor skipping \"name=\" or \"url=\" in advertisements, let's introduce\na skip_field_name_prefix() helper function to keep parsing clean\nand easy to understand.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 23 +++++++++++++++++++----\n 1 file changed, 19 insertions(+), 4 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 98ba59e952..3913e32c11 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -314,6 +314,12 @@ static int allow_unsanitized(char ch)\n \treturn ch > 32 && ch < 127;\n }\n \n+/*\n+ * All the fields used in \"promisor-remote\" protocol capability,\n+ * including the mandatory \"name\" and \"url\" ones.\n+ */\n+static const char promisor_field_name[] = \"name\";\n+static const char promisor_field_url[] = \"url\";\n static const char promisor_field_filter[] = \"partialCloneFilter\";\n static const char promisor_field_token[] = \"token\";\n \n@@ -497,9 +503,9 @@ char *promisor_remote_info(struct repository *repo)\n \t\tif (item != config_info.items)\n \t\t\tstrbuf_addch(&sb, ';');\n \n-\t\tstrbuf_addstr(&sb, \"name=\");\n+\t\tstrbuf_addf(&sb, \"%s=\", promisor_field_name);\n \t\tstrbuf_addstr_urlencode(&sb, p->name, allow_unsanitized);\n-\t\tstrbuf_addstr(&sb, \",url=\");\n+\t\tstrbuf_addf(&sb, \",%s=\", promisor_field_url);\n \t\tstrbuf_addstr_urlencode(&sb, p->url, allow_unsanitized);\n \n \t\tif (p->filter) {\n@@ -563,6 +569,15 @@ static int should_accept_remote(enum accept_promisor accept,\n \treturn 0;\n }\n \n+static int skip_field_name_prefix(const char *elem, const char *field_name, const char **value)\n+{\n+\tconst char *p;\n+\tif (!skip_prefix(elem, field_name, &p) || *p != '=')\n+\t\treturn 0;\n+\t*value = p + 1;\n+\treturn 1;\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@@ -610,8 +625,8 @@ static void filter_promisor_remote(struct repository *repo,\n \n \t\tfor (size_t j = 0; elems[j]; j++) {\n \t\t\tstrbuf_strip_suffix(elems[j], \",\");\n-\t\t\tif (!skip_prefix(elems[j]->buf, \"name=\", &remote_name))\n-\t\t\t\tskip_prefix(elems[j]->buf, \"url=\", &remote_url);\n+\t\t\tif (!skip_field_name_prefix(elems[j]->buf, promisor_field_name, &remote_name))\n+\t\t\t\tskip_field_name_prefix(elems[j]->buf, promisor_field_url, &remote_url);\n \t\t}\n \n \t\tif (remote_name)\n-- \n2.51.0.168.gc8716bf361\n\n"},{"id":"525758","messageId":"20250908053056.956907-5-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250908053056.956907-1-christian.couder@gmail.com","subject":"[PATCH v8 4/7] promisor-remote: refactor how we parse advertised fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-09-08T05:30:50Z","receivedAt":"2025-09-08T05:31:22Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"In a follow up commit we are going to parse more fields, like a filter\nand a token, coming from the server when it advertises promisor remotes\nusing the \"promisor-remote\" capability.\n\nTo prepare for this, let's refactor the code that parses the advertised\nfields coming from the server into a new parse_one_advertised_remote()\nfunction that will populate a `struct promisor_info` with the content\nof the fields it parsed.\n\nWhile at it, let's also pass this `struct promisor_info` to the\nshould_accept_remote() function, instead of passing it the parsed name\nand url.\n\nThese changes will make it simpler to both parse more fields and access\nthe content of these parsed fields in follow up commits.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 86 +++++++++++++++++++++++++++++++----------------\n 1 file changed, 57 insertions(+), 29 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex 3913e32c11..c22128d09e 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -405,16 +405,20 @@ struct promisor_info {\n \tconst char *token;\n };\n \n+static void promisor_info_free(struct promisor_info *p)\n+{\n+\tfree((char *)p->name);\n+\tfree((char *)p->url);\n+\tfree((char *)p->filter);\n+\tfree((char *)p->token);\n+\tfree(p);\n+}\n+\n static void promisor_info_list_clear(struct string_list *list)\n {\n-\tfor (size_t i = 0; i < list->nr; i++) {\n-\t\tstruct promisor_info *p = list->items[i].util;\n-\t\tfree((char *)p->name);\n-\t\tfree((char *)p->url);\n-\t\tfree((char *)p->filter);\n-\t\tfree((char *)p->token);\n-\t}\n-\tstring_list_clear(list, 1);\n+\tfor (size_t i = 0; i < list->nr; i++)\n+\t\tpromisor_info_free(list->items[i].util);\n+\tstring_list_clear(list, 0);\n }\n \n static void set_one_field(struct promisor_info *p,\n@@ -531,11 +535,13 @@ enum accept_promisor {\n };\n \n static int should_accept_remote(enum accept_promisor accept,\n-\t\t\t\tconst char *remote_name, const char *remote_url,\n+\t\t\t\tstruct promisor_info *advertised,\n \t\t\t\tstruct string_list *config_info)\n {\n \tstruct promisor_info *p;\n \tstruct string_list_item *item;\n+\tconst char *remote_name = advertised->name;\n+\tconst char *remote_url = advertised->url;\n \n \tif (accept == ACCEPT_ALL)\n \t\treturn 1;\n@@ -578,6 +584,41 @@ static int skip_field_name_prefix(const char *elem, const char *field_name, cons\n \treturn 1;\n }\n \n+static struct promisor_info *parse_one_advertised_remote(const char *remote_info)\n+{\n+\tstruct promisor_info *info = xcalloc(1, sizeof(*info));\n+\tstruct string_list elem_list = STRING_LIST_INIT_DUP;\n+\tstruct string_list_item *item;\n+\n+\tstring_list_split(&elem_list, remote_info, \",\", -1);\n+\n+\tfor_each_string_list_item(item, &elem_list) {\n+\t\tconst char *elem = item->string;\n+\t\tconst char *p = strchr(elem, '=');\n+\n+\t\tif (!p) {\n+\t\t\twarning(_(\"invalid element '%s' from remote info\"), elem);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tif (skip_field_name_prefix(elem, promisor_field_name, &p))\n+\t\t\tinfo->name = url_percent_decode(p);\n+\t\telse if (skip_field_name_prefix(elem, promisor_field_url, &p))\n+\t\t\tinfo->url = url_percent_decode(p);\n+\t}\n+\n+\tstring_list_clear(&elem_list, 0);\n+\n+\tif (!info->name || !info->url) {\n+\t\twarning(_(\"server advertised a promisor remote without a name or URL: %s\"),\n+\t\t\tremote_info);\n+\t\tpromisor_info_free(info);\n+\t\treturn NULL;\n+\t}\n+\n+\treturn info;\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@@ -614,32 +655,19 @@ static void filter_promisor_remote(struct repository *repo,\n \tremotes = strbuf_split_str(info, ';', 0);\n \n \tfor (size_t i = 0; remotes[i]; i++) {\n-\t\tstruct strbuf **elems;\n-\t\tconst char *remote_name = NULL;\n-\t\tconst char *remote_url = NULL;\n-\t\tchar *decoded_name = NULL;\n-\t\tchar *decoded_url = NULL;\n+\t\tstruct promisor_info *advertised;\n \n \t\tstrbuf_strip_suffix(remotes[i], \";\");\n-\t\telems = strbuf_split(remotes[i], ',');\n \n-\t\tfor (size_t j = 0; elems[j]; j++) {\n-\t\t\tstrbuf_strip_suffix(elems[j], \",\");\n-\t\t\tif (!skip_field_name_prefix(elems[j]->buf, promisor_field_name, &remote_name))\n-\t\t\t\tskip_field_name_prefix(elems[j]->buf, promisor_field_url, &remote_url);\n-\t\t}\n+\t\tadvertised = parse_one_advertised_remote(remotes[i]->buf);\n \n-\t\tif (remote_name)\n-\t\t\tdecoded_name = url_percent_decode(remote_name);\n-\t\tif (remote_url)\n-\t\t\tdecoded_url = url_percent_decode(remote_url);\n+\t\tif (!advertised)\n+\t\t\tcontinue;\n \n-\t\tif (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, &config_info))\n-\t\t\tstrvec_push(accepted, decoded_name);\n+\t\tif (should_accept_remote(accept, advertised, &config_info))\n+\t\t\tstrvec_push(accepted, advertised->name);\n \n-\t\tstrbuf_list_free(elems);\n-\t\tfree(decoded_name);\n-\t\tfree(decoded_url);\n+\t\tpromisor_info_free(advertised);\n \t}\n \n \tpromisor_info_list_clear(&config_info);\n-- \n2.51.0.168.gc8716bf361\n\n"},{"id":"525759","messageId":"20250908053056.956907-6-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250908053056.956907-1-christian.couder@gmail.com","subject":"[PATCH v8 5/7] promisor-remote: use string_list_split() in filter_promisor_remote()","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-09-08T05:30:51Z","receivedAt":"2025-09-08T05:31:22Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"A previous commit introduced a new parse_one_advertised_remote()\nfunction that takes a `const char *` argument. This function is called\nfrom filter_promisor_remote() and parses all the fields for one remote.\n\nThis means that in filter_promisor_remote() we no longer need to split\nthe remote information that will be passed to\nparse_one_advertised_remote() into an array of relatively heavy and\ncomplex `struct strbuf`.\n\nTo use something lighter, let's then replace strbuf_split_str() with\nstring_list_split() in filter_promisor_remote() to parse the remote\ninformation that is passed to parse_one_advertised_remote().\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 13 ++++++-------\n 1 file changed, 6 insertions(+), 7 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex c22128d09e..afec0d081d 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -623,10 +623,11 @@ 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 {\n-\tstruct strbuf **remotes;\n \tconst char *accept_str;\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 string_list_item *item;\n \n \tif (!repo_config_get_string_tmp(the_repository, \"promisor.acceptfromserver\", &accept_str)) {\n \t\tif (!*accept_str || !strcasecmp(\"None\", accept_str))\n@@ -652,14 +653,12 @@ static void filter_promisor_remote(struct repository *repo,\n \n \t/* Parse remote info received */\n \n-\tremotes = strbuf_split_str(info, ';', 0);\n+\tstring_list_split(&remote_info, info, \";\", -1);\n \n-\tfor (size_t i = 0; remotes[i]; i++) {\n+\tfor_each_string_list_item(item, &remote_info) {\n \t\tstruct promisor_info *advertised;\n \n-\t\tstrbuf_strip_suffix(remotes[i], \";\");\n-\n-\t\tadvertised = parse_one_advertised_remote(remotes[i]->buf);\n+\t\tadvertised = parse_one_advertised_remote(item->string);\n \n \t\tif (!advertised)\n \t\t\tcontinue;\n@@ -671,7 +670,7 @@ static void filter_promisor_remote(struct repository *repo,\n \t}\n \n \tpromisor_info_list_clear(&config_info);\n-\tstrbuf_list_free(remotes);\n+\tstring_list_clear(&remote_info, 0);\n }\n \n char *promisor_remote_reply(const char *info)\n-- \n2.51.0.168.gc8716bf361\n\n"},{"id":"525760","messageId":"20250908053056.956907-7-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250908053056.956907-1-christian.couder@gmail.com","subject":"[PATCH v8 6/7] promisor-remote: allow a client to check fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-09-08T05:30:52Z","receivedAt":"2025-09-08T05:31:23Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"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\nLet's make it possible for a client to check if these fields match\nwhat it expects before accepting a promisor remote.\n\nWe allow this by introducing a new \"promisor.checkFields\"\nconfiguration variable. It should contain a comma or space separated\nlist of fields that will be checked.\n\nBy limiting the protocol to specific well-defined fields, we ensure\nboth server and client have a shared understanding of field\nsemantics and usage.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/config/promisor.adoc    | 39 ++++++++++++\n promisor-remote.c                     | 89 ++++++++++++++++++++++++---\n t/t5710-promisor-remote-capability.sh | 34 ++++++++++\n 3 files changed, 154 insertions(+), 8 deletions(-)\n\ndiff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc\nindex b4a72c2152..93e5e0d9b5 100644\n--- a/Documentation/config/promisor.adoc\n+++ b/Documentation/config/promisor.adoc\n@@ -50,3 +50,42 @@ promisor.acceptFromServer::\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+\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+\ttransmitted by a server correspond to the values of these\n+\tfields in its own configuration before accepting a promisor\n+\tremote. Currently, \"partialCloneFilter\" and \"token\" are the\n+\tonly supported field names.\n++\n+If one of these field names (e.g., \"token\") is being checked for an\n+advertised promisor remote (e.g., \"foo\"), three conditions must be met\n+for the check of this specific field to pass:\n++\n+1. The corresponding local configuration (e.g., `remote.foo.token`)\n+   must be set.\n+2. The server must advertise the \"token\" field for remote \"foo\".\n+3. The value of the locally configured `remote.foo.token` must exactly\n+   match the value advertised by the server for the \"token\" field.\n++\n+If any of these conditions is not met for any field name listed in\n+`promisor.checkFields`, the advertised remote \"foo\" is rejected.\n++\n+For the \"partialCloneFilter\" field, this allows the client to ensure\n+that the server's filter matches what it expects locally, preventing\n+inconsistencies in filtering behavior. For the \"token\" field, this can\n+be used to verify that authentication credentials match expected\n+values.\n++\n+Field values are compared case-sensitively.\n++\n+The \"name\" and \"url\" fields are always checked according to the\n+`promisor.acceptFromServer` policy, independently of this setting.\n++\n+The field names and values should be passed by the server through the\n+\"promisor-remote\" capability by using the `promisor.sendFields` config\n+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].\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex afec0d081d..a6cfade223 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -389,6 +389,20 @@ static struct string_list *fields_sent(void)\n \treturn &fields_list;\n }\n \n+static struct string_list *fields_checked(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.checkFields\");\n+\t\tinitialized = 1;\n+\t}\n+\n+\treturn &fields_list;\n+}\n+\n /*\n  * Struct for promisor remotes involved in the \"promisor-remote\"\n  * protocol capability.\n@@ -534,6 +548,61 @@ enum accept_promisor {\n \tACCEPT_ALL\n };\n \n+static int match_field_against_config(const char *field, const char *value,\n+\t\t\t\t      struct promisor_info *config_info)\n+{\n+\tif (config_info->filter && !strcasecmp(field, promisor_field_filter))\n+\t\treturn !strcmp(config_info->filter, value);\n+\telse if (config_info->token && !strcasecmp(field, promisor_field_token))\n+\t\treturn !strcmp(config_info->token, value);\n+\n+\treturn 0;\n+}\n+\n+static int all_fields_match(struct promisor_info *advertised,\n+\t\t\t    struct string_list *config_info,\n+\t\t\t    int in_list)\n+{\n+\tstruct string_list *fields = fields_checked();\n+\tstruct string_list_item *item_checked;\n+\n+\tfor_each_string_list_item(item_checked, fields) {\n+\t\tint match = 0;\n+\t\tconst char *field = item_checked->string;\n+\t\tconst char *value = NULL;\n+\t\tstruct string_list_item *item;\n+\n+\t\tif (!strcasecmp(field, promisor_field_filter))\n+\t\t\tvalue = advertised->filter;\n+\t\telse if (!strcasecmp(field, promisor_field_token))\n+\t\t\tvalue = advertised->token;\n+\n+\t\tif (!value)\n+\t\t\treturn 0;\n+\n+\t\tif (in_list) {\n+\t\t\tfor_each_string_list_item(item, config_info) {\n+\t\t\t\tstruct promisor_info *p = item->util;\n+\t\t\t\tif (match_field_against_config(field, value, p)) {\n+\t\t\t\t\tmatch = 1;\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t}\n+\t\t} else {\n+\t\t\titem = string_list_lookup(config_info, advertised->name);\n+\t\t\tif (item) {\n+\t\t\t\tstruct promisor_info *p = item->util;\n+\t\t\t\tmatch = match_field_against_config(field, value, p);\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (!match)\n+\t\t\treturn 0;\n+\t}\n+\n+\treturn 1;\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@@ -544,7 +613,7 @@ static int should_accept_remote(enum accept_promisor accept,\n \tconst char *remote_url = advertised->url;\n \n \tif (accept == ACCEPT_ALL)\n-\t\treturn 1;\n+\t\treturn all_fields_match(advertised, config_info, 1);\n \n \t/* Get config info for that promisor remote */\n \titem = string_list_lookup(config_info, remote_name);\n@@ -556,7 +625,7 @@ static int should_accept_remote(enum accept_promisor accept,\n \tp = item->util;\n \n \tif (accept == ACCEPT_KNOWN_NAME)\n-\t\treturn 1;\n+\t\treturn all_fields_match(advertised, config_info, 0);\n \n \tif (accept != ACCEPT_KNOWN_URL)\n \t\tBUG(\"Unhandled 'enum accept_promisor' value '%d'\", accept);\n@@ -567,7 +636,7 @@ static int should_accept_remote(enum accept_promisor accept,\n \t}\n \n \tif (!strcmp(p->url, remote_url))\n-\t\treturn 1;\n+\t\treturn all_fields_match(advertised, config_info, 0);\n \n \twarning(_(\"known remote named '%s' but with URL '%s' instead of '%s'\"),\n \t\tremote_name, p->url, remote_url);\n@@ -605,6 +674,10 @@ static struct promisor_info *parse_one_advertised_remote(const char *remote_info\n \t\t\tinfo->name = url_percent_decode(p);\n \t\telse if (skip_field_name_prefix(elem, promisor_field_url, &p))\n \t\t\tinfo->url = url_percent_decode(p);\n+\t\telse if (skip_field_name_prefix(elem, promisor_field_filter, &p))\n+\t\t\tinfo->filter = url_percent_decode(p);\n+\t\telse if (skip_field_name_prefix(elem, promisor_field_token, &p))\n+\t\t\tinfo->token = url_percent_decode(p);\n \t}\n \n \tstring_list_clear(&elem_list, 0);\n@@ -646,11 +719,6 @@ static void filter_promisor_remote(struct repository *repo,\n \tif (accept == ACCEPT_NONE)\n \t\treturn;\n \n-\tif (accept != ACCEPT_ALL) {\n-\t\tpromisor_config_info_list(repo, &config_info, NULL);\n-\t\tstring_list_sort(&config_info);\n-\t}\n-\n \t/* Parse remote info received */\n \n \tstring_list_split(&remote_info, info, \";\", -1);\n@@ -663,6 +731,11 @@ static void filter_promisor_remote(struct repository *repo,\n \t\tif (!advertised)\n \t\t\tcontinue;\n \n+\t\tif (!config_info.nr) {\n+\t\t\tpromisor_config_info_list(repo, &config_info, fields_checked());\n+\t\t\tstring_list_sort(&config_info);\n+\t\t}\n+\n \t\tif (should_accept_remote(accept, advertised, &config_info))\n \t\t\tstrvec_push(accepted, advertised->name);\n \ndiff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh\nindex 204528b2e0..023735d6a8 100755\n--- a/t/t5710-promisor-remote-capability.sh\n+++ b/t/t5710-promisor-remote-capability.sh\n@@ -326,6 +326,40 @@ test_expect_success \"clone with promisor.sendFields\" '\n \tcheck_missing_objects server 1 \"$oid\"\n '\n \n+test_expect_success \"clone with promisor.checkFields\" '\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+\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.partialCloneFilter=\"blob:none\" \\\n+\t\t-c promisor.acceptfromserver=All \\\n+\t\t-c promisor.checkFields=partialcloneFilter \\\n+\t\t--no-local --filter=\"blob:limit=5k\" server client &&\n+\n+\t# Check that fields are properly transmitted\n+\tENCODED_URL=$(echo \"file://$(pwd)/lop\" | sed -e \"s/ /%20/g\") &&\n+\tPR1=\"name=lop,url=$ENCODED_URL,partialCloneFilter=blob:none\" &&\n+\tPR2=\"name=otherLop,url=https://invalid.invalid,partialCloneFilter=blob:limit=10k,token=fooBar\" &&\n+\ttest_grep \"clone< promisor-remote=$PR1;$PR2\" trace &&\n+\ttest_grep \"clone> promisor-remote=lop\" trace &&\n+\ttest_grep ! \"clone> promisor-remote=lop;otherLop\" 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 \n-- \n2.51.0.168.gc8716bf361\n\n"},{"id":"525761","messageId":"20250908053056.956907-8-christian.couder@gmail.com","threadId":"63293","inReplyTo":"20250908053056.956907-1-christian.couder@gmail.com","subject":"[PATCH v8 7/7] promisor-remote: use string_list_split() in mark_remotes_as_accepted()","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-09-08T05:30:53Z","receivedAt":"2025-09-08T05:31:24Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"Previous commits replaced some strbuf_split*() calls with calls to\nstring_list_split*() in \"promisor-remote.c\".\n\nFor consistency, let's also replace the strbuf_split_str() call in\nmark_remotes_as_accepted() with a call to string_list_split(), as we\ndon't need the splitted strings to be managed by a `struct strbuf`.\nUsing the lighter-weight `string_list` API is enough for our needs.\n\nWhile at it let's remove a useless call to `strbuf_strip_suffix()`.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n promisor-remote.c | 15 +++++++--------\n 1 file changed, 7 insertions(+), 8 deletions(-)\n\ndiff --git a/promisor-remote.c b/promisor-remote.c\nindex a6cfade223..77ebf537e2 100644\n--- a/promisor-remote.c\n+++ b/promisor-remote.c\n@@ -769,16 +769,15 @@ char *promisor_remote_reply(const char *info)\n \n void mark_promisor_remotes_as_accepted(struct repository *r, const char *remotes)\n {\n-\tstruct strbuf **accepted_remotes = strbuf_split_str(remotes, ';', 0);\n+\tstruct string_list accepted_remotes = STRING_LIST_INIT_DUP;\n+\tstruct string_list_item *item;\n \n-\tfor (size_t i = 0; accepted_remotes[i]; i++) {\n-\t\tstruct promisor_remote *p;\n-\t\tchar *decoded_remote;\n+\tstring_list_split(&accepted_remotes, remotes, \";\", -1);\n \n-\t\tstrbuf_strip_suffix(accepted_remotes[i], \";\");\n-\t\tdecoded_remote = url_percent_decode(accepted_remotes[i]->buf);\n+\tfor_each_string_list_item(item, &accepted_remotes) {\n+\t\tchar *decoded_remote = url_percent_decode(item->string);\n+\t\tstruct promisor_remote *p = repo_promisor_remote_find(r, decoded_remote);\n \n-\t\tp = repo_promisor_remote_find(r, decoded_remote);\n \t\tif (p)\n \t\t\tp->accepted = 1;\n \t\telse\n@@ -788,5 +787,5 @@ void mark_promisor_remotes_as_accepted(struct repository *r, const char *remotes\n \t\tfree(decoded_remote);\n \t}\n \n-\tstrbuf_list_free(accepted_remotes);\n+\tstring_list_clear(&accepted_remotes, 0);\n }\n-- \n2.51.0.168.gc8716bf361\n\n"},{"id":"525762","messageId":"CAP8UFD2XBwxH-2+qxWMq7Q28iGOoJJyMrNAb5zTe2B1bZ+6tkg@mail.gmail.com","threadId":"63293","inReplyTo":"xmqqh5yss5xx.fsf@gitster.g","subject":"Re: [PATCH v7 3/5] promisor-remote: refactor how we parse advertised fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-09-08T05:31:32Z","receivedAt":"2025-09-08T05:31:46Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Thu, Jul 31, 2025 at 6:04 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Christian Couder <christian.couder@gmail.com> writes:\n>\n> > +static struct promisor_info *parse_one_advertised_remote(struct strbuf *remote_info)\n> > +{\n> > +     struct promisor_info *info = xcalloc(1, sizeof(*info));\n> > +     struct string_list elem_list = STRING_LIST_INIT_NODUP;\n> > +     struct string_list_item *item;\n> > +\n> > +     string_list_split_in_place(&elem_list, remote_info->buf, \",\", -1);\n>\n> This munges a strbuf pointed by a structure that was supplied by the\n> caller as a parameter to this function.  Most notably, all commas in\n> remote_info->buf are replaced with NULs.\n\nYeah, this is bad. This is fixed in the v8 by using\nstring_list_split() instead of string_list_split_in_place(). See\nbelow.\n\n> A quick read of the sole caller of this function reveals that the\n> caller passes one element of an array of strbuf it obtained by\n> calling strbuf_split_str() for the only purpose of calling this\n> function, and the caller never looks at the .buf member itself, so I\n> think this munging with _in_place() would not hurt the caller.\n>\n> > +     for_each_string_list_item(item, &elem_list) {\n> > +             char *elem = item->string;\n> > +             char *value;\n> > +             char *p = strchr(elem, '=');\n> > +\n> > +             if (!p) {\n> > +                     warning(_(\"invalid element '%s' from remote info\"), elem);\n> > +                     continue;\n> > +             }\n>\n> We find the first '=' and ...\n>\n> > +             *p = '\\0';\n>\n> ... replace it with NUL; the item->string here is now split into elem & value.\n>\n> > +             value = url_percent_decode(p + 1);\n>\n> And the value gets decoded.\n>\n> > +             if (!strcmp(elem, \"name\"))\n> > +                     info->name = value;\n> > +             else if (!strcmp(elem, \"url\"))\n> > +                     info->url = value;\n> > +             else\n> > +                     free(value);\n> > +     }\n> > +\n> > +     string_list_clear(&elem_list, 0);\n>\n> And we are done with the string list that holds the pieces of remote\n> info split out.\n>\n> > +     if (!info->name || !info->url) {\n> > +             warning(_(\"server advertised a promisor remote without a name or URL: %s\"),\n> > +                     remote_info->buf);\n>\n> But this use of remote_info->buf will no longer give us much useful\n> information.  It might say something like \"name\", and the warning\n> would not let us see what the rest of the remote_info->buf used to\n> have before _in_place splitting.\n>\n> I think initializing elem_list with INIT_DUP and using string_list_split()\n> without _in_place should be sufficient to fix this.\n\nYeah, right, this solution is used in v8. Thanks.\n\n> > @@ -604,32 +651,19 @@ static void filter_promisor_remote(struct repository *repo,\n> >       remotes = strbuf_split_str(info, ';', 0);\n>\n> If we splitted this into a string list, then each item in it\n> will not have the terminating ';' at its end, therefore ...\n>\n> >       for (size_t i = 0; remotes[i]; i++) {\n> > -             struct strbuf **elems;\n> > -             const char *remote_name = NULL;\n> > -             const char *remote_url = NULL;\n> > -             char *decoded_name = NULL;\n> > -             char *decoded_url = NULL;\n> > +             struct promisor_info *advertised;\n> >\n> >               strbuf_strip_suffix(remotes[i], \";\");\n>\n> ... this strip_suffix() will become unnecessary.\n>\n> > +             advertised = parse_one_advertised_remote(remotes[i]);\n>\n> Such a change would require this call to be made with const char *\n> not struct strbuf * as a parameter, but the callee we just saw above\n> uses its parameter (i.e. remote_info) only to look at the .buf member,\n> and does not take any advantage of it being a strbuf (like it can be\n> trimmed or its length is known without running strlen(.buf) on it),\n> so that should be an easy conversion.\n\nYeah, right, all these suggestions (splitting into a string list,\nremoving the strbuf_strip_suffix() call and passing a const char * to\nparse_one_advertised_remote()) are implemented in v8. Thanks.\n\nWhile at it, in v8, all the calls to strbuf_split*() functions left in\n\"promisor-remote.c\" were replaced with calls to string_list_split*()\nfunctions.\n"},{"id":"525763","messageId":"CAP8UFD0RHf9W3YCHYEtv5t1cnPe=CmfT52zh_898sCErEtiaKg@mail.gmail.com","threadId":"63293","inReplyTo":"xmqqqzwvqay9.fsf@gitster.g","subject":"Re: [PATCH v7 0/5] Make the \"promisor-remote\" capability support more fields","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-09-08T05:36:04Z","receivedAt":"2025-09-08T05:36:18Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Fri, Aug 29, 2025 at 1:32 AM Junio C Hamano <gitster@pobox.com> wrote:\n\n> As I do not want to keep an inactive topic in 'seen' for more than a\n> month, I was doing my usual \"sweep\" of the topics, and found this\n> one.\n>\n> I think I gave a review on one step that pointed out a few problems\n> with an outline for a possible solution, but I did not see anybody\n> else reviewing, and nothing happened since the end of last month.\n>\n> Since the summer is a slow season, I do not mind keeping it for a\n> few more weeks in 'seen', but I can simply discard the one I have,\n> and requeue a new version in 'seen' when it materializes.\n\nSorry for the late reply on this series. I had a long vacation in\nAugust indeed. In such cases feel free to discard my topics without\nasking.\n\nNow I have just sent a v8, and I hope I will be more (re)active on this.\n"},{"id":"525867","messageId":"xmqqqzwgx2zu.fsf@gitster.g","threadId":"63293","inReplyTo":"20250908053056.956907-1-christian.couder@gmail.com","subject":"Re: [PATCH v8 0/7] Make the \"promisor-remote\" capability support more fields","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-09-08T17:34:13Z","receivedAt":"2025-09-08T17:34:17Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> Changes since v7\n> ----------------\n> ...\n\nThanks for a detailed write-up.\n\nWill queue.  From a cursory look, these all looked sensible.\n"}]}