From 6967171b8f039ed926762c81de0096976832d2ac Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 26 Apr 2021 22:08:18 +0200 Subject: [PATCH 01/13] scalar: implement a minimal JSON parser No grown-up C project comes without their own JSON parser. Just kidding! We need to parse a JSON result when determining which cache server to use. It would appear that searching for needles `"CacheServers":[`, `"Url":"` and `"GlobalDefault":true` _happens_ to work right now, it is fragile as it depends on no whitespace padding and on the order of the fields remaining as-is. Let's implement a super simple JSON parser (at the cost of being slightly inefficient) for that purpose. To avoid allocating a ton of memory, we implement a callback-based one. And to save on complexity, let's not even bother validating the input properly (we will just go ahead and instead rely on Azure Repos to produce correct JSON). Note: An alternative would have been to use existing solutions such as JSON-C, CentiJSON or JSMN. However, they are all a lot larger than the current solution; The smallest, JSMN, which does not even provide parsed string values (something we actually need) weighs in with 471 lines, while we get away with 182 + 29 lines for the C and the header file, respectively. Signed-off-by: Johannes Schindelin --- Makefile | 2 +- contrib/scalar/Makefile | 4 +- contrib/scalar/json-parser.c | 182 +++++++++++++++++++++++++++++++++++ contrib/scalar/json-parser.h | 29 ++++++ 4 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 contrib/scalar/json-parser.c create mode 100644 contrib/scalar/json-parser.h diff --git a/Makefile b/Makefile index be4f5e6dab3c89..cf1380dca19763 100644 --- a/Makefile +++ b/Makefile @@ -2541,7 +2541,7 @@ ifndef NO_CURL OBJECTS += http.o http-walker.o remote-curl.o endif -SCALAR_SOURCES := contrib/scalar/scalar.c +SCALAR_SOURCES := contrib/scalar/scalar.c contrib/scalar/json-parser.c SCALAR_OBJECTS := $(SCALAR_SOURCES:c=o) OBJECTS += $(SCALAR_OBJECTS) diff --git a/contrib/scalar/Makefile b/contrib/scalar/Makefile index 089d9e301d9faf..ce2b5a42aaf409 100644 --- a/contrib/scalar/Makefile +++ b/contrib/scalar/Makefile @@ -19,7 +19,7 @@ include ../../config.mak.uname -include ../../config.mak.autogen -include ../../config.mak -TARGETS = scalar$(X) scalar.o +TARGETS = scalar$(X) scalar.o json-parser.o GITLIBS = ../../common-main.o ../../libgit.a ../../xdiff/lib.a all: scalar$(X) ../../bin-wrappers/scalar @@ -27,7 +27,7 @@ all: scalar$(X) ../../bin-wrappers/scalar $(GITLIBS): $(QUIET_SUBDIR0)../.. $(QUIET_SUBDIR1) $(subst ../../,,$@) -$(TARGETS): $(GITLIBS) scalar.c +$(TARGETS): $(GITLIBS) scalar.c json-parser.c json-parser.h $(QUIET_SUBDIR0)../.. $(QUIET_SUBDIR1) $(patsubst %,contrib/scalar/%,$@) clean: diff --git a/contrib/scalar/json-parser.c b/contrib/scalar/json-parser.c new file mode 100644 index 00000000000000..30799e17dc0a04 --- /dev/null +++ b/contrib/scalar/json-parser.c @@ -0,0 +1,182 @@ +#include "cache.h" +#include "json-parser.h" + +static int reset_iterator(struct json_iterator *it) +{ + it->p = it->begin = it->json; + strbuf_release(&it->key); + strbuf_release(&it->string_value); + it->type = JSON_NULL; + return -1; +} + +static int parse_json_string(struct json_iterator *it, struct strbuf *out) +{ + const char *begin = it->p; + + if (*(it->p)++ != '"') + return error("expected double quote: '%.*s'", 5, begin), + reset_iterator(it); + + strbuf_reset(&it->string_value); +#define APPEND(c) strbuf_addch(out, c) + while (*it->p != '"') { + switch (*it->p) { + case '\0': + return error("incomplete string: '%s'", begin), + reset_iterator(it); + case '\\': + it->p++; + if (*it->p == '\\' || *it->p == '"') + APPEND(*it->p); + else if (*it->p == 'b') + APPEND(8); + else if (*it->p == 't') + APPEND(9); + else if (*it->p == 'n') + APPEND(10); + else if (*it->p == 'f') + APPEND(12); + else if (*it->p == 'r') + APPEND(13); + else if (*it->p == 'u') { + unsigned char binary[2]; + int i; + + if (hex_to_bytes(binary, it->p + 1, 2) < 0) + return error("invalid: '%.*s'", + 6, it->p - 1), + reset_iterator(it); + it->p += 4; + + i = (binary[0] << 8) | binary[1]; + if (i < 0x80) + APPEND(i); + else if (i < 0x0800) { + APPEND(0xc0 | ((i >> 6) & 0x1f)); + APPEND(0x80 | (i & 0x3f)); + } else if (i < 0x10000) { + APPEND(0xe0 | ((i >> 12) & 0x0f)); + APPEND(0x80 | ((i >> 6) & 0x3f)); + APPEND(0x80 | (i & 0x3f)); + } else { + APPEND(0xf0 | ((i >> 18) & 0x07)); + APPEND(0x80 | ((i >> 12) & 0x3f)); + APPEND(0x80 | ((i >> 6) & 0x3f)); + APPEND(0x80 | (i & 0x3f)); + } + } + break; + default: + APPEND(*it->p); + } + it->p++; + } + + it->end = it->p++; + return 0; +} + +static void skip_whitespace(struct json_iterator *it) +{ + while (isspace(*it->p)) + it->p++; +} + +int iterate_json(struct json_iterator *it) +{ + skip_whitespace(it); + it->begin = it->p; + + switch (*it->p) { + case '\0': + return reset_iterator(it), 0; + case 'n': + if (!starts_with(it->p, "null")) + return error("unexpected value: %.*s", 4, it->p), + reset_iterator(it); + it->type = JSON_NULL; + it->end = it->p = it->begin + 4; + break; + case 't': + if (!starts_with(it->p, "true")) + return error("unexpected value: %.*s", 4, it->p), + reset_iterator(it); + it->type = JSON_TRUE; + it->end = it->p = it->begin + 4; + break; + case 'f': + if (!starts_with(it->p, "false")) + return error("unexpected value: %.*s", 5, it->p), + reset_iterator(it); + it->type = JSON_FALSE; + it->end = it->p = it->begin + 5; + break; + case '-': case '.': + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + it->type = JSON_NUMBER; + it->end = it->p = it->begin + strspn(it->p, "-.0123456789"); + break; + case '"': + it->type = JSON_STRING; + if (parse_json_string(it, &it->string_value) < 0) + return -1; + break; + case '[': { + const char *save = it->begin; + size_t key_offset = it->key.len; + int i = 0, res; + + for (it->p++, skip_whitespace(it); *it->p != ']'; i++) { + strbuf_addf(&it->key, "[%d]", i); + + if ((res = iterate_json(it))) + return reset_iterator(it), res; + strbuf_setlen(&it->key, key_offset); + + skip_whitespace(it); + if (*it->p == ',') + it->p++; + } + + it->type = JSON_ARRAY; + it->begin = save; + it->end = it->p; + it->p++; + break; + } + case '{': { + const char *save = it->begin; + size_t key_offset = it->key.len; + int res; + + strbuf_addch(&it->key, '.'); + for (it->p++, skip_whitespace(it); *it->p != '}'; ) { + strbuf_setlen(&it->key, key_offset + 1); + if (parse_json_string(it, &it->key) < 0) + return -1; + skip_whitespace(it); + if (*(it->p)++ != ':') + return error("expected colon: %.*s", 5, it->p), + reset_iterator(it); + + if ((res = iterate_json(it))) + return res; + + skip_whitespace(it); + if (*it->p == ',') + it->p++; + } + strbuf_setlen(&it->key, key_offset); + + it->type = JSON_OBJECT; + it->begin = save; + it->end = it->p; + it->p++; + break; + } + } + + return it->fn(it); +} diff --git a/contrib/scalar/json-parser.h b/contrib/scalar/json-parser.h new file mode 100644 index 00000000000000..ce1fdc5ee23928 --- /dev/null +++ b/contrib/scalar/json-parser.h @@ -0,0 +1,29 @@ +#ifndef JSON_PARSER_H +#define JSON_PARSER_H + +#include "strbuf.h" + +struct json_iterator { + const char *json, *p, *begin, *end; + struct strbuf key, string_value; + enum { + JSON_NULL = 0, + JSON_FALSE, + JSON_TRUE, + JSON_NUMBER, + JSON_STRING, + JSON_ARRAY, + JSON_OBJECT + } type; + int (*fn)(struct json_iterator *it); + void *fn_data; +}; +#define JSON_ITERATOR_INIT(json_, fn_, fn_data_) { \ + .json = json_, .p = json_, \ + .key = STRBUF_INIT, .string_value = STRBUF_INIT, \ + .fn = fn_, .fn_data = fn_data_ \ +} + +int iterate_json(struct json_iterator *it); + +#endif From 46a4f62c5f49a93bd7d021cb1753356a298b54e3 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 16 Apr 2021 22:15:49 +0200 Subject: [PATCH 02/13] scalar clone: support GVFS-enabled remote repositories With this change, we come a big step closer to feature parity with Scalar: this allows cloning from Azure Repos (which do not support partial clones at time of writing). We use the just-implemented JSON parser to parse the response we got from the `gvfs/config` endpoint; Please note that this response might, or might not, contain information about a cache server. The presence or absence of said cache server, however, has nothing to do with the ability to speak the GVFS protocol (but the presence of the `gvfs/config` endpoint does that). An alternative considered during the development of this patch was to perform simple string matching instead of parsing the JSON-formatted data; However, this would have been fragile, as the response contains free-form text (e.g. the repository's description) which might contain parts that would confuse a simple string matcher (but not a proper JSON parser). Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 124 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 3 deletions(-) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index 43d7275ae5d77e..f8522c06a9cb13 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -13,6 +13,7 @@ #include "help.h" #include "simple-ipc.h" #include "fsmonitor-ipc.h" +#include "json-parser.h" /* * Remove the deepest subdirectory in the provided path string. Path must not @@ -522,6 +523,80 @@ static int set_config(const char *fmt, ...) return res; } +/* Find N for which .CacheServers[N].GlobalDefault == true */ +static int get_cache_server_index(struct json_iterator *it) +{ + const char *p; + char *q; + long l; + + if (it->type == JSON_TRUE && + skip_iprefix(it->key.buf, ".CacheServers[", &p) && + (l = strtol(p, &q, 10)) >= 0 && p != q && + !strcasecmp(q, "].GlobalDefault")) { + *(long *)it->fn_data = l; + return 1; + } + + return 0; +} + +struct cache_server_url_data { + char *key, *url; +}; + +/* Get .CacheServers[N].Url */ +static int get_cache_server_url(struct json_iterator *it) +{ + struct cache_server_url_data *data = it->fn_data; + + if (it->type == JSON_STRING && + !strcasecmp(data->key, it->key.buf)) { + data->url = strbuf_detach(&it->string_value, NULL); + return 1; + } + + return 0; +} + +/* + * If `cache_server_url` is `NULL`, print the list to `stdout`. + * + * Since `gvfs-helper` requires a Git directory, this _must_ be run in + * a worktree. + */ +static int supports_gvfs_protocol(const char *url, char **cache_server_url) +{ + struct child_process cp = CHILD_PROCESS_INIT; + struct strbuf out = STRBUF_INIT; + + cp.git_cmd = 1; + strvec_pushl(&cp.args, "gvfs-helper", "--remote", url, "config", NULL); + if (!pipe_command(&cp, NULL, 0, &out, 512, NULL, 0)) { + long l = 0; + struct json_iterator it = + JSON_ITERATOR_INIT(out.buf, get_cache_server_index, &l); + struct cache_server_url_data data = { .url = NULL }; + + if (iterate_json(&it) < 0) { + strbuf_release(&out); + return error("JSON parse error"); + } + data.key = xstrfmt(".CacheServers[%ld].Url", l); + it.fn = get_cache_server_url; + it.fn_data = &data; + if (iterate_json(&it) < 0) { + strbuf_release(&out); + return error("JSON parse error"); + } + *cache_server_url = data.url; + free(data.key); + return 1; + } + strbuf_release(&out); + return 0; /* error out quietly */ +} + static char *remote_default_branch(const char *url) { struct child_process cp = CHILD_PROCESS_INIT; @@ -613,6 +688,8 @@ static int cmd_clone(int argc, const char **argv) { const char *branch = NULL; int full_clone = 0, single_branch = 0; + const char *cache_server_url = NULL; + char *default_cache_server_url = NULL; struct option clone_options[] = { OPT_STRING('b', "branch", &branch, N_(""), N_("branch to checkout after clone")), @@ -621,6 +698,9 @@ static int cmd_clone(int argc, const char **argv) OPT_BOOL(0, "single-branch", &single_branch, N_("only download metadata for the branch that will " "be checked out")), + OPT_STRING(0, "cache-server-url", &cache_server_url, + N_(""), + N_("the url or friendly name of the cache server")), OPT_END(), }; const char * const clone_usage[] = { @@ -693,13 +773,44 @@ static int cmd_clone(int argc, const char **argv) set_config("remote.origin.fetch=" "+refs/heads/%s:refs/remotes/origin/%s", single_branch ? branch : "*", - single_branch ? branch : "*") || - set_config("remote.origin.promisor=true") || - set_config("remote.origin.partialCloneFilter=blob:none")) { + single_branch ? branch : "*")) { res = error(_("could not configure remote in '%s'"), dir); goto cleanup; } + if (set_config("credential.https://dev.azure.com.useHttpPath=true")) { + res = error(_("could not configure credential.useHttpPath")); + goto cleanup; + } + + if (cache_server_url || + supports_gvfs_protocol(url, &default_cache_server_url)) { + if (!cache_server_url) + cache_server_url = default_cache_server_url; + if (set_config("core.useGVFSHelper=true") || + set_config("core.gvfs=150") || + set_config("http.version=HTTP/1.1")) { + res = error(_("could not turn on GVFS helper")); + goto cleanup; + } + if (cache_server_url && + set_config("gvfs.cache-server=%s", cache_server_url)) { + res = error(_("could not configure cache server")); + goto cleanup; + } + if (cache_server_url) + fprintf(stderr, "Cache server URL: %s\n", + cache_server_url); + } else { + if (set_config("core.useGVFSHelper=false") || + set_config("remote.origin.promisor=true") || + set_config("remote.origin.partialCloneFilter=blob:none")) { + res = error(_("could not configure partial clone in " + "'%s'"), dir); + goto cleanup; + } + } + if (!full_clone && (res = run_git("sparse-checkout", "init", "--cone", NULL))) goto cleanup; @@ -738,6 +849,7 @@ static int cmd_clone(int argc, const char **argv) free(enlistment); free(dir); strbuf_release(&buf); + free(default_cache_server_url); return res; } @@ -841,6 +953,7 @@ static int cmd_diagnose(int argc, const char **argv) time_t now = time(NULL); struct tm tm; struct strbuf path = STRBUF_INIT, buf = STRBUF_INIT; + char *cache_server_url = NULL; int res = 0; argc = parse_options(argc, argv, NULL, options, @@ -864,6 +977,10 @@ static int cmd_diagnose(int argc, const char **argv) get_version_info(&buf, 1); strbuf_addf(&buf, "Enlistment root: %s\n", the_repository->worktree); + + git_config_get_string("gvfs.cache-server", &cache_server_url); + strbuf_addf(&buf, "Cache Server: %s\n\n", + cache_server_url ? cache_server_url : "None"); get_disk_info(&buf); fwrite(buf.buf, buf.len, 1, stdout); @@ -904,6 +1021,7 @@ static int cmd_diagnose(int argc, const char **argv) strbuf_release(&tmp_dir); strbuf_release(&path); strbuf_release(&buf); + free(cache_server_url); return res; } From bea642fc85d3f90d5f360e3bb0c5620d97a7b1a7 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 16 Apr 2021 19:47:05 +0200 Subject: [PATCH 03/13] test-gvfs-protocol: also serve smart protocol This comes in handy, as we want to verify that `scalar clone` also works against a GVFS-enabled remote repository. Note that we have to set `MSYS2_ENV_CONV_EXCL` to prevent MSYS2 from mangling `PATH_TRANSLATED`: The value _does_ look like a Unix-style path, but no, MSYS2 must not be allowed to convert that into a Windows path: `http-backend` needs it in the unmodified form. (The MSYS2 runtime comes in when `git` is run via `bin-wrappers/git`, which is a shell script.) Signed-off-by: Johannes Schindelin --- t/helper/test-gvfs-protocol.c | 49 +++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/t/helper/test-gvfs-protocol.c b/t/helper/test-gvfs-protocol.c index d5874780216e5d..5202c6c297c6e1 100644 --- a/t/helper/test-gvfs-protocol.c +++ b/t/helper/test-gvfs-protocol.c @@ -1480,6 +1480,8 @@ static enum worker_result req__read(struct req *req, int fd) static enum worker_result dispatch(struct req *req) { + static regex_t *smart_http_regex; + static int initialized; const char *method; enum worker_result wr; @@ -1528,6 +1530,53 @@ static enum worker_result dispatch(struct req *req) return do__gvfs_prefetch__get(req); } + if (!initialized) { + smart_http_regex = xmalloc(sizeof(*smart_http_regex)); + if (regcomp(smart_http_regex, "^/(HEAD|info/refs|" + "objects/info/[^/]+|git-(upload|receive)-pack)$", + REG_EXTENDED)) { + warning("could not compile smart HTTP regex"); + smart_http_regex = NULL; + } + initialized = 1; + } + + if (smart_http_regex && + !regexec(smart_http_regex, req->uri_base.buf, 0, NULL, 0)) { + const char *ok = "HTTP/1.1 200 OK\r\n"; + struct child_process cp = CHILD_PROCESS_INIT; + int i, res; + + if (write(1, ok, strlen(ok)) < 0) + return error(_("could not send '%s'"), ok); + + strvec_pushf(&cp.env_array, "REQUEST_METHOD=%s", method); + strvec_pushf(&cp.env_array, "PATH_TRANSLATED=%s", + req->uri_base.buf); + /* Prevent MSYS2 from "converting to a Windows path" */ + strvec_pushf(&cp.env_array, + "MSYS2_ENV_CONV_EXCL=PATH_TRANSLATED"); + strvec_push(&cp.env_array, "SERVER_PROTOCOL=HTTP/1.1"); + if (req->quest_args.len) + strvec_pushf(&cp.env_array, "QUERY_STRING=%s", + req->quest_args.buf); + for (i = 0; i < req->header_list.nr; i++) { + const char *header = req->header_list.items[i].string; + if (!strncasecmp("Content-Type: ", header, 14)) + strvec_pushf(&cp.env_array, "CONTENT_TYPE=%s", + header + 14); + else if (!strncasecmp("Content-Length: ", header, 16)) + strvec_pushf(&cp.env_array, "CONTENT_LENGTH=%s", + header + 16); + } + cp.git_cmd = 1; + strvec_push(&cp.args, "http-backend"); + res = run_command(&cp); + close(1); + close(0); + return !!res; + } + return send_http_error(1, 501, "Not Implemented", -1, WR_OK | WR_HANGUP); } From bf2753926e393713c26668d34d0a7b551e669630 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 26 Apr 2021 17:31:40 +0200 Subject: [PATCH 04/13] gvfs-helper: add the `endpoint` command We already have the `config` command that accesses the `gvfs/config` endpoint. To implement `scalar`, we also need to be able to access the `vsts/info` endpoint. Let's add a command to do precisely that. 2021-10-30: Compiler rules got more strict about using 'return' in a 'void' function. Signed-off-by: Johannes Schindelin Signed-off-by: Derrick Stolee --- gvfs-helper.c | 61 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/gvfs-helper.c b/gvfs-helper.c index eaf7f0439f933d..f6833f21a41892 100644 --- a/gvfs-helper.c +++ b/gvfs-helper.c @@ -202,6 +202,12 @@ // [2] Documentation/technical/long-running-process-protocol.txt // [3] See GIT_TRACE_PACKET // +// endpoint +// +// Fetch the given endpoint from the main Git server (specifying +// `gvfs/config` as endpoint is idempotent to the `config` +// command mentioned above). +// ////////////////////////////////////////////////////////////////// #include "cache.h" @@ -3108,18 +3114,20 @@ static void do_req__with_fallback(const char *url_component, * * Return server's response buffer. This is probably a raw JSON string. */ -static void do__http_get__gvfs_config(struct gh__response_status *status, - struct strbuf *config_data) +static void do__http_get__simple_endpoint(struct gh__response_status *status, + struct strbuf *response, + const char *endpoint, + const char *tr2_label) { struct gh__request_params params = GH__REQUEST_PARAMS_INIT; - strbuf_addstr(¶ms.tr2_label, "GET/config"); + strbuf_addstr(¶ms.tr2_label, tr2_label); params.b_is_post = 0; params.b_write_to_file = 0; /* cache-servers do not handle gvfs/config REST calls */ params.b_permit_cache_server_if_defined = 0; - params.buffer = config_data; + params.buffer = response; params.objects_mode = GH__OBJECTS_MODE__NONE; params.object_count = 1; /* a bit of a lie */ @@ -3141,15 +3149,22 @@ static void do__http_get__gvfs_config(struct gh__response_status *status, * see any need to report progress on the upload side of * the GET. So just report progress on the download side. */ - strbuf_addstr(¶ms.progress_base_phase3_msg, - "Receiving gvfs/config"); + strbuf_addf(¶ms.progress_base_phase3_msg, + "Receiving %s", endpoint); } - do_req__with_fallback("gvfs/config", ¶ms, status); + do_req__with_fallback(endpoint, ¶ms, status); gh__request_params__release(¶ms); } +static void do__http_get__gvfs_config(struct gh__response_status *status, + struct strbuf *config_data) +{ + do__http_get__simple_endpoint(status, config_data, "gvfs/config", + "GET/config"); +} + static void setup_gvfs_objects_progress(struct gh__request_params *params, unsigned long num, unsigned long den) { @@ -3594,6 +3609,35 @@ static enum gh__error_code do_sub_cmd__config(int argc, const char **argv) return ec; } +static enum gh__error_code do_sub_cmd__endpoint(int argc, const char **argv) +{ + struct gh__response_status status = GH__RESPONSE_STATUS_INIT; + struct strbuf data = STRBUF_INIT; + enum gh__error_code ec = GH__ERROR_CODE__OK; + const char *endpoint; + + if (argc != 2) + return GH__ERROR_CODE__ERROR; + endpoint = argv[1]; + + trace2_cmd_mode(endpoint); + + finish_init(0); + + do__http_get__simple_endpoint(&status, &data, endpoint, endpoint); + ec = status.ec; + + if (ec == GH__ERROR_CODE__OK) + printf("%s\n", data.buf); + else + error("config: %s", status.error_message.buf); + + gh__response_status__release(&status); + strbuf_release(&data); + + return ec; +} + /* * Read a list of objects from stdin and fetch them as a series of * single object HTTP GET requests. @@ -4085,6 +4129,9 @@ static enum gh__error_code do_sub_cmd(int argc, const char **argv) if (!strcmp(argv[0], "config")) return do_sub_cmd__config(argc, argv); + if (!strcmp(argv[0], "endpoint")) + return do_sub_cmd__endpoint(argc, argv); + if (!strcmp(argv[0], "prefetch")) return do_sub_cmd__prefetch(argc, argv); From f282428b169eea5d26138c1f67109d3cf900b64d Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 15 May 2021 00:04:20 +0200 Subject: [PATCH 05/13] dir_inside_of(): handle directory separators correctly On Windows, both the forward slash and the backslash are directory separators. Which means that `a\b\c` really is inside `a/b`. Therefore, we need to special-case the directory separators in the helper function `cmp_icase()` that is used in the loop in `dir_inside_of()`. Signed-off-by: Johannes Schindelin --- dir.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dir.c b/dir.c index 5306b930ad0957..f272be3fbeb986 100644 --- a/dir.c +++ b/dir.c @@ -3040,6 +3040,8 @@ static int cmp_icase(char a, char b) { if (a == b) return 0; + if (is_dir_sep(a)) + return is_dir_sep(b) ? 0 : -1; if (ignore_case) return toupper(a) - toupper(b); return a - b; From 2a080998a7fc5fb22731ad3760240514fde51088 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 6 May 2021 14:35:12 +0200 Subject: [PATCH 06/13] scalar: disable authentication in unattended mode Modified to remove call to is_unattended() that has not been implemented yet. Signed-off-by: Johannes Schindelin --- contrib/scalar/t/t9099-scalar.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/contrib/scalar/t/t9099-scalar.sh b/contrib/scalar/t/t9099-scalar.sh index 3566c4d83ddbde..9fef639a95e19e 100755 --- a/contrib/scalar/t/t9099-scalar.sh +++ b/contrib/scalar/t/t9099-scalar.sh @@ -13,6 +13,13 @@ PATH=$PWD/..:$PATH GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab ../cron.txt,launchctl:true,schtasks:true" export GIT_TEST_MAINT_SCHEDULER +# Do not write any files outside the trash directory +Scalar_UNATTENDED=1 +export Scalar_UNATTENDED + +GIT_ASKPASS=true +export GIT_ASKPASS + test_lazy_prereq BUILTIN_FSMONITOR ' git version --build-options | grep -q "feature:.*fsmonitor--daemon" ' From 29045b8d2eef42253590454c4aec0da54c3827a3 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 3 May 2021 23:41:28 +0200 Subject: [PATCH 07/13] scalar: do initialize `gvfs.sharedCache` This finalizes the port of the `QueryVstsInfo()` function: we already taught `gvfs-helper` to access the `vsts/info` endpoint on demand, we implemented proper JSON parsing, and now it is time to hook it all up. To that end, we also provide a default local cache root directory. It works the same way as the .NET version of Scalar: it uses C:\scalarCache on Windows, ~/.scalarCache/ on macOS and ~/.cache/scalar on Linux Modified to include call to is_unattended() that was removed from a previous commit. Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 180 ++++++++++++++++++++++++++++++++++++-- contrib/scalar/scalar.txt | 14 ++- 2 files changed, 188 insertions(+), 6 deletions(-) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index f8522c06a9cb13..176d19964706ca 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -15,6 +15,10 @@ #include "fsmonitor-ipc.h" #include "json-parser.h" +static int is_unattended(void) { + return git_env_bool("Scalar_UNATTENDED", 0); +} + /* * Remove the deepest subdirectory in the provided path string. Path must not * include a trailing path separator. Returns 1 if parent directory found, @@ -114,6 +118,19 @@ static int run_git(const char *arg, ...) return res; } +static const char *ensure_absolute_path(const char *path, char **absolute) +{ + struct strbuf buf = STRBUF_INIT; + + if (is_absolute_path(path)) + return path; + + strbuf_realpath_forgiving(&buf, path, 1); + free(*absolute); + *absolute = strbuf_detach(&buf, NULL); + return *absolute; +} + static int set_recommended_config(int reconfigure) { struct { @@ -597,6 +614,87 @@ static int supports_gvfs_protocol(const char *url, char **cache_server_url) return 0; /* error out quietly */ } +static char *default_cache_root(const char *root) +{ + const char *env; + + if (is_unattended()) + return xstrfmt("%s/.scalarCache", root); + +#ifdef WIN32 + (void)env; + return xstrfmt("%.*s.scalarCache", offset_1st_component(root), root); +#elif defined(__APPLE__) + if ((env = getenv("HOME")) && *env) + return xstrfmt("%s/.scalarCache", env); + return NULL; +#else + if ((env = getenv("XDG_CACHE_HOME")) && *env) + return xstrfmt("%s/scalar", env); + if ((env = getenv("HOME")) && *env) + return xstrfmt("%s/.cache/scalar", env); + return NULL; +#endif +} + +static int get_repository_id(struct json_iterator *it) +{ + if (it->type == JSON_STRING && + !strcasecmp(".repository.id", it->key.buf)) { + *(char **)it->fn_data = strbuf_detach(&it->string_value, NULL); + return 1; + } + + return 0; +} + +/* Needs to run this in a worktree; gvfs-helper requires a Git repository */ +static char *get_cache_key(const char *url) +{ + struct child_process cp = CHILD_PROCESS_INIT; + struct strbuf out = STRBUF_INIT; + char *cache_key = NULL; + + cp.git_cmd = 1; + strvec_pushl(&cp.args, "gvfs-helper", "--remote", url, + "endpoint", "vsts/info", NULL); + if (!pipe_command(&cp, NULL, 0, &out, 512, NULL, 0)) { + char *id = NULL; + struct json_iterator it = + JSON_ITERATOR_INIT(out.buf, get_repository_id, &id); + + if (iterate_json(&it) < 0) + warning("JSON parse error (%s)", out.buf); + else if (id) + cache_key = xstrfmt("id_%s", id); + free(id); + } + + if (!cache_key) { + struct strbuf downcased = STRBUF_INIT; + int hash_algo_index = hash_algo_by_name("sha1"); + const struct git_hash_algo *hash_algo = hash_algo_index < 0 ? + the_hash_algo : &hash_algos[hash_algo_index]; + git_hash_ctx ctx; + unsigned char hash[GIT_MAX_RAWSZ]; + + strbuf_addstr(&downcased, url); + strbuf_tolower(&downcased); + + hash_algo->init_fn(&ctx); + hash_algo->update_fn(&ctx, downcased.buf, downcased.len); + hash_algo->final_fn(hash, &ctx); + + strbuf_release(&downcased); + + cache_key = xstrfmt("url_%s", + hash_to_hex_algop(hash, hash_algo)); + } + + strbuf_release(&out); + return cache_key; +} + static char *remote_default_branch(const char *url) { struct child_process cp = CHILD_PROCESS_INIT; @@ -688,8 +786,8 @@ static int cmd_clone(int argc, const char **argv) { const char *branch = NULL; int full_clone = 0, single_branch = 0; - const char *cache_server_url = NULL; - char *default_cache_server_url = NULL; + const char *cache_server_url = NULL, *local_cache_root = NULL; + char *default_cache_server_url = NULL, *local_cache_root_abs = NULL; struct option clone_options[] = { OPT_STRING('b', "branch", &branch, N_(""), N_("branch to checkout after clone")), @@ -701,6 +799,9 @@ static int cmd_clone(int argc, const char **argv) OPT_STRING(0, "cache-server-url", &cache_server_url, N_(""), N_("the url or friendly name of the cache server")), + OPT_STRING(0, "local-cache-path", &local_cache_root, + N_(""), + N_("override the path for the local Scalar cache")), OPT_END(), }; const char * const clone_usage[] = { @@ -709,6 +810,7 @@ static int cmd_clone(int argc, const char **argv) }; const char *url; char *enlistment = NULL, *dir = NULL; + char *cache_key = NULL, *shared_cache_path = NULL; struct strbuf buf = STRBUF_INIT; int res; @@ -740,8 +842,20 @@ static int cmd_clone(int argc, const char **argv) if (is_directory(enlistment)) die(_("directory '%s' exists already"), enlistment); + ensure_absolute_path(enlistment, &enlistment); + dir = xstrfmt("%s/src", enlistment); + if (!local_cache_root) + local_cache_root = local_cache_root_abs = + default_cache_root(enlistment); + else + local_cache_root = ensure_absolute_path(local_cache_root, + &local_cache_root_abs); + + if (!local_cache_root) + die(_("could not determine local cache root")); + strbuf_reset(&buf); if (branch) strbuf_addf(&buf, "init.defaultBranch=%s", branch); @@ -761,7 +875,27 @@ static int cmd_clone(int argc, const char **argv) setup_git_directory(); + git_config(git_default_config, NULL); + + /* + * This `dir_inside_of()` call relies on git_config() having parsed the + * newly-initialized repository config's `core.ignoreCase` value. + */ + if (dir_inside_of(local_cache_root, dir) >= 0) { + struct strbuf path = STRBUF_INIT; + + strbuf_addstr(&path, enlistment); + if (chdir("../..") < 0 || + remove_dir_recursively(&path, 0) < 0) + die(_("'--local-cache-path' cannot be inside the src " + "folder;\nCould not remove '%s'"), enlistment); + + die(_("'--local-cache-path' cannot be inside the src folder")); + } + /* common-main already logs `argv` */ + trace2_data_intmax("scalar", the_repository, "unattended", + is_unattended()); trace2_def_repo(the_repository); if (!branch && !(branch = remote_default_branch(url))) { @@ -769,6 +903,30 @@ static int cmd_clone(int argc, const char **argv) goto cleanup; } + if (!(cache_key = get_cache_key(url))) { + res = error(_("could not determine cache key for '%s'"), url); + goto cleanup; + } + + shared_cache_path = xstrfmt("%s/%s", local_cache_root, cache_key); + if (set_config("gvfs.sharedCache=%s", shared_cache_path)) { + res = error(_("could not configure shared cache")); + goto cleanup; + } + + strbuf_reset(&buf); + strbuf_addf(&buf, "%s/pack", shared_cache_path); + switch (safe_create_leading_directories(buf.buf)) { + case SCLD_OK: case SCLD_EXISTS: + break; /* okay */ + default: + res = error_errno(_("could not initialize '%s'"), buf.buf); + goto cleanup; + } + + write_file_buf(git_path("objects/info/alternates"), + shared_cache_path, strlen(shared_cache_path)); + if (set_config("remote.origin.url=%s", url) || set_config("remote.origin.fetch=" "+refs/heads/%s:refs/remotes/origin/%s", @@ -850,6 +1008,9 @@ static int cmd_clone(int argc, const char **argv) free(dir); strbuf_release(&buf); free(default_cache_server_url); + free(local_cache_root_abs); + free(cache_key); + free(shared_cache_path); return res; } @@ -953,7 +1114,7 @@ static int cmd_diagnose(int argc, const char **argv) time_t now = time(NULL); struct tm tm; struct strbuf path = STRBUF_INIT, buf = STRBUF_INIT; - char *cache_server_url = NULL; + char *cache_server_url = NULL, *shared_cache = NULL; int res = 0; argc = parse_options(argc, argv, NULL, options, @@ -979,8 +1140,10 @@ static int cmd_diagnose(int argc, const char **argv) strbuf_addf(&buf, "Enlistment root: %s\n", the_repository->worktree); git_config_get_string("gvfs.cache-server", &cache_server_url); - strbuf_addf(&buf, "Cache Server: %s\n\n", - cache_server_url ? cache_server_url : "None"); + git_config_get_string("gvfs.sharedCache", &shared_cache); + strbuf_addf(&buf, "Cache Server: %s\nLocal Cache: %s\n\n", + cache_server_url ? cache_server_url : "None", + shared_cache ? shared_cache : "None"); get_disk_info(&buf); fwrite(buf.buf, buf.len, 1, stdout); @@ -1022,6 +1185,7 @@ static int cmd_diagnose(int argc, const char **argv) strbuf_release(&path); strbuf_release(&buf); free(cache_server_url); + free(shared_cache); return res; } @@ -1356,6 +1520,12 @@ int cmd_main(int argc, const char **argv) struct strbuf scalar_usage = STRBUF_INIT; int i; + if (is_unattended()) { + setenv("GIT_ASKPASS", "", 0); + setenv("GIT_TERMINAL_PROMPT", "false", 0); + git_config_push_parameter("credential.interactive=never"); + } + while (argc > 1 && *argv[1] == '-') { if (!strcmp(argv[1], "-C")) { if (argc < 3) diff --git a/contrib/scalar/scalar.txt b/contrib/scalar/scalar.txt index f416d637289c2c..653a32f0c02d11 100644 --- a/contrib/scalar/scalar.txt +++ b/contrib/scalar/scalar.txt @@ -8,7 +8,8 @@ scalar - an opinionated repository management tool SYNOPSIS -------- [verse] -scalar clone [--single-branch] [--branch ] [--full-clone] [] +scalar clone [--single-branch] [--branch ] [--full-clone] + [--local-cache-path ] [--cache-server-url ] [] scalar list scalar register [] scalar unregister [] @@ -74,6 +75,17 @@ cloning. If the HEAD at the remote did not point at any branch when A sparse-checkout is initialized by default. This behavior can be turned off via `--full-clone`. +--local-cache-path :: + Override the path to the local cache root directory; Pre-fetched objects + are stored into a repository-dependent subdirectory of that path. ++ +The default is `:\.scalarCache` on Windows (on the same drive as the +clone), and `~/.scalarCache` on macOS. + +--cache-server-url :: + Retrieve missing objects from the specified remote, which is expected to + understand the GVFS protocol. + List ~~~~ From 3f20adbc18ee00ac6a39d8cb5eaacbf560795eec Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 1 Jun 2021 23:18:14 +0200 Subject: [PATCH 08/13] scalar diagnose: include shared cache info Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 43 +++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index 176d19964706ca..7fb7d0ca98429f 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -398,7 +398,7 @@ static int stage(const char *git_dir, struct strbuf *buf, const char *path) return res; } -static int stage_file(const char *git_dir, const char *path) +static int stage_file(const char *git_dir, const char *path, size_t skip_chars) { struct strbuf buf = STRBUF_INIT; int res; @@ -406,13 +406,14 @@ static int stage_file(const char *git_dir, const char *path) if (strbuf_read_file(&buf, path, 0) < 0) return error(_("could not read '%s'"), path); - res = stage(git_dir, &buf, path); + res = stage(git_dir, &buf, path + skip_chars); strbuf_release(&buf); return res; } -static int stage_directory(const char *git_dir, const char *path, int recurse) +static int stage_directory(const char *git_dir, + const char *path, size_t skip_chars, int recurse) { int at_root = !*path; DIR *dir = opendir(at_root ? "." : path); @@ -435,9 +436,10 @@ static int stage_directory(const char *git_dir, const char *path, int recurse) strbuf_setlen(&buf, len); strbuf_addstr(&buf, e->d_name); - if ((e->d_type == DT_REG && stage_file(git_dir, buf.buf)) || + if ((e->d_type == DT_REG && + stage_file(git_dir, buf.buf, skip_chars)) || (e->d_type == DT_DIR && recurse && - stage_directory(git_dir, buf.buf, recurse))) + stage_directory(git_dir, buf.buf, skip_chars, recurse))) res = -1; } @@ -1162,13 +1164,34 @@ static int cmd_diagnose(int argc, const char **argv) if ((res = stage(tmp_dir.buf, &buf, "objects-local.txt"))) goto diagnose_cleanup; - if ((res = stage_directory(tmp_dir.buf, ".git", 0)) || - (res = stage_directory(tmp_dir.buf, ".git/hooks", 0)) || - (res = stage_directory(tmp_dir.buf, ".git/info", 0)) || - (res = stage_directory(tmp_dir.buf, ".git/logs", 1)) || - (res = stage_directory(tmp_dir.buf, ".git/objects/info", 0))) + if ((res = stage_directory(tmp_dir.buf, ".git", 0, 0)) || + (res = stage_directory(tmp_dir.buf, ".git/hooks", 0, 0)) || + (res = stage_directory(tmp_dir.buf, ".git/info", 0, 0)) || + (res = stage_directory(tmp_dir.buf, ".git/logs", 0, 1)) || + (res = stage_directory(tmp_dir.buf, ".git/objects/info", 0, 0))) goto diagnose_cleanup; + if (shared_cache) { + strbuf_reset(&path); + strbuf_addf(&path, "%s/pack", shared_cache); + strbuf_reset(&buf); + dir_file_stats(&buf, path.buf); + if ((res = stage(tmp_dir.buf, &buf, "packs-cached.txt"))) + goto diagnose_cleanup; + + strbuf_reset(&buf); + loose_objs_stats(&buf, shared_cache); + if ((res = stage(tmp_dir.buf, &buf, "objects-cached.txt"))) + goto diagnose_cleanup; + + strbuf_reset(&path); + strbuf_addf(&path, "%s/info", shared_cache); + if (is_directory(path.buf) && + (res = stage_directory(tmp_dir.buf, + path.buf, path.len - 4, 0))) + goto diagnose_cleanup; + } + res = index_to_zip(tmp_dir.buf); if (!res) From b7461034d45c685a2e384033aa5c8e4499d95654 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 28 Apr 2021 13:56:16 +0200 Subject: [PATCH 09/13] scalar: only try GVFS protocol on https:// URLs Well, technically also the http:// protocol is allowed _when testing_... Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 47 +++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index 7fb7d0ca98429f..e380cda0778953 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -578,6 +578,13 @@ static int get_cache_server_url(struct json_iterator *it) return 0; } +static int can_url_support_gvfs(const char *url) +{ + return starts_with(url, "https://") || + (git_env_bool("GIT_TEST_ALLOW_GVFS_VIA_HTTP", 0) && + starts_with(url, "http://")); +} + /* * If `cache_server_url` is `NULL`, print the list to `stdout`. * @@ -589,6 +596,13 @@ static int supports_gvfs_protocol(const char *url, char **cache_server_url) struct child_process cp = CHILD_PROCESS_INIT; struct strbuf out = STRBUF_INIT; + /* + * The GVFS protocol is only supported via https://; For testing, we + * also allow http://. + */ + if (!can_url_support_gvfs(url)) + return 0; + cp.git_cmd = 1; strvec_pushl(&cp.args, "gvfs-helper", "--remote", url, "config", NULL); if (!pipe_command(&cp, NULL, 0, &out, 512, NULL, 0)) { @@ -657,19 +671,26 @@ static char *get_cache_key(const char *url) struct strbuf out = STRBUF_INIT; char *cache_key = NULL; - cp.git_cmd = 1; - strvec_pushl(&cp.args, "gvfs-helper", "--remote", url, - "endpoint", "vsts/info", NULL); - if (!pipe_command(&cp, NULL, 0, &out, 512, NULL, 0)) { - char *id = NULL; - struct json_iterator it = - JSON_ITERATOR_INIT(out.buf, get_repository_id, &id); - - if (iterate_json(&it) < 0) - warning("JSON parse error (%s)", out.buf); - else if (id) - cache_key = xstrfmt("id_%s", id); - free(id); + /* + * The GVFS protocol is only supported via https://; For testing, we + * also allow http://. + */ + if (can_url_support_gvfs(url)) { + cp.git_cmd = 1; + strvec_pushl(&cp.args, "gvfs-helper", "--remote", url, + "endpoint", "vsts/info", NULL); + if (!pipe_command(&cp, NULL, 0, &out, 512, NULL, 0)) { + char *id = NULL; + struct json_iterator it = + JSON_ITERATOR_INIT(out.buf, get_repository_id, + &id); + + if (iterate_json(&it) < 0) + warning("JSON parse error (%s)", out.buf); + else if (id) + cache_key = xstrfmt("id_%s", id); + free(id); + } } if (!cache_key) { From 4160e449f991240733928ebcdf1e9bf192c9073b Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 16 Apr 2021 21:43:57 +0200 Subject: [PATCH 10/13] scalar: verify that we can use a GVFS-enabled repository Azure Repos does not support partial clones at the moment, but it does support the GVFS protocol. To that end, the Microsoft fork of Git has a `gvfs-helper` command that is optionally used to perform essentially the same functionality as partial clone. Let's verify that `scalar clone` detects that situation and enables the GVFS helper. Signed-off-by: Johannes Schindelin --- contrib/scalar/t/t9099-scalar.sh | 84 ++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/contrib/scalar/t/t9099-scalar.sh b/contrib/scalar/t/t9099-scalar.sh index 9fef639a95e19e..43efe4ff751874 100755 --- a/contrib/scalar/t/t9099-scalar.sh +++ b/contrib/scalar/t/t9099-scalar.sh @@ -120,4 +120,88 @@ test_expect_success 'scalar delete with enlistment' ' test_path_is_missing cloned ' +GIT_TEST_ALLOW_GVFS_VIA_HTTP=1 +export GIT_TEST_ALLOW_GVFS_VIA_HTTP + +test_set_port GIT_TEST_GVFS_PROTOCOL_PORT +HOST_PORT=127.0.0.1:$GIT_TEST_GVFS_PROTOCOL_PORT +PID_FILE="$(pwd)"/pid-file.pid +SERVER_LOG="$(pwd)"/OUT.server.log + +test_atexit ' + test -f "$PID_FILE" || return 0 + + # The server will shutdown automatically when we delete the pid-file. + rm -f "$PID_FILE" + + test -z "$verbose$verbose_log" || { + echo "server log:" + cat "$SERVER_LOG" + } + + # Give it a few seconds to shutdown (mainly to completely release the + # port before the next test start another instance and it attempts to + # bind to it). + for k in $(test_seq 5) + do + grep -q "Starting graceful shutdown" "$SERVER_LOG" && + return 0 || + sleep 1 + done + + echo "stop_gvfs_protocol_server: timeout waiting for server shutdown" + return 1 +' + +start_gvfs_enabled_http_server () { + GIT_HTTP_EXPORT_ALL=1 \ + test-gvfs-protocol --verbose \ + --listen=127.0.0.1 \ + --port=$GIT_TEST_GVFS_PROTOCOL_PORT \ + --reuseaddr \ + --pid-file="$PID_FILE" \ + 2>"$SERVER_LOG" & + + for k in 0 1 2 3 4 + do + if test -f "$PID_FILE" + then + return 0 + fi + sleep 1 + done + return 1 +} + +test_expect_success 'start GVFS-enabled server' ' + git config uploadPack.allowFilter false && + git config uploadPack.allowAnySHA1InWant false && + start_gvfs_enabled_http_server +' + +test_expect_success '`scalar clone` with GVFS-enabled server' ' + : the fake cache server requires fake authentication && + git config --global core.askPass true && + scalar clone --single-branch -- http://$HOST_PORT/ using-gvfs && + + : verify that the shared cache has been configured && + cache_key="url_$(printf "%s" http://$HOST_PORT/ | + tr A-Z a-z | + test-tool sha1)" && + echo "$(pwd)/using-gvfs/.scalarCache/$cache_key" >expect && + git -C using-gvfs/src config gvfs.sharedCache >actual && + test_cmp expect actual && + + second=$(git rev-parse --verify second:second.t) && + ( + cd using-gvfs/src && + test_path_is_missing 1/2 && + GIT_TRACE=$PWD/trace.txt git cat-file blob $second >actual && + : verify that the gvfs-helper was invoked to fetch it && + test_i18ngrep gvfs-helper trace.txt && + echo "second" >expect && + test_cmp expect actual + ) +' + test_done From 76fd80501e1c5210e8aa35e3df43f7fbbd8aa42b Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 23 Apr 2021 16:12:33 +0200 Subject: [PATCH 11/13] scalar: add the `cache-server` command This allows setting the GVFS-enabled cache server, or listing the one(s) associated with the remote repository. Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 102 +++++++++++++++++++++++++++++++++++++- contrib/scalar/scalar.txt | 22 ++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index e380cda0778953..e8fb3af0e7f602 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -14,6 +14,7 @@ #include "simple-ipc.h" #include "fsmonitor-ipc.h" #include "json-parser.h" +#include "remote.h" static int is_unattended(void) { return git_env_bool("Scalar_UNATTENDED", 0); @@ -542,6 +543,21 @@ static int set_config(const char *fmt, ...) return res; } +static int list_cache_server_urls(struct json_iterator *it) +{ + const char *p; + char *q; + long l; + + if (it->type == JSON_STRING && + skip_iprefix(it->key.buf, ".CacheServers[", &p) && + (l = strtol(p, &q, 10)) >= 0 && p != q && + !strcasecmp(q, "].Url")) + printf("#%ld: %s\n", l, it->string_value.buf); + + return 0; +} + /* Find N for which .CacheServers[N].GlobalDefault == true */ static int get_cache_server_index(struct json_iterator *it) { @@ -611,6 +627,16 @@ static int supports_gvfs_protocol(const char *url, char **cache_server_url) JSON_ITERATOR_INIT(out.buf, get_cache_server_index, &l); struct cache_server_url_data data = { .url = NULL }; + if (!cache_server_url) { + it.fn = list_cache_server_urls; + if (iterate_json(&it) < 0) { + strbuf_release(&out); + return error("JSON parse error"); + } + strbuf_release(&out); + return 0; + } + if (iterate_json(&it) < 0) { strbuf_release(&out); return error("JSON parse error"); @@ -627,7 +653,9 @@ static int supports_gvfs_protocol(const char *url, char **cache_server_url) return 1; } strbuf_release(&out); - return 0; /* error out quietly */ + /* error out quietly, unless we wanted to list URLs */ + return cache_server_url ? + 0 : error(_("Could not access gvfs/config endpoint")); } static char *default_cache_root(const char *root) @@ -1542,6 +1570,77 @@ static int cmd_version(int argc, const char **argv) return 0; } +static int cmd_cache_server(int argc, const char **argv) +{ + int get = 0; + char *set = NULL, *list = NULL; + const char *default_remote = "(default)"; + struct option options[] = { + OPT_BOOL(0, "get", &get, + N_("get the configured cache-server URL")), + OPT_STRING(0, "set", &set, N_("URL"), + N_("configure the cache-server to use")), + { OPTION_STRING, 0, "list", &list, N_("remote"), + N_("list the possible cache-server URLs"), + PARSE_OPT_OPTARG, NULL, (intptr_t) default_remote }, + OPT_END(), + }; + const char * const usage[] = { + N_("scalar cache_server " + "[--get | --set | --list []] []"), + NULL + }; + int res = 0; + + argc = parse_options(argc, argv, NULL, options, + usage, 0); + + if (get + !!set + !!list > 1) + usage_msg_opt(_("--get/--set/--list are mutually exclusive"), + usage, options); + + setup_enlistment_directory(argc, argv, usage, options, NULL); + + if (list) { + const char *name = list, *url = list; + + if (list == default_remote) + list = NULL; + + if (!list || !strchr(list, '/')) { + struct remote *remote; + + /* Look up remote */ + remote = remote_get(list); + if (!remote) { + error("no such remote: '%s'", name); + free(list); + return 1; + } + if (!remote->url) { + free(list); + return error(_("remote '%s' has no URLs"), + name); + } + url = remote->url[0]; + } + res = supports_gvfs_protocol(url, NULL); + free(list); + } else if (set) { + res = set_config("gvfs.cache-server=%s", set); + free(set); + } else { + char *url = NULL; + + printf("Using cache server: %s\n", + git_config_get_string("gvfs.cache-server", &url) ? + "(undefined)" : url); + free(url); + } + + return !!res; +} + static struct { const char *name; int (*fn)(int, const char **); @@ -1556,6 +1655,7 @@ static struct { { "help", cmd_help }, { "version", cmd_version }, { "diagnose", cmd_diagnose }, + { "cache-server", cmd_cache_server }, { NULL, NULL}, }; diff --git a/contrib/scalar/scalar.txt b/contrib/scalar/scalar.txt index 653a32f0c02d11..86698e8751cd16 100644 --- a/contrib/scalar/scalar.txt +++ b/contrib/scalar/scalar.txt @@ -16,6 +16,7 @@ scalar unregister [] scalar run ( all | config | commit-graph | fetch | loose-objects | pack-files ) [] scalar reconfigure [ --all | ] scalar delete +scalar cache-server ( --get | --set | --list [] ) [] DESCRIPTION ----------- @@ -148,6 +149,27 @@ delete :: This subcommand lets you delete an existing Scalar enlistment from your local file system, unregistering the repository. +Cache-server +~~~~~~~~~~~~ + +cache-server ( --get | --set | --list [] ) []:: + This command lets you query or set the GVFS-enabled cache server used + to fetch missing objects. + +--get:: + This is the default command mode: query the currently-configured cache + server URL, if any. + +--list:: + Access the `gvfs/info` endpoint of the specified remote (default: + `origin`) to figure out which cache servers are available, if any. ++ +In contrast to the `--get` command mode (which only accesses the local +repository), this command mode triggers a request via the network that +potentially requires authentication. If authentication is required, the +configured credential helper is employed (see linkgit:git-credential[1] +for details). + SEE ALSO -------- linkgit:git-clone[1], linkgit:git-maintenance[1]. From 7d550707652fa8be167b3f2375be8cfa4e02d891 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 12 May 2021 17:59:58 +0200 Subject: [PATCH 12/13] scalar: add a test toggle to skip accessing the vsts/info endpoint In Scalar's functional tests, we do not do anything with authentication. Therefore, we do want to avoid accessing the `vsts/info` endpoint because it requires authentication even on otherwise public repositories. Let's introduce the environment variable `SCALAR_TEST_SKIP_VSTS_INFO` which can be set to `true` to simply skip that step (and force the `url_*` style repository IDs instead of `id_*` whenever possible). Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index e8fb3af0e7f602..c779d02675ca7f 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -703,7 +703,8 @@ static char *get_cache_key(const char *url) * The GVFS protocol is only supported via https://; For testing, we * also allow http://. */ - if (can_url_support_gvfs(url)) { + if (!git_env_bool("SCALAR_TEST_SKIP_VSTS_INFO", 0) && + can_url_support_gvfs(url)) { cp.git_cmd = 1; strvec_pushl(&cp.args, "gvfs-helper", "--remote", url, "endpoint", "vsts/info", NULL); From 57e86de6d1386a81f16ff97a3f2d2bf09bb51a40 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 19 Jan 2022 00:18:58 +0100 Subject: [PATCH 13/13] scalar: accept `--no-fetch-commits-and-trees` for backwards compat It does not do anything. This function did not do anything for quite some time already, but Scalar's Functional Tests still rely on it. Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index c779d02675ca7f..954b2f4b3c0115 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -837,7 +837,7 @@ void load_builtin_commands(const char *prefix, struct cmdnames *cmds) static int cmd_clone(int argc, const char **argv) { const char *branch = NULL; - int full_clone = 0, single_branch = 0; + int full_clone = 0, single_branch = 0, dummy = 0; const char *cache_server_url = NULL, *local_cache_root = NULL; char *default_cache_server_url = NULL, *local_cache_root_abs = NULL; struct option clone_options[] = { @@ -854,6 +854,8 @@ static int cmd_clone(int argc, const char **argv) OPT_STRING(0, "local-cache-path", &local_cache_root, N_(""), N_("override the path for the local Scalar cache")), + OPT_HIDDEN_BOOL(0, "no-fetch-commits-and-trees", + &dummy, N_("no longer used")), OPT_END(), }; const char * const clone_usage[] = {