Merge pull request #371 from dscho/run-scalar-functional-tests-and-fix-built-in-fsmonitor

Fix the built-in FSMonitor, and run Scalar's Functional Tests as part of the automated builds
This commit is contained in:
Johannes Schindelin 2021-06-09 15:41:33 +02:00 коммит произвёл Johannes Schindelin
Родитель e96a8763d7 c5213af4f1
Коммит 5b677de104
21 изменённых файлов: 1675 добавлений и 43 удалений

220
.github/workflows/scalar-functional-tests.yml поставляемый Normal file
Просмотреть файл

@ -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-11, 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

Просмотреть файл

@ -833,3 +833,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.

Просмотреть файл

@ -9,7 +9,8 @@ SYNOPSIS
--------
[verse]
scalar clone [--single-branch] [--branch <main-branch>] [--full-clone]
[--[no-]src] <url> [<enlistment>]
[--[no-]src] [--local-cache-path <path>] [--cache-server-url <url>]
<url> [<enlistment>]
scalar list
scalar register [<enlistment>]
scalar unregister [<enlistment>]
@ -17,6 +18,7 @@ scalar run ( all | config | commit-graph | fetch | loose-objects | pack-files )
scalar reconfigure [ --all | <enlistment> ]
scalar diagnose [<enlistment>]
scalar delete <enlistment>
scalar cache-server ( --get | --set <url> | --list [<remote>] ) [<enlistment>]
DESCRIPTION
-----------
@ -90,6 +92,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 <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 `<drive>:\.scalarCache` on Windows (on the same drive as the
clone), and `~/.scalarCache` on macOS.
--cache-server-url <url>::
Retrieve missing objects from the specified remote, which is expected to
understand the GVFS protocol.
List
~~~~
@ -163,6 +176,27 @@ delete <enlistment>::
This subcommand lets you delete an existing Scalar enlistment from your
local file system, unregistering the repository.
Cache-server
~~~~~~~~~~~~
cache-server ( --get | --set <url> | --list [<remote>] ) [<enlistment>]::
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].

Просмотреть файл

@ -2763,6 +2763,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)
@ -2912,7 +2913,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)

Просмотреть файл

@ -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);
}
/*

Просмотреть файл

@ -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 "/").

Просмотреть файл

@ -3371,6 +3371,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;
@ -3391,11 +3392,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;

Просмотреть файл

@ -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)

Просмотреть файл

@ -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 <url>` 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 `<repo>/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.

Просмотреть файл

@ -0,0 +1,109 @@
Getting Started
===============
Registering existing Git repos
------------------------------
To add a repository to the list of registered repos, run `scalar register [<path>]`.
If `<path>` 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 [<path>]` 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, or using the
[GVFS protocol](https://github.com/microsoft/VFSForGit/blob/HEAD/Protocol.md),
such as Azure Repos.
```
scalar clone [options] <url> [<dir>]
```
Create a local copy of the repository at `<url>`. If specified, create the `<dir>`
directory and place the repository there. Otherwise, the last section of the `<url>`
will be used for `<dir>`.
At the end, the repo is located at `<dir>/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 <url>`
* Please choose the **Clone with HTTPS** option in the `Clone Repository` dialog in Azure Repos, not **Clone with SSH**.
* `cd <root>\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 <dir1> <dir2> ...`
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 <path>` to discover the directories
in `<path>`.
### 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 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=<url>`: If specified, set the intended cache server to
the specified `<url>`. All object queries will use the GVFS protocol to this
`<url>` instead of the origin remote. If the remote supplies a list of
cache servers via the `<url>/gvfs/config` endpoint, then the `clone` command
will select a nearby cache server from that list.
* `--branch=<ref>`: Specify the branch to checkout after clone.
* `--local-cache-path=<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 `<Volume>:\.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
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 <path>`
from outside of your enlistment to unregister the enlistment from the filesystem
watcher and delete the enlistment at `<path>`.

Просмотреть файл

@ -0,0 +1,54 @@
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 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
-------------
* [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` 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?
* [Frequently Asked Questions](faq.md)

Просмотреть файл

@ -0,0 +1,71 @@
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.
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!
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!
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 `<repo-name>/src` to make it easy to
place build artifacts outside of the Git repository, such as in
`<repo-name>/bin` or `<repo-name>/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/

Просмотреть файл

@ -0,0 +1,40 @@
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.
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=<url>` to set your cache server to `<url>`.

Просмотреть файл

@ -11,6 +11,7 @@
#include "packfile.h"
#include "parse-options.h"
#include "write-or-die.h"
#include "config.h"
struct archive_dir {
const char *path;
@ -71,6 +72,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);
@ -183,7 +217,8 @@ 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;
struct strbuf buf = STRBUF_INIT;
char *cache_server_url = NULL, *shared_cache = NULL;
struct strbuf buf = STRBUF_INIT, path = STRBUF_INIT;
int res, i;
struct archive_dir archive_dirs[] = {
{ ".git", 0 },
@ -218,6 +253,13 @@ 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);
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);
strvec_pushf(&archiver_args,
@ -248,6 +290,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);
@ -261,10 +349,13 @@ int create_diagnostics_archive(struct strbuf *zip_path, enum diagnose_mode mode)
goto diagnose_cleanup;
}
fprintf(stderr, "\n"
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) {
@ -275,6 +366,8 @@ diagnose_cleanup:
free(argv_copy);
strvec_clear(&archiver_args);
strbuf_release(&buf);
free(cache_server_url);
free(shared_cache);
return res;
}

2
dir.c
Просмотреть файл

@ -3191,6 +3191,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;

Просмотреть файл

@ -207,6 +207,12 @@
// [2] Documentation/technical/long-running-process-protocol.txt
// [3] See GIT_TRACE_PACKET
//
// endpoint
//
// Fetch the given endpoint from the main Git server (specifying
// `gvfs/config` as endpoint is idempotent to the `config`
// command mentioned above).
//
//////////////////////////////////////////////////////////////////
#include "git-compat-util.h"
@ -3116,18 +3122,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(&params.tr2_label, "GET/config");
strbuf_addstr(&params.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 */
@ -3149,15 +3157,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(&params.progress_base_phase3_msg,
"Receiving gvfs/config");
strbuf_addf(&params.progress_base_phase3_msg,
"Receiving %s", endpoint);
}
do_req__with_fallback("gvfs/config", &params, status);
do_req__with_fallback(endpoint, &params, status);
gh__request_params__release(&params);
}
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)
{
@ -3602,6 +3617,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.
@ -4097,6 +4141,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);

183
json-parser.c Normal file
Просмотреть файл

@ -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);
}

29
json-parser.h Normal file
Просмотреть файл

@ -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

510
scalar.c
Просмотреть файл

@ -5,6 +5,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"
@ -13,10 +14,19 @@
#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 "remote.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,
@ -44,6 +54,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;
@ -70,20 +83,46 @@ static void setup_enlistment_directory(int argc, const char **argv,
strbuf_release(&path);
}
static int git_retries = 3;
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);
for (attempts = 0, res = 1;
res && attempts < git_retries;
attempts++) {
struct child_process cmd = CHILD_PROCESS_INIT;
cmd.git_cmd = 1;
return run_command(&cmd);
strvec_pushv(&cmd.args, argv.v);
res = run_command(&cmd);
}
strvec_clear(&argv);
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 {
@ -124,23 +163,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 */
@ -167,11 +190,29 @@ static int set_recommended_config(int reconfigure)
{ "core.autoCRLF", "false" },
{ "core.safeCRLF", "false" },
{ "fetch.showForcedUpdates", "false" },
{ "core.configWriteLockTimeoutMS", "150" },
{ NULL, NULL },
};
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"),
@ -208,6 +249,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",
@ -217,10 +263,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);
@ -308,6 +358,215 @@ 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)
{
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;
}
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`.
*
* 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;
/*
* 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)) {
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 (!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");
}
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);
/* 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)
{
const char *env;
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;
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;
/*
* The GVFS protocol is only supported via https://; For testing, we
* also allow http://.
*/
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);
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;
@ -405,11 +664,50 @@ 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)
{
int dummy = 0;
const char *branch = NULL;
int full_clone = 0, single_branch = 0, show_progress = isatty(2);
int src = 1;
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_("<branch>"),
N_("branch to checkout after clone")),
@ -420,6 +718,14 @@ 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_("<url>"),
N_("the url or friendly name of the cache server")),
OPT_STRING(0, "local-cache-path", &local_cache_root,
N_("<path>"),
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[] = {
@ -431,6 +737,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);
@ -460,11 +767,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);
@ -484,8 +803,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);
@ -496,13 +835,48 @@ 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 ((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") ||
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;
@ -513,6 +887,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") ||
@ -545,6 +924,8 @@ cleanup:
free(enlistment);
free(dir);
strbuf_release(&buf);
free(default_cache_server_url);
free(local_cache_root_abs);
return res;
}
@ -566,6 +947,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);
@ -924,6 +1307,77 @@ static int cmd_version(int argc, const char **argv)
return 0;
}
static int cmd_cache_server(int argc, const char **argv)
{
int get = 0;
char *set = NULL, *list = NULL;
const char *default_remote = "(default)";
struct option options[] = {
OPT_BOOL(0, "get", &get,
N_("get the configured cache-server URL")),
OPT_STRING(0, "set", &set, N_("URL"),
N_("configure the cache-server to use")),
{ OPTION_STRING, 0, "list", &list, N_("remote"),
N_("list the possible cache-server URLs"),
PARSE_OPT_OPTARG, NULL, (intptr_t) default_remote },
OPT_END(),
};
const char * const usage[] = {
N_("scalar cache_server "
"[--get | --set <url> | --list [<remote>]] [<enlistment>]"),
NULL
};
int res = 0;
argc = parse_options(argc, argv, NULL, options,
usage, 0);
if (get + !!set + !!list > 1)
usage_msg_opt(_("--get/--set/--list are mutually exclusive"),
usage, options);
setup_enlistment_directory(argc, argv, usage, options, NULL);
if (list) {
const char *name = list, *url = list;
if (list == default_remote)
list = NULL;
if (!list || !strchr(list, '/')) {
struct remote *remote;
/* Look up remote */
remote = remote_get(list);
if (!remote) {
error("no such remote: '%s'", name);
free(list);
return 1;
}
if (!remote->url) {
free(list);
return error(_("remote '%s' has no URLs"),
name);
}
url = remote->url[0];
}
res = supports_gvfs_protocol(url, NULL);
free(list);
} else if (set) {
res = set_config("gvfs.cache-server=%s", set);
free(set);
} else {
char *url = NULL;
printf("Using cache server: %s\n",
git_config_get_string("gvfs.cache-server", &url) ?
"(undefined)" : url);
free(url);
}
return !!res;
}
static struct {
const char *name;
int (*fn)(int, const char **);
@ -938,6 +1392,7 @@ static struct {
{ "help", cmd_help },
{ "version", cmd_version },
{ "diagnose", cmd_diagnose },
{ "cache-server", cmd_cache_server },
{ NULL, NULL},
};
@ -946,6 +1401,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)
@ -969,6 +1430,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);

Просмотреть файл

@ -1,5 +1,6 @@
#include "git-compat-util.h"
#include "environment.h"
#include "gettext.h"
#include "hex.h"
#include "alloc.h"
#include "setup.h"
@ -1568,6 +1569,8 @@ done:
static enum worker_result dispatch(struct req *req)
{
static regex_t *smart_http_regex;
static int initialized;
const char *method;
enum worker_result wr;
@ -1616,6 +1619,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);
}

Просмотреть файл

@ -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
'
@ -238,4 +245,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)/.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