From 18b478ba2906440691b327d3c6ea8daed5176632 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 18 May 2021 22:48:24 +0200 Subject: [PATCH 01/30] 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 458a3f4f7f5d80..c81882b358ec8d 100644 --- a/Documentation/config/core.txt +++ b/Documentation/config/core.txt @@ -777,3 +777,12 @@ core.WSLCompat:: The default value is false. When set to true, Git will set the mode bits of the file in the way of wsl, so that the executable flag of files can be set or read correctly. + +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 5b0d48b94e81cd..7b4c068cff0b5e 100644 --- a/config.c +++ b/config.c @@ -3299,6 +3299,7 @@ int git_config_set_multivar_in_file_gently(const char *config_filename, const char *comment, unsigned flags) { + static unsigned long timeout_ms = ULONG_MAX; int fd = -1, in_fd = -1; int ret; struct lock_file lock = LOCK_INIT; @@ -3319,11 +3320,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 98c5aee002f493020338e9ec500f3f4b0d87b546 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 18 May 2021 23:22:56 +0200 Subject: [PATCH 02/30] 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 --- scalar.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scalar.c b/scalar.c index 1fe8a93e6529a2..9806d37ec42566 100644 --- a/scalar.c +++ b/scalar.c @@ -170,6 +170,7 @@ static int set_recommended_config(int reconfigure) { "core.autoCRLF", "false" }, { "core.safeCRLF", "false" }, { "fetch.showForcedUpdates", "false" }, + { "core.configWriteLockTimeoutMS", "150" }, { NULL, NULL }, }; int i; @@ -211,6 +212,11 @@ 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", enable ? NULL : "--force", @@ -220,10 +226,14 @@ static int toggle_maintenance(int enable) 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 0d2ced9ce64040783e985ac87e939951e4fbaf26 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Wed, 16 Jun 2021 10:01:37 -0400 Subject: [PATCH 03/30] scalar: add docs from microsoft/scalar These docs have been altered to fit the version implemented in C within microsoft/git. This means in particular that the advanced.md file no longer applied at all. Some other areas were removed or significantly edited. Signed-off-by: Derrick Stolee --- contrib/scalar/docs/faq.md | 51 ++++++++++++++ contrib/scalar/docs/getting-started.md | 93 ++++++++++++++++++++++++++ contrib/scalar/docs/index.md | 50 ++++++++++++++ contrib/scalar/docs/philosophy.md | 66 ++++++++++++++++++ contrib/scalar/docs/troubleshooting.md | 20 ++++++ 5 files changed, 280 insertions(+) create mode 100644 contrib/scalar/docs/faq.md create mode 100644 contrib/scalar/docs/getting-started.md create mode 100644 contrib/scalar/docs/index.md create mode 100644 contrib/scalar/docs/philosophy.md create mode 100644 contrib/scalar/docs/troubleshooting.md diff --git a/contrib/scalar/docs/faq.md b/contrib/scalar/docs/faq.md new file mode 100644 index 00000000000000..a14f78a996d5d5 --- /dev/null +++ b/contrib/scalar/docs/faq.md @@ -0,0 +1,51 @@ +Frequently Asked Questions +========================== + +Using Scalar +------------ + +### I don't want a sparse clone, I want every file after I clone! + +Run `scalar clone --full-clone ` to initialize your repo to include +every file. You can switch to a sparse-checkout later by running +`git sparse-checkout init --cone`. + +### I already cloned without `--full-clone`. How do I get everything? + +Run `git sparse-checkout disable`. + +Scalar Design Decisions +----------------------- + +There may be many design decisions within Scalar that are confusing at first +glance. Some of them may cause friction when you use Scalar with your existing +repos and existing habits. + +> Scalar has the most benefit when users design repositories +> with efficient patterns. + +For example: Scalar uses the sparse-checkout feature to limit the size of the +working directory within a large monorepo. It is designed to work efficiently +with monorepos that are highly componentized, allowing most developers to +need many fewer files in their daily work. + +### Why does `scalar clone` create a `/src` folder? + +Scalar uses a file system watcher to keep track of changes under this `src` folder. +Any activity in this folder is assumed to be important to Git operations. By +creating the `src` folder, we are making it easy for your build system to +create output folders outside the `src` directory. We commonly see systems +create folders for build outputs and package downloads. Scalar itself creates +these folders during its builds. + +Your build system may create build artifacts such as `.obj` or `.lib` files +next to your source code. These are commonly "hidden" from Git using +`.gitignore` files. Having such artifacts in your source tree creates +additional work for Git because it needs to look at these files and match them +against the `.gitignore` patterns. + +By following the `src` pattern Scalar tries to establish and placing your build +intermediates and outputs parallel with the `src` folder and not inside it, +you can help optimize Git command performance for developers in the repository +by limiting the number of files Git needs to consider for many common +operations. diff --git a/contrib/scalar/docs/getting-started.md b/contrib/scalar/docs/getting-started.md new file mode 100644 index 00000000000000..ef7ea07b0f948c --- /dev/null +++ b/contrib/scalar/docs/getting-started.md @@ -0,0 +1,93 @@ +Getting Started +=============== + +Registering existing Git repos +------------------------------ + +To add a repository to the list of registered repos, run `scalar register []`. +If `` is not provided, then the "current repository" is discovered from +the working directory by scanning the parent paths for a path containing a `.git` +folder, possibly inside a `src` folder. + +To see which repositories are currently tracked by the service, run +`scalar list`. + +Run `scalar unregister []` to remove the repo from this list. + +Creating a new Scalar clone +--------------------------------------------------- + +The `clone` verb creates a local enlistment of a remote repository using the +partial clone feature available e.g. on GitHub. + + +``` +scalar clone [options] [] +``` + +Create a local copy of the repository at ``. If specified, create the `` +directory and place the repository there. Otherwise, the last section of the `` +will be used for ``. + +At the end, the repo is located at `/src`. By default, the sparse-checkout +feature is enabled and the only files present are those in the root of your +Git repository. 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. You +can explore the subdirectories outside your sparse-checkout specification using +`git ls-tree HEAD`. + +### Sparse Repo Mode + +By default, Scalar reduces your working directory to only the files at the +root of the repository. You need to add the folders you care about to build up +to your working set. + +* `scalar clone ` + * Please choose the **Clone with HTTPS** option in the `Clone Repository` dialog in Azure Repos, not **Clone with SSH**. +* `cd \src` +* At this point, your `src` directory only contains files that appear in your root + tree. No folders are populated. +* Set the directory list for your sparse-checkout using: + 1. `git sparse-checkout set ...` + 2. `git sparse-checkout set --stdin < dir-list.txt` +* Run git commands as you normally would. +* To fully populate your working directory, run `git sparse-checkout disable`. + +If instead you want to start with all files on-disk, you can clone with the +`--full-clone` option. To enable sparse-checkout after the fact, run +`git sparse-checkout init --cone`. This will initialize your sparse-checkout +patterns to only match the files at root. + +If you are unfamiliar with what directories are available in the repository, +then you can run `git ls-tree -d --name-only HEAD` to discover the directories +at root, or `git ls-tree -d --name-only HEAD ` to discover the directories +in ``. + +### Options + +These options allow a user to customize their initial enlistment. + +* `--full-clone`: If specified, do not initialize the sparse-checkout feature. + All files will be present in your `src` directory. This uses a Git partial + clone: blobs are downloaded on demand. + +* `--branch=`: Specify the branch to checkout after clone. + +### Advanced Options + +The options below are not intended for use by a typical user. These are +usually used by build machines to create a temporary enlistment that +operates on a single commit. + +* `--single-branch`: Use this option to only download metadata for the branch + that will be checked out. This is helpful for build machines that target + a remote with many branches. Any `git fetch` commands after the clone will + still ask for all branches. + +Removing a Scalar Clone +----------------------- + +Since the `scalar clone` command sets up a file-system watcher (when available), +that watcher could prevent deleting the enlistment. Run `scalar delete ` +from outside of your enlistment to unregister the enlistment from the filesystem +watcher and delete the enlistment at ``. diff --git a/contrib/scalar/docs/index.md b/contrib/scalar/docs/index.md new file mode 100644 index 00000000000000..f9f5ab06e09253 --- /dev/null +++ b/contrib/scalar/docs/index.md @@ -0,0 +1,50 @@ +Scalar: Enabling Git at Scale +============================= + +Scalar is a tool that helps Git scale to some of the largest Git repositories. +It achieves this by enabling some advanced Git features, such as: + +* *Partial clone:* reduces time to get a working repository by not + downloading all Git objects right away. + +* *Background prefetch:* downloads Git object data from all remotes every + hour, reducing the amount of time for foreground `git fetch` calls. + +* *Sparse-checkout:* limits the size of your working directory. + +* *File system monitor:* tracks the recently modified files and eliminates + the need for Git to scan the entire worktree. + +* *Commit-graph:* accelerates commit walks and reachability calculations, + speeding up commands like `git log`. + +* *Multi-pack-index:* enables fast object lookups across many pack-files. + +* *Incremental repack:* Repacks the packed Git data into fewer pack-file + without disrupting concurrent commands by using the multi-pack-index. + +By running `scalar register` in any Git repo, Scalar will automatically enable +these features for that repo (except partial clone) and start running suggested +maintenance in the background using +[the `git maintenance` feature](https://git-scm.com/docs/git-maintenance). + +Repos cloned with the `scalar clone` command use partial clone to significantly +reduce the amount of data required to get started using a repository. By +delaying all blob downloads until they are required, Scalar allows you to work +with very large repositories quickly. + +Documentation +------------- + +* [Getting Started](getting-started.md): Get started with Scalar. + Includes `scalar register`, `scalar unregister`, `scalar clone`, and + `scalar delete`. + +* [Troubleshooting](troubleshooting.md): + Collect diagnostic information or update custom settings. Includes + `scalar diagnose`. + +* [The Philosophy of Scalar](philosophy.md): Why does Scalar work the way + it does, and how do we make decisions about its future? + +* [Frequently Asked Questions](faq.md) diff --git a/contrib/scalar/docs/philosophy.md b/contrib/scalar/docs/philosophy.md new file mode 100644 index 00000000000000..51486a75e41f0d --- /dev/null +++ b/contrib/scalar/docs/philosophy.md @@ -0,0 +1,66 @@ +The Philosophy of Scalar +======================== + +The team building Scalar has **opinions** about Git performance. Scalar +takes out the guesswork by automatically configuring your Git repositories +to take advantage of the latest and greatest features. It is difficult to +say that these are the absolute best settings for every repository, but +these settings do work for some of the largest repositories in the world. + +Scalar intends to do very little more than the standard Git client. We +actively implement new features into Git instead of Scalar, then update +Scalar only to configure those new settings. In particular, we ported +features like background maintenance to Git to make Scalar simpler and +make Git more powerful. + +Services such as GitHub support partial clone , a standard adopted by the Git +project to download only part of the Git objects when cloning, and fetching +further objects on demand. If your hosting service supports partial clone, then +we absolutely recommend it as a way to greatly speed up your clone and fetch +times and to reduce how much disk space your Git repository requires. Scalar +will help with this! + +Most of the value of Scalar can be found in the core Git client. However, most +of the advanced features that really optimize Git's performance are off by +default for compatibility reasons. To really take advantage of Git's latest and +greatest features, you either need to study the [`git config` +documentation](https://git-scm.com/docs/git-config) and regularly read [the Git +release notes](https://github.com/git/git/tree/master/Documentation/RelNotes). +Even if you do all that work and customize your Git settings on your machines, +you likely will want to share those settings with other team members. Or, you +can just use Scalar! + +Using `scalar register` on an existing Git repository will give you these +benefits: + +* Additional compression of your `.git/index` file. +* Hourly background `git fetch` operations, keeping you in-sync with your + remotes. +* Advanced data structures, such as the `commit-graph` and `multi-pack-index` + are updated automatically in the background. +* If using macOS or Windows, then Scalar configures Git's builtin File System + Monitor, providing faster commands such as `git status` or `git add`. + +Additionally, if you use `scalar clone` to create a new repository, then +you will automatically get these benefits: + +* Use Git's partial clone feature to only download the files you need for + your current checkout. +* Use Git's [sparse-checkout feature][sparse-checkout] to minimize the + number of files required in your working directory. + [Read more about sparse-checkout here.][sparse-checkout-blog] +* Create the Git repository inside `/src` to make it easy to + place build artifacts outside of the Git repository, such as in + `/bin` or `/packages`. + +We also admit that these **opinions** can always be improved! If you have +an idea of how to improve our setup, consider +[creating an issue](https://github.com/microsoft/scalar/issues/new) or +contributing a pull request! Some [existing](https://github.com/microsoft/scalar/issues/382) +[issues](https://github.com/microsoft/scalar/issues/388) have already +improved our configuration settings and roadmap! + +[gvfs-protocol]: https://github.com/microsoft/VFSForGit/blob/HEAD/Protocol.md +[microsoft-git]: https://github.com/microsoft/git +[sparse-checkout]: https://git-scm.com/docs/git-sparse-checkout +[sparse-checkout-blog]: https://github.blog/2020-01-17-bring-your-monorepo-down-to-size-with-sparse-checkout/ diff --git a/contrib/scalar/docs/troubleshooting.md b/contrib/scalar/docs/troubleshooting.md new file mode 100644 index 00000000000000..8ec56ad437ff09 --- /dev/null +++ b/contrib/scalar/docs/troubleshooting.md @@ -0,0 +1,20 @@ +Troubleshooting +=============== + +Diagnosing Issues +----------------- + +The `scalar diagnose` command collects logs and config details for the current +repository. The resulting zip file helps root-cause issues. + +When run inside your repository, creates a zip file containing several important +files for that repository. This includes: + +* Configuration files from your `.git` folder, such as the `config` file, + `index`, `hooks`, and `refs`. + +* A summary of your Git object database, including the number of loose objects + and the names and sizes of pack-files. + +As the `diagnose` command completes, it provides the path of the resulting +zip file. This zip can be attached to bug reports to make the analysis easier. From 2a9be73b7ce2b72fecd0db88013898705c8b6dd6 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 10 May 2022 13:43:05 +0200 Subject: [PATCH 04/30] scalar (Windows): use forward slashes as directory separators Git traditionally uses those, not backslashes, ever. Signed-off-by: Johannes Schindelin --- scalar.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scalar.c b/scalar.c index 9806d37ec42566..022e527f3c9246 100644 --- a/scalar.c +++ b/scalar.c @@ -46,6 +46,9 @@ static void setup_enlistment_directory(int argc, const char **argv, die(_("need a working directory")); strbuf_trim_trailing_dir_sep(&path); +#ifdef GIT_WINDOWS_NATIVE + convert_slashes(path.buf); +#endif /* check if currently in enlistment root with src/ workdir */ len = path.len; From 0c924c87c2c243d148d00b6b24206a32ce1b9ed2 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Thu, 17 Jun 2021 11:40:09 -0400 Subject: [PATCH 05/30] scalar: add retry logic to run_git() Use a fixed 3 tries total to see how that increases our chances of success for subcommands such as 'git fetch'. We special-case the `diagnose` command here: When 672196a3073 (scalar-diagnose: use 'git diagnose --mode=all', 2022-08-12) updated 'scalar diagnose' to run 'git diagnose' as a subprocess, it was passed through the run_git() caller. We need to avoid repeating the call when the underlying 'git diagnose' command fails. Signed-off-by: Derrick Stolee --- scalar.c | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/scalar.c b/scalar.c index 022e527f3c9246..fba49fdb7bda32 100644 --- a/scalar.c +++ b/scalar.c @@ -75,21 +75,34 @@ static void setup_enlistment_directory(int argc, const char **argv, strbuf_release(&path); } +static int git_retries = 3; + LAST_ARG_MUST_BE_NULL static int run_git(const char *arg, ...) { - struct child_process cmd = CHILD_PROCESS_INIT; va_list args; const char *p; + struct strvec argv = STRVEC_INIT; + int res = 0, attempts; va_start(args, arg); - strvec_push(&cmd.args, arg); + strvec_push(&argv, arg); while ((p = va_arg(args, const char *))) - strvec_push(&cmd.args, p); + strvec_push(&argv, p); va_end(args); - cmd.git_cmd = 1; - return run_command(&cmd); + for (attempts = 0, res = 1; + res && attempts < git_retries; + attempts++) { + struct child_process cmd = CHILD_PROCESS_INIT; + + cmd.git_cmd = 1; + strvec_pushv(&cmd.args, argv.v); + res = run_command(&cmd); + } + + strvec_clear(&argv); + return res; } struct scalar_config { @@ -579,6 +592,8 @@ static int cmd_diagnose(int argc, const char **argv) setup_enlistment_directory(argc, argv, usage, options, &diagnostics_root); strbuf_addstr(&diagnostics_root, "/.scalarDiagnostics"); + /* Here, a failure should not repeat itself. */ + git_retries = 1; res = run_git("diagnose", "--mode=all", "-s", "%Y%m%d_%H%M%S", "-o", diagnostics_root.buf, NULL); From 5ddb00ed7dbad59792d7055ec62b2ecd1e3c8cd3 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 27 May 2021 07:26:11 +0200 Subject: [PATCH 06/30] 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 --- scalar.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scalar.c b/scalar.c index fba49fdb7bda32..9266d2a2041a9b 100644 --- a/scalar.c +++ b/scalar.c @@ -1001,6 +1001,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 4b6244171139b0a87d3d0b7df3ee521b3a919221 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 26 Apr 2021 22:08:18 +0200 Subject: [PATCH 07/30] 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 | 3 +- contrib/buildsystems/CMakeLists.txt | 2 +- json-parser.c | 183 ++++++++++++++++++++++++++++ json-parser.h | 29 +++++ 4 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 json-parser.c create mode 100644 json-parser.h diff --git a/Makefile b/Makefile index a1186e324b8e33..8e2841cbc39cb2 100644 --- a/Makefile +++ b/Makefile @@ -2766,6 +2766,7 @@ GIT_OBJS += git.o .PHONY: git-objs git-objs: $(GIT_OBJS) +SCALAR_OBJS := json-parser.o SCALAR_OBJS += scalar.o .PHONY: scalar-objs scalar-objs: $(SCALAR_OBJS) @@ -2915,7 +2916,7 @@ $(REMOTE_CURL_PRIMARY): remote-curl.o http.o http-walker.o $(LAZYLOAD_LIBCURL_OB $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \ $(CURL_LIBCURL) $(EXPAT_LIBEXPAT) $(LIBS) -scalar$X: scalar.o GIT-LDFLAGS $(GITLIBS) +scalar$X: $(SCALAR_OBJS) GIT-LDFLAGS $(GITLIBS) $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \ $(filter %.o,$^) $(LIBS) diff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt index fcb04b79806de1..30379e37549ae6 100644 --- a/contrib/buildsystems/CMakeLists.txt +++ b/contrib/buildsystems/CMakeLists.txt @@ -799,7 +799,7 @@ target_link_libraries(git-sh-i18n--envsubst common-main) add_executable(git-shell ${CMAKE_SOURCE_DIR}/shell.c) target_link_libraries(git-shell common-main) -add_executable(scalar ${CMAKE_SOURCE_DIR}/scalar.c) +add_executable(scalar ${CMAKE_SOURCE_DIR}/scalar.c ${CMAKE_SOURCE_DIR}/json-parser.c) target_link_libraries(scalar common-main) if(CURL_FOUND) diff --git a/json-parser.c b/json-parser.c new file mode 100644 index 00000000000000..539320b7b2f940 --- /dev/null +++ b/json-parser.c @@ -0,0 +1,183 @@ +#include "git-compat-util.h" +#include "hex.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/json-parser.h b/json-parser.h new file mode 100644 index 00000000000000..ce1fdc5ee23928 --- /dev/null +++ b/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 fb49882ceaa2facca29d65162b7be9d87da84e7f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 24 Aug 2021 21:05:42 +0200 Subject: [PATCH 08/30] 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). Note: we need to limit the re-try logic in `git clone` to handle only the non-GVFS case: the call to `set_config()` to un-set the partial clone settings would otherwise fail because those settings would not exist in the GVFS protocol case. This will at least give us a clearer reason why such a fetch fails. Co-authored-by: Derrick Stolee Signed-off-by: Johannes Schindelin Signed-off-by: Derrick Stolee --- diagnose.c | 8 ++++ scalar.c | 126 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 131 insertions(+), 3 deletions(-) diff --git a/diagnose.c b/diagnose.c index cc2d535b60d7c8..37dc5de5aa26a1 100644 --- a/diagnose.c +++ b/diagnose.c @@ -13,6 +13,7 @@ #include "packfile.h" #include "parse-options.h" #include "write-or-die.h" +#include "config.h" struct archive_dir { const char *path; @@ -185,6 +186,7 @@ int create_diagnostics_archive(struct strbuf *zip_path, enum diagnose_mode mode) struct strvec archiver_args = STRVEC_INIT; char **argv_copy = NULL; int stdout_fd = -1, archiver_fd = -1; + char *cache_server_url = NULL; struct strbuf buf = STRBUF_INIT; int res, i; struct archive_dir archive_dirs[] = { @@ -220,6 +222,11 @@ int create_diagnostics_archive(struct strbuf *zip_path, enum diagnose_mode mode) get_version_info(&buf, 1); strbuf_addf(&buf, "Repository 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); write_or_die(stdout_fd, buf.buf, buf.len); strvec_pushf(&archiver_args, @@ -277,6 +284,7 @@ int create_diagnostics_archive(struct strbuf *zip_path, enum diagnose_mode mode) free(argv_copy); strvec_clear(&archiver_args); strbuf_release(&buf); + free(cache_server_url); return res; } diff --git a/scalar.c b/scalar.c index 9266d2a2041a9b..451ed97042d87e 100644 --- a/scalar.c +++ b/scalar.c @@ -19,6 +19,7 @@ #include "help.h" #include "setup.h" #include "trace2.h" +#include "json-parser.h" static void setup_enlistment_directory(int argc, const char **argv, const char * const *usagestr, @@ -338,6 +339,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; @@ -436,6 +511,8 @@ static int cmd_clone(int argc, const char **argv) const char *branch = NULL; int full_clone = 0, single_branch = 0, show_progress = isatty(2); int src = 1; + 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")), @@ -446,6 +523,9 @@ static int cmd_clone(int argc, const char **argv) "be checked out")), OPT_BOOL(0, "src", &src, N_("create repository within 'src' directory")), + 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[] = { @@ -457,6 +537,7 @@ static int cmd_clone(int argc, const char **argv) char *enlistment = NULL, *dir = NULL; struct strbuf buf = STRBUF_INIT; int res; + int gvfs_protocol; argc = parse_options(argc, argv, NULL, clone_options, clone_usage, 0); @@ -522,13 +603,46 @@ 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; + } + + gvfs_protocol = cache_server_url || + supports_gvfs_protocol(url, &default_cache_server_url); + + if (gvfs_protocol) { + 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; @@ -539,6 +653,11 @@ static int cmd_clone(int argc, const char **argv) if ((res = run_git("fetch", "--quiet", show_progress ? "--progress" : "--no-progress", "origin", NULL))) { + if (gvfs_protocol) { + res = error(_("failed to prefetch commits and trees")); + goto cleanup; + } + warning(_("partial clone failed; attempting full clone")); if (set_config("remote.origin.promisor") || @@ -571,6 +690,7 @@ static int cmd_clone(int argc, const char **argv) free(enlistment); free(dir); strbuf_release(&buf); + free(default_cache_server_url); return res; } From 105eca3bfda871d86c53b893877754b3f89c376c Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 16 Apr 2021 19:47:05 +0200 Subject: [PATCH 09/30] 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 | 50 +++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/t/helper/test-gvfs-protocol.c b/t/helper/test-gvfs-protocol.c index fb70b7a47dee2b..52ab0c04d0ce9e 100644 --- a/t/helper/test-gvfs-protocol.c +++ b/t/helper/test-gvfs-protocol.c @@ -1,6 +1,7 @@ #define USE_THE_REPOSITORY_VARIABLE #include "git-compat-util.h" #include "environment.h" +#include "gettext.h" #include "hex.h" #include "alloc.h" #include "setup.h" @@ -1500,6 +1501,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; @@ -1548,6 +1551,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, "REQUEST_METHOD=%s", method); + strvec_pushf(&cp.env, "PATH_TRANSLATED=%s", + req->uri_base.buf); + /* Prevent MSYS2 from "converting to a Windows path" */ + strvec_pushf(&cp.env, + "MSYS2_ENV_CONV_EXCL=PATH_TRANSLATED"); + strvec_push(&cp.env, "SERVER_PROTOCOL=HTTP/1.1"); + if (req->quest_args.len) + strvec_pushf(&cp.env, "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, "CONTENT_TYPE=%s", + header + 14); + else if (!strncasecmp("Content-Length: ", header, 16)) + strvec_pushf(&cp.env, "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 c2ec76d014601c978b821e88a61c2b42a977d8ba Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 26 Apr 2021 17:31:40 +0200 Subject: [PATCH 10/30] 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 04c9c460c6ce36..d53470ad80f775 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). +// ////////////////////////////////////////////////////////////////// #define USE_THE_REPOSITORY_VARIABLE @@ -3126,18 +3132,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 */ @@ -3159,15 +3167,22 @@ static void do__http_get__gvfs_config(struct gh__response_status *status, * see any need to report progress on the upload side of * the GET. So just report progress on the download side. */ - strbuf_addstr(¶ms.progress_base_phase3_msg, - "Receiving gvfs/config"); + strbuf_addf(¶ms.progress_base_phase3_msg, + "Receiving %s", endpoint); } - do_req__with_fallback("gvfs/config", ¶ms, status); + do_req__with_fallback(endpoint, ¶ms, status); gh__request_params__release(¶ms); } +static void do__http_get__gvfs_config(struct gh__response_status *status, + struct strbuf *config_data) +{ + do__http_get__simple_endpoint(status, config_data, "gvfs/config", + "GET/config"); +} + static void setup_gvfs_objects_progress(struct gh__request_params *params, unsigned long num, unsigned long den) { @@ -3612,6 +3627,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. @@ -4103,6 +4147,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 4977bc048a31ee42c45e8d1097de45526af013c4 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 15 May 2021 00:04:20 +0200 Subject: [PATCH 11/30] 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 0c2b2039791361..6d0f4e3d5a46a6 100644 --- a/dir.c +++ b/dir.c @@ -3226,6 +3226,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 ba48f969f151e1307696f686c6285c12f5a7123f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 6 May 2021 14:35:12 +0200 Subject: [PATCH 12/30] scalar: disable authentication in unattended mode Modified to remove call to is_unattended() that has not been implemented yet. Signed-off-by: Johannes Schindelin --- t/t9210-scalar.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/t/t9210-scalar.sh b/t/t9210-scalar.sh index a41b4fcc0859db..57377313fec6be 100755 --- a/t/t9210-scalar.sh +++ b/t/t9210-scalar.sh @@ -7,6 +7,13 @@ test_description='test the `scalar` command' GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab cron.txt,launchctl:true,schtasks:true" export GIT_TEST_MAINT_SCHEDULER +# Do not write any files outside the trash directory +Scalar_UNATTENDED=1 +export Scalar_UNATTENDED + +GIT_ASKPASS=true +export GIT_ASKPASS + test_expect_success 'scalar shows a usage' ' test_expect_code 129 scalar -h ' From a6bea324c02379ece93020b9bbf092a5cb80fb33 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 3 May 2021 23:41:28 +0200 Subject: [PATCH 13/30] 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 --- Documentation/scalar.txt | 14 ++- diagnose.c | 9 +- scalar.c | 186 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 203 insertions(+), 6 deletions(-) diff --git a/Documentation/scalar.txt b/Documentation/scalar.txt index 361f51a64736fa..8e69c608b4c9d6 100644 --- a/Documentation/scalar.txt +++ b/Documentation/scalar.txt @@ -9,7 +9,8 @@ SYNOPSIS -------- [verse] scalar clone [--single-branch] [--branch ] [--full-clone] - [--[no-]src] [] + [--[no-]src] [--local-cache-path ] [--cache-server-url ] + [] scalar list scalar register [] scalar unregister [] @@ -90,6 +91,17 @@ cloning. If the HEAD at the remote did not point at any branch when A sparse-checkout is initialized by default. This behavior can be turned off via `--full-clone`. +--local-cache-path :: + Override the path to the local cache root directory; Pre-fetched objects + are stored into a repository-dependent subdirectory of that path. ++ +The default is `:\.scalarCache` on Windows (on the same drive as the +clone), and `~/.scalarCache` on macOS. + +--cache-server-url :: + Retrieve missing objects from the specified remote, which is expected to + understand the GVFS protocol. + List ~~~~ diff --git a/diagnose.c b/diagnose.c index 37dc5de5aa26a1..dab7543d41a5ae 100644 --- a/diagnose.c +++ b/diagnose.c @@ -186,7 +186,7 @@ int create_diagnostics_archive(struct strbuf *zip_path, enum diagnose_mode mode) struct strvec archiver_args = STRVEC_INIT; char **argv_copy = NULL; int stdout_fd = -1, archiver_fd = -1; - char *cache_server_url = NULL; + char *cache_server_url = NULL, *shared_cache = NULL; struct strbuf buf = STRBUF_INIT; int res, i; struct archive_dir archive_dirs[] = { @@ -224,8 +224,10 @@ int create_diagnostics_archive(struct strbuf *zip_path, enum diagnose_mode mode) strbuf_addf(&buf, "Repository 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); write_or_die(stdout_fd, buf.buf, buf.len); @@ -285,6 +287,7 @@ int create_diagnostics_archive(struct strbuf *zip_path, enum diagnose_mode mode) strvec_clear(&archiver_args); strbuf_release(&buf); free(cache_server_url); + free(shared_cache); return res; } diff --git a/scalar.c b/scalar.c index 451ed97042d87e..cb3ad7ef320e13 100644 --- a/scalar.c +++ b/scalar.c @@ -7,6 +7,7 @@ #include "git-compat-util.h" #include "abspath.h" #include "gettext.h" +#include "hex.h" #include "parse-options.h" #include "config.h" #include "run-command.h" @@ -15,11 +16,18 @@ #include "fsmonitor-settings.h" #include "refs.h" #include "dir.h" +#include "object-file.h" #include "packfile.h" #include "help.h" #include "setup.h" +#include "wrapper.h" #include "trace2.h" #include "json-parser.h" +#include "path.h" + +static int is_unattended(void) { + return git_env_bool("Scalar_UNATTENDED", 0); +} static void setup_enlistment_directory(int argc, const char **argv, const char * const *usagestr, @@ -106,6 +114,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; +} + struct scalar_config { const char *key; const char *value; @@ -413,6 +434,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; @@ -506,13 +608,49 @@ void load_builtin_commands(const char *prefix, struct cmdnames *cmds) die("not implemented"); } +static int init_shared_object_cache(const char *url, + const char *local_cache_root) +{ + struct strbuf buf = STRBUF_INIT; + int res = 0; + char *cache_key = NULL, *shared_cache_path = NULL; + + 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_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(git_path("objects/info/alternates"),"%s\n", shared_cache_path); + + cleanup: + strbuf_release(&buf); + free(shared_cache_path); + free(cache_key); + return res; +} + static int cmd_clone(int argc, const char **argv) { const char *branch = NULL; int full_clone = 0, single_branch = 0, show_progress = isatty(2); int src = 1; - 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")), @@ -526,6 +664,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[] = { @@ -567,11 +708,23 @@ static int cmd_clone(int argc, const char **argv) if (is_directory(enlistment)) die(_("directory '%s' exists already"), enlistment); + ensure_absolute_path(enlistment, &enlistment); + if (src) dir = xstrfmt("%s/src", enlistment); else dir = xstrdup(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); @@ -591,8 +744,28 @@ 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_def_repo(the_repository); + 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); @@ -617,6 +790,8 @@ static int cmd_clone(int argc, const char **argv) supports_gvfs_protocol(url, &default_cache_server_url); if (gvfs_protocol) { + if ((res = init_shared_object_cache(url, local_cache_root))) + goto cleanup; if (!cache_server_url) cache_server_url = default_cache_server_url; if (set_config("core.useGVFSHelper=true") || @@ -691,6 +866,7 @@ static int cmd_clone(int argc, const char **argv) free(dir); strbuf_release(&buf); free(default_cache_server_url); + free(local_cache_root_abs); return res; } @@ -1098,6 +1274,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=false"); + } + while (argc > 1 && *argv[1] == '-') { if (!strcmp(argv[1], "-C")) { if (argc < 3) From 54807ace31bb5d394a66769985f8499107487e6b Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 1 Jun 2021 23:18:14 +0200 Subject: [PATCH 14/30] scalar diagnose: include shared cache info Signed-off-by: Johannes Schindelin --- diagnose.c | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/diagnose.c b/diagnose.c index dab7543d41a5ae..7a70c1f93711a4 100644 --- a/diagnose.c +++ b/diagnose.c @@ -74,6 +74,39 @@ static int dir_file_stats(struct object_directory *object_dir, void *data) return 0; } +static void dir_stats(struct strbuf *buf, const char *path) +{ + DIR *dir = opendir(path); + struct dirent *e; + struct stat e_stat; + struct strbuf file_path = STRBUF_INIT; + size_t 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 count_files(struct strbuf *path) { DIR *dir = opendir(path->buf); @@ -187,7 +220,7 @@ int create_diagnostics_archive(struct strbuf *zip_path, enum diagnose_mode mode) char **argv_copy = NULL; int stdout_fd = -1, archiver_fd = -1; char *cache_server_url = NULL, *shared_cache = NULL; - struct strbuf buf = STRBUF_INIT; + struct strbuf buf = STRBUF_INIT, path = STRBUF_INIT; int res, i; struct archive_dir archive_dirs[] = { { ".git", 0 }, @@ -259,6 +292,52 @@ int create_diagnostics_archive(struct strbuf *zip_path, enum diagnose_mode mode) } } + if (shared_cache) { + size_t path_len; + + strbuf_reset(&buf); + strbuf_addf(&path, "%s/pack", shared_cache); + strbuf_reset(&buf); + strbuf_addstr(&buf, "--add-virtual-file=packs-cached.txt:"); + dir_stats(&buf, path.buf); + strvec_push(&archiver_args, buf.buf); + + strbuf_reset(&buf); + strbuf_addstr(&buf, "--add-virtual-file=objects-cached.txt:"); + loose_objs_stats(&buf, shared_cache); + strvec_push(&archiver_args, buf.buf); + + strbuf_reset(&path); + strbuf_addf(&path, "%s/info", shared_cache); + path_len = path.len; + + if (is_directory(path.buf)) { + DIR *dir = opendir(path.buf); + struct dirent *e; + + while ((e = readdir(dir))) { + if (!strcmp(".", e->d_name) || !strcmp("..", e->d_name)) + continue; + if (e->d_type == DT_DIR) + continue; + + strbuf_reset(&buf); + strbuf_addf(&buf, "--add-virtual-file=info/%s:", e->d_name); + + strbuf_setlen(&path, path_len); + strbuf_addch(&path, '/'); + strbuf_addstr(&path, e->d_name); + + if (strbuf_read_file(&buf, path.buf, 0) < 0) { + res = error_errno(_("could not read '%s'"), path.buf); + goto diagnose_cleanup; + } + strvec_push(&archiver_args, buf.buf); + } + closedir(dir); + } + } + strvec_pushl(&archiver_args, "--prefix=", oid_to_hex(the_hash_algo->empty_tree), "--", NULL); From 74833a55f0b16764afa496032e601b6ca99cecc8 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 28 Apr 2021 13:56:16 +0200 Subject: [PATCH 15/30] scalar: only try GVFS protocol on https:// URLs Well, technically also the http:// protocol is allowed _when testing_... Signed-off-by: Johannes Schindelin --- scalar.c | 47 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/scalar.c b/scalar.c index cb3ad7ef320e13..51914f242db66d 100644 --- a/scalar.c +++ b/scalar.c @@ -396,6 +396,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`. * @@ -407,6 +414,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)) { @@ -475,19 +489,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 518202560e7291df365efac9f6f28f1cd008428a Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 16 Apr 2021 21:43:57 +0200 Subject: [PATCH 16/30] 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 --- t/t9210-scalar.sh | 157 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/t/t9210-scalar.sh b/t/t9210-scalar.sh index 57377313fec6be..6f0596eca05a98 100755 --- a/t/t9210-scalar.sh +++ b/t/t9210-scalar.sh @@ -283,4 +283,161 @@ test_expect_success UNZIP 'scalar diagnose' ' grep "^Total: [1-9]" out ' +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 && + + # We must set credential.interactive=true to bypass a setting + # in "scalar clone" that disables interactive credentials during + # an unattended command. + scalar -c credential.interactive=true 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_grep gvfs-helper trace.txt && + echo "second" >expect && + test_cmp expect actual + ) +' + +test_expect_success '`scalar register` parallel to worktree is unsupported' ' + git init test-repo/src && + mkdir -p test-repo/out && + + : parallel to worktree is unsupported && + test_must_fail env GIT_CEILING_DIRECTORIES="$(pwd)" \ + scalar register test-repo/out && + test_must_fail git config --get --global --fixed-value \ + maintenance.repo "$(pwd)/test-repo/src" && + scalar list >scalar.repos && + ! grep -F "$(pwd)/test-repo/src" scalar.repos && + + : at enlistment root, i.e. parent of repository, is supported && + GIT_CEILING_DIRECTORIES="$(pwd)" scalar register test-repo && + 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 properly unregisters enlistment && + scalar delete test-repo && + test_must_fail git config --get --global --fixed-value \ + maintenance.repo "$(pwd)/test-repo/src" && + scalar list >scalar.repos && + ! grep -F "$(pwd)/test-repo/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_expect_success '`scalar delete` with existing repo' ' + git init existing && + scalar register existing && + scalar delete existing && + test_path_is_missing existing +' + test_done From 7097478b05b32428a9d1182ac04ad53541acf9ff Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 23 Apr 2021 16:12:33 +0200 Subject: [PATCH 17/30] 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 --- Documentation/scalar.txt | 22 ++++++++++ scalar.c | 93 +++++++++++++++++++++++++++++++++++++++- t/t9210-scalar.sh | 34 +++++++++++++++ 3 files changed, 148 insertions(+), 1 deletion(-) diff --git a/Documentation/scalar.txt b/Documentation/scalar.txt index 8e69c608b4c9d6..38612f4bcae6e2 100644 --- a/Documentation/scalar.txt +++ b/Documentation/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 ----------- @@ -175,6 +176,27 @@ delete :: This subcommand lets you delete an existing Scalar enlistment from your local file system, unregistering the repository. +Cache-server +~~~~~~~~~~~~ + +cache-server ( --get | --set | --list [] ) []:: + This command lets you query or set the GVFS-enabled cache server used + to fetch missing objects. + +--get:: + This is the default command mode: query the currently-configured cache + server URL, if any. + +--list:: + Access the `gvfs/info` endpoint of the specified remote (default: + `origin`) to figure out which cache servers are available, if any. ++ +In contrast to the `--get` command mode (which only accesses the local +repository), this command mode triggers a request via the network that +potentially requires authentication. If authentication is required, the +configured credential helper is employed (see linkgit:git-credential[1] +for details). + SEE ALSO -------- linkgit:git-clone[1], linkgit:git-maintenance[1]. diff --git a/scalar.c b/scalar.c index 51914f242db66d..6f334bd1716058 100644 --- a/scalar.c +++ b/scalar.c @@ -23,6 +23,7 @@ #include "wrapper.h" #include "trace2.h" #include "json-parser.h" +#include "remote.h" #include "path.h" static int is_unattended(void) { @@ -360,6 +361,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) { @@ -429,6 +445,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"); @@ -445,7 +471,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) @@ -1273,6 +1301,68 @@ static int cmd_version(int argc, const char **argv) return 0; } +static int cmd_cache_server(int argc, const char **argv) +{ + int get = 0; + const char *set = NULL, *list = NULL; + struct option options[] = { + OPT_CMDMODE(0, "get", &get, + N_("get the configured cache-server URL"), 1), + OPT_STRING(0, "set", &set, N_("URL"), + N_("configure the cache-server to use")), + OPT_STRING(0, "list", &list, N_("remote"), + N_("list the possible cache-server URLs")), + 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 (!strchr(list, '/')) { + struct remote *remote; + + /* Look up remote */ + remote = remote_get(list); + if (!remote) { + error("no such remote: '%s'", name); + return 1; + } + if (!remote->url.nr) { + return error(_("remote '%s' has no URLs"), + name); + } + url = remote->url.v[0]; + } + res = supports_gvfs_protocol(url, NULL); + } else if (set) { + res = set_config("gvfs.cache-server=%s", 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 **); @@ -1287,6 +1377,7 @@ static struct { { "help", cmd_help }, { "version", cmd_version }, { "diagnose", cmd_diagnose }, + { "cache-server", cmd_cache_server }, { NULL, NULL}, }; diff --git a/t/t9210-scalar.sh b/t/t9210-scalar.sh index 6f0596eca05a98..b14221a061dd26 100755 --- a/t/t9210-scalar.sh +++ b/t/t9210-scalar.sh @@ -440,4 +440,38 @@ test_expect_success '`scalar delete` with existing repo' ' test_path_is_missing existing ' +test_expect_success 'scalar cache-server basics' ' + repo=with-cache-server && + git init $repo && + scalar cache-server --get $repo >out && + cat >expect <<-EOF && + Using cache server: (undefined) + EOF + test_cmp expect out && + + scalar cache-server --set http://fake-server/url $repo && + test_cmp_config -C $repo http://fake-server/url gvfs.cache-server && + scalar delete $repo && + test_path_is_missing $repo +' + +test_expect_success 'scalar cache-server list URL' ' + repo=with-real-gvfs && + git init $repo && + git -C $repo remote add origin http://$HOST_PORT/ && + scalar cache-server --list origin $repo >out && + + cat >expect <<-EOF && + #0: http://$HOST_PORT/servertype/cache + EOF + + test_cmp expect out && + + test_must_fail scalar -C $repo cache-server --list 2>err && + grep "requires a value" err && + + scalar delete $repo && + test_path_is_missing $repo +' + test_done From c59eda2a8cbbf5fcbd365b8d5ce2178bbd159603 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 12 May 2021 17:59:58 +0200 Subject: [PATCH 18/30] 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 --- scalar.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scalar.c b/scalar.c index 6f334bd1716058..5c0880614a4ae9 100644 --- a/scalar.c +++ b/scalar.c @@ -521,7 +521,8 @@ static char *get_cache_key(const char *url) * The GVFS protocol is only supported via https://; For testing, we * also allow http://. */ - if (can_url_support_gvfs(url)) { + if (!git_env_bool("SCALAR_TEST_SKIP_VSTS_INFO", 0) && + can_url_support_gvfs(url)) { cp.git_cmd = 1; strvec_pushl(&cp.args, "gvfs-helper", "--remote", url, "endpoint", "vsts/info", NULL); From 8786f4edc5585f6e5ba7de20990ea8f339932612 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 25 Jan 2022 23:49:21 +0100 Subject: [PATCH 19/30] scalar: adjust documentation to the microsoft/git fork Scalar in Microsoft's Git fork can do a little more than Scalar in upstream Git: in Microsoft's Git, it supports the GVFS protocol so that Scalar can clone from Azure DevOps. Signed-off-by: Johannes Schindelin --- contrib/scalar/docs/getting-started.md | 24 +++++++++++++++---- contrib/scalar/docs/index.md | 14 +++++++---- contrib/scalar/docs/philosophy.md | 33 +++++++++++++++----------- contrib/scalar/docs/troubleshooting.md | 20 ++++++++++++++++ 4 files changed, 68 insertions(+), 23 deletions(-) diff --git a/contrib/scalar/docs/getting-started.md b/contrib/scalar/docs/getting-started.md index ef7ea07b0f948c..d5125330320d2c 100644 --- a/contrib/scalar/docs/getting-started.md +++ b/contrib/scalar/docs/getting-started.md @@ -18,8 +18,9 @@ Creating a new Scalar clone --------------------------------------------------- The `clone` verb creates a local enlistment of a remote repository using the -partial clone feature available e.g. on GitHub. - +partial clone feature available e.g. on GitHub, or using the +[GVFS protocol](https://github.com/microsoft/VFSForGit/blob/HEAD/Protocol.md), +such as Azure Repos. ``` scalar clone [options] [] @@ -68,11 +69,26 @@ in ``. These options allow a user to customize their initial enlistment. * `--full-clone`: If specified, do not initialize the sparse-checkout feature. - All files will be present in your `src` directory. This uses a Git partial - clone: blobs are downloaded on demand. + All files will be present in your `src` directory. This behaves very similar + to a Git partial clone in that blobs are downloaded on demand. However, it + will use the GVFS protocol to download all Git objects. + +* `--cache-server-url=`: If specified, set the intended cache server to + the specified ``. All object queries will use the GVFS protocol to this + `` instead of the origin remote. If the remote supplies a list of + cache servers via the `/gvfs/config` endpoint, then the `clone` command + will select a nearby cache server from that list. * `--branch=`: Specify the branch to checkout after clone. +* `--local-cache-path=`: Use this option to override the path for the + local Scalar cache. If not specified, then Scalar will select a default + path to share objects with your other enlistments. On Windows, this path + is a subdirectory of `:\.scalarCache\`. On Mac, this path is a + subdirectory of `~/.scalarCache/`. The default cache path is recommended so + multiple enlistments of the same remote repository share objects on the + same device. + ### Advanced Options The options below are not intended for use by a typical user. These are diff --git a/contrib/scalar/docs/index.md b/contrib/scalar/docs/index.md index f9f5ab06e09253..4f56e2b0ebbac6 100644 --- a/contrib/scalar/docs/index.md +++ b/contrib/scalar/docs/index.md @@ -28,10 +28,14 @@ these features for that repo (except partial clone) and start running suggested maintenance in the background using [the `git maintenance` feature](https://git-scm.com/docs/git-maintenance). -Repos cloned with the `scalar clone` command use partial clone to significantly -reduce the amount of data required to get started using a repository. By -delaying all blob downloads until they are required, Scalar allows you to work -with very large repositories quickly. +Repos cloned with the `scalar clone` command use partial clone or the +[GVFS protocol](https://github.com/microsoft/VFSForGit/blob/HEAD/Protocol.md) +to significantly reduce the amount of data required to get started +using a repository. By delaying all blob downloads until they are required, +Scalar allows you to work with very large repositories quickly. The GVFS +protocol allows a network of _cache servers_ to serve objects with lower +latency and higher throughput. The cache servers also reduce load on the +central server. Documentation ------------- @@ -42,7 +46,7 @@ Documentation * [Troubleshooting](troubleshooting.md): Collect diagnostic information or update custom settings. Includes - `scalar diagnose`. + `scalar diagnose` and `scalar cache-server`. * [The Philosophy of Scalar](philosophy.md): Why does Scalar work the way it does, and how do we make decisions about its future? diff --git a/contrib/scalar/docs/philosophy.md b/contrib/scalar/docs/philosophy.md index 51486a75e41f0d..e3dfa025a2504c 100644 --- a/contrib/scalar/docs/philosophy.md +++ b/contrib/scalar/docs/philosophy.md @@ -13,22 +13,27 @@ Scalar only to configure those new settings. In particular, we ported features like background maintenance to Git to make Scalar simpler and make Git more powerful. -Services such as GitHub support partial clone , a standard adopted by the Git -project to download only part of the Git objects when cloning, and fetching -further objects on demand. If your hosting service supports partial clone, then -we absolutely recommend it as a way to greatly speed up your clone and fetch -times and to reduce how much disk space your Git repository requires. Scalar -will help with this! +Scalar ships inside [a custom version of Git][microsoft-git], but we are +working to make it available in other forks of Git. The only feature +that is not intended to ever reach the standard Git client is Scalar's use +of [the GVFS Protocol][gvfs-protocol], which is essentially an older +version of [Git's partial clone feature](https://github.blog/2020-12-21-get-up-to-speed-with-partial-clone-and-shallow-clone/) +that was available first in Azure Repos. Services such as GitHub support +only partial clone instead of the GVFS protocol because that is the +standard adopted by the Git project. If your hosting service supports +partial clone, then we absolutely recommend it as a way to greatly speed +up your clone and fetch times and to reduce how much disk space your Git +repository requires. Scalar will help with this! -Most of the value of Scalar can be found in the core Git client. However, most -of the advanced features that really optimize Git's performance are off by -default for compatibility reasons. To really take advantage of Git's latest and -greatest features, you either need to study the [`git config` -documentation](https://git-scm.com/docs/git-config) and regularly read [the Git -release notes](https://github.com/git/git/tree/master/Documentation/RelNotes). +If you don't use the GVFS Protocol, then most of the value of Scalar can +be found in the core Git client. However, most of the advanced features +that really optimize Git's performance are off by default for compatibility +reasons. To really take advantage of Git's latest and greatest features, +you either need to study the [`git config` documentation](https://git-scm.com/docs/git-config) +and regularly read [the Git release notes](https://github.com/git/git/tree/master/Documentation/RelNotes). Even if you do all that work and customize your Git settings on your machines, -you likely will want to share those settings with other team members. Or, you -can just use Scalar! +you likely will want to share those settings with other team members. +Or, you can just use Scalar! Using `scalar register` on an existing Git repository will give you these benefits: diff --git a/contrib/scalar/docs/troubleshooting.md b/contrib/scalar/docs/troubleshooting.md index 8ec56ad437ff09..c54d2438f22523 100644 --- a/contrib/scalar/docs/troubleshooting.md +++ b/contrib/scalar/docs/troubleshooting.md @@ -18,3 +18,23 @@ files for that repository. This includes: As the `diagnose` command completes, it provides the path of the resulting zip file. This zip can be attached to bug reports to make the analysis easier. + +Modifying Configuration Values +------------------------------ + +The Scalar-specific configuration is only available for repos using the +GVFS protocol. + +### Cache Server URL + +When using an enlistment cloned with `scalar clone` and the GVFS protocol, +you will have a value called the cache server URL. Cache servers are a feature +of the GVFS protocol to provide low-latency access to the on-demand object +requests. This modifies the `gvfs.cache-server` setting in your local Git config +file. + +Run `scalar cache-server --get` to see the current cache server. + +Run `scalar cache-server --list` to see the available cache server URLs. + +Run `scalar cache-server --set=` to set your cache server to ``. From 6e36741bcc8f9211d5212dff13b79d1c0dcbda2a Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Mon, 21 Jun 2021 08:21:48 -0400 Subject: [PATCH 20/30] scalar: enable untracked cache unconditionally Signed-off-by: Derrick Stolee --- scalar.c | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/scalar.c b/scalar.c index 5c0880614a4ae9..000c19c87d3faa 100644 --- a/scalar.c +++ b/scalar.c @@ -166,23 +166,7 @@ static int set_recommended_config(int reconfigure) { "core.FSCache", "true", 1 }, { "core.multiPackIndex", "true", 1 }, { "core.preloadIndex", "true", 1 }, -#ifndef WIN32 { "core.untrackedCache", "true", 1 }, -#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", 1 }, -#endif { "core.logAllRefUpdates", "true", 1 }, { "credential.https://dev.azure.com.useHttpPath", "true", 1 }, { "credential.validate", "false", 1 }, /* GCM4W-only */ From 009deb88277920bd38f33cfb872b080d918f325c Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 24 Aug 2021 18:01:47 +0200 Subject: [PATCH 21/30] scalar: parse `clone --no-fetch-commits-and-trees` for backwards compatibility This option does not do anything anymore, though. Signed-off-by: Johannes Schindelin --- scalar.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scalar.c b/scalar.c index 000c19c87d3faa..13cea9b9e525b6 100644 --- a/scalar.c +++ b/scalar.c @@ -680,6 +680,7 @@ static int init_shared_object_cache(const char *url, static int cmd_clone(int argc, const char **argv) { + int dummy = 0; const char *branch = NULL; int full_clone = 0, single_branch = 0, show_progress = isatty(2); int src = 1; @@ -701,6 +702,8 @@ static int cmd_clone(int argc, const char **argv) OPT_STRING(0, "local-cache-path", &local_cache_root, N_(""), N_("override the path for the local Scalar cache")), + OPT_HIDDEN_BOOL(0, "no-fetch-commits-and-trees", + &dummy, N_("no longer used")), OPT_END(), }; const char * const clone_usage[] = { From e8f274af3e0e4388b01062a629b2ba77b6237498 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Wed, 1 May 2024 10:08:11 -0400 Subject: [PATCH 22/30] scalar: make GVFS Protocol a forced choice In the Office monorepo, we've recently had an uptick in issues with `scalar clone`. These issues didn't make sense at first and seemed like the users weren't using `microsoft/git` but instead the upstream version's `scalar clone`. Instead of using GVFS cache servers, they were attempting to use the Git protocol's partial clone (which times out). It turns out that what's actually happening is that some network issue is causing the connection with Azure DevOps to error out during the `/gvfs/config` request. In the Git traces, we see the following error during this request: (curl:56) Failure when receiving data from the peer [transient] This isn't 100% of the time, but has increased enough to cause problems for a variety of users. The solution being proposed in this pull request is to remove the fall-back mechanism and instead have an explicit choice to use the GVFS protocol. To avoid significant disruption to Azure DevOps customers (the vast majority of `microsoft/git` users who use `scalar clone` based on my understanding), I added some inferring of a default value from the clone URL. This fallback mechanism was first implemented in the C# version of Scalar in microsoft/scalar#339. This was an attempt to make the Scalar client interesting to non-Azure DevOps customers, especially as GitHub was about to launch the availability of partial clones. Now that the `scalar` client is available upstream, users don't need the GVFS-enabled version to get these benefits. In addition, this will resolve #384 since those requests won't happen against non-ADO URLs unless requested. Signed-off-by: Derrick Stolee --- Documentation/scalar.txt | 20 ++++++++++++++++++++ scalar.c | 19 ++++++++++++++++--- t/t9210-scalar.sh | 25 ++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/Documentation/scalar.txt b/Documentation/scalar.txt index 38612f4bcae6e2..dab62be71fe496 100644 --- a/Documentation/scalar.txt +++ b/Documentation/scalar.txt @@ -103,6 +103,26 @@ clone), and `~/.scalarCache` on macOS. Retrieve missing objects from the specified remote, which is expected to understand the GVFS protocol. +--[no-]gvfs-protocol:: + When cloning from a `` with either `dev.azure.com` or + `visualstudio.com` in the name, `scalar clone` will attempt to use the GVFS + Protocol to access Git objects, specifically from a cache server when + available, and will fail to clone if there is an error over that protocol. + + To enable the GVFS Protocol regardless of the origin ``, use + `--gvfs-protocol`. This will cause `scalar clone` to fail when the origin + server fails to provide a valid response to the `gvfs/config` endpoint. + + To disable the GVFS Protocol, use `--no-gvfs-protocol` and `scalar clone` + will only use the Git protocol, starting with a partial clone. This can be + helpful if your `` points to Azure Repos but the repository does not + have GVFS cache servers enabled. It is likely more efficient to use its + partial clone functionality through the Git protocol. + + Previous versions of `scalar clone` could fall back to a partial clone over + the Git protocol if there is any issue gathering GVFS configuration + information from the origin server. + List ~~~~ diff --git a/scalar.c b/scalar.c index 13cea9b9e525b6..e4a21b9b2caf73 100644 --- a/scalar.c +++ b/scalar.c @@ -686,6 +686,8 @@ static int cmd_clone(int argc, const char **argv) int src = 1; const char *cache_server_url = NULL, *local_cache_root = NULL; char *default_cache_server_url = NULL, *local_cache_root_abs = NULL; + int gvfs_protocol = -1; + struct option clone_options[] = { OPT_STRING('b', "branch", &branch, N_(""), N_("branch to checkout after clone")), @@ -696,6 +698,8 @@ static int cmd_clone(int argc, const char **argv) "be checked out")), OPT_BOOL(0, "src", &src, N_("create repository within 'src' directory")), + OPT_BOOL(0, "gvfs-protocol", &gvfs_protocol, + N_("force enable (or disable) the GVFS Protocol")), OPT_STRING(0, "cache-server-url", &cache_server_url, N_(""), N_("the url or friendly name of the cache server")), @@ -715,7 +719,6 @@ static int cmd_clone(int argc, const char **argv) char *enlistment = NULL, *dir = NULL; struct strbuf buf = STRBUF_INIT; int res; - int gvfs_protocol; argc = parse_options(argc, argv, NULL, clone_options, clone_usage, 0); @@ -823,8 +826,18 @@ static int cmd_clone(int argc, const char **argv) goto cleanup; } - gvfs_protocol = cache_server_url || - supports_gvfs_protocol(url, &default_cache_server_url); + /* Is --[no-]gvfs-protocol unspecified? Infer from url. */ + if (gvfs_protocol < 0) { + if (cache_server_url || + strstr(url, "dev.azure.com") || + strstr(url, "visualstudio.com")) + gvfs_protocol = 1; + else + gvfs_protocol = 0; + } + + if (gvfs_protocol && !supports_gvfs_protocol(url, &default_cache_server_url)) + die(_("failed to contact server via GVFS Protocol")); if (gvfs_protocol) { if ((res = init_shared_object_cache(url, local_cache_root))) diff --git a/t/t9210-scalar.sh b/t/t9210-scalar.sh index b14221a061dd26..43e20bbdbe8b87 100755 --- a/t/t9210-scalar.sh +++ b/t/t9210-scalar.sh @@ -349,7 +349,12 @@ test_expect_success '`scalar clone` with GVFS-enabled server' ' # We must set credential.interactive=true to bypass a setting # in "scalar clone" that disables interactive credentials during # an unattended command. - scalar -c credential.interactive=true clone --single-branch -- http://$HOST_PORT/ using-gvfs && + GIT_TRACE2_EVENT="$(pwd)/clone-trace-with-gvfs" scalar \ + -c credential.interactive=true \ + clone --gvfs-protocol \ + --single-branch -- http://$HOST_PORT/ using-gvfs && + + grep "GET/config(main)" Date: Tue, 10 May 2022 00:29:55 +0200 Subject: [PATCH 23/30] scalar diagnose: accommodate Scalar's Functional Tests Those tests specifically verify that the `.zip` file path is shown on `stdout`. Let's do that again, under the assumption that there are scripts out there that rely on this behavior. Signed-off-by: Johannes Schindelin --- diagnose.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/diagnose.c b/diagnose.c index 7a70c1f93711a4..6386e93aaab939 100644 --- a/diagnose.c +++ b/diagnose.c @@ -351,10 +351,13 @@ int create_diagnostics_archive(struct strbuf *zip_path, enum diagnose_mode mode) goto diagnose_cleanup; } - fprintf(stderr, "\n" - "Diagnostics complete.\n" - "All of the gathered info is captured in '%s'\n", - zip_path->buf); + strbuf_reset(&buf); + strbuf_addf(&buf, "\n" + "Diagnostics complete.\n" + "All of the gathered info is captured in '%s'\n", + zip_path->buf); + write_or_die(stdout_fd, buf.buf, buf.len); + write_or_die(2, buf.buf, buf.len); diagnose_cleanup: if (archiver_fd >= 0) { From b8c428daa24d31040307dbe2ecb44d659dd44d43 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 8 Jun 2021 09:43:36 +0200 Subject: [PATCH 24/30] ci: run Scalar's Functional Tests Scalar's Functional Test suite is pretty comprehensive, and caught more than just one bug in the built-in FSMonitor that was missed by Git's own test suite. To benefit from this test suite, automatically run it on the `vfs-*` and `features/*` branches. Note: for simplicity, we're building Git from scratch in all matrix jobs. Also note: for speed, we are using `git-sdk-64-minimal`, even if it lacks the `/bin/install` that we need to install Git's files; We're providing a minimal shell script shim instead. Also, we do not need to bother with the Tcl/Tk parts, therefore we're skipping them, too. Finally, we use GIT_FORCE_UNTRACKED_CACHE in the functional tests, to give the untracked cache a thorough work-out. Signed-off-by: Johannes Schindelin --- .github/workflows/scalar-functional-tests.yml | 220 ++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 .github/workflows/scalar-functional-tests.yml diff --git a/.github/workflows/scalar-functional-tests.yml b/.github/workflows/scalar-functional-tests.yml new file mode 100644 index 00000000000000..da9f3a0d14bb2f --- /dev/null +++ b/.github/workflows/scalar-functional-tests.yml @@ -0,0 +1,220 @@ +name: Scalar Functional Tests + +env: + SCALAR_REPOSITORY: microsoft/scalar + SCALAR_REF: main + DEBUG_WITH_TMATE: false + SCALAR_TEST_SKIP_VSTS_INFO: true + +on: + push: + branches: [ vfs-*, tentative/vfs-* ] + pull_request: + branches: [ vfs-*, features/* ] + +jobs: + scalar: + name: "Scalar Functional Tests" + + strategy: + fail-fast: false + matrix: + # Order by runtime (in descending order) + os: [windows-2019, macos-13, ubuntu-20.04, ubuntu-22.04] + # Scalar.NET used to be tested using `features: [false, experimental]` + # But currently, Scalar/C ignores `feature.scalar` altogether, so let's + # save some electrons and run only one of them... + features: [ignored] + exclude: + # The built-in FSMonitor is not (yet) supported on Linux + - os: ubuntu-20.04 + features: experimental + - os: ubuntu-22.04 + features: experimental + runs-on: ${{ matrix.os }} + + env: + BUILD_FRAGMENT: bin/Release/netcoreapp3.1 + GIT_FORCE_UNTRACKED_CACHE: 1 + + steps: + - name: Check out Git's source code + uses: actions/checkout@v3 + + - name: Setup build tools on Windows + if: runner.os == 'Windows' + uses: git-for-windows/setup-git-for-windows-sdk@v1 + + - name: Provide a minimal `install` on Windows + if: runner.os == 'Windows' + shell: bash + run: | + test -x /usr/bin/install || + tr % '\t' >/usr/bin/install <<-\EOF + #!/bin/sh + + cmd=cp + while test $# != 0 + do + %case "$1" in + %-d) cmd="mkdir -p";; + %-m) shift;; # ignore mode + %*) break;; + %esac + %shift + done + + exec $cmd "$@" + EOF + + - name: Install build dependencies for Git (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get -q -y install libssl-dev libcurl4-openssl-dev gettext + + - name: Build and install Git + shell: bash + env: + NO_TCLTK: Yup + run: | + # We do require a VFS version + def_ver="$(sed -n 's/DEF_VER=\(.*vfs.*\)/\1/p' GIT-VERSION-GEN)" + test -n "$def_ver" + + # Ensure that `git version` reflects DEF_VER + case "$(git describe --match "v[0-9]*vfs*" HEAD)" in + ${def_ver%%.vfs.*}.vfs.*) ;; # okay, we can use this + *) git -c user.name=ci -c user.email=ci@github tag -m for-testing ${def_ver}.NNN.g$(git rev-parse --short HEAD);; + esac + + SUDO= + extra= + case "${{ runner.os }}" in + Windows) + extra=DESTDIR=/c/Progra~1/Git + cygpath -aw "/c/Program Files/Git/cmd" >>$GITHUB_PATH + ;; + Linux) + SUDO=sudo + extra=prefix=/usr + ;; + macOS) + SUDO=sudo + extra=prefix=/usr/local + ;; + esac + + $SUDO make -j5 $extra install + + - name: Ensure that we use the built Git and Scalar + shell: bash + run: | + type -p git + git version + case "$(git version)" in *.vfs.*) echo Good;; *) exit 1;; esac + type -p scalar + scalar version + case "$(scalar version 2>&1)" in *.vfs.*) echo Good;; *) exit 1;; esac + + - name: Check out Scalar's source code + uses: actions/checkout@v3 + with: + fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works. + path: scalar + repository: ${{ env.SCALAR_REPOSITORY }} + ref: ${{ env.SCALAR_REF }} + + - name: Setup .NET Core + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '3.1.x' + + - name: Install dependencies + run: dotnet restore + working-directory: scalar + env: + DOTNET_NOLOGO: 1 + + - name: Build + working-directory: scalar + run: dotnet build --configuration Release --no-restore -p:UseAppHost=true # Force generation of executable on macOS. + + - name: Setup platform (Linux) + if: runner.os == 'Linux' + run: | + echo "BUILD_PLATFORM=${{ runner.os }}" >>$GITHUB_ENV + echo "TRACE2_BASENAME=Trace2.${{ github.run_id }}__${{ github.run_number }}__${{ matrix.os }}__${{ matrix.features }}" >>$GITHUB_ENV + + - name: Setup platform (Mac) + if: runner.os == 'macOS' + run: | + echo 'BUILD_PLATFORM=Mac' >>$GITHUB_ENV + echo "TRACE2_BASENAME=Trace2.${{ github.run_id }}__${{ github.run_number }}__${{ matrix.os }}__${{ matrix.features }}" >>$GITHUB_ENV + + - name: Setup platform (Windows) + if: runner.os == 'Windows' + run: | + echo "BUILD_PLATFORM=${{ runner.os }}" >>$env:GITHUB_ENV + echo 'BUILD_FILE_EXT=.exe' >>$env:GITHUB_ENV + echo "TRACE2_BASENAME=Trace2.${{ github.run_id }}__${{ github.run_number }}__${{ matrix.os }}__${{ matrix.features }}" >>$env:GITHUB_ENV + + - name: Configure feature.scalar + run: git config --global feature.scalar ${{ matrix.features }} + + - id: functional_test + name: Functional test + timeout-minutes: 60 + working-directory: scalar + shell: bash + run: | + export GIT_TRACE2_EVENT="$PWD/$TRACE2_BASENAME/Event" + export GIT_TRACE2_PERF="$PWD/$TRACE2_BASENAME/Perf" + export GIT_TRACE2_EVENT_BRIEF=true + export GIT_TRACE2_PERF_BRIEF=true + mkdir -p "$TRACE2_BASENAME" + mkdir -p "$TRACE2_BASENAME/Event" + mkdir -p "$TRACE2_BASENAME/Perf" + git version --build-options + cd ../out + Scalar.FunctionalTests/$BUILD_FRAGMENT/Scalar.FunctionalTests$BUILD_FILE_EXT --test-scalar-on-path --test-git-on-path --timeout=300000 --full-suite + + - name: Force-stop FSMonitor daemons and Git processes (Windows) + if: runner.os == 'Windows' && (success() || failure()) + shell: bash + run: | + set -x + wmic process get CommandLine,ExecutablePath,HandleCount,Name,ParentProcessID,ProcessID + wmic process where "CommandLine Like '%fsmonitor--daemon %run'" delete + wmic process where "ExecutablePath Like '%git.exe'" delete + + - id: trace2_zip_unix + if: runner.os != 'Windows' && ( success() || failure() ) && ( steps.functional_test.conclusion == 'success' || steps.functional_test.conclusion == 'failure' ) + name: Zip Trace2 Logs (Unix) + shell: bash + working-directory: scalar + run: zip -q -r $TRACE2_BASENAME.zip $TRACE2_BASENAME/ + + - id: trace2_zip_windows + if: runner.os == 'Windows' && ( success() || failure() ) && ( steps.functional_test.conclusion == 'success' || steps.functional_test.conclusion == 'failure' ) + name: Zip Trace2 Logs (Windows) + working-directory: scalar + run: Compress-Archive -DestinationPath ${{ env.TRACE2_BASENAME }}.zip -Path ${{ env.TRACE2_BASENAME }} + + - name: Archive Trace2 Logs + if: ( success() || failure() ) && ( steps.trace2_zip_unix.conclusion == 'success' || steps.trace2_zip_windows.conclusion == 'success' ) + uses: actions/upload-artifact@v3 + with: + name: ${{ env.TRACE2_BASENAME }}.zip + path: scalar/${{ env.TRACE2_BASENAME }}.zip + retention-days: 3 + + # The GitHub Action `action-tmate` allows developers to connect to the running agent + # using SSH (it will be a `tmux` session; on Windows agents it will be inside the MSYS2 + # environment in `C:\msys64`, therefore it can be slightly tricky to interact with + # Git for Windows, which runs a slightly incompatible MSYS2 runtime). + - name: action-tmate + if: env.DEBUG_WITH_TMATE == 'true' && failure() + uses: mxschmitt/action-tmate@v3 + with: + limit-access-to-actor: true From e3962e9c81c8627d41427f03d381a4bbd0f0538d Mon Sep 17 00:00:00 2001 From: Victoria Dye Date: Tue, 5 Apr 2022 13:42:23 -0700 Subject: [PATCH 25/30] scalar: upgrade to newest FSMonitor config setting When FSMonitor was upstreamed, the 'core.useBuiltinFSMonitor' config was deprecated and replaced with an overload of the 'core.fsmonitor' config (i.e., if a boolean value was specified in 'core.fsmonitor', it is treated the way 'core.useBuiltinFSMonitor' originally was). Because 'scalar register' actively sets that config, use it to upgrade the deprecated config setting. Co-authored-by: Johannes Schindelin Signed-off-by: Victoria Dye --- scalar.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/scalar.c b/scalar.c index e4a21b9b2caf73..8033ee4d332bc3 100644 --- a/scalar.c +++ b/scalar.c @@ -199,6 +199,23 @@ static int set_recommended_config(int reconfigure) int i; char *value; + /* + * If a user has "core.usebuiltinfsmonitor" enabled, try to switch to + * the new (non-deprecated) setting (core.fsmonitor). + */ + if (!git_config_get_string("core.usebuiltinfsmonitor", &value)) { + char *dummy = NULL; + if (git_config_get_string("core.fsmonitor", &dummy) && + git_config_set_gently("core.fsmonitor", value) < 0) + return error(_("could not configure %s=%s"), + "core.fsmonitor", value); + if (git_config_set_gently("core.usebuiltinfsmonitor", NULL) < 0) + return error(_("could not configure %s=%s"), + "core.useBuiltinFSMonitor", "NULL"); + free(value); + free(dummy); + } + for (i = 0; config[i].key; i++) { if (set_scalar_config(config + i, reconfigure)) return error(_("could not configure %s=%s"), From 237dde55dd48b6e691af9952bfd94c9bc04d25ab Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Tue, 22 Aug 2023 11:24:10 -0400 Subject: [PATCH 26/30] credential: add new interactive config option When scripts or background maintenance wish to perform HTTP(S) requests, there is a risk that our stored credentials might be invalid. At the moment, this causes the credential helper to ping the user and block the process. Even if the credential helper does not ping the user, Git falls back to the 'askpass' method, which includes a direct ping to the user via the terminal. Even setting the 'core.askPass' config as something like 'echo' will causes Git to fallback to a terminal prompt. It uses git_terminal_prompt(), which finds the terminal from the environment and ignores whether stdin has been redirected. This can also block the process awaiting input. Create a new config option to prevent user interaction, favoring a failure to a blocked process. The chosen name, 'credential.interactive', is taken from the config option used by Git Credential Manager to already avoid user interactivity, so there is already one credential helper that integrates with this option. However, older versions of Git Credential Manager also accepted other string values, including 'auto', 'never', and 'always'. The modern use is to use a boolean value, but we should still be careful that some users could have these non-booleans. Further, we should respect 'never' the same as 'false'. This is respected by the implementation and test, but not mentioned in the documentation. The implementation for the Git interactions takes place within credential_getpass(). The method prototype is modified to return an 'int' instead of 'void'. This allows us to detect that no attempt was made to fill the given credential, changing the single caller slightly. Also, a new trace2 region is added around the interactive portion of the credential request. This provides a way to measure the amount of time spent in that region for commands that _are_ interactive. It also makes a conventient way to test that the config option works with 'test_region'. Signed-off-by: Derrick Stolee --- Documentation/config/credential.txt | 8 ++++++++ credential.c | 31 ++++++++++++++++++++++++++--- t/t5551-http-fetch-smart.sh | 22 ++++++++++++++++++++ 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/Documentation/config/credential.txt b/Documentation/config/credential.txt index 0221c3e620da89..470482ff4c2a38 100644 --- a/Documentation/config/credential.txt +++ b/Documentation/config/credential.txt @@ -9,6 +9,14 @@ credential.helper:: Note that multiple helpers may be defined. See linkgit:gitcredentials[7] for details and examples. +credential.interactive:: + By default, Git and any configured credential helpers will ask for + user input when new credentials are required. Many of these helpers + will succeed based on stored credentials if those credentials are + still valid. To avoid the possibility of user interactivity from + Git, set `credential.interactive=false`. Some credential helpers + respect this option as well. + credential.useHttpPath:: When acquiring credentials, consider the "path" component of an http or https URL to be important. Defaults to false. See diff --git a/credential.c b/credential.c index 4b1a2b94feff73..7f0875b727c865 100644 --- a/credential.c +++ b/credential.c @@ -1,3 +1,4 @@ +#define USE_THE_REPOSITORY_VARIABLE #include "git-compat-util.h" #include "abspath.h" #include "config.h" @@ -11,6 +12,8 @@ #include "strbuf.h" #include "urlmatch.h" #include "git-compat-util.h" +#include "trace2.h" +#include "repository.h" void credential_init(struct credential *c) { @@ -249,14 +252,36 @@ static char *credential_ask_one(const char *what, struct credential *c, return xstrdup(r); } -static void credential_getpass(struct credential *c) +static int credential_getpass(struct credential *c) { + int interactive; + char *value; + if (!git_config_get_maybe_bool("credential.interactive", &interactive) && + !interactive) { + trace2_data_intmax("credential", the_repository, + "interactive/skipped", 1); + return -1; + } + if (!git_config_get_string("credential.interactive", &value)) { + int same = !strcmp(value, "never"); + free(value); + if (same) { + trace2_data_intmax("credential", the_repository, + "interactive/skipped", 1); + return -1; + } + } + + trace2_region_enter("credential", "interactive", the_repository); if (!c->username) c->username = credential_ask_one("Username", c, PROMPT_ASKPASS|PROMPT_ECHO); if (!c->password) c->password = credential_ask_one("Password", c, PROMPT_ASKPASS); + trace2_region_leave("credential", "interactive", the_repository); + + return 0; } int credential_has_capability(const struct credential_capability *capa, @@ -499,8 +524,8 @@ void credential_fill(struct credential *c, int all_capabilities) c->helpers.items[i].string); } - credential_getpass(c); - if (!c->username && !c->password && !c->credential) + if (credential_getpass(c) || + (!c->username && !c->password && !c->credential)) die("unable to get password from user"); } diff --git a/t/t5551-http-fetch-smart.sh b/t/t5551-http-fetch-smart.sh index 7b5ab0eae16012..ceb3336a5c494f 100755 --- a/t/t5551-http-fetch-smart.sh +++ b/t/t5551-http-fetch-smart.sh @@ -186,6 +186,28 @@ test_expect_success 'clone from password-protected repository' ' test_cmp expect actual ' +test_expect_success 'credential.interactive=false skips askpass' ' + set_askpass bogus nonsense && + ( + GIT_TRACE2_EVENT="$(pwd)/interactive-true" && + export GIT_TRACE2_EVENT && + test_must_fail git clone --bare "$HTTPD_URL/auth/smart/repo.git" interactive-true-dir && + test_region credential interactive interactive-true && + + GIT_TRACE2_EVENT="$(pwd)/interactive-false" && + export GIT_TRACE2_EVENT && + test_must_fail git -c credential.interactive=false \ + clone --bare "$HTTPD_URL/auth/smart/repo.git" interactive-false-dir && + test_region ! credential interactive interactive-false && + + GIT_TRACE2_EVENT="$(pwd)/interactive-never" && + export GIT_TRACE2_EVENT && + test_must_fail git -c credential.interactive=never \ + clone --bare "$HTTPD_URL/auth/smart/repo.git" interactive-never-dir && + test_region ! credential interactive interactive-never + ) +' + test_expect_success 'clone from auth-only-for-push repository' ' echo two >expect && set_askpass wrong && From 923ed10eaef9a4774f7e5c9c96b6119245f064c4 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Wed, 16 Aug 2023 13:32:02 -0400 Subject: [PATCH 27/30] maintenance: add custom config to background jobs At the moment, some background jobs are getting blocked on credentials during the 'prefetch' task. This leads to other tasks, such as incremental repacks, getting blocked. Further, if a user manages to fix their credentials, then they still need to cancel the background process before their background maintenance can continue working. Update the background schedules for our four scheduler integrations to include these config options via '-c' options: * 'credential.interactive=false' will stop Git and some credential helpers from prompting in the UI (assuming the '-c' parameters are carried through and respected by GCM). * 'core.askPass=true' will replace the text fallback for a username and password into the 'true' command, which will return a success in its exit code, but Git will treat the empty string returned as an invalid password and move on. We can do some testing that the credentials are passed, at least in the systemd case due to writing the service files. Signed-off-by: Derrick Stolee Signed-off-by: Johannes Schindelin --- builtin/gc.c | 53 ++++++++++++++++++++++++++++++++++++------ t/t7900-maintenance.sh | 3 +++ 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/builtin/gc.c b/builtin/gc.c index 72bac2554fc4b9..387591f7741c8d 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -1664,6 +1664,42 @@ static const char *get_frequency(enum schedule_priority schedule) } } +static const char *extraconfig[] = { + "credential.interactive=false", + "core.askPass=true", /* 'true' returns success, but no output. */ + NULL +}; + +static const char *get_extra_config_parameters(void) { + static const char *result = NULL; + struct strbuf builder = STRBUF_INIT; + + if (result) + return result; + + for (const char **s = extraconfig; s && *s; s++) + strbuf_addf(&builder, "-c %s ", *s); + + result = strbuf_detach(&builder, NULL); + return result; +} + +static const char *get_extra_launchctl_strings(void) { + static const char *result = NULL; + struct strbuf builder = STRBUF_INIT; + + if (result) + return result; + + for (const char **s = extraconfig; s && *s; s++) { + strbuf_addstr(&builder, "-c\n"); + strbuf_addf(&builder, "%s\n", *s); + } + + result = strbuf_detach(&builder, NULL); + return result; +} + /* * get_schedule_cmd` reads the GIT_TEST_MAINT_SCHEDULER environment variable * to mock the schedulers that `git maintenance start` rely on. @@ -1870,6 +1906,7 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit "\n" "%s/git\n" "--exec-path=%s\n" + "%s" /* For extra config parameters. */ "for-each-repo\n" "--keep-going\n" "--config=maintenance.repo\n" @@ -1879,7 +1916,8 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit "\n" "StartCalendarInterval\n" "\n"; - strbuf_addf(&plist, preamble, name, exec_path, exec_path, frequency); + strbuf_addf(&plist, preamble, name, exec_path, exec_path, + get_extra_launchctl_strings(), frequency); switch (schedule) { case SCHEDULE_HOURLY: @@ -2114,11 +2152,12 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority "\n" "\n" "\"%s\\headless-git.exe\"\n" - "--exec-path=\"%s\" for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%s\n" + "--exec-path=\"%s\" %s for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%s\n" "\n" "\n" "\n"; - fprintf(tfile->fp, xml, exec_path, exec_path, frequency); + fprintf(tfile->fp, xml, exec_path, exec_path, + get_extra_config_parameters(), frequency); strvec_split(&child.args, cmd); strvec_pushl(&child.args, "/create", "/tn", name, "/f", "/xml", get_tempfile_path(tfile), NULL); @@ -2259,8 +2298,8 @@ static int crontab_update_schedule(int run_maintenance, int fd) "# replaced in the future by a Git command.\n\n"); strbuf_addf(&line_format, - "%%d %%s * * %%s \"%s/git\" --exec-path=\"%s\" for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%%s\n", - exec_path, exec_path); + "%%d %%s * * %%s \"%s/git\" --exec-path=\"%s\" %s for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%%s\n", + exec_path, exec_path, get_extra_config_parameters()); fprintf(cron_in, line_format.buf, minute, "1-23", "*", "hourly"); fprintf(cron_in, line_format.buf, minute, "0", "1-6", "daily"); fprintf(cron_in, line_format.buf, minute, "0", "0", "weekly"); @@ -2460,7 +2499,7 @@ static int systemd_timer_write_service_template(const char *exec_path) "\n" "[Service]\n" "Type=oneshot\n" - "ExecStart=\"%s/git\" --exec-path=\"%s\" for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%%i\n" + "ExecStart=\"%s/git\" --exec-path=\"%s\" %s for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%%i\n" "LockPersonality=yes\n" "MemoryDenyWriteExecute=yes\n" "NoNewPrivileges=yes\n" @@ -2470,7 +2509,7 @@ static int systemd_timer_write_service_template(const char *exec_path) "RestrictSUIDSGID=yes\n" "SystemCallArchitectures=native\n" "SystemCallFilter=@system-service\n"; - if (fprintf(file, unit, exec_path, exec_path) < 0) { + if (fprintf(file, unit, exec_path, exec_path, get_extra_config_parameters()) < 0) { error(_("failed to write to '%s'"), filename); fclose(file); goto error; diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh index 8595489cebe25c..cab90b6796c6ae 100755 --- a/t/t7900-maintenance.sh +++ b/t/t7900-maintenance.sh @@ -800,6 +800,9 @@ test_expect_success 'start and stop Linux/systemd maintenance' ' test_systemd_analyze_verify "systemd/user/git-maintenance@daily.service" && test_systemd_analyze_verify "systemd/user/git-maintenance@weekly.service" && + grep "core.askPass=true" "systemd/user/git-maintenance@.service" && + grep "credential.interactive=false" "systemd/user/git-maintenance@.service" && + printf -- "--user enable --now git-maintenance@%s.timer\n" hourly daily weekly >expect && test_cmp expect args && From 56507fd3a56f661e89cc1d371d1385226412a905 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Mon, 21 Aug 2023 11:14:48 -0400 Subject: [PATCH 28/30] scalar: configure maintenance during 'reconfigure' The 'scalar reconfigure' command is intended to update registered repos with the latest settings available. However, up to now we were not reregistering the repos with background maintenance. In particular, this meant that the background maintenance schedule would not be updated if there are improvements between versions. Be sure to register repos for maintenance during the reconfigure step. Signed-off-by: Derrick Stolee --- scalar.c | 3 ++- t/t9210-scalar.sh | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/scalar.c b/scalar.c index 8033ee4d332bc3..40b0741b141676 100644 --- a/scalar.c +++ b/scalar.c @@ -1109,7 +1109,8 @@ static int cmd_reconfigure(int argc, const char **argv) old_repo = the_repository; the_repository = &r; - if (set_recommended_config(1) >= 0) + if (set_recommended_config(1) >= 0 && + toggle_maintenance(1) >= 0) succeeded = 1; the_repository = old_repo; diff --git a/t/t9210-scalar.sh b/t/t9210-scalar.sh index 43e20bbdbe8b87..926238e791eee6 100755 --- a/t/t9210-scalar.sh +++ b/t/t9210-scalar.sh @@ -183,8 +183,11 @@ test_expect_success 'scalar reconfigure' ' 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)" + rm one/src/cron.txt && + GIT_TRACE2_EVENT="$(pwd)/reconfigure" scalar reconfigure -a && + test_path_is_file one/src/cron.txt && + test true = "$(git -C one/src config core.preloadIndex)" && + test_subcommand git maintenance start Date: Tue, 4 Oct 2022 08:01:17 -0400 Subject: [PATCH 29/30] abspath: make strip_last_path_component() global The strip_last_component() method is helpful for finding the parent directory of a path stored in a strbuf. Extract it to a global method advertised in abspath.h. With that additional visibility, it is helpful to rename it to be more specific to paths. Signed-off-by: Derrick Stolee --- abspath.c | 6 +++--- abspath.h | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/abspath.c b/abspath.c index 0c17e98654e4b0..e899f46d02097a 100644 --- a/abspath.c +++ b/abspath.c @@ -14,7 +14,7 @@ int is_directory(const char *path) } /* removes the last path component from 'path' except if 'path' is root */ -static void strip_last_component(struct strbuf *path) +void strip_last_path_component(struct strbuf *path) { size_t offset = offset_1st_component(path->buf); size_t len = path->len; @@ -119,7 +119,7 @@ static char *strbuf_realpath_1(struct strbuf *resolved, const char *path, continue; /* '.' component */ } else if (next.len == 2 && !strcmp(next.buf, "..")) { /* '..' component; strip the last path component */ - strip_last_component(resolved); + strip_last_path_component(resolved); continue; } @@ -171,7 +171,7 @@ static char *strbuf_realpath_1(struct strbuf *resolved, const char *path, * strip off the last component since it will * be replaced with the contents of the symlink */ - strip_last_component(resolved); + strip_last_path_component(resolved); } /* diff --git a/abspath.h b/abspath.h index 4653080d5e4b7a..06241ba13cf646 100644 --- a/abspath.h +++ b/abspath.h @@ -10,6 +10,11 @@ char *real_pathdup(const char *path, int die_on_error); const char *absolute_path(const char *path); char *absolute_pathdup(const char *path); +/** + * Remove the last path component from 'path' except if 'path' is root. + */ +void strip_last_path_component(struct strbuf *path); + /* * Concatenate "prefix" (if len is non-zero) and "path", with no * connecting characters (so "prefix" should end with a "/"). From 306494b6032d59b7bc01cf094c51dbb68b6abb46 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Fri, 30 Sep 2022 12:41:27 -0400 Subject: [PATCH 30/30] scalar: .scalarCache should live above enlistment We should not be putting the .scalarCache inside the enlistment as a sibling to the 'src' directory. This only happens in "unattended" mode, but it also negates any benefit of a shared object cache because each enlistment absolutely does not share any objects with others. Move the shared object cache in this case to a level above the enlistment, so at least there is some hope that it can be reused. This is also critical to the upcoming --no-src option, since the shared object cache cannot be located within the Git repository. Signed-off-by: Derrick Stolee --- scalar.c | 9 +++++++-- t/t9210-scalar.sh | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/scalar.c b/scalar.c index 8033ee4d332bc3..68dd857fb50e42 100644 --- a/scalar.c +++ b/scalar.c @@ -481,8 +481,13 @@ static char *default_cache_root(const char *root) { const char *env; - if (is_unattended()) - return xstrfmt("%s/.scalarCache", root); + if (is_unattended()) { + struct strbuf path = STRBUF_INIT; + strbuf_addstr(&path, root); + strip_last_path_component(&path); + strbuf_addstr(&path, "/.scalarCache"); + return strbuf_detach(&path, NULL); + } #ifdef WIN32 (void)env; diff --git a/t/t9210-scalar.sh b/t/t9210-scalar.sh index 43e20bbdbe8b87..accfab5a2e10c3 100755 --- a/t/t9210-scalar.sh +++ b/t/t9210-scalar.sh @@ -360,7 +360,7 @@ test_expect_success '`scalar clone` with GVFS-enabled server' ' cache_key="url_$(printf "%s" http://$HOST_PORT/ | tr A-Z a-z | test-tool sha1)" && - echo "$(pwd)/using-gvfs/.scalarCache/$cache_key" >expect && + echo "$(pwd)/.scalarCache/$cache_key" >expect && git -C using-gvfs/src config gvfs.sharedCache >actual && test_cmp expect actual &&