From 91e1e3c36fab5daf7d2e6e9ea18ea64a2d617dc5 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 16 Apr 2021 14:58:54 +0200 Subject: [PATCH 01/49] Implement `scalar diagnose` Over the course of Scalar's development, it became obvious that there is a need for a command that can gather all kinds of useful information that can help identify the most typical problems with large worktrees/repositories. The `diagnose` command is the culmination of this hard-won knowledge: it gathers the installed hooks, the config, a couple statistics describing the data shape, among other pieces of information, and then wraps everything up in a tidy, neat `.zip` archive. Note: in the .NET version we have the luxury of a comprehensive standard library that includes basic functionality such as writing a `.zip` file. In the C version, we lack such a commodity. Rather than introducing a dependency on, say, libzip, we slightly abuse Git's `archive` command: instead of writing the `.zip` file directly, we stage the file contents in a Git index of a temporary, bare repository, only to let `git archive` have at it, and finally removing the temporary repository. Also note: Due to the frequent spawned `git hash-object` processes, this command is quite a bit slow on Windows. Should it turn out to be a big problem, the lack of a batch mode of the `hash-object` command could potentially be worked around via using `git fast-import` with a crafted `stdin`. Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 181 +++++++++++++++++++++++++++++++ contrib/scalar/t/t9099-scalar.sh | 13 +++ 2 files changed, 194 insertions(+) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index 361833aa204add..62c6067f800e27 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -8,6 +8,8 @@ #include "config.h" #include "run-command.h" #include "refs.h" +#include "help.h" +#include "dir.h" /* * Remove the deepest subdirectory in the provided path string. Path must not @@ -280,6 +282,108 @@ static int unregister_dir(void) return res; } +static int stage(const char *git_dir, struct strbuf *buf, const char *path) +{ + struct strbuf cacheinfo = STRBUF_INIT; + struct child_process cp = CHILD_PROCESS_INIT; + int res; + + strbuf_addstr(&cacheinfo, "100644,"); + + cp.git_cmd = 1; + strvec_pushl(&cp.args, "--git-dir", git_dir, + "hash-object", "-w", "--stdin", NULL); + res = pipe_command(&cp, buf->buf, buf->len, &cacheinfo, 256, NULL, 0); + if (!res) { + strbuf_rtrim(&cacheinfo); + strbuf_addch(&cacheinfo, ','); + /* We cannot stage `.git`, use `_git` instead. */ + if (starts_with(path, ".git/")) + strbuf_addf(&cacheinfo, "_%s", path + 1); + else + strbuf_addstr(&cacheinfo, path); + + child_process_init(&cp); + cp.git_cmd = 1; + strvec_pushl(&cp.args, "--git-dir", git_dir, + "update-index", "--add", "--cacheinfo", + cacheinfo.buf, NULL); + res = run_command(&cp); + } + + strbuf_release(&cacheinfo); + return res; +} + +static int stage_file(const char *git_dir, const char *path) +{ + struct strbuf buf = STRBUF_INIT; + int res; + + if (strbuf_read_file(&buf, path, 0) < 0) + return error(_("could not read '%s'"), path); + + res = stage(git_dir, &buf, path); + + strbuf_release(&buf); + return res; +} + +static int stage_directory(const char *git_dir, const char *path, int recurse) +{ + int at_root = !*path; + DIR *dir = opendir(at_root ? "." : path); + struct dirent *e; + struct strbuf buf = STRBUF_INIT; + size_t len; + int res = 0; + + if (!dir) + return error(_("could not open directory '%s'"), path); + + if (!at_root) + strbuf_addf(&buf, "%s/", path); + len = buf.len; + + while (!res && (e = readdir(dir))) { + if (!strcmp(".", e->d_name) || !strcmp("..", e->d_name)) + continue; + + strbuf_setlen(&buf, len); + strbuf_addstr(&buf, e->d_name); + + if ((e->d_type == DT_REG && stage_file(git_dir, buf.buf)) || + (e->d_type == DT_DIR && recurse && + stage_directory(git_dir, buf.buf, recurse))) + res = -1; + } + + closedir(dir); + strbuf_release(&buf); + return res; +} + +static int index_to_zip(const char *git_dir) +{ + struct child_process cp = CHILD_PROCESS_INIT; + struct strbuf oid = STRBUF_INIT; + + cp.git_cmd = 1; + strvec_pushl(&cp.args, "--git-dir", git_dir, "write-tree", NULL); + if (pipe_command(&cp, NULL, 0, &oid, the_hash_algo->hexsz + 1, + NULL, 0)) + return error(_("could not write temporary tree object")); + + strbuf_rtrim(&oid); + child_process_init(&cp); + cp.git_cmd = 1; + strvec_pushl(&cp.args, "--git-dir", git_dir, "archive", "-o", NULL); + strvec_pushf(&cp.args, "%s.zip", git_dir); + strvec_pushl(&cp.args, oid.buf, "--", NULL); + strbuf_release(&oid); + return run_command(&cp); +} + /* printf-style interface, expects `=` argument */ static int set_config(const char *fmt, ...) { @@ -485,6 +589,82 @@ static int cmd_clone(int argc, const char **argv) return res; } +/* + * Dummy implementation; Using `get_version_info()` would cause a link error + * without this. + */ +void load_builtin_commands(const char *prefix, struct cmdnames *cmds) +{ + die("not implemented"); +} + +static int cmd_diagnose(int argc, const char **argv) +{ + struct option options[] = { + OPT_END(), + }; + const char * const usage[] = { + N_("scalar diagnose []"), + NULL + }; + struct strbuf tmp_dir = STRBUF_INIT; + time_t now = time(NULL); + struct tm tm; + struct strbuf path = STRBUF_INIT, buf = STRBUF_INIT; + int res = 0; + + argc = parse_options(argc, argv, NULL, options, + usage, 0); + + setup_enlistment_directory(argc, argv, usage, options, &buf); + + strbuf_addstr(&buf, "/.scalarDiagnostics/scalar_"); + strbuf_addftime(&buf, "%Y%m%d_%H%M%S", localtime_r(&now, &tm), 0, 0); + if (run_git("init", "-q", "-b", "dummy", "--bare", buf.buf, NULL)) { + res = error(_("could not initialize temporary repository: %s"), + buf.buf); + goto diagnose_cleanup; + } + strbuf_realpath(&tmp_dir, buf.buf, 1); + + strbuf_reset(&buf); + strbuf_addf(&buf, "Collecting diagnostic info into temp folder %s\n\n", + tmp_dir.buf); + + get_version_info(&buf, 1); + + strbuf_addf(&buf, "Enlistment root: %s\n", the_repository->worktree); + fwrite(buf.buf, buf.len, 1, stdout); + + if ((res = stage(tmp_dir.buf, &buf, "diagnostics.log"))) + 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))) + goto diagnose_cleanup; + + res = index_to_zip(tmp_dir.buf); + + if (!res) + res = remove_dir_recursively(&tmp_dir, 0); + + if (!res) + printf("\n" + "Diagnostics complete.\n" + "All of the gathered info is captured in '%s.zip'\n", + tmp_dir.buf); + +diagnose_cleanup: + strbuf_release(&tmp_dir); + strbuf_release(&path); + strbuf_release(&buf); + + return res; +} + static int cmd_list(int argc, const char **argv) { if (argc != 1) @@ -723,6 +903,7 @@ static struct { { "unregister", cmd_unregister }, { "run", cmd_run }, { "reconfigure", cmd_reconfigure }, + { "diagnose", cmd_diagnose }, { NULL, NULL}, }; diff --git a/contrib/scalar/t/t9099-scalar.sh b/contrib/scalar/t/t9099-scalar.sh index a2df78737a6a9e..1ecd257f93325a 100755 --- a/contrib/scalar/t/t9099-scalar.sh +++ b/contrib/scalar/t/t9099-scalar.sh @@ -65,6 +65,19 @@ test_expect_success 'scalar clone' ' ) ' +SQ="'" +test_expect_success UNZIP 'scalar diagnose' ' + scalar diagnose cloned >out && + sed -n "s/.*$SQ\\(.*\\.zip\\)$SQ.*/\\1/p" zip_path && + zip_path=$(cat zip_path) && + test -n "$zip_path" && + unzip -v "$zip_path" && + folder=${zip_path%.zip} && + test_path_is_missing "$folder" && + unzip -p "$zip_path" diagnostics.log >out && + test_file_not_empty out +' + test_expect_success 'scalar reconfigure' ' git init one/src && scalar register one && From e41061d4bf732af93e3b391b9dd34509aca16616 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Mon, 12 Apr 2021 14:15:36 -0400 Subject: [PATCH 02/49] scalar register: set recommended config settings Let's start implementing the `register` command. With this commit, recommended settings are configured upon `scalar register`. Co-authored-by: Victoria Dye Signed-off-by: Victoria Dye Signed-off-by: Derrick Stolee Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 222 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index 65220b20a1cb32..e3d6888ad03149 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -5,11 +5,233 @@ #include "cache.h" #include "gettext.h" #include "parse-options.h" +#include "config.h" + +/* + * Remove the deepest subdirectory in the provided path string. Path must not + * include a trailing path separator. Returns 1 if parent directory found, + * otherwise 0. + */ +static int strbuf_parentdir(struct strbuf *buf) +{ + size_t len = buf->len; + size_t offset = offset_1st_component(buf->buf); + char *path_sep = find_last_dir_sep(buf->buf + offset); + strbuf_setlen(buf, path_sep ? path_sep - buf->buf : offset); + + return buf->len < len; +} + +static void setup_enlistment_directory(int argc, const char **argv, + const char * const *usagestr, + const struct option *options, + struct strbuf *enlistment_root) +{ + struct strbuf path = STRBUF_INIT; + char *root; + int enlistment_found = 0; + + if (startup_info->have_repository) + BUG("gitdir already set up?!?"); + + if (argc > 1) + usage_with_options(usagestr, options); + + /* find the worktree, determine its corresponding root */ + if (argc == 1) { + strbuf_add_absolute_path(&path, argv[0]); + } else if (strbuf_getcwd(&path) < 0) { + die(_("need a working directory")); + } + + strbuf_trim_trailing_dir_sep(&path); + do { + const size_t len = path.len; + + /* check if currently in enlistment root with src/ workdir */ + strbuf_addstr(&path, "/src/.git"); + if (is_git_directory(path.buf)) { + strbuf_strip_suffix(&path, "/.git"); + + if (enlistment_root) + strbuf_add(enlistment_root, path.buf, len); + + enlistment_found = 1; + break; + } + + /* reset to original path */ + strbuf_setlen(&path, len); + + /* check if currently in workdir */ + strbuf_addstr(&path, "/.git"); + if (is_git_directory(path.buf)) { + strbuf_setlen(&path, len); + + if (enlistment_root) { + /* + * If the worktree's directory's name is `src`, the enlistment is the + * parent directory, otherwise it is identical to the worktree. + */ + root = strip_path_suffix(path.buf, "src"); + strbuf_addstr(enlistment_root, root ? root : path.buf); + free(root); + } + + enlistment_found = 1; + break; + } + + strbuf_setlen(&path, len); + } while (strbuf_parentdir(&path)); + + if (!enlistment_found) + die(_("could not find enlistment root")); + + if (chdir(path.buf) < 0) + die_errno(_("could not switch to '%s'"), path.buf); + + strbuf_release(&path); + setup_git_directory(); +} + +static int set_recommended_config(void) +{ + struct { + const char *key; + const char *value; + } config[] = { + { "am.keepCR", "true" }, + { "core.FSCache", "true" }, + { "core.multiPackIndex", "true" }, + { "core.preloadIndex", "true" }, +#ifndef WIN32 + { "core.untrackedCache", "true" }, +#else + /* + * Unfortunately, Scalar's Functional Tests demonstrated + * that the untracked cache feature is unreliable on Windows + * (which is a bummer because that platform would benefit the + * most from it). For some reason, freshly created files seem + * not to update the directory's `lastModified` time + * immediately, but the untracked cache would need to rely on + * that. + * + * Therefore, with a sad heart, we disable this very useful + * feature on Windows. + */ + { "core.untrackedCache", "false" }, +#endif + { "core.bare", "false" }, + { "core.logAllRefUpdates", "true" }, + { "credential.https://dev.azure.com.useHttpPath", "true" }, + { "credential.validate", "false" }, /* GCM4W-only */ + { "gc.auto", "0" }, + { "gui.GCWarning", "false" }, + { "index.threads", "true" }, + { "index.version", "4" }, + { "merge.stat", "false" }, + { "merge.renames", "false" }, + { "pack.useBitmaps", "false" }, + { "pack.useSparse", "true" }, + { "receive.autoGC", "false" }, + { "reset.quiet", "true" }, + { "feature.manyFiles", "false" }, + { "feature.experimental", "false" }, + { "fetch.unpackLimit", "1" }, + { "fetch.writeCommitGraph", "false" }, +#ifdef WIN32 + { "http.sslBackend", "schannel" }, +#endif + { "status.aheadBehind", "false" }, + { "commitGraph.generationVersion", "1" }, + { "core.autoCRLF", "false" }, + { "core.safeCRLF", "false" }, + { "maintenance.gc.enabled", "false" }, + { "maintenance.prefetch.enabled", "true" }, + { "maintenance.prefetch.auto", "0" }, + { "maintenance.prefetch.schedule", "hourly" }, + { "maintenance.commit-graph.enabled", "true" }, + { "maintenance.commit-graph.auto", "0" }, + { "maintenance.commit-graph.schedule", "hourly" }, + { "maintenance.loose-objects.enabled", "true" }, + { "maintenance.loose-objects.auto", "0" }, + { "maintenance.loose-objects.schedule", "daily" }, + { "maintenance.incremental-repack.enabled", "true" }, + { "maintenance.incremental-repack.auto", "0" }, + { "maintenance.incremental-repack.schedule", "daily" }, + { NULL, NULL }, + }; + int i; + char *value; + + for (i = 0; config[i].key; i++) { + if (git_config_get_string(config[i].key, &value)) { + trace2_data_string("scalar", the_repository, config[i].key, "created"); + if (git_config_set_gently(config[i].key, + config[i].value) < 0) + return error(_("could not configure %s=%s"), + config[i].key, config[i].value); + } else { + trace2_data_string("scalar", the_repository, config[i].key, "exists"); + free(value); + } + } + + /* + * The `log.excludeDecoration` setting is special because we want to + * set multiple values. + */ + if (git_config_get_string("log.excludeDecoration", &value)) { + trace2_data_string("scalar", the_repository, + "log.excludeDecoration", "created"); + if (git_config_set_multivar_gently("log.excludeDecoration", + "refs/scalar/*", + CONFIG_REGEX_NONE, 0) || + git_config_set_multivar_gently("log.excludeDecoration", + "refs/prefetch/*", + CONFIG_REGEX_NONE, 0)) + return error(_("could not configure " + "log.excludeDecoration")); + } else { + trace2_data_string("scalar", the_repository, + "log.excludeDecoration", "exists"); + free(value); + } + + return 0; +} + +static int register_dir(void) +{ + int res = set_recommended_config(); + + return res; +} + +static int cmd_register(int argc, const char **argv) +{ + struct option options[] = { + OPT_END(), + }; + const char * const usage[] = { + N_("scalar register []"), + NULL + }; + + argc = parse_options(argc, argv, NULL, options, + usage, 0); + + setup_enlistment_directory(argc, argv, usage, options, NULL); + + return register_dir(); +} static struct { const char *name; int (*fn)(int, const char **); } builtins[] = { + { "register", cmd_register }, { NULL, NULL}, }; From ae3298d67668eadef5adae88f07998f764744a2b Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 12 Apr 2021 15:07:23 +0200 Subject: [PATCH 03/49] scalar: implement the `clone` subcommand This implements Scalar's opinionated `clone` command: it tries to use a partial clone and sets up a sparse checkout by default. In contrast to `git clone`, `scalar clone` sets up the worktree in the `src/` subdirectory, to encourage a separation between the source files and the build output (which helps Git tremendously because it avoids untracked files that have to be specifically ignored when refreshing the index). Also, it registers the repository for regular, scheduled maintenance, and configures a slur of configuration settings based on the experience of the Microsoft Windows and the Microsoft Office development teams. Note: We intentionally use a slightly wasteful `set_config()` function (which does not reuse a single `strbuf`, for example, though performance _really_ does not matter here) because it is very, very convenient. Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 192 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index 183ce4de70418d..3b3e63dcba2cab 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -275,6 +275,197 @@ static int unregister_dir(void) return res; } +/* printf-style interface, expects `=` argument */ +static int set_config(const char *fmt, ...) +{ + struct strbuf buf = STRBUF_INIT; + char *value; + int res; + va_list args; + + va_start(args, fmt); + strbuf_vaddf(&buf, fmt, args); + va_end(args); + + value = strchr(buf.buf, '='); + if (value) + *(value++) = '\0'; + res = git_config_set_gently(buf.buf, value); + strbuf_release(&buf); + + return res; +} + +static char *remote_default_branch(const char *url) +{ + struct child_process cp = CHILD_PROCESS_INIT; + struct strbuf out = STRBUF_INIT; + + cp.git_cmd = 1; + strvec_pushl(&cp.args, "ls-remote", "--symref", url, "HEAD", NULL); + strbuf_addstr(&out, "-\n"); + if (!pipe_command(&cp, NULL, 0, &out, 0, NULL, 0)) { + char *ref = out.buf; + + while ((ref = strstr(ref + 1, "\nref: "))) { + const char *p; + char *head, *branch; + + ref += strlen("\nref: "); + head = strstr(ref, "\tHEAD"); + + if (!head || memchr(ref, '\n', head - ref)) + continue; + + if (skip_prefix(ref, "refs/heads/", &p)) { + branch = xstrndup(p, head - p); + strbuf_release(&out); + return branch; + } + + error(_("remote HEAD is not a branch: '%.*s'"), + (int)(head - ref), ref); + strbuf_release(&out); + return NULL; + } + } + warning(_("failed to get default branch name from remote; " + "using local default")); + strbuf_reset(&out); + + child_process_init(&cp); + cp.git_cmd = 1; + strvec_pushl(&cp.args, "symbolic-ref", "--short", "HEAD", NULL); + if (!pipe_command(&cp, NULL, 0, &out, 0, NULL, 0)) { + strbuf_trim(&out); + return strbuf_detach(&out, NULL); + } + + strbuf_release(&out); + error(_("failed to get default branch name")); + return NULL; +} + +static int cmd_clone(int argc, const char **argv) +{ + const char *branch = NULL; + int no_fetch_commits_and_trees = 0, full_clone = 0; + struct option clone_options[] = { + OPT_STRING('b', "branch", &branch, N_(""), + N_("branch to checkout after clone")), + OPT_BOOL(0, "no-fetch-commits-and-trees", + &no_fetch_commits_and_trees, + N_("skip fetching commits and trees after clone")), + OPT_BOOL(0, "full-clone", &full_clone, + N_("when cloning, create full working directory")), + OPT_END(), + }; + const char * const clone_usage[] = { + N_("scalar clone [] [--] []"), + NULL + }; + const char *url; + char *enlistment = NULL, *dir = NULL; + struct strbuf buf = STRBUF_INIT; + int res; + + argc = parse_options(argc, argv, NULL, clone_options, clone_usage, 0); + + if (argc == 2) { + url = argv[0]; + enlistment = xstrdup(argv[1]); + } else if (argc == 1) { + url = argv[0]; + + strbuf_addstr(&buf, url); + /* Strip trailing slashes, if any */ + while (buf.len > 0 && is_dir_sep(buf.buf[buf.len - 1])) + strbuf_setlen(&buf, buf.len - 1); + /* Strip suffix `.git`, if any */ + strbuf_strip_suffix(&buf, ".git"); + + enlistment = find_last_dir_sep(buf.buf); + if (!enlistment) { + die(_("cannot deduce worktree name from '%s'"), url); + } + enlistment = xstrdup(enlistment + 1); + } else { + usage_msg_opt(N_("need a URL"), clone_usage, clone_options); + } + + if (is_directory(enlistment)) + die(_("directory '%s' exists already"), enlistment); + + dir = xstrfmt("%s/src", enlistment); + + if ((res = run_git("init", "--", dir, NULL))) + goto cleanup; + + if (chdir(dir) < 0) { + res = error_errno(_("could not switch to '%s'"), dir); + goto cleanup; + } + + setup_git_directory(); + + /* common-main already logs `argv` */ + trace2_data_string("scalar", the_repository, "dir", dir); + + if (!branch && !(branch = remote_default_branch(url))) { + res = error(_("failed to get default branch for '%s'"), url); + goto cleanup; + } + + if (set_config("remote.origin.url=%s", url) || + set_config("remote.origin.fetch=" + "+refs/heads/*:refs/remotes/origin/*") || + set_config("remote.origin.promisor=true") || + set_config("remote.origin.partialCloneFilter=blob:none")) { + res = error(_("could not configure remote in '%s'"), dir); + goto cleanup; + } + + if (!full_clone && + (res = run_git("sparse-checkout", "init", "--cone", NULL))) + goto cleanup; + + if (set_recommended_config()) + return error(_("could not configure '%s'"), dir); + + if ((res = run_git("fetch", "--quiet", "origin", NULL))) { + warning(_("Partial clone failed; Trying full clone")); + + if (set_config("remote.origin.promisor") || + set_config("remote.origin.partialCloneFilter")) { + res = error(_("could not configure for full clone")); + goto cleanup; + } + + if ((res = run_git("fetch", "--quiet", "origin", NULL))) + goto cleanup; + } + + if ((res = set_config("branch.%s.remote=origin", branch))) + goto cleanup; + if ((res = set_config("branch.%s.merge=refs/heads/%s", + branch, branch))) + goto cleanup; + + strbuf_reset(&buf); + strbuf_addf(&buf, "origin/%s", branch); + res = run_git("checkout", "-f", "-t", buf.buf, NULL); + if (res) + goto cleanup; + + res = register_dir(); + +cleanup: + free(enlistment); + free(dir); + strbuf_release(&buf); + return res; +} + static int cmd_list(int argc, const char **argv) { if (argc != 1) @@ -371,6 +562,7 @@ static struct { const char *name; int (*fn)(int, const char **); } builtins[] = { + { "clone", cmd_clone }, { "list", cmd_list }, { "register", cmd_register }, { "unregister", cmd_unregister }, From 018cd881b62e69d1fd1f5916293a0bbd74885ed8 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 22 Apr 2021 23:49:42 +0200 Subject: [PATCH 04/49] scalar diagnose: include disk space information Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 53 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index 62c6067f800e27..3f4eb2f1482b40 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -384,6 +384,58 @@ static int index_to_zip(const char *git_dir) return run_command(&cp); } +#ifndef WIN32 +#include +#endif + +static int get_disk_info(struct strbuf *out) +{ +#ifdef WIN32 + struct strbuf buf = STRBUF_INIT; + char volume_name[MAX_PATH], fs_name[MAX_PATH]; + DWORD serial_number, component_length, flags; + ULARGE_INTEGER avail2caller, total, avail; + + strbuf_realpath(&buf, ".", 1); + if (!GetDiskFreeSpaceExA(buf.buf, &avail2caller, &total, &avail)) { + error(_("could not determine free disk size for '%s'"), + buf.buf); + strbuf_release(&buf); + return -1; + } + + strbuf_setlen(&buf, offset_1st_component(buf.buf)); + if (!GetVolumeInformationA(buf.buf, volume_name, sizeof(volume_name), + &serial_number, &component_length, &flags, + fs_name, sizeof(fs_name))) { + error(_("could not get info for '%s'"), buf.buf); + strbuf_release(&buf); + return -1; + } + strbuf_addf(out, "Available space on '%s': ", buf.buf); + strbuf_humanise_bytes(out, avail2caller.QuadPart); + strbuf_addch(out, '\n'); + strbuf_release(&buf); +#else + struct strbuf buf = STRBUF_INIT; + struct statvfs stat; + + strbuf_realpath(&buf, ".", 1); + if (statvfs(buf.buf, &stat) < 0) { + error_errno(_("could not determine free disk size for '%s'"), + buf.buf); + strbuf_release(&buf); + return -1; + } + + strbuf_addf(out, "Available space on '%s': ", buf.buf); + strbuf_humanise_bytes(out, st_mult(stat.f_bsize, stat.f_bavail)); + strbuf_addf(out, " (mount flags 0x%lx)\n", stat.f_flag); + strbuf_release(&buf); +#endif + return 0; +} + /* printf-style interface, expects `=` argument */ static int set_config(const char *fmt, ...) { @@ -634,6 +686,7 @@ static int cmd_diagnose(int argc, const char **argv) get_version_info(&buf, 1); strbuf_addf(&buf, "Enlistment root: %s\n", the_repository->worktree); + get_disk_info(&buf); fwrite(buf.buf, buf.len, 1, stdout); if ((res = stage(tmp_dir.buf, &buf, "diagnostics.log"))) From 8b106718b3e0393713473d95f44e741132e00d50 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 3 May 2021 15:34:03 +0200 Subject: [PATCH 05/49] scalar: start documenting the command This commit establishes the infrastructure to build the manual page for te `scalar` command. Signed-off-by: Johannes Schindelin --- contrib/scalar/.gitignore | 3 +++ contrib/scalar/Makefile | 15 ++++++++++++- contrib/scalar/scalar.txt | 47 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 contrib/scalar/scalar.txt diff --git a/contrib/scalar/.gitignore b/contrib/scalar/.gitignore index ff3d47e84d0436..00441073f59cf5 100644 --- a/contrib/scalar/.gitignore +++ b/contrib/scalar/.gitignore @@ -1,2 +1,5 @@ +/*.xml +/*.1 +/*.html /*.exe /scalar diff --git a/contrib/scalar/Makefile b/contrib/scalar/Makefile index aa547c302086bb..1f39f7bd335b18 100644 --- a/contrib/scalar/Makefile +++ b/contrib/scalar/Makefile @@ -19,6 +19,8 @@ ifndef V QUIET_SUBDIR0 = +@subdir= QUIET_SUBDIR1 = ;$(NO_SUBDIR) echo ' ' SUBDIR $$subdir; \ $(MAKE) $(PRINT_DIR) -C $$subdir + QUIET = @ + export V export QUIET_GEN export QUIET_BUILT_IN @@ -44,6 +46,7 @@ $(TARGETS): $(GITLIBS) scalar.c clean: $(RM) $(TARGETS) ../../bin-wrappers/scalar + $(RM) scalar.1 scalar.html scalar.xml ../../bin-wrappers/scalar: ../../wrap-for-bin.sh Makefile @mkdir -p ../../bin-wrappers @@ -55,4 +58,14 @@ clean: test: all $(MAKE) -C t -.PHONY: all clean test FORCE +docs: scalar.html scalar.1 + +scalar.html: | scalar.1 # prevent them from trying to build `doc.dep` in parallel + +scalar.html scalar.1: scalar.txt + $(QUIET_SUBDIR0)../../Documentation$(QUIET_SUBDIR1) \ + MAN_TXT=../contrib/scalar/scalar.txt \ + ../contrib/scalar/$@ + $(QUIET)test scalar.1 != "$@" || mv ../../Documentation/$@ . + +.PHONY: all clean test docs FORCE diff --git a/contrib/scalar/scalar.txt b/contrib/scalar/scalar.txt new file mode 100644 index 00000000000000..4edebe5e67160d --- /dev/null +++ b/contrib/scalar/scalar.txt @@ -0,0 +1,47 @@ +scalar(1) +========= + +NAME +---- +scalar - an opinionated repository management tool + +SYNOPSIS +-------- +[verse] +scalar [] + +DESCRIPTION +----------- + +Scalar is an opinionated repository management tool. By creating new +repositories or registering existing repositories with Scalar, your Git +experience will speed up. Scalar sets advanced Git config settings, +maintains your repositories in the background, and helps reduce data sent +across the network. + +An important Scalar concept is the enlistment: this is the top-level directory +of the project. It contains the subdirectory `src/` which is a Git worktree. +This encourages the separation between tracked files (inside `src/`) and +untracked files (outside `src/`). + +The command implements various subcommands, and different options depending +on the subcommand. With the exception of `clone` and `list`, all subcommands +expect to be run in an enlistment. + +The following options can be specified _before_ the subcommand: + +-C :: + Before running the subcommand, change the working directory. This + option imitates the same option of linkgit:git[1]. + +-c =:: + For the duration of running the specified subcommand, configure this + setting. This option imitates the same option of linkgit:git[1]. + +SEE ALSO +-------- +linkgit:git-clone[1], linkgit:git-maintenance[1]. + +Scalar +--- +Associated with the linkgit:git[1] suite From 7eefafcfef748f4a3cb6d1abe4ced293bdae73db Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Mon, 12 Apr 2021 14:25:30 -0400 Subject: [PATCH 06/49] scalar register/unregister: start/stop maintenance on repository Arguably, the biggest learning from the Scalar project is that scheduled maintenance is crucial to keep large repositories in a good shape. With this commit, `scalar register` starts those scheduled maintenance tasks, and `scalar unregister` stops them. Co-authored-by: Victoria Dye Signed-off-by: Victoria Dye Signed-off-by: Derrick Stolee Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 52 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index e3d6888ad03149..494b913a8e6635 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -6,6 +6,7 @@ #include "gettext.h" #include "parse-options.h" #include "config.h" +#include "run-command.h" /* * Remove the deepest subdirectory in the provided path string. Path must not @@ -95,6 +96,25 @@ static void setup_enlistment_directory(int argc, const char **argv, setup_git_directory(); } +static int run_git(const char *arg, ...) +{ + struct strvec argv = STRVEC_INIT; + va_list args; + const char *p; + int res; + + va_start(args, arg); + strvec_push(&argv, arg); + while ((p = va_arg(args, const char *))) + strvec_push(&argv, p); + va_end(args); + + res = run_command_v_opt(argv.v, RUN_GIT_CMD); + + strvec_clear(&argv); + return res; +} + static int set_recommended_config(void) { struct { @@ -202,13 +222,26 @@ static int set_recommended_config(void) return 0; } +static int toggle_maintenance(int enable) +{ + return run_git("maintenance", enable ? "start" : "unregister", NULL); +} + static int register_dir(void) { int res = set_recommended_config(); + if (!res) + res = toggle_maintenance(1); + return res; } +static int unregister_dir(void) +{ + return toggle_maintenance(0); +} + static int cmd_register(int argc, const char **argv) { struct option options[] = { @@ -227,11 +260,30 @@ static int cmd_register(int argc, const char **argv) return register_dir(); } +static int cmd_unregister(int argc, const char **argv) +{ + struct option options[] = { + OPT_END(), + }; + const char * const usage[] = { + N_("scalar unregister []"), + NULL + }; + + argc = parse_options(argc, argv, NULL, options, + usage, 0); + + setup_enlistment_directory(argc, argv, usage, options, NULL); + + return unregister_dir(); +} + static struct { const char *name; int (*fn)(int, const char **); } builtins[] = { { "register", cmd_register }, + { "unregister", cmd_unregister }, { NULL, NULL}, }; From 1d3cff36097e64de7de0d21711c153aaf73e2b2a Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 15 Apr 2021 00:50:59 +0200 Subject: [PATCH 07/49] scalar: test `scalar clone` This commit adds a simple regression test, modeled after Git's own test suite. A more comprehensive functional (or: integration) test suite can be found at https://github.com/microsoft/scalar; There is no intention to port that fuller test suite to `contrib/scalar/`; Instead, it will still be used to verify the `scalar` functionality in Microsoft's Git fork. Signed-off-by: Johannes Schindelin --- contrib/scalar/t/t9099-scalar.sh | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/contrib/scalar/t/t9099-scalar.sh b/contrib/scalar/t/t9099-scalar.sh index 58e0482fceb6e7..bd7ca4cd9b8406 100755 --- a/contrib/scalar/t/t9099-scalar.sh +++ b/contrib/scalar/t/t9099-scalar.sh @@ -10,6 +10,9 @@ PATH=$(pwd)/..:$PATH . ../../../t/test-lib.sh +GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab ../cron.txt" +export GIT_TEST_MAINT_SCHEDULER + test_expect_success 'scalar shows a usage' ' test_expect_code 129 scalar -h ' @@ -29,6 +32,35 @@ test_expect_success 'scalar unregister' ' ! grep -F "$(pwd)/vanish/src" scalar.repos ' +test_expect_success 'set up repository to clone' ' + test_commit first && + test_commit second && + test_commit third && + git switch -c parallel first && + mkdir -p 1/2 && + test_commit 1/2/3 && + git config uploadPack.allowFilter true && + git config uploadPack.allowAnySHA1InWant true +' + +test_expect_success 'scalar clone' ' + second=$(git rev-parse --verify second:second.t) && + scalar clone "file://$(pwd)" cloned && + ( + cd cloned/src && + + git config --get --global --fixed-value maintenance.repo \ + "$(pwd)" && + + test_path_is_missing 1/2 && + test_must_fail git rev-list --missing=print $second && + git rev-list $second && + git cat-file blob $second >actual && + echo "second" >expect && + test_cmp expect actual + ) +' + test_expect_success '`scalar register` & `unregister` with existing repo' ' git init existing && scalar register existing && From aee3f62c81a9a6e309be87fec5bd8ed7fb50f7e2 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 27 May 2021 06:12:39 +0200 Subject: [PATCH 08/49] scalar: allow reconfiguring an existing enlistment This comes in handy during Scalar upgrades, or when config settings were messed up by mistake. Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 81 ++++++++++++++++++++------------ contrib/scalar/t/t9099-scalar.sh | 8 ++++ 2 files changed, 60 insertions(+), 29 deletions(-) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index fe264d0aec336f..4cf85768d73944 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -116,18 +116,20 @@ static int run_git(const char *arg, ...) return res; } -static int set_recommended_config(void) +static int set_recommended_config(int reconfigure) { struct { const char *key; const char *value; + int overwrite_on_reconfigure; } config[] = { - { "am.keepCR", "true" }, - { "core.FSCache", "true" }, - { "core.multiPackIndex", "true" }, - { "core.preloadIndex", "true" }, + /* Required */ + { "am.keepCR", "true", 1 }, + { "core.FSCache", "true", 1 }, + { "core.multiPackIndex", "true", 1 }, + { "core.preloadIndex", "true", 1 }, #ifndef WIN32 - { "core.untrackedCache", "true" }, + { "core.untrackedCache", "true", 1 }, #else /* * Unfortunately, Scalar's Functional Tests demonstrated @@ -141,29 +143,30 @@ static int set_recommended_config(void) * Therefore, with a sad heart, we disable this very useful * feature on Windows. */ - { "core.untrackedCache", "false" }, + { "core.untrackedCache", "false", 1 }, #endif - { "core.bare", "false" }, - { "core.logAllRefUpdates", "true" }, - { "credential.https://dev.azure.com.useHttpPath", "true" }, - { "credential.validate", "false" }, /* GCM4W-only */ - { "gc.auto", "0" }, - { "gui.GCWarning", "false" }, - { "index.threads", "true" }, - { "index.version", "4" }, - { "merge.stat", "false" }, - { "merge.renames", "false" }, - { "pack.useBitmaps", "false" }, - { "pack.useSparse", "true" }, - { "receive.autoGC", "false" }, - { "reset.quiet", "true" }, - { "feature.manyFiles", "false" }, - { "feature.experimental", "false" }, - { "fetch.unpackLimit", "1" }, - { "fetch.writeCommitGraph", "false" }, + { "core.bare", "false", 1 }, + { "core.logAllRefUpdates", "true", 1 }, + { "credential.https://dev.azure.com.useHttpPath", "true", 1 }, + { "credential.validate", "false", 1 }, /* GCM4W-only */ + { "gc.auto", "0", 1 }, + { "gui.GCWarning", "false", 1 }, + { "index.threads", "true", 1 }, + { "index.version", "4", 1 }, + { "merge.stat", "false", 1 }, + { "merge.renames", "false", 1 }, + { "pack.useBitmaps", "false", 1 }, + { "pack.useSparse", "true", 1 }, + { "receive.autoGC", "false", 1 }, + { "reset.quiet", "true", 1 }, + { "feature.manyFiles", "false", 1 }, + { "feature.experimental", "false", 1 }, + { "fetch.unpackLimit", "1", 1 }, + { "fetch.writeCommitGraph", "false", 1 }, #ifdef WIN32 - { "http.sslBackend", "schannel" }, + { "http.sslBackend", "schannel", 1 }, #endif + /* Optional */ { "status.aheadBehind", "false" }, { "commitGraph.generationVersion", "1" }, { "core.autoCRLF", "false" }, @@ -187,7 +190,8 @@ static int set_recommended_config(void) char *value; for (i = 0; config[i].key; i++) { - if (git_config_get_string(config[i].key, &value)) { + if ((reconfigure && config[i].overwrite_on_reconfigure) || + git_config_get_string(config[i].key, &value)) { trace2_data_string("scalar", the_repository, config[i].key, "created"); if (git_config_set_gently(config[i].key, config[i].value) < 0) @@ -255,7 +259,7 @@ static int register_dir(void) int res = add_or_remove_enlistment(1); if (!res) - res = set_recommended_config(); + res = set_recommended_config(0); if (!res) res = toggle_maintenance(1); @@ -444,7 +448,7 @@ static int cmd_clone(int argc, const char **argv) (res = run_git("sparse-checkout", "init", "--cone", NULL))) goto cleanup; - if (set_recommended_config()) + if (set_recommended_config(0)) return error(_("could not configure '%s'"), dir); if ((res = run_git("fetch", "--quiet", "origin", NULL))) { @@ -509,6 +513,24 @@ static int cmd_register(int argc, const char **argv) return register_dir(); } +static int cmd_reconfigure(int argc, const char **argv) +{ + struct option options[] = { + OPT_END(), + }; + const char * const usage[] = { + N_("scalar reconfigure []"), + NULL + }; + + argc = parse_options(argc, argv, NULL, options, + usage, 0); + + setup_enlistment_directory(argc, argv, usage, options, NULL); + + return set_recommended_config(1); +} + static int cmd_run(int argc, const char **argv) { struct option options[] = { @@ -645,6 +667,7 @@ static struct { { "register", cmd_register }, { "unregister", cmd_unregister }, { "run", cmd_run }, + { "reconfigure", cmd_reconfigure }, { NULL, NULL}, }; diff --git a/contrib/scalar/t/t9099-scalar.sh b/contrib/scalar/t/t9099-scalar.sh index 725922cd7f5d15..a59c2e52908641 100755 --- a/contrib/scalar/t/t9099-scalar.sh +++ b/contrib/scalar/t/t9099-scalar.sh @@ -65,6 +65,14 @@ test_expect_success 'scalar clone' ' ) ' +test_expect_success 'scalar reconfigure' ' + git init one/src && + scalar register one && + git -C one/src config core.preloadIndex false && + scalar reconfigure one && + test true = "$(git -C one/src config core.preloadIndex)" +' + test_expect_success '`scalar register` & `unregister` with existing repo' ' git init existing && scalar register existing && From 354d45442a48b4b9abf755d77a22bc9e01718523 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 19 May 2021 15:49:08 +0100 Subject: [PATCH 09/49] scalar: teach `diagnose` to gather packfile info Teach the `scalar diagnose` command to gather file size information about pack files. Signed-off-by: Matthew John Cheetham --- contrib/scalar/scalar.c | 39 ++++++++++++++++++++++++++++++++ contrib/scalar/t/t9099-scalar.sh | 2 ++ 2 files changed, 41 insertions(+) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index 3f4eb2f1482b40..8b19ac455f30d3 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -650,6 +650,39 @@ void load_builtin_commands(const char *prefix, struct cmdnames *cmds) die("not implemented"); } +static void dir_file_stats(struct strbuf *buf, const char *path) +{ + DIR *dir = opendir(path); + struct dirent *e; + struct stat e_stat; + struct strbuf file_path = STRBUF_INIT; + int base_path_len; + + if (!dir) + return; + + strbuf_addstr(buf, "Contents of "); + strbuf_add_absolute_path(buf, path); + strbuf_addstr(buf, ":\n"); + + strbuf_add_absolute_path(&file_path, path); + strbuf_addch(&file_path, '/'); + base_path_len = file_path.len; + + while ((e = readdir(dir)) != NULL) + if (!is_dot_or_dotdot(e->d_name) && e->d_type == DT_REG) { + strbuf_setlen(&file_path, base_path_len); + strbuf_addstr(&file_path, e->d_name); + if (!stat(file_path.buf, &e_stat)) + strbuf_addf(buf, "%-70s %16"PRIuMAX"\n", + e->d_name, + (uintmax_t)e_stat.st_size); + } + + strbuf_release(&file_path); + closedir(dir); +} + static int cmd_diagnose(int argc, const char **argv) { struct option options[] = { @@ -692,6 +725,12 @@ static int cmd_diagnose(int argc, const char **argv) if ((res = stage(tmp_dir.buf, &buf, "diagnostics.log"))) goto diagnose_cleanup; + strbuf_reset(&buf); + dir_file_stats(&buf, ".git/objects/pack"); + + if ((res = stage(tmp_dir.buf, &buf, "packs-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)) || diff --git a/contrib/scalar/t/t9099-scalar.sh b/contrib/scalar/t/t9099-scalar.sh index 1ecd257f93325a..ff86b9b7ee1b1d 100755 --- a/contrib/scalar/t/t9099-scalar.sh +++ b/contrib/scalar/t/t9099-scalar.sh @@ -75,6 +75,8 @@ test_expect_success UNZIP 'scalar diagnose' ' folder=${zip_path%.zip} && test_path_is_missing "$folder" && unzip -p "$zip_path" diagnostics.log >out && + test_file_not_empty out && + unzip -p "$zip_path" packs-local.txt >out && test_file_not_empty out ' From b098a090f2b04a1ba9b02efecb659516082a2965 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 27 Apr 2021 22:25:50 +0200 Subject: [PATCH 10/49] scalar: document the `clone` subcommand Let's populate the manual page of `scalar` a bit. Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.txt | 45 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/contrib/scalar/scalar.txt b/contrib/scalar/scalar.txt index 4edebe5e67160d..2737808c15a663 100644 --- a/contrib/scalar/scalar.txt +++ b/contrib/scalar/scalar.txt @@ -8,7 +8,8 @@ scalar - an opinionated repository management tool SYNOPSIS -------- [verse] -scalar [] +scalar clone [--single-branch] [--branch ] [--full-clone] + [--no-fetch-commits-and-trees] [] DESCRIPTION ----------- @@ -38,6 +39,48 @@ The following options can be specified _before_ the subcommand: For the duration of running the specified subcommand, configure this setting. This option imitates the same option of linkgit:git[1]. +COMMANDS +-------- + +Clone +~~~~~ + +clone [] []:: + Clones the specified repository, similar to linkgit:git-clone[1]. By + default, only commit and tree objects are cloned. Once finished, the + worktree is located at `/src`. ++ +The sparse-checkout feature is enabled (except when run with `--full-clone`) +and the only files present are those in the top-level directory. Use +`git sparse-checkout set` to expand the set of directories you want to see, +or `git sparse-checkout disable` to expand to all files (see +linkgit:git-sparse-checkout[1] for more details). You can explore the +subdirectories outside your sparse-checkout by using `git ls-tree HEAD`. + +-b :: +--branch :: + Instead of checking out the branch pointed to by the cloned repository's + HEAD, check out the `` branch instead. + +--[no-]single-branch:: + Clone only the history leading to the tip of a single branch, + either specified by the `--branch` option or the primary + branch remote's `HEAD` points at. ++ +Further fetches into the resulting repository will only update the +remote-tracking branch for the branch this option was used for the initial +cloning. If the HEAD at the remote did not point at any branch when +`--single-branch` clone was made, no remote-tracking branch is created. + +--[no-]full-clone:: + A sparse-checkout is initialized by default. This behavior can be turned + off via `--no-full-clone`. + +--[no-]fetch-commits-and-trees:: + The default behavior is to clone all commit and tree objects, with blob + objects being fetched on demand. With the `--no-fetch-commits-and-trees` + option, commit and tree objects are also fetched only as needed. + SEE ALSO -------- linkgit:git-clone[1], linkgit:git-maintenance[1]. From 7b896d106f5a1f7f65991e9cc56821668430b11a Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 21 May 2021 22:58:17 +0200 Subject: [PATCH 11/49] maintenance: create `launchctl` configuration using a lock file When two `git maintenance` processes try to write the `.plist` file, we need to help them with serializing their efforts. The 150ms time-out value was determined from thin air. Signed-off-by: Johannes Schindelin --- builtin/gc.c | 47 ++++++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/builtin/gc.c b/builtin/gc.c index 33d0faa8f07058..1c16dadde8d449 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -1602,16 +1602,14 @@ static int launchctl_remove_plists(const char *cmd) static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule, const char *cmd) { - FILE *plist; - int i; + int i, fd; const char *preamble, *repeat; const char *frequency = get_frequency(schedule); char *name = launchctl_service_name(frequency); char *filename = launchctl_service_filename(name); - - if (safe_create_leading_directories(filename)) - die(_("failed to create directories for '%s'"), filename); - plist = xfopen(filename, "w"); + struct lock_file lk = LOCK_INIT; + static unsigned long lock_file_timeout_ms = ULONG_MAX; + struct strbuf plist = STRBUF_INIT; preamble = "\n" "\n" @@ -1630,7 +1628,7 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit "\n" "StartCalendarInterval\n" "\n"; - fprintf(plist, preamble, name, exec_path, exec_path, frequency); + strbuf_addf(&plist, preamble, name, exec_path, exec_path, frequency); switch (schedule) { case SCHEDULE_HOURLY: @@ -1639,7 +1637,7 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit "Minute0\n" "\n"; for (i = 1; i <= 23; i++) - fprintf(plist, repeat, i); + strbuf_addf(&plist, repeat, i); break; case SCHEDULE_DAILY: @@ -1649,24 +1647,38 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit "Minute0\n" "\n"; for (i = 1; i <= 6; i++) - fprintf(plist, repeat, i); + strbuf_addf(&plist, repeat, i); break; case SCHEDULE_WEEKLY: - fprintf(plist, - "\n" - "Day0\n" - "Hour0\n" - "Minute0\n" - "\n"); + strbuf_addstr(&plist, + "\n" + "Day0\n" + "Hour0\n" + "Minute0\n" + "\n"); break; default: /* unreachable */ break; } - fprintf(plist, "\n\n\n"); - fclose(plist); + strbuf_addstr(&plist, "\n\n\n"); + + if (safe_create_leading_directories(filename)) + die(_("failed to create directories for '%s'"), filename); + + if ((long)lock_file_timeout_ms < 0 && + git_config_get_ulong("gc.launchctlplistlocktimeoutms", + &lock_file_timeout_ms)) + lock_file_timeout_ms = 150; + + fd = hold_lock_file_for_update_timeout(&lk, filename, LOCK_DIE_ON_ERROR, + lock_file_timeout_ms); + + if (write_in_full(fd, plist.buf, plist.len) < 0 || + commit_lock_file(&lk)) + die_errno(_("could not write '%s'"), filename); /* bootout might fail if not already running, so ignore */ launchctl_boot_plist(0, filename, cmd); @@ -1675,6 +1687,7 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit free(filename); free(name); + strbuf_release(&plist); return 0; } From 18ee232437b135b0ec93ae8be2e5477a1615ff6c Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 10 Apr 2021 23:50:27 +0200 Subject: [PATCH 12/49] Start porting `scalar.exe` to C With this patch, we start the journey from the C# project at https://github.com/microsoft/scalar to move what is left to Git's own `contrib/` directory. The idea of Scalar, and before that VFS for Git, has always been to prove that Git _can_ scale, and to upstream whatever strategies have been demonstrated to help. For example, while the virtual filesystem provided by VFS for Git helped the team developing the Windows operating system to move onto Git, it is not really an upstreamable strategy: getting it to work, and the required server-side support, make this not quite feasible. The Scalar project learned from that and tackled the problem with different tactics: instead of pretending to Git that the working directory is fully populated, it _specifically_ teaches Git about partial clone (which is based on VFS for Git's cache server), about sparse checkout (which VFS for Git tried to do transparently, in the file system layer), and regularly runs maintenance tasks to keep the repository in a healthy state. With partial clone, sparse checkout and `git maintenance` having been upstreamed, there is little left that `scalar.exe` does that which `git.exe` cannot do. One such thing is that `scalar clone ` will automatically set up a partial, sparse clone, and configure known-helpful settings from the start. Let's bring this convenience directly into Git's tree. The idea here is that you can (optionally) build Scalar via make -C contrib/scalar/Makefile This will build the `scalar` executable and put it into the contrib/scalar/ subdirectory. The slightly awkward addition of the `contrib/scalar/*` bits to the top-level `Makefile` are actually really required: we want to link to `libgit.a`, which means that we will need to use the very same `CFLAGS` and `LDFLAGS` as the rest of Git. An early development version of this patch tried to replicate the respective conditionals in `contrib/scalar/Makefile` (just like `contrib/svn-fe/Makefile` tried to do). It turned out to be quite the whack-a-mole game: the SHA-1-related flags, the flags enabling/disabling `compat/poll/`, `compat/regex/`, `compat/win32mmap.c` etc based on the current platform... To put it mildly: it was a major mess. Instead, this patch makes minimal changes to the top-level `Makefile` so that the bits in `contrib/scalar/` can be compiled and linked, and adds a `contrib/scalar/Makefile` that uses the top-level `Makefile` in a most minimal way to do the actual compiling. Note: With this commit, we only establish the infrastructure, no Scalar functionality is implemented yet; We will do that incrementally over the next few commits. Signed-off-by: Johannes Schindelin --- Makefile | 8 +++++++ contrib/scalar/.gitignore | 2 ++ contrib/scalar/Makefile | 48 +++++++++++++++++++++++++++++++++++++++ contrib/scalar/scalar.c | 36 +++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+) create mode 100644 contrib/scalar/.gitignore create mode 100644 contrib/scalar/Makefile create mode 100644 contrib/scalar/scalar.c diff --git a/Makefile b/Makefile index 9e74db39da6e67..6842b16e60e0d1 100644 --- a/Makefile +++ b/Makefile @@ -2499,6 +2499,10 @@ endif .PHONY: objects objects: $(OBJECTS) +SCALAR_SOURCES := contrib/scalar/scalar.c +SCALAR_OBJECTS := $(SCALAR_SOURCES:c=o) +OBJECTS += $(SCALAR_OBJECTS) + dep_files := $(foreach f,$(OBJECTS),$(dir $f).depend/$(notdir $f).d) dep_dirs := $(addsuffix .depend,$(sort $(dir $(OBJECTS)))) @@ -2644,6 +2648,10 @@ $(REMOTE_CURL_PRIMARY): remote-curl.o http.o http-walker.o GIT-LDFLAGS $(GITLIBS $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \ $(CURL_LIBCURL) $(EXPAT_LIBEXPAT) $(LIBS) +contrib/scalar/scalar$X: $(SCALAR_OBJECTS) GIT-LDFLAGS $(GITLIBS) + $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \ + $(filter %.o,$^) $(LIBS) + git-gvfs-helper$X: gvfs-helper.o http.o GIT-LDFLAGS $(GITLIBS) $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \ $(CURL_LIBCURL) $(EXPAT_LIBEXPAT) $(LIBS) diff --git a/contrib/scalar/.gitignore b/contrib/scalar/.gitignore new file mode 100644 index 00000000000000..ff3d47e84d0436 --- /dev/null +++ b/contrib/scalar/.gitignore @@ -0,0 +1,2 @@ +/*.exe +/scalar diff --git a/contrib/scalar/Makefile b/contrib/scalar/Makefile new file mode 100644 index 00000000000000..369e49fe2deff3 --- /dev/null +++ b/contrib/scalar/Makefile @@ -0,0 +1,48 @@ +CC = cc +RM = rm -f +MV = mv + +CFLAGS = -g -O2 -Wall +LDFLAGS = +EXTLIBS = -lz + +DESTDIR_SQ = $(subst ','\'',$(DESTDIR)) + +QUIET_SUBDIR0 = +$(MAKE) -C # space to separate -C and subdir +QUIET_SUBDIR1 = + +ifneq ($(findstring s,$(MAKEFLAGS)),s) +ifndef V + QUIET_CC = @echo ' ' CC $@; + QUIET_LINK = @echo ' ' LINK $@; + QUIET_GEN = @echo ' ' GEN $@; + QUIET_SUBDIR0 = +@subdir= + QUIET_SUBDIR1 = ;$(NO_SUBDIR) echo ' ' SUBDIR $$subdir; \ + $(MAKE) $(PRINT_DIR) -C $$subdir + export V + export QUIET_GEN + export QUIET_BUILT_IN +endif +endif + +all: + +include ../../config.mak.uname +-include ../../config.mak.autogen +-include ../../config.mak + +TARGETS = scalar$(X) scalar.o +GITLIBS = ../../common-main.o ../../libgit.a ../../xdiff/lib.a + +all: scalar$X + +$(GITLIBS): + $(QUIET_SUBDIR0)../.. $(QUIET_SUBDIR1) $(subst ../../,,$@) + +$(TARGETS): $(GITLIBS) scalar.c + $(QUIET_SUBDIR0)../.. $(QUIET_SUBDIR1) $(patsubst %,contrib/scalar/%,$@) + +clean: + $(RM) $(TARGETS) + +.PHONY: all clean FORCE diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c new file mode 100644 index 00000000000000..65220b20a1cb32 --- /dev/null +++ b/contrib/scalar/scalar.c @@ -0,0 +1,36 @@ +/* + * This is a port of Scalar to C. + */ + +#include "cache.h" +#include "gettext.h" +#include "parse-options.h" + +static struct { + const char *name; + int (*fn)(int, const char **); +} builtins[] = { + { NULL, NULL}, +}; + +int cmd_main(int argc, const char **argv) +{ + struct strbuf scalar_usage = STRBUF_INIT; + int i; + + if (argc > 1) { + argv++; + argc--; + + for (i = 0; builtins[i].name; i++) + if (!strcmp(builtins[i].name, argv[0])) + return !!builtins[i].fn(argc, argv); + } + + strbuf_addstr(&scalar_usage, + N_("scalar []\n\nCommands:\n")); + for (i = 0; builtins[i].name; i++) + strbuf_addf(&scalar_usage, "\t%s\n", builtins[i].name); + + usage(scalar_usage.buf); +} From c0af1b66ae8a096f58801f4fb8b1d3ac09c69f2b Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Mon, 12 Apr 2021 15:27:13 -0400 Subject: [PATCH 13/49] scalar: implement 'scalar list' The list is simply those registered under the multi-valued scalar.repo config setting. Signed-off-by: Derrick Stolee --- contrib/scalar/scalar.c | 48 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index 494b913a8e6635..a8ee01551dab17 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -227,9 +227,34 @@ static int toggle_maintenance(int enable) return run_git("maintenance", enable ? "start" : "unregister", NULL); } +static int add_or_remove_enlistment(int add) +{ + int res; + + if (!the_repository->worktree) + die(_("Scalar enlistments require a worktree")); + + res = run_git("config", "--global", "--get", "--fixed-value", + "scalar.repo", the_repository->worktree, NULL); + + /* + * If we want to add and the setting is already there, then do nothing. + * If we want to remove and the setting is not there, then do nothing. + */ + if ((add && !res) || (!add && res)) + return 0; + + return run_git("config", "--global", add ? "--add" : "--unset", + add ? "--no-fixed-value" : "--fixed-value", + "scalar.repo", the_repository->worktree, NULL); +} + static int register_dir(void) { - int res = set_recommended_config(); + int res = add_or_remove_enlistment(1); + + if (!res) + res = set_recommended_config(); if (!res) res = toggle_maintenance(1); @@ -239,7 +264,25 @@ static int register_dir(void) static int unregister_dir(void) { - return toggle_maintenance(0); + int res = 0; + + if (toggle_maintenance(0) < 0) + res = -1; + + if (add_or_remove_enlistment(0) < 0) + res = -1; + + return res; +} + +static int cmd_list(int argc, const char **argv) +{ + if (argc != 1) + die(_("`scalar list` does not take arguments")); + + if (run_git("config", "--global", "--get-all", "scalar.repo", NULL) < 0) + return -1; + return 0; } static int cmd_register(int argc, const char **argv) @@ -282,6 +325,7 @@ static struct { const char *name; int (*fn)(int, const char **); } builtins[] = { + { "list", cmd_list }, { "register", cmd_register }, { "unregister", cmd_unregister }, { NULL, NULL}, From d7b7bfc23fc3bd34ac72aebf64673683ead496ac Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 14 Apr 2021 23:45:16 +0200 Subject: [PATCH 14/49] scalar clone: suppress warning about `init.defaultBranch` Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index 3b3e63dcba2cab..f36c8fad5b75d2 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -7,6 +7,7 @@ #include "parse-options.h" #include "config.h" #include "run-command.h" +#include "refs.h" /* * Remove the deepest subdirectory in the provided path string. Path must not @@ -398,7 +399,16 @@ static int cmd_clone(int argc, const char **argv) dir = xstrfmt("%s/src", enlistment); - if ((res = run_git("init", "--", dir, NULL))) + strbuf_reset(&buf); + if (branch) + strbuf_addf(&buf, "init.defaultBranch=%s", branch); + else { + char *b = repo_default_branch_name(the_repository, 1); + strbuf_addf(&buf, "init.defaultBranch=%s", b); + free(b); + } + + if ((res = run_git("-c", buf.buf, "init", "--", dir, NULL))) goto cleanup; if (chdir(dir) < 0) { From f4317dd05f443fc5002bab8874634958002996f7 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 27 May 2021 07:25:18 +0200 Subject: [PATCH 15/49] scalar reconfigure: optionally handle all registered enlistments For example after a Scalar upgrade, it can come in really handy if there is an easy way to reconfigure all Scalar enlistments. This new option offers this functionality. Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 61 ++++++++++++++++++++++++++++++-- contrib/scalar/t/t9099-scalar.sh | 3 ++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index 4cf85768d73944..be2d4001c0a1ca 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -513,22 +513,77 @@ static int cmd_register(int argc, const char **argv) return register_dir(); } +static int get_scalar_repos(const char *key, const char *value, void *data) +{ + struct string_list *list = data; + + if (!strcmp(key, "scalar.repo")) + string_list_append(list, value); + + return 0; +} + static int cmd_reconfigure(int argc, const char **argv) { + int all = 0; struct option options[] = { + OPT_BOOL('a', "all", &all, + N_("reconfigure all registered enlistments")), OPT_END(), }; const char * const usage[] = { - N_("scalar reconfigure []"), + N_("scalar reconfigure [--all | ]"), NULL }; + struct string_list scalar_repos = STRING_LIST_INIT_DUP; + int i, res = 0; + struct repository r = { NULL }; + struct strbuf commondir = STRBUF_INIT, gitdir = STRBUF_INIT; argc = parse_options(argc, argv, NULL, options, usage, 0); - setup_enlistment_directory(argc, argv, usage, options, NULL); + if (!all) { + setup_enlistment_directory(argc, argv, usage, options, NULL); + + return set_recommended_config(1); + } + + if (argc > 0) + usage_msg_opt(_("--all or , but not both"), + usage, options); + + git_config(get_scalar_repos, &scalar_repos); - return set_recommended_config(1); + for (i = 0; i < scalar_repos.nr; i++) { + const char *dir = scalar_repos.items[i].string; + + strbuf_reset(&commondir); + strbuf_reset(&gitdir); + + if (chdir(dir) < 0) { + warning_errno(_("could not switch to '%s'"), dir); + res = -1; + } else if (discover_git_directory(&commondir, &gitdir) < 0) { + warning_errno(_("Git repository gone in '%s'"), dir); + res = -1; + } else { + git_config_clear(); + + the_repository = &r; + r.commondir = commondir.buf; + r.gitdir = gitdir.buf; + + if (set_recommended_config(1) < 0) + res = -1; + } + } + + string_list_clear(&scalar_repos, 1); + strbuf_release(&commondir); + strbuf_release(&gitdir); + + return res; } static int cmd_run(int argc, const char **argv) diff --git a/contrib/scalar/t/t9099-scalar.sh b/contrib/scalar/t/t9099-scalar.sh index a59c2e52908641..a2df78737a6a9e 100755 --- a/contrib/scalar/t/t9099-scalar.sh +++ b/contrib/scalar/t/t9099-scalar.sh @@ -70,6 +70,9 @@ test_expect_success 'scalar reconfigure' ' scalar register one && git -C one/src config core.preloadIndex false && scalar reconfigure one && + test true = "$(git -C one/src config core.preloadIndex)" && + git -C one/src config core.preloadIndex false && + scalar reconfigure -a && test true = "$(git -C one/src config core.preloadIndex)" ' From 57be57a21f7357442189c156a07bf76a6dc23655 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 19 May 2021 15:49:55 +0100 Subject: [PATCH 16/49] scalar: teach `diagnose` to gather loose objs info Teach the `scalar diagnose` command to gather loose object counts. Signed-off-by: Matthew John Cheetham --- contrib/scalar/scalar.c | 60 ++++++++++++++++++++++++++++++++ contrib/scalar/t/t9099-scalar.sh | 2 ++ 2 files changed, 62 insertions(+) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index 8b19ac455f30d3..d750ff63085d63 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -683,6 +683,60 @@ static void dir_file_stats(struct strbuf *buf, const char *path) closedir(dir); } +static int count_files(char *path) +{ + DIR *dir = opendir(path); + struct dirent *e; + int count = 0; + + if (!dir) + return 0; + + while ((e = readdir(dir)) != NULL) + if (!is_dot_or_dotdot(e->d_name) && e->d_type == DT_REG) + count++; + + closedir(dir); + return count; +} + +static void loose_objs_stats(struct strbuf *buf, const char *path) +{ + DIR *dir = opendir(path); + struct dirent *e; + int count; + int total = 0; + unsigned char c; + struct strbuf count_path = STRBUF_INIT; + int base_path_len; + + if (!dir) + return; + + strbuf_addstr(buf, "Object directory stats for "); + strbuf_add_absolute_path(buf, path); + strbuf_addstr(buf, ":\n"); + + strbuf_add_absolute_path(&count_path, path); + strbuf_addch(&count_path, '/'); + base_path_len = count_path.len; + + while ((e = readdir(dir)) != NULL) + if (!is_dot_or_dotdot(e->d_name) && + e->d_type == DT_DIR && strlen(e->d_name) == 2 && + !hex_to_bytes(&c, e->d_name, 1)) { + strbuf_setlen(&count_path, base_path_len); + strbuf_addstr(&count_path, e->d_name); + total += (count = count_files(count_path.buf)); + strbuf_addf(buf, "%s : %7d files\n", e->d_name, count); + } + + strbuf_addf(buf, "Total: %d loose objects", total); + + strbuf_release(&count_path); + closedir(dir); +} + static int cmd_diagnose(int argc, const char **argv) { struct option options[] = { @@ -731,6 +785,12 @@ static int cmd_diagnose(int argc, const char **argv) if ((res = stage(tmp_dir.buf, &buf, "packs-local.txt"))) goto diagnose_cleanup; + strbuf_reset(&buf); + loose_objs_stats(&buf, ".git/objects"); + + 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)) || diff --git a/contrib/scalar/t/t9099-scalar.sh b/contrib/scalar/t/t9099-scalar.sh index ff86b9b7ee1b1d..e6e73893c8ea6f 100755 --- a/contrib/scalar/t/t9099-scalar.sh +++ b/contrib/scalar/t/t9099-scalar.sh @@ -77,6 +77,8 @@ test_expect_success UNZIP 'scalar diagnose' ' unzip -p "$zip_path" diagnostics.log >out && test_file_not_empty out && unzip -p "$zip_path" packs-local.txt >out && + test_file_not_empty out && + unzip -p "$zip_path" objects-local.txt >out && test_file_not_empty out ' From 9368cbfcad6e34b1518500c9201b382f4213d039 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 19 May 2021 11:06:08 +0100 Subject: [PATCH 17/49] scalar: enable built-in FSMonitor on `register` Using the built-in FSMonitor makes many common commands quite a bit faster. So let's teach the `scalar register` command to enable the built-in FSMonitor and kick-start the fsmonitor--daemon process (for convenience). For simplicity, we only support the built-in FSMonitor (and no external file system monitor such as e.g. Watchman). Signed-off-by: Matthew John Cheetham Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 36 ++++++++++++++++++++++++++++++++ contrib/scalar/t/t9099-scalar.sh | 11 ++++++++++ 2 files changed, 47 insertions(+) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index ae06df4077b897..87574071826278 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -10,6 +10,8 @@ #include "refs.h" #include "help.h" #include "dir.h" +#include "simple-ipc.h" +#include "fsmonitor-ipc.h" /* * Remove the deepest subdirectory in the provided path string. Path must not @@ -186,6 +188,12 @@ static int set_recommended_config(int reconfigure) { "maintenance.incremental-repack.enabled", "true" }, { "maintenance.incremental-repack.auto", "0" }, { "maintenance.incremental-repack.schedule", "daily" }, +#ifdef HAVE_FSMONITOR_DAEMON_BACKEND + /* + * Enable the built-in FSMonitor on supported platforms. + */ + { "core.useBuiltinFSMonitor", "true" }, +#endif { NULL, NULL }, }; int i; @@ -256,6 +264,31 @@ static int add_or_remove_enlistment(int add) "scalar.repo", the_repository->worktree, NULL); } +static int start_fsmonitor_daemon(void) +{ +#ifdef HAVE_FSMONITOR_DAEMON_BACKEND + struct strbuf err = STRBUF_INIT; + struct child_process cp = CHILD_PROCESS_INIT; + + cp.git_cmd = 1; + strvec_pushl(&cp.args, "fsmonitor--daemon", "start", NULL); + if (!pipe_command(&cp, NULL, 0, NULL, 0, &err, 0)) { + strbuf_release(&err); + return 0; + } + + if (fsmonitor_ipc__get_state() != IPC_STATE__LISTENING) { + write_in_full(2, err.buf, err.len); + strbuf_release(&err); + return error(_("could not start the FSMonitor daemon")); + } + + strbuf_release(&err); +#endif + + return 0; +} + static int register_dir(void) { int res = add_or_remove_enlistment(1); @@ -266,6 +299,9 @@ static int register_dir(void) if (!res) res = toggle_maintenance(1); + if (!res) + res = start_fsmonitor_daemon(); + return res; } diff --git a/contrib/scalar/t/t9099-scalar.sh b/contrib/scalar/t/t9099-scalar.sh index 7fb9749b891295..bb867537f053ab 100755 --- a/contrib/scalar/t/t9099-scalar.sh +++ b/contrib/scalar/t/t9099-scalar.sh @@ -13,10 +13,21 @@ PATH=$(pwd)/..:$PATH GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab ../cron.txt" export GIT_TEST_MAINT_SCHEDULER +test_lazy_prereq BUILTIN_FSMONITOR ' + git version --build-options | grep -q "feature:.*fsmonitor--daemon" +' + test_expect_success 'scalar shows a usage' ' test_expect_code 129 scalar -h ' +test_expect_success BUILTIN_FSMONITOR 'scalar register starts fsmon daemon' ' + git init test/src && + test_must_fail git -C test/src fsmonitor--daemon status && + scalar register test/src && + git -C test/src fsmonitor--daemon status +' + test_expect_success 'scalar unregister' ' git init vanish/src && scalar register vanish/src && From 7b23cf0d43a561cada6a68b8ed1bb4baa63cf6f8 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 27 Apr 2021 22:32:05 +0200 Subject: [PATCH 18/49] scalar: document `list`, `register` and `unregister` Continuing the documentation journey. Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.txt | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/contrib/scalar/scalar.txt b/contrib/scalar/scalar.txt index 2737808c15a663..4193e06be49f7a 100644 --- a/contrib/scalar/scalar.txt +++ b/contrib/scalar/scalar.txt @@ -10,6 +10,9 @@ SYNOPSIS [verse] scalar clone [--single-branch] [--branch ] [--full-clone] [--no-fetch-commits-and-trees] [] +scalar list +scalar register [] +scalar unregister [] DESCRIPTION ----------- @@ -81,6 +84,30 @@ cloning. If the HEAD at the remote did not point at any branch when objects being fetched on demand. With the `--no-fetch-commits-and-trees` option, commit and tree objects are also fetched only as needed. +List +~~~~ + +list:: + To see which repositories are currently registered by the service, run + `scalar list`. This command, like `clone`, does not need to be run + inside a Git worktree. + +Register +~~~~~~~~ + +register []:: + Adds a repository to the list of registered repositories. If + `` is not provided, then the enlistment associated with the + current working directory is registered. + +Unregister +~~~~~~~~~~ + +unregister []:: + Remove the specified repository from the list of repositories + registered with Scalar. This stops the scheduled maintenance and the + built-in FSMonitor. + SEE ALSO -------- linkgit:git-clone[1], linkgit:git-maintenance[1]. From 807eed5fc0a7b9368ff1ea32b44a5a142672d18b Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 18 May 2021 22:48:24 +0200 Subject: [PATCH 19/49] git_config_set_multivar_in_file_gently(): add a lock timeout In particular when multiple processes want to write to the config simultaneously, it would come in handy to not fail immediately when another process locked the config, but to gently try again. This will help with Scalar's functional test suite which wants to register multiple repositories for maintenance semi-simultaneously. As not all code paths calling this function read the config (e.g. `git config`), we have to read the config setting via `git_config_get_ulong()`. Signed-off-by: Johannes Schindelin --- Documentation/config/core.txt | 9 +++++++++ config.c | 8 +++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Documentation/config/core.txt b/Documentation/config/core.txt index 50a574eb2b6703..d3a499f7ae5579 100644 --- a/Documentation/config/core.txt +++ b/Documentation/config/core.txt @@ -726,3 +726,12 @@ core.abbrev:: If set to "no", no abbreviation is made and the object names are shown in their full length. The minimum length is 4. + +core.configWriteLockTimeoutMS:: + When processes try to write to the config concurrently, it is likely + that one process "wins" and the other process(es) fail to lock the + config file. By configuring a timeout larger than zero, Git can be + told to try to lock the config again a couple times within the + specified timeout. If the timeout is configure to zero (which is the + default), Git will fail immediately when the config is already + locked. diff --git a/config.c b/config.c index 7504b96b1b2ed3..ce040332b41e19 100644 --- a/config.c +++ b/config.c @@ -2999,6 +2999,7 @@ int git_config_set_multivar_in_file_gently(const char *config_filename, const char *value_pattern, unsigned flags) { + static unsigned long timeout_ms = ULONG_MAX; int fd = -1, in_fd = -1; int ret; struct lock_file lock = LOCK_INIT; @@ -3019,11 +3020,16 @@ int git_config_set_multivar_in_file_gently(const char *config_filename, if (!config_filename) config_filename = filename_buf = git_pathdup("config"); + if ((long)timeout_ms < 0 && + git_config_get_ulong("core.configWriteLockTimeoutMS", &timeout_ms)) + timeout_ms = 0; + /* * The lock serves a purpose in addition to locking: the new * contents of .git/config will be written into it. */ - fd = hold_lock_file_for_update(&lock, config_filename, 0); + fd = hold_lock_file_for_update_timeout(&lock, config_filename, 0, + timeout_ms); if (fd < 0) { error_errno(_("could not lock config file %s"), config_filename); ret = CONFIG_NO_LOCK; From 8464b821d4de0eacdb6a8b285bcd0fe151ed19ca Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Fri, 21 May 2021 12:12:49 -0400 Subject: [PATCH 20/49] maintenance: skip bootout/bootstrap when plist is registered On macOS, we use launchctl to manage the background maintenance schedule. This uses a set of .plist files to describe the schedule, but these files are also registered with 'launchctl bootstrap'. If multiple 'git maintenance start' commands run concurrently, then they can collide replacing these schedule files and registering them with launchctl. To avoid extra launchctl commands, do a check for the .plist files on disk and check if they are registered using 'launchctl list '. This command will return with exit code 0 if it exists, or exit code 113 if it does not. We can test this behavior using the GIT_TEST_MAINT_SCHEDULER environment variable. Signed-off-by: Derrick Stolee Signed-off-by: Johannes Schindelin --- builtin/gc.c | 54 +++++++++++++++++++++++++++++++++++------- t/t7900-maintenance.sh | 17 +++++++++++++ 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/builtin/gc.c b/builtin/gc.c index 1c16dadde8d449..211476d147e156 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -1600,6 +1600,29 @@ static int launchctl_remove_plists(const char *cmd) launchctl_remove_plist(SCHEDULE_WEEKLY, cmd); } +static int launchctl_list_contains_plist(const char *name, const char *cmd) +{ + int result; + struct child_process child = CHILD_PROCESS_INIT; + char *uid = launchctl_get_uid(); + + strvec_split(&child.args, cmd); + strvec_pushl(&child.args, "list", name, NULL); + + child.no_stderr = 1; + child.no_stdout = 1; + + if (start_command(&child)) + die(_("failed to start launchctl")); + + result = finish_command(&child); + + free(uid); + + /* Returns failure if 'name' doesn't exist. */ + return !result; +} + static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule, const char *cmd) { int i, fd; @@ -1609,7 +1632,8 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit char *filename = launchctl_service_filename(name); struct lock_file lk = LOCK_INIT; static unsigned long lock_file_timeout_ms = ULONG_MAX; - struct strbuf plist = STRBUF_INIT; + struct strbuf plist = STRBUF_INIT, plist2 = STRBUF_INIT; + struct stat st; preamble = "\n" "\n" @@ -1676,18 +1700,30 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit fd = hold_lock_file_for_update_timeout(&lk, filename, LOCK_DIE_ON_ERROR, lock_file_timeout_ms); - if (write_in_full(fd, plist.buf, plist.len) < 0 || - commit_lock_file(&lk)) - die_errno(_("could not write '%s'"), filename); - - /* bootout might fail if not already running, so ignore */ - launchctl_boot_plist(0, filename, cmd); - if (launchctl_boot_plist(1, filename, cmd)) - die(_("failed to bootstrap service %s"), filename); + /* + * Does this file already exist? With the intended contents? Is it + * registered already? Then it does not need to be re-registered. + */ + if (!stat(filename, &st) && st.st_size == plist.len && + strbuf_read_file(&plist2, filename, plist.len) == plist.len && + !strbuf_cmp(&plist, &plist2) && + launchctl_list_contains_plist(name, cmd)) + rollback_lock_file(&lk); + else { + if (write_in_full(fd, plist.buf, plist.len) < 0 || + commit_lock_file(&lk)) + die_errno(_("could not write '%s'"), filename); + + /* bootout might fail if not already running, so ignore */ + launchctl_boot_plist(0, filename, cmd); + if (launchctl_boot_plist(1, filename, cmd)) + die(_("failed to bootstrap service %s"), filename); + } free(filename); free(name); strbuf_release(&plist); + strbuf_release(&plist2); return 0; } diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh index 58f46c77e66604..fc16ac22585b90 100755 --- a/t/t7900-maintenance.sh +++ b/t/t7900-maintenance.sh @@ -582,6 +582,23 @@ test_expect_success 'start and stop macOS maintenance' ' test_line_count = 0 actual ' +test_expect_success 'use launchctl list to prevent extra work' ' + # ensure we are registered + GIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start && + + # do it again on a fresh args file + rm -f args && + GIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start && + + ls "$HOME/Library/LaunchAgents" >actual && + cat >expect <<-\EOF && + list org.git-scm.git.hourly + list org.git-scm.git.daily + list org.git-scm.git.weekly + EOF + test_cmp expect args +' + test_expect_success 'start and stop Windows maintenance' ' write_script print-args <<-\EOF && echo $* >>args From 13d82ec12a37192a514c5a80959c326033fc343f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 14 Apr 2021 20:02:49 +0200 Subject: [PATCH 21/49] scalar: add a test script ... which does not do much, yet... Signed-off-by: Johannes Schindelin --- contrib/scalar/Makefile | 16 +++++-- contrib/scalar/t/Makefile | 78 ++++++++++++++++++++++++++++++++ contrib/scalar/t/t9099-scalar.sh | 17 +++++++ 3 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 contrib/scalar/t/Makefile create mode 100755 contrib/scalar/t/t9099-scalar.sh diff --git a/contrib/scalar/Makefile b/contrib/scalar/Makefile index 369e49fe2deff3..aa547c302086bb 100644 --- a/contrib/scalar/Makefile +++ b/contrib/scalar/Makefile @@ -34,7 +34,7 @@ include ../../config.mak.uname TARGETS = scalar$(X) scalar.o GITLIBS = ../../common-main.o ../../libgit.a ../../xdiff/lib.a -all: scalar$X +all: scalar$X ../../bin-wrappers/scalar $(GITLIBS): $(QUIET_SUBDIR0)../.. $(QUIET_SUBDIR1) $(subst ../../,,$@) @@ -43,6 +43,16 @@ $(TARGETS): $(GITLIBS) scalar.c $(QUIET_SUBDIR0)../.. $(QUIET_SUBDIR1) $(patsubst %,contrib/scalar/%,$@) clean: - $(RM) $(TARGETS) + $(RM) $(TARGETS) ../../bin-wrappers/scalar -.PHONY: all clean FORCE +../../bin-wrappers/scalar: ../../wrap-for-bin.sh Makefile + @mkdir -p ../../bin-wrappers + $(QUIET_GEN)sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \ + -e 's|@@BUILD_DIR@@|$(shell cd ../.. && pwd)|' \ + -e 's|@@PROG@@|contrib/scalar/scalar$(X)|' < $< > $@ && \ + chmod +x $@ + +test: all + $(MAKE) -C t + +.PHONY: all clean test FORCE diff --git a/contrib/scalar/t/Makefile b/contrib/scalar/t/Makefile new file mode 100644 index 00000000000000..6170672bb371f9 --- /dev/null +++ b/contrib/scalar/t/Makefile @@ -0,0 +1,78 @@ +# Run scalar tests +# +# Copyright (c) 2005,2021 Junio C Hamano, Johannes Schindelin +# + +-include ../../../config.mak.autogen +-include ../../../config.mak + +SHELL_PATH ?= $(SHELL) +PERL_PATH ?= /usr/bin/perl +RM ?= rm -f +PROVE ?= prove +DEFAULT_TEST_TARGET ?= test +TEST_LINT ?= test-lint + +ifdef TEST_OUTPUT_DIRECTORY +TEST_RESULTS_DIRECTORY = $(TEST_OUTPUT_DIRECTORY)/test-results +else +TEST_RESULTS_DIRECTORY = ../../../t/test-results +endif + +# Shell quote; +SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH)) +PERL_PATH_SQ = $(subst ','\'',$(PERL_PATH)) +TEST_RESULTS_DIRECTORY_SQ = $(subst ','\'',$(TEST_RESULTS_DIRECTORY)) + +T = $(sort $(wildcard t[0-9][0-9][0-9][0-9]-*.sh)) + +all: $(DEFAULT_TEST_TARGET) + +test: $(TEST_LINT) + $(MAKE) aggregate-results-and-cleanup + +prove: $(TEST_LINT) + @echo "*** prove ***"; GIT_CONFIG=.git/config $(PROVE) --exec '$(SHELL_PATH_SQ)' $(GIT_PROVE_OPTS) $(T) :: $(GIT_TEST_OPTS) + $(MAKE) clean-except-prove-cache + +$(T): + @echo "*** $@ ***"; GIT_CONFIG=.git/config '$(SHELL_PATH_SQ)' $@ $(GIT_TEST_OPTS) + +clean-except-prove-cache: + $(RM) -r 'trash directory'.* '$(TEST_RESULTS_DIRECTORY_SQ)' + $(RM) -r valgrind/bin + +clean: clean-except-prove-cache + $(RM) .prove + +test-lint: test-lint-duplicates test-lint-executable test-lint-shell-syntax + +test-lint-duplicates: + @dups=`echo $(T) | tr ' ' '\n' | sed 's/-.*//' | sort | uniq -d` && \ + test -z "$$dups" || { \ + echo >&2 "duplicate test numbers:" $$dups; exit 1; } + +test-lint-executable: + @bad=`for i in $(T); do test -x "$$i" || echo $$i; done` && \ + test -z "$$bad" || { \ + echo >&2 "non-executable tests:" $$bad; exit 1; } + +test-lint-shell-syntax: + @'$(PERL_PATH_SQ)' ../../../t/check-non-portable-shell.pl $(T) + +aggregate-results-and-cleanup: $(T) + $(MAKE) aggregate-results + $(MAKE) clean + +aggregate-results: + for f in '$(TEST_RESULTS_DIRECTORY_SQ)'/t*-*.counts; do \ + echo "$$f"; \ + done | '$(SHELL_PATH_SQ)' ../../../t/aggregate-results.sh + +valgrind: + $(MAKE) GIT_TEST_OPTS="$(GIT_TEST_OPTS) --valgrind" + +test-results: + mkdir -p test-results + +.PHONY: $(T) aggregate-results clean valgrind diff --git a/contrib/scalar/t/t9099-scalar.sh b/contrib/scalar/t/t9099-scalar.sh new file mode 100755 index 00000000000000..f2fb1db9d000fa --- /dev/null +++ b/contrib/scalar/t/t9099-scalar.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +test_description='test the `scalar` command' + +TEST_DIRECTORY=$(pwd)/../../../t +export TEST_DIRECTORY + +# Make it work with --no-bin-wrappers +PATH=$(pwd)/..:$PATH + +. ../../../t/test-lib.sh + +test_expect_success 'scalar shows a usage' ' + test_expect_code 129 scalar -h +' + +test_done From d14cc6d0d461d5daa95a3bd9bd764b0537abea3e Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 14 May 2021 23:00:48 +0200 Subject: [PATCH 22/49] scalar unregister: handle deleted enlistment directory gracefully When a user deleted an enlistment manually, let's be generous and _still_ unregister it. Co-authored-by: Victoria Dye Signed-off-by: Victoria Dye Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 46 +++++++++++++++++++++++++++++ contrib/scalar/t/t9099-scalar.sh | 50 ++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index a8ee01551dab17..183ce4de70418d 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -303,6 +303,24 @@ static int cmd_register(int argc, const char **argv) return register_dir(); } +static int remove_deleted_enlistment(struct strbuf *path) +{ + int res = 0; + strbuf_realpath_forgiving(path, path->buf, 1); + + if (run_git("config", "--global", + "--unset", "--fixed-value", + "scalar.repo", path->buf, NULL) < 0) + res = -1; + + if (run_git("config", "--global", + "--unset", "--fixed-value", + "maintenance.repo", path->buf, NULL) < 0) + res = -1; + + return res; +} + static int cmd_unregister(int argc, const char **argv) { struct option options[] = { @@ -316,6 +334,34 @@ static int cmd_unregister(int argc, const char **argv) argc = parse_options(argc, argv, NULL, options, usage, 0); + /* + * Be forgiving when the enlistment or worktree does not even exist any + * longer; This can be the case if a user deleted the worktree by + * mistake and _still_ wants to unregister the thing. + */ + if (argc == 1) { + struct strbuf src_path = STRBUF_INIT, workdir_path = STRBUF_INIT; + + strbuf_addf(&src_path, "%s/src/.git", argv[0]); + strbuf_addf(&workdir_path, "%s/.git", argv[0]); + if (!is_directory(src_path.buf) && !is_directory(workdir_path.buf)) { + /* remove possible matching registrations */ + int res = -1; + + strbuf_strip_suffix(&src_path, "/.git"); + res = remove_deleted_enlistment(&src_path) && res; + + strbuf_strip_suffix(&workdir_path, "/.git"); + res = remove_deleted_enlistment(&workdir_path) && res; + + strbuf_release(&src_path); + strbuf_release(&workdir_path); + return res; + } + strbuf_release(&src_path); + strbuf_release(&workdir_path); + } + setup_enlistment_directory(argc, argv, usage, options, NULL); return unregister_dir(); diff --git a/contrib/scalar/t/t9099-scalar.sh b/contrib/scalar/t/t9099-scalar.sh index f2fb1db9d000fa..58e0482fceb6e7 100755 --- a/contrib/scalar/t/t9099-scalar.sh +++ b/contrib/scalar/t/t9099-scalar.sh @@ -14,4 +14,54 @@ test_expect_success 'scalar shows a usage' ' test_expect_code 129 scalar -h ' +test_expect_success 'scalar unregister' ' + git init vanish/src && + scalar register vanish/src && + git config --get --global --fixed-value \ + maintenance.repo "$(pwd)/vanish/src" && + scalar list >scalar.repos && + grep -F "$(pwd)/vanish/src" scalar.repos && + rm -rf vanish/src/.git && + scalar unregister vanish && + test_must_fail git config --get --global --fixed-value \ + maintenance.repo "$(pwd)/vanish/src" && + scalar list >scalar.repos && + ! grep -F "$(pwd)/vanish/src" scalar.repos +' + +test_expect_success '`scalar register` & `unregister` with existing repo' ' + git init existing && + scalar register existing && + git config --get --global --fixed-value \ + maintenance.repo "$(pwd)/existing" && + scalar list >scalar.repos && + grep -F "$(pwd)/existing" scalar.repos && + scalar unregister existing && + test_must_fail git config --get --global --fixed-value \ + maintenance.repo "$(pwd)/existing" && + scalar list >scalar.repos && + ! grep -F "$(pwd)/existing" scalar.repos +' + +test_expect_success '`scalar unregister` with existing repo, deleted .git' ' + scalar register existing && + rm -rf existing/.git && + scalar unregister existing && + test_must_fail git config --get --global --fixed-value \ + maintenance.repo "$(pwd)/existing" && + scalar list >scalar.repos && + ! grep -F "$(pwd)/existing" scalar.repos +' + +test_expect_success '`scalar register` existing repo with `src` folder' ' + git init existing && + mkdir -p existing/src && + scalar register existing/src && + scalar list >scalar.repos && + grep -F "$(pwd)/existing" scalar.repos && + scalar unregister existing && + scalar list >scalar.repos && + ! grep -F "$(pwd)/existing" scalar.repos +' + test_done From a52a3b7baa03529ff13624dc4958f5911564789c Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 17 Apr 2021 01:51:37 +0200 Subject: [PATCH 23/49] scalar clone: respect --single-branch Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 9 +++++++-- contrib/scalar/t/t9099-scalar.sh | 6 +++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index f36c8fad5b75d2..6ef00aa4a992d7 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -350,7 +350,7 @@ static char *remote_default_branch(const char *url) static int cmd_clone(int argc, const char **argv) { const char *branch = NULL; - int no_fetch_commits_and_trees = 0, full_clone = 0; + int no_fetch_commits_and_trees = 0, full_clone = 0, single_branch = 0; struct option clone_options[] = { OPT_STRING('b', "branch", &branch, N_(""), N_("branch to checkout after clone")), @@ -359,6 +359,9 @@ static int cmd_clone(int argc, const char **argv) N_("skip fetching commits and trees after clone")), OPT_BOOL(0, "full-clone", &full_clone, N_("when cloning, create full working directory")), + OPT_BOOL(0, "single-branch", &single_branch, + N_("only download metadata for the branch that will " + "be checked out")), OPT_END(), }; const char * const clone_usage[] = { @@ -428,7 +431,9 @@ static int cmd_clone(int argc, const char **argv) if (set_config("remote.origin.url=%s", url) || set_config("remote.origin.fetch=" - "+refs/heads/*:refs/remotes/origin/*") || + "+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")) { res = error(_("could not configure remote in '%s'"), dir); diff --git a/contrib/scalar/t/t9099-scalar.sh b/contrib/scalar/t/t9099-scalar.sh index bd7ca4cd9b8406..725922cd7f5d15 100755 --- a/contrib/scalar/t/t9099-scalar.sh +++ b/contrib/scalar/t/t9099-scalar.sh @@ -45,13 +45,17 @@ test_expect_success 'set up repository to clone' ' test_expect_success 'scalar clone' ' second=$(git rev-parse --verify second:second.t) && - scalar clone "file://$(pwd)" cloned && + scalar clone "file://$(pwd)" cloned --single-branch && ( cd cloned/src && git config --get --global --fixed-value maintenance.repo \ "$(pwd)" && + git for-each-ref --format="%(refname)" refs/remotes/origin/ >actual && + echo "refs/remotes/origin/parallel" >expect && + test_cmp expect actual && + test_path_is_missing 1/2 && test_must_fail git rev-list --missing=print $second && git rev-list $second && From 3430f62db08dc4063094274a2e0f5ab35bbfdbf6 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Wed, 14 Apr 2021 16:16:30 -0400 Subject: [PATCH 24/49] scalar: implement the `run` command This is mostly just a shim for `git maintenance`, mapping task names from the way Scalar called them to the way Git calls them. Signed-off-by: Derrick Stolee Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 64 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index 6ef00aa4a992d7..fe264d0aec336f 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -509,6 +509,69 @@ static int cmd_register(int argc, const char **argv) return register_dir(); } +static int cmd_run(int argc, const char **argv) +{ + struct option options[] = { + OPT_END(), + }; + struct { + const char *arg, *task; + } tasks[] = { + { "config", NULL }, + { "commit-graph", "commit-graph" }, + { "fetch", "prefetch" }, + { "loose-objects", "loose-objects" }, + { "pack-files", "incremental-repack" }, + { NULL, NULL } + }; + struct strbuf buf = STRBUF_INIT; + const char *usagestr[] = { NULL, NULL }; + int i; + + strbuf_addstr(&buf, N_("scalar run []\nTasks:\n")); + for (i = 0; tasks[i].arg; i++) + strbuf_addf(&buf, "\t%s\n", tasks[i].arg); + usagestr[0] = buf.buf; + + argc = parse_options(argc, argv, NULL, options, + usagestr, 0); + + if (argc == 0) + usage_with_options(usagestr, options); + + if (!strcmp("all", argv[0])) + i = -1; + else { + for (i = 0; tasks[i].arg && strcmp(tasks[i].arg, argv[0]); i++) + ; /* keep looking for the task */ + + if (i > 0 && !tasks[i].arg) { + error(_("no such task: '%s'"), argv[0]); + usage_with_options(usagestr, options); + } + } + + argc--; + argv++; + setup_enlistment_directory(argc, argv, usagestr, options, NULL); + strbuf_release(&buf); + + if (i == 0) + return register_dir(); + + if (i > 0) + return run_git("maintenance", "run", + "--task", tasks[i].task, NULL); + + if (register_dir()) + return -1; + for (i = 1; tasks[i].arg; i++) + if (run_git("maintenance", "run", + "--task", tasks[i].task, NULL)) + return -1; + return 0; +} + static int remove_deleted_enlistment(struct strbuf *path) { int res = 0; @@ -581,6 +644,7 @@ static struct { { "list", cmd_list }, { "register", cmd_register }, { "unregister", cmd_unregister }, + { "run", cmd_run }, { NULL, NULL}, }; From 1bb016d58d3c6d2a793cc98c169bafe34dac5b06 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 27 May 2021 07:26:11 +0200 Subject: [PATCH 25/49] scalar: support the `config` command for backwards compatibility The .NET version supported running `scalar config` to reconfigure the current enlistment, and now the C port does, too. Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index be2d4001c0a1ca..361833aa204add 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -735,6 +735,9 @@ int cmd_main(int argc, const char **argv) argv++; argc--; + if (!strcmp(argv[0], "config")) + argv[0] = "reconfigure"; + for (i = 0; builtins[i].name; i++) if (!strcmp(builtins[i].name, argv[0])) return !!builtins[i].fn(argc, argv); From dfc57743109e5848bba99f20d723dc8a1c6dd7d8 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 28 Apr 2021 23:20:20 +0200 Subject: [PATCH 26/49] scalar diagnose: show a spinner while staging content Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index d750ff63085d63..ab0a36742bf10c 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -282,12 +282,26 @@ static int unregister_dir(void) return res; } +static void spinner(void) +{ + static const char whee[] = "|\010/\010-\010\\\010", *next = whee; + + if (!next) + return; + if (write(2, next, 2) < 0) + next = NULL; + else + next = next[2] ? next + 2 : whee; +} + static int stage(const char *git_dir, struct strbuf *buf, const char *path) { struct strbuf cacheinfo = STRBUF_INIT; struct child_process cp = CHILD_PROCESS_INIT; int res; + spinner(); + strbuf_addstr(&cacheinfo, "100644,"); cp.git_cmd = 1; From 4e1582101ca65da509a925a64fc4a512ec2c54fa Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 12 May 2021 12:06:40 +0100 Subject: [PATCH 27/49] scalar: implement the `delete` command Delete an enlistment by first unregistering the repository and then deleting the enlisment directory (the directory containing the worktree `src` directory). On Windows, if the current directory is inside the enlismtnet directory then change to the parent of the enlistment directory, to allow us to delete the enlistment. Co-authored-by: Victoria Dye Signed-off-by: Victoria Dye Signed-off-by: Matthew John Cheetham Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 52 ++++++++++++++++++++++++++++++++ contrib/scalar/t/t9099-scalar.sh | 27 +++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index ab0a36742bf10c..b4b8967291c28f 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -521,6 +521,31 @@ static char *remote_default_branch(const char *url) return NULL; } +static int delete_enlistment(struct strbuf *enlistment) +{ +#ifdef WIN32 + struct strbuf parent = STRBUF_INIT; +#endif + + if (unregister_dir()) + die(_("failed to unregister repository")); + +#ifdef WIN32 + /* Change current directory to one outside of the enlistment + so that we may delete everything underneath it. */ + strbuf_addbuf(&parent, enlistment); + strbuf_parentdir(&parent); + if (chdir(parent.buf) < 0) + die_errno(_("could not switch to '%s'"), parent.buf); + strbuf_release(&parent); +#endif + + if (remove_dir_recursively(enlistment, 0)) + die(_("failed to delete enlistment directory")); + + return 0; +} + static int cmd_clone(int argc, const char **argv) { const char *branch = NULL; @@ -1059,6 +1084,32 @@ static int cmd_unregister(int argc, const char **argv) return unregister_dir(); } +static int cmd_delete(int argc, const char **argv) +{ + struct option options[] = { + OPT_END(), + }; + const char * const usage[] = { + N_("scalar delete "), + NULL + }; + struct strbuf enlistment = STRBUF_INIT; + int res = 0; + + argc = parse_options(argc, argv, NULL, options, + usage, 0); + + if (argc != 1) + usage_with_options(usage, options); + + setup_enlistment_directory(argc, argv, usage, options, &enlistment); + + res = delete_enlistment(&enlistment); + strbuf_release(&enlistment); + + return res; +} + static struct { const char *name; int (*fn)(int, const char **); @@ -1070,6 +1121,7 @@ static struct { { "run", cmd_run }, { "reconfigure", cmd_reconfigure }, { "diagnose", cmd_diagnose }, + { "delete", cmd_delete }, { NULL, NULL}, }; diff --git a/contrib/scalar/t/t9099-scalar.sh b/contrib/scalar/t/t9099-scalar.sh index e6e73893c8ea6f..7fb9749b891295 100755 --- a/contrib/scalar/t/t9099-scalar.sh +++ b/contrib/scalar/t/t9099-scalar.sh @@ -93,6 +93,26 @@ test_expect_success 'scalar reconfigure' ' test true = "$(git -C one/src config core.preloadIndex)" ' +test_expect_success 'scalar delete without enlistment shows a usage' ' + test_expect_code 129 scalar delete +' + +test_expect_success 'scalar delete with enlistment' ' + scalar delete cloned && + test_path_is_missing cloned +' + +test_expect_success '`scalar register` parallel to worktree' ' + git init test-repo/src && + mkdir -p test-repo/out && + scalar register test-repo/out && + git config --get --global --fixed-value \ + maintenance.repo "$(pwd)/test-repo/src" && + scalar list >scalar.repos && + grep -F "$(pwd)/test-repo/src" scalar.repos && + scalar delete test-repo +' + test_expect_success '`scalar register` & `unregister` with existing repo' ' git init existing && scalar register existing && @@ -128,4 +148,11 @@ test_expect_success '`scalar register` existing repo with `src` folder' ' ! grep -F "$(pwd)/existing" scalar.repos ' +test_expect_success '`scalar delete` with existing repo' ' + git init existing && + scalar register existing && + scalar delete existing && + test_path_is_missing existing +' + test_done From c6de7ef8e97be34d1623c08c8855637cf7a2af1d Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 10 May 2021 22:44:33 +0200 Subject: [PATCH 28/49] scalar: implement the `version` command The .NET version of Scalar has a `version` command. This was necessary because it was versioned independently of Git. Since Scalar is now tightly coupled with Git, it does not make sense for them to show different versions. Therefore, it shows the same output as `git versions`. For backwards-compatibility with the .NET version, `scalar version` prints to `stderr`, though (`git version` prints to `stdout` instead). Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index b4b8967291c28f..26afd03442b3a4 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -1110,6 +1110,34 @@ static int cmd_delete(int argc, const char **argv) return res; } +static int cmd_version(int argc, const char **argv) +{ + int verbose = 0, build_options = 0; + struct option options[] = { + OPT__VERBOSE(&verbose, N_("include Git version")), + OPT_BOOL(0, "build-options", &build_options, + N_("include Git's build options")), + OPT_END(), + }; + const char * const usage[] = { + N_("scalar verbose [-v | --verbose] [--build-options]"), + NULL + }; + struct strbuf buf = STRBUF_INIT; + + argc = parse_options(argc, argv, NULL, options, + usage, 0); + + if (argc != 0) + usage_with_options(usage, options); + + get_version_info(&buf, build_options); + fprintf(stderr, "%s\n", buf.buf); + strbuf_release(&buf); + + return 0; +} + static struct { const char *name; int (*fn)(int, const char **); @@ -1122,6 +1150,7 @@ static struct { { "reconfigure", cmd_reconfigure }, { "diagnose", cmd_diagnose }, { "delete", cmd_delete }, + { "version", cmd_version }, { NULL, NULL}, }; From 6000b088d3ab5bb40e96ee32349219325a7525e8 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 29 Apr 2021 15:06:42 +0200 Subject: [PATCH 29/49] scalar: accept -C and -c options before the subcommand The `git` executable has these two very useful options: -C : switch to the specified directory before performing any actions -c =: temporarily configure this setting for the duration of the specified scalar subcommand With this commit, we teach the `scalar` executable the same trick. Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index 26afd03442b3a4..ae06df4077b897 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -1159,6 +1159,25 @@ int cmd_main(int argc, const char **argv) struct strbuf scalar_usage = STRBUF_INIT; int i; + while (argc > 1 && *argv[1] == '-') { + if (!strcmp(argv[1], "-C")) { + if (argc < 3) + die(_("-C requires a ")); + if (chdir(argv[2]) < 0) + die_errno(_("could not change to '%s'"), + argv[2]); + argc -= 2; + argv += 2; + } else if (!strcmp(argv[1], "-c")) { + if (argc < 3) + die(_("-c requires a = argument")); + git_config_push_parameter(argv[2]); + argc -= 2; + argv += 2; + } else + break; + } + if (argc > 1) { argv++; argc--; @@ -1172,7 +1191,8 @@ int cmd_main(int argc, const char **argv) } strbuf_addstr(&scalar_usage, - N_("scalar []\n\nCommands:\n")); + N_("scalar [-C ] [-c =] " + " []\n\nCommands:\n")); for (i = 0; builtins[i].name; i++) strbuf_addf(&scalar_usage, "\t%s\n", builtins[i].name); From 5379a34686169dc9d6b2958a3cbee302683c7e83 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 23 Apr 2021 00:17:20 +0200 Subject: [PATCH 30/49] scalar unregister: stop FSMonitor daemon Especially on Windows, we will need to stop that daemon, just in case that the directory needs to be removed (the daemon would otherwise hold a handle to that directory, preventing it from being deleted). Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index 87574071826278..b83474c5893100 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -289,6 +289,31 @@ static int start_fsmonitor_daemon(void) return 0; } +static int stop_fsmonitor_daemon(void) +{ +#ifdef HAVE_FSMONITOR_DAEMON_BACKEND + struct strbuf err = STRBUF_INIT; + struct child_process cp = CHILD_PROCESS_INIT; + + cp.git_cmd = 1; + strvec_pushl(&cp.args, "fsmonitor--daemon", "stop", NULL); + if (!pipe_command(&cp, NULL, 0, NULL, 0, &err, 0)) { + strbuf_release(&err); + return 0; + } + + if (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) { + write_in_full(2, err.buf, err.len); + strbuf_release(&err); + return error(_("could not stop the FSMonitor daemon")); + } + + strbuf_release(&err); +#endif + + return 0; +} + static int register_dir(void) { int res = add_or_remove_enlistment(1); @@ -315,6 +340,9 @@ static int unregister_dir(void) if (add_or_remove_enlistment(0) < 0) res = -1; + if (stop_fsmonitor_daemon() < 0) + res = -1; + return res; } From 2bc882cfe08b5cb554102578c7dd5176b8fca5cb Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 3 May 2021 17:22:57 +0200 Subject: [PATCH 31/49] scalar: document the remaining subcommands This completes the manual page for the `scalar` command. Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.txt | 52 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/contrib/scalar/scalar.txt b/contrib/scalar/scalar.txt index 4193e06be49f7a..272ad12f6f9979 100644 --- a/contrib/scalar/scalar.txt +++ b/contrib/scalar/scalar.txt @@ -13,6 +13,10 @@ scalar clone [--single-branch] [--branch ] [--full-clone] scalar list scalar register [] scalar unregister [] +scalar run ( all | config | commit-graph | fetch | loose-objects | pack-files ) [] +scalar reconfigure [ --all | ] +scalar diagnose [] +scalar delete DESCRIPTION ----------- @@ -108,6 +112,54 @@ unregister []:: registered with Scalar. This stops the scheduled maintenance and the built-in FSMonitor. +Run +~~~ + +scalar run ( all | config | commit-graph | fetch | loose-objects | pack-files ) []:: + Run the given maintenance task (or all tasks, if `all` was specified). + Except for `all` and `config`, this subcommand simply hands off to + linkgit:git-maintenance[1] (mapping `fetch` to `prefetch` and + `pack-files` to `incremental-repack`). ++ +These tasks are run automatically as part of the scheduled maintenance, +as soon as the repository is registered with Scalar. It should therefore +not be necessary to run this command manually. ++ +The `config` task is specific to Scalar and configures all those +opinionated default settings that make Git work more efficiently with +large repositories. As this task is run as part of `scalar clone` +automatically, explicit invocations of this task are rarely needed. + +Reconfigure +~~~~~~~~~~~ + +After a Scalar upgrade, or when the configuration of a Scalar enlistment +was somehow corrupted or changed by mistake, this command allows to +reconfigure the enlistment. + +With the `--all` option, all enlistments currently registered with Scalar +will be reconfigured. This option is meant to to be run every time Scalar +was upgraded. + +Diagnose +~~~~~~~~ + +diagnose []:: + When reporting issues with Scalar, it is often helpful to provide the + information gathered by this command, including logs and certain + statistics describing the data shape of the current enlistment. ++ +The output of this command is a `.zip` file that is written into +a directory adjacent to the worktree in the `src` directory. + +Delete +~~~~~~ + +delete :: + This command lets you delete an existing Scalar enlistment from your + local file system, unregistering the repository and stopping any file + watcher daemons (FSMonitor). + SEE ALSO -------- linkgit:git-clone[1], linkgit:git-maintenance[1]. From d9fa3987f6d1154b594f5545bf641feec17c2b93 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 18 May 2021 23:22:56 +0200 Subject: [PATCH 32/49] scalar: set the config write-lock timeout to 150ms By default, Git fails immediately when locking a config file for writing fails due to an existing lock. With this change, Scalar-registered repositories will fall back to trying a couple times within a 150ms timeout. Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index b83474c5893100..3d33003ca381d4 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -194,6 +194,7 @@ static int set_recommended_config(int reconfigure) */ { "core.useBuiltinFSMonitor", "true" }, #endif + { "core.configWriteLockTimeoutMS", "150" }, { NULL, NULL }, }; int i; @@ -239,16 +240,25 @@ static int set_recommended_config(int reconfigure) static int toggle_maintenance(int enable) { + unsigned long ul; + + if (git_config_get_ulong("core.configWriteLockTimeoutMS", &ul)) + git_config_push_parameter("core.configWriteLockTimeoutMS=150"); + return run_git("maintenance", enable ? "start" : "unregister", NULL); } static int add_or_remove_enlistment(int add) { int res; + unsigned long ul; if (!the_repository->worktree) die(_("Scalar enlistments require a worktree")); + if (git_config_get_ulong("core.configWriteLockTimeoutMS", &ul)) + git_config_push_parameter("core.configWriteLockTimeoutMS=150"); + res = run_git("config", "--global", "--get", "--fixed-value", "scalar.repo", the_repository->worktree, NULL); From 9ec6f61cac3bfec2b516226acc495b5a9cd54419 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 26 Apr 2021 22:08:18 +0200 Subject: [PATCH 33/49] 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 610f783da11eb7..c45b60db8c4486 100644 --- a/Makefile +++ b/Makefile @@ -2503,7 +2503,7 @@ endif .PHONY: objects objects: $(OBJECTS) -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 1f39f7bd335b18..869150bf34b6de 100644 --- a/contrib/scalar/Makefile +++ b/contrib/scalar/Makefile @@ -33,7 +33,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 @@ -41,7 +41,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 413d1830b15886bb2088dc03fd25f53cdd625b23 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 16 Apr 2021 22:15:49 +0200 Subject: [PATCH 34/49] 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 c9f61c6d8cf4c2..635d5548e2da97 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -12,6 +12,7 @@ #include "dir.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 @@ -545,6 +546,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; @@ -624,6 +699,8 @@ static int cmd_clone(int argc, const char **argv) { const char *branch = NULL; int no_fetch_commits_and_trees = 0, 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")), @@ -635,6 +712,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[] = { @@ -706,13 +786,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; @@ -751,6 +862,7 @@ static int cmd_clone(int argc, const char **argv) free(enlistment); free(dir); strbuf_release(&buf); + free(default_cache_server_url); return res; } @@ -863,6 +975,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, @@ -886,6 +999,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); @@ -926,6 +1043,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 82e799c74d6c19d9d2c37d3626b11ab15cf661f1 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 16 Apr 2021 19:47:05 +0200 Subject: [PATCH 35/49] 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 25bb4f7bbf5417..d0d67df1d08a0c 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 45cf8d71c3aba4cd4efba9d655e89a0c08b1e8b2 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 26 Apr 2021 17:31:40 +0200 Subject: [PATCH 36/49] 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. Signed-off-by: Johannes Schindelin --- gvfs-helper.c | 61 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/gvfs-helper.c b/gvfs-helper.c index 68a5c4e0c8a3be..94e05d4942bed0 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" @@ -3103,18 +3109,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 */ @@ -3136,15 +3144,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) +{ + return 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) { @@ -3589,6 +3604,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. @@ -4080,6 +4124,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 86b3106d7f09cd36c35495ebc076daa2e45cb76f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 15 May 2021 00:04:20 +0200 Subject: [PATCH 37/49] 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 194a3061eb6814..f3ce5c8860454b 100644 --- a/dir.c +++ b/dir.c @@ -2973,6 +2973,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 5bff0ac9acc8b6e116492d7c7c8d4a9150c1887f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 6 May 2021 14:35:12 +0200 Subject: [PATCH 38/49] 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 bb867537f053ab..20cbd8a7bde17b 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" 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 5fc49773480131e3f8f453f74abfda64b3dc9cce Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 3 May 2021 23:41:28 +0200 Subject: [PATCH 39/49] 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 635d5548e2da97..581dfbf617ad06 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -14,6 +14,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, @@ -121,6 +125,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 { @@ -620,6 +637,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; @@ -699,8 +797,8 @@ static int cmd_clone(int argc, const char **argv) { const char *branch = NULL; int no_fetch_commits_and_trees = 0, 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")), @@ -715,6 +813,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[] = { @@ -723,6 +824,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; @@ -753,8 +855,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); @@ -774,14 +888,58 @@ 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_string("scalar", the_repository, "dir", dir); + trace2_data_intmax("scalar", the_repository, "unattended", + is_unattended()); if (!branch && !(branch = remote_default_branch(url))) { res = error(_("failed to get default branch for '%s'"), url); 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", @@ -863,6 +1021,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; } @@ -975,7 +1136,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, @@ -1001,8 +1162,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); @@ -1044,6 +1207,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; } @@ -1371,6 +1535,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 272ad12f6f9979..8991b7576938ba 100644 --- a/contrib/scalar/scalar.txt +++ b/contrib/scalar/scalar.txt @@ -9,7 +9,8 @@ SYNOPSIS -------- [verse] scalar clone [--single-branch] [--branch ] [--full-clone] - [--no-fetch-commits-and-trees] [] + [--no-fetch-commits-and-trees] [--local-cache-path ] + [--cache-server-url ] [] scalar list scalar register [] scalar unregister [] @@ -88,6 +89,17 @@ cloning. If the HEAD at the remote did not point at any branch when objects being fetched on demand. With the `--no-fetch-commits-and-trees` option, commit and tree objects are also fetched only as needed. +--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 3c830cb44991bf23189892467239209723ebab86 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 1 Jun 2021 23:18:14 +0200 Subject: [PATCH 40/49] 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 581dfbf617ad06..2a19e00f4888f8 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -421,7 +421,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; @@ -429,13 +429,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); @@ -458,9 +459,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; } @@ -1184,13 +1186,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 f231d0f37524b8356327aaab7fc2f8c3fdff9a72 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 4 May 2021 10:32:24 +0200 Subject: [PATCH 41/49] git help: special-case `scalar` With this commit, `git help scalar` will open the appropriate manual or HTML page (instead of looking for `gitscalar`). Signed-off-by: Johannes Schindelin --- builtin/help.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/builtin/help.c b/builtin/help.c index b7eec06c3de8a9..b04f807045c312 100644 --- a/builtin/help.c +++ b/builtin/help.c @@ -395,6 +395,8 @@ static const char *cmd_to_page(const char *git_cmd) return git_cmd; else if (is_git_command(git_cmd)) return xstrfmt("git-%s", git_cmd); + else if (!strcmp("scalar", git_cmd)) + return xstrdup(git_cmd); else return xstrfmt("git%s", git_cmd); } From f66c73fce27f9fdfd5cb4ff7f9e7c78b775e1a96 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 28 Apr 2021 13:56:16 +0200 Subject: [PATCH 42/49] 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 2a19e00f4888f8..e8375ef33608f7 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -601,6 +601,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`. * @@ -612,6 +619,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)) { @@ -680,19 +694,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 00ad7ac24dbe7f8dc8bede63c4e3bb175266fd1d Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 18 May 2021 22:32:58 +0200 Subject: [PATCH 43/49] scalar: implement the `help` subcommand It is merely handing off to `git help scalar`. Signed-off-by: Johannes Schindelin --- contrib/scalar/scalar.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c index 3d33003ca381d4..c9f61c6d8cf4c2 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -1184,6 +1184,25 @@ static int cmd_delete(int argc, const char **argv) return res; } +static int cmd_help(int argc, const char **argv) +{ + struct option options[] = { + OPT_END(), + }; + const char * const usage[] = { + N_("scalar help"), + NULL + }; + + argc = parse_options(argc, argv, NULL, options, + usage, 0); + + if (argc != 0) + usage_with_options(usage, options); + + return run_git("help", "scalar", NULL); +} + static int cmd_version(int argc, const char **argv) { int verbose = 0, build_options = 0; @@ -1224,6 +1243,7 @@ static struct { { "reconfigure", cmd_reconfigure }, { "diagnose", cmd_diagnose }, { "delete", cmd_delete }, + { "help", cmd_help }, { "version", cmd_version }, { NULL, NULL}, }; From 47f5e3dff85001031a8728d189714dac3e4f4cbc Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 16 Apr 2021 21:43:57 +0200 Subject: [PATCH 44/49] 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 20cbd8a7bde17b..9644f91a6cd6b7 100755 --- a/contrib/scalar/t/t9099-scalar.sh +++ b/contrib/scalar/t/t9099-scalar.sh @@ -120,6 +120,90 @@ 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_expect_success '`scalar register` parallel to worktree' ' git init test-repo/src && mkdir -p test-repo/out && From 9344672b6fdfb6d2027eeed08f297d5243bbbd49 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 14 Apr 2021 19:20:25 +0200 Subject: [PATCH 45/49] NOT-TO-UPSTREAM: ci: build `scalar.exe`, too Signed-off-by: Johannes Schindelin --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 94b4729807bf5e..94279aa6593687 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,6 +4,7 @@ on: [push, pull_request] env: DEVELOPER: 1 + INCLUDE_SCALAR: YesPlease jobs: ci-config: From af1535a39c369170447c5719231da41405d9967e Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 4 May 2021 10:24:57 +0200 Subject: [PATCH 46/49] Optionally include `scalar` when building/testing Git The Scalar project started out as a separate project. As designated successor of VFS for Git, its goal has been from the start to apply the learnings from the VFS for Git project to allow Git to scale. In contrast to VFS for Git, Scalar very much wants Git to be aware that the checkout is sparse and the clone is partial (as opposed to VFS for Git which tried to pretend that everything is there via a virtual file system driver). The intention has always been to integrate as much as possible of Scalar into Git proper. And this has been the case: the partial clone feature, the sparse checkout (cone mode), the commit graph, multi-pack index files, scheduled maintenance tasks, all of these wonderful things can now be enjoyed by Git users _without_ having to install Scalar. So what remains of Scalar was ported to C, and put into `contrib/scalar/`. And to make it easier for folks to build and install it along with Git, this here patch makes it possible to simply drop `INCLUDE_SCALAR = YesPlease` into the `config.mak` file to make it so. Signed-off-by: Johannes Schindelin --- Makefile | 37 +++++++++++++++++++++++++++++ contrib/buildsystems/CMakeLists.txt | 15 +++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6842b16e60e0d1..610f783da11eb7 100644 --- a/Makefile +++ b/Makefile @@ -1986,6 +1986,10 @@ ifndef PAGER_ENV PAGER_ENV = LESS=FRX LV=-c endif +ifneq (,$(INCLUDE_SCALAR)) +EXTRA_PROGRAMS += contrib/scalar/scalar$X +endif + QUIET_SUBDIR0 = +$(MAKE) -C # space to separate -C and subdir QUIET_SUBDIR1 = @@ -2652,6 +2656,9 @@ contrib/scalar/scalar$X: $(SCALAR_OBJECTS) GIT-LDFLAGS $(GITLIBS) $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \ $(filter %.o,$^) $(LIBS) +bin-wrappers/scalar: contrib/scalar/Makefile + $(QUIET_SUBDIR0)contrib/scalar $(QUIET_SUBDIR1) ../../bin-wrappers/scalar + git-gvfs-helper$X: gvfs-helper.o http.o GIT-LDFLAGS $(GITLIBS) $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \ $(CURL_LIBCURL) $(EXPAT_LIBEXPAT) $(LIBS) @@ -2675,14 +2682,23 @@ Documentation/GIT-EXCLUDED-PROGRAMS: FORCE .PHONY: doc man man-perl html info pdf doc: man-perl $(MAKE) -C Documentation all +ifneq (,$(INCLUDE_SCALAR)) + $(QUIET_SUBDIR0)contrib/scalar $(QUIET_SUBDIR1) scalar.html scalar.1 +endif man: man-perl $(MAKE) -C Documentation man +ifneq (,$(INCLUDE_SCALAR)) + $(QUIET_SUBDIR0)contrib/scalar $(QUIET_SUBDIR1) scalar.1 +endif man-perl: perl/build/man/man3/Git.3pm html: $(MAKE) -C Documentation html +ifneq (,$(INCLUDE_SCALAR)) + $(QUIET_SUBDIR0)contrib/scalar $(QUIET_SUBDIR1) scalar.html +endif info: $(MAKE) -C Documentation info @@ -2930,6 +2946,10 @@ endif test_bindir_programs := $(patsubst %,bin-wrappers/%,$(BINDIR_PROGRAMS_NEED_X) $(BINDIR_PROGRAMS_NO_X) $(TEST_PROGRAMS_NEED_X)) +ifneq (,$(INCLUDE_SCALAR)) +test_bindir_programs += bin-wrappers/scalar +endif + all:: $(TEST_PROGRAMS) $(test_bindir_programs) bin-wrappers/%: wrap-for-bin.sh @@ -2950,6 +2970,9 @@ export TEST_NO_MALLOC_CHECK test: all $(MAKE) -C t/ all +ifneq (,$(INCLUDE_SCALAR)) + $(MAKE) -C contrib/scalar/t +endif perf: all $(MAKE) -C t/perf/ all @@ -3075,6 +3098,9 @@ install: all $(INSTALL) $(ALL_PROGRAMS) '$(DESTDIR_SQ)$(gitexec_instdir_SQ)' $(INSTALL) -m 644 $(SCRIPT_LIB) '$(DESTDIR_SQ)$(gitexec_instdir_SQ)' $(INSTALL) $(install_bindir_programs) '$(DESTDIR_SQ)$(bindir_SQ)' +ifneq (,$(INCLUDE_SCALAR)) + $(INSTALL) contrib/scalar/scalar$X '$(DESTDIR_SQ)$(bindir_SQ)' +endif ifdef MSVC # We DO NOT install the individual foo.o.pdb files because they # have already been rolled up into the exe's pdb file. @@ -3168,6 +3194,10 @@ install-doc: install-man-perl install-man: install-man-perl $(MAKE) -C Documentation install-man +ifneq (,$(INCLUDE_SCALAR)) + $(MAKE) -C contrib/scalar scalar.1 + $(INSTALL) contrib/scalar/scalar.1 '$(DESTDIR_SQ)$(mandir_SQ)/man1' +endif install-man-perl: man-perl $(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(mandir_SQ)/man3' @@ -3176,6 +3206,10 @@ install-man-perl: man-perl install-html: $(MAKE) -C Documentation install-html +ifneq (,$(INCLUDE_SCALAR)) + $(MAKE) -C contrib/scalar scalar.html + $(INSTALL) contrib/scalar/scalar.html '$(DESTDIR_SQ)$(htmldir)' +endif install-info: $(MAKE) -C Documentation install-info @@ -3313,6 +3347,9 @@ endif ifndef NO_TCLTK $(MAKE) -C gitk-git clean $(MAKE) -C git-gui clean +endif +ifneq (,$(INCLUDE_SCALAR)) + $(MAKE) -C contrib/scalar clean endif $(RM) GIT-VERSION-FILE GIT-CFLAGS GIT-LDFLAGS GIT-BUILD-OPTIONS $(RM) GIT-USER-AGENT GIT-PREFIX diff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt index e17d2e6ce0b240..75f7bde880c74f 100644 --- a/contrib/buildsystems/CMakeLists.txt +++ b/contrib/buildsystems/CMakeLists.txt @@ -751,6 +751,13 @@ if(CURL_FOUND) target_link_libraries(git-gvfs-helper http_obj common-main ${CURL_LIBRARIES} ) endif() +if(DEFINED ENV{INCLUDE_SCALAR} AND NOT ENV{INCLUDE_SCALAR} STREQUAL "") + add_executable(scalar ${CMAKE_SOURCE_DIR}/contrib/scalar/scalar.c ${CMAKE_SOURCE_DIR}/contrib/scalar/json-parser.c) + target_link_libraries(scalar common-main) + set_target_properties(scalar PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}/contrib/scalar) + set_target_properties(scalar PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/contrib/scalar) +endif() + parse_makefile_for_executables(git_builtin_extra "BUILT_INS") option(SKIP_DASHED_BUILT_INS "Skip hardlinking the dashed versions of the built-ins") @@ -969,7 +976,6 @@ if(CURL_FOUND) endif() endif() - foreach(script ${wrapper_scripts}) file(STRINGS ${CMAKE_SOURCE_DIR}/wrap-for-bin.sh content NEWLINE_CONSUME) string(REPLACE "@@BUILD_DIR@@" "${CMAKE_BINARY_DIR}" content "${content}") @@ -989,6 +995,13 @@ string(REPLACE "@@BUILD_DIR@@" "${CMAKE_BINARY_DIR}" content "${content}") string(REPLACE "@@PROG@@" "git-cvsserver" content "${content}") file(WRITE ${CMAKE_BINARY_DIR}/bin-wrappers/git-cvsserver ${content}) +if(DEFINED ENV{INCLUDE_SCALAR} AND NOT ENV{INCLUDE_SCALAR} STREQUAL "") + file(STRINGS ${CMAKE_SOURCE_DIR}/wrap-for-bin.sh content NEWLINE_CONSUME) + string(REPLACE "@@BUILD_DIR@@" "${CMAKE_BINARY_DIR}" content "${content}") + string(REPLACE "@@PROG@@" "contrib/scalar/scalar${EXE_EXTENSION}" content "${content}") + file(WRITE ${CMAKE_BINARY_DIR}/bin-wrappers/scalar ${content}) +endif() + #options for configuring test options option(PERL_TESTS "Perform tests that use perl" ON) option(PYTHON_TESTS "Perform tests that use python" ON) From 18a0787623d570c819eb5f7a048465b94ac7e4da Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 23 Apr 2021 16:12:33 +0200 Subject: [PATCH 47/49] 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 e8375ef33608f7..7ae318b32d5472 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -13,6 +13,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); @@ -565,6 +566,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) { @@ -634,6 +650,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"); @@ -650,7 +676,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) @@ -1557,6 +1585,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 **); @@ -1571,6 +1670,7 @@ static struct { { "delete", cmd_delete }, { "help", cmd_help }, { "version", cmd_version }, + { "cache-server", cmd_cache_server }, { NULL, NULL}, }; diff --git a/contrib/scalar/scalar.txt b/contrib/scalar/scalar.txt index 8991b7576938ba..81d3d806ae6601 100644 --- a/contrib/scalar/scalar.txt +++ b/contrib/scalar/scalar.txt @@ -18,6 +18,7 @@ scalar run ( all | config | commit-graph | fetch | loose-objects | pack-files ) scalar reconfigure [ --all | ] scalar diagnose [] scalar delete +scalar cache-server ( --get | --set | --list [] ) [] DESCRIPTION ----------- @@ -172,6 +173,27 @@ delete :: local file system, unregistering the repository and stopping any file watcher daemons (FSMonitor). +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 4e0ddc9c10d5b80838eff08f87c7da870f19afca Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 14 Apr 2021 20:33:39 +0200 Subject: [PATCH 48/49] ci(windows): also run `scalar` tests Sadly, this is a bit trickier than merely flipping the `INCLUDE_SCALAR=YesPlease` switch: The Windows tests are run in a very different way. Signed-off-by: Johannes Schindelin --- ci/run-test-slice.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ci/run-test-slice.sh b/ci/run-test-slice.sh index e7f0f923cc5a51..a179f9b255abbd 100755 --- a/ci/run-test-slice.sh +++ b/ci/run-test-slice.sh @@ -17,4 +17,9 @@ make --quiet -C t T="$(cd t && # Run the git subtree tests only if main tests succeeded test 0 != "$1" || make -C contrib/subtree test +if test 0 = "$1" && test -n "$INCLUDE_SCALAR" +then + make -C contrib/scalar/t +fi + check_unignored_build_artifacts From 5c5eccdb11665084d5d6d5b845dfd03aef4a1fa9 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 12 May 2021 17:59:58 +0200 Subject: [PATCH 49/49] 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 7ae318b32d5472..1a0419861cce1b 100644 --- a/contrib/scalar/scalar.c +++ b/contrib/scalar/scalar.c @@ -726,7 +726,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);