aboutsummaryrefslogtreecommitdiffstats
path: root/promisor-remote.c
diff options
authorChristian Couder <christian.couder@gmail.com>2025-02-18 12:32:02 +0100
committerJunio C Hamano <gitster@pobox.com>2025-02-18 11:05:37 -0800
commitd460267613da14eba959eb225e2cbf6a1e132eb1 (patch)
treee999cf3689249f8931e6ee63a7fc6feb47ff11f2 /promisor-remote.c
parent03944513488db4a81fdb4c21c3b515e4cb260b05 (diff)
downloadgit-d460267613da14eba959eb225e2cbf6a1e132eb1.tar.gz
Add 'promisor-remote' capability to protocol v2
When a server S knows that some objects from a repository are available from a promisor remote X, S might want to suggest to a client C cloning or fetching the repo from S that C may use X directly instead of S for these objects. Note that this could happen both in the case S itself doesn't have the objects and borrows them from X, and in the case S has the objects but knows that X is better connected to the world (e.g., it is in a $LARGEINTERNETCOMPANY datacenter with petabit/s backbone connections) than S. Implementation of the latter case, which would require S to omit in its response the objects available on X, is left for future improvement though. Then C might or might not, want to get the objects from X. If S and C can agree on C using X directly, S can then omit objects that can be obtained from X when answering C's request. To allow S and C to agree and let each other know about C using X or not, let's introduce a new "promisor-remote" capability in the protocol v2, as well as a few new configuration variables: - "promisor.advertise" on the server side, and: - "promisor.acceptFromServer" on the client side. By default, or if "promisor.advertise" is set to 'false', a server S will not advertise the "promisor-remote" capability. If S doesn't advertise the "promisor-remote" capability, then a client C replying to S shouldn't advertise the "promisor-remote" capability either. If "promisor.advertise" is set to 'true', S will advertise its promisor remotes with a string like: promisor-remote=<pr-info>[;<pr-info>]... where each <pr-info> element contains information about a single promisor remote in the form: name=<pr-name>[,url=<pr-url>] where <pr-name> is the urlencoded name of a promisor remote and <pr-url> is the urlencoded URL of the promisor remote named <pr-name>. For now, the URL is passed in addition to the name. In the future, it might be possible to pass other information like a filter-spec that the client may use when cloning from S, or a token that the client may use when retrieving objects from X. It is C's responsibility to arrange how it can reach X though, so pieces of information that are usually outside Git's concern, like proxy configuration, must not be distributed over this protocol. It might also be possible in the future for "promisor.advertise" to have other values. For example a value like "onlyName" could prevent S from advertising URLs, which could help in case C should use a different URL for X than the URL S is using. (The URL S is using might be an internal one on the server side for example.) By default or if "promisor.acceptFromServer" is set to "None", C will not accept to use the promisor remotes that might have been advertised by S. In this case, C will not advertise any "promisor-remote" capability in its reply to S. If "promisor.acceptFromServer" is set to "All" and S advertised some promisor remotes, then on the contrary, C will accept to use all the promisor remotes that S advertised and C will reply with a string like: promisor-remote=<pr-name>[;<pr-name>]... where the <pr-name> elements are the urlencoded names of all the promisor remotes S advertised. In a following commit, other values for "promisor.acceptFromServer" will be implemented, so that C will be able to decide the promisor remotes it accepts depending on the name and URL it received from S. So even if that name and URL information is not used much right now, it will be needed soon. Helped-by: Taylor Blau <me@ttaylorr.com> Helped-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Christian Couder <chriscool@tuxfamily.org> Signed-off-by: Junio C Hamano <gitster@pobox.com>
Diffstat (limited to 'promisor-remote.c')
-rw-r--r--promisor-remote.c194
1 files changed, 194 insertions, 0 deletions
diff --git a/promisor-remote.c b/promisor-remote.c
index c714f4f007..918be6528f 100644
--- a/promisor-remote.c
+++ b/promisor-remote.c
@@ -11,6 +11,8 @@
#include "strvec.h"
#include "packfile.h"
#include "environment.h"
+#include "url.h"
+#include "version.h"
struct promisor_remote_config {
struct promisor_remote *promisors;
@@ -221,6 +223,18 @@ int repo_has_promisor_remote(struct repository *r)
return !!repo_promisor_remote_find(r, NULL);
}
+int repo_has_accepted_promisor_remote(struct repository *r)
+{
+ struct promisor_remote *p;
+
+ promisor_remote_init(r);
+
+ for (p = r->promisor_remote_config->promisors; p; p = p->next)
+ if (p->accepted)
+ return 1;
+ return 0;
+}
+
static int remove_fetched_oids(struct repository *repo,
struct object_id **oids,
int oid_nr, int to_free)
@@ -292,3 +306,183 @@ all_fetched:
if (to_free)
free(remaining_oids);
}
+
+static int allow_unsanitized(char ch)
+{
+ if (ch == ',' || ch == ';' || ch == '%')
+ return 0;
+ return ch > 32 && ch < 127;
+}
+
+static void promisor_info_vecs(struct repository *repo,
+ struct strvec *names,
+ struct strvec *urls)
+{
+ struct promisor_remote *r;
+
+ promisor_remote_init(repo);
+
+ for (r = repo->promisor_remote_config->promisors; r; r = r->next) {
+ char *url;
+ char *url_key = xstrfmt("remote.%s.url", r->name);
+
+ strvec_push(names, r->name);
+ strvec_push(urls, git_config_get_string(url_key, &url) ? NULL : url);
+
+ free(url);
+ free(url_key);
+ }
+}
+
+char *promisor_remote_info(struct repository *repo)
+{
+ struct strbuf sb = STRBUF_INIT;
+ int advertise_promisors = 0;
+ struct strvec names = STRVEC_INIT;
+ struct strvec urls = STRVEC_INIT;
+
+ git_config_get_bool("promisor.advertise", &advertise_promisors);
+
+ if (!advertise_promisors)
+ return NULL;
+
+ promisor_info_vecs(repo, &names, &urls);
+
+ if (!names.nr)
+ return NULL;
+
+ for (size_t i = 0; i < names.nr; i++) {
+ if (i)
+ strbuf_addch(&sb, ';');
+ strbuf_addstr(&sb, "name=");
+ strbuf_addstr_urlencode(&sb, names.v[i], allow_unsanitized);
+ if (urls.v[i]) {
+ strbuf_addstr(&sb, ",url=");
+ strbuf_addstr_urlencode(&sb, urls.v[i], allow_unsanitized);
+ }
+ }
+
+ strvec_clear(&names);
+ strvec_clear(&urls);
+
+ return strbuf_detach(&sb, NULL);
+}
+
+enum accept_promisor {
+ ACCEPT_NONE = 0,
+ ACCEPT_ALL
+};
+
+static int should_accept_remote(enum accept_promisor accept,
+ const char *remote_name UNUSED,
+ const char *remote_url UNUSED)
+{
+ if (accept == ACCEPT_ALL)
+ return 1;
+
+ BUG("Unhandled 'enum accept_promisor' value '%d'", accept);
+}
+
+static void filter_promisor_remote(struct strvec *accepted, const char *info)
+{
+ struct strbuf **remotes;
+ const char *accept_str;
+ enum accept_promisor accept = ACCEPT_NONE;
+
+ if (!git_config_get_string_tmp("promisor.acceptfromserver", &accept_str)) {
+ if (!*accept_str || !strcasecmp("None", accept_str))
+ accept = ACCEPT_NONE;
+ else if (!strcasecmp("All", accept_str))
+ accept = ACCEPT_ALL;
+ else
+ warning(_("unknown '%s' value for '%s' config option"),
+ accept_str, "promisor.acceptfromserver");
+ }
+
+ if (accept == ACCEPT_NONE)
+ return;
+
+ /* Parse remote info received */
+
+ remotes = strbuf_split_str(info, ';', 0);
+
+ for (size_t i = 0; remotes[i]; i++) {
+ struct strbuf **elems;
+ const char *remote_name = NULL;
+ const char *remote_url = NULL;
+ char *decoded_name = NULL;
+ char *decoded_url = NULL;
+
+ strbuf_strip_suffix(remotes[i], ";");
+ elems = strbuf_split(remotes[i], ',');
+
+ for (size_t j = 0; elems[j]; j++) {
+ int res;
+ strbuf_strip_suffix(elems[j], ",");
+ res = skip_prefix(elems[j]->buf, "name=", &remote_name) ||
+ skip_prefix(elems[j]->buf, "url=", &remote_url);
+ if (!res)
+ warning(_("unknown element '%s' from remote info"),
+ elems[j]->buf);
+ }
+
+ if (remote_name)
+ decoded_name = url_percent_decode(remote_name);
+ if (remote_url)
+ decoded_url = url_percent_decode(remote_url);
+
+ if (decoded_name && should_accept_remote(accept, decoded_name, decoded_url))
+ strvec_push(accepted, decoded_name);
+
+ strbuf_list_free(elems);
+ free(decoded_name);
+ free(decoded_url);
+ }
+
+ strbuf_list_free(remotes);
+}
+
+char *promisor_remote_reply(const char *info)
+{
+ struct strvec accepted = STRVEC_INIT;
+ struct strbuf reply = STRBUF_INIT;
+
+ filter_promisor_remote(&accepted, info);
+
+ if (!accepted.nr)
+ return NULL;
+
+ for (size_t i = 0; i < accepted.nr; i++) {
+ if (i)
+ strbuf_addch(&reply, ';');
+ strbuf_addstr_urlencode(&reply, accepted.v[i], allow_unsanitized);
+ }
+
+ strvec_clear(&accepted);
+
+ return strbuf_detach(&reply, NULL);
+}
+
+void mark_promisor_remotes_as_accepted(struct repository *r, const char *remotes)
+{
+ struct strbuf **accepted_remotes = strbuf_split_str(remotes, ';', 0);
+
+ for (size_t i = 0; accepted_remotes[i]; i++) {
+ struct promisor_remote *p;
+ char *decoded_remote;
+
+ strbuf_strip_suffix(accepted_remotes[i], ";");
+ decoded_remote = url_percent_decode(accepted_remotes[i]->buf);
+
+ p = repo_promisor_remote_find(r, decoded_remote);
+ if (p)
+ p->accepted = 1;
+ else
+ warning(_("accepted promisor remote '%s' not found"),
+ decoded_remote);
+
+ free(decoded_remote);
+ }
+
+ strbuf_list_free(accepted_remotes);
+}