Merge branch 'js/scalar'

Add pieces from "scalar" to contrib/.

* js/scalar:
  scalar: implement the `version` command
  scalar: implement the `delete` command
  scalar: teach 'reconfigure' to optionally handle all registered enlistments
  scalar: allow reconfiguring an existing enlistment
  scalar: implement the `run` command
  scalar: teach 'clone' to support the --single-branch option
  scalar: implement the `clone` subcommand
  scalar: implement 'scalar list'
  scalar: let 'unregister' handle a deleted enlistment directory gracefully
  scalar: 'unregister' stops background maintenance
  scalar: 'register' sets recommended config and starts maintenance
  scalar: create test infrastructure
  scalar: start documenting the command
  scalar: create a rudimentary executable
  scalar: add a README with a roadmap
maint
Junio C Hamano 2021-12-21 15:03:17 -08:00
commit 62e83d4f69
8 changed files with 1275 additions and 0 deletions

View File

@ -2492,6 +2492,11 @@ OBJECTS += $(REFTABLE_OBJS) $(REFTABLE_TEST_OBJS)
ifndef NO_CURL
OBJECTS += http.o http-walker.o remote-curl.o
endif

SCALAR_SOURCES := contrib/scalar/scalar.c
SCALAR_OBJECTS := $(SCALAR_SOURCES:c=o)
OBJECTS += $(SCALAR_OBJECTS)

.PHONY: objects
objects: $(OBJECTS)

@ -2625,6 +2630,10 @@ $(REMOTE_CURL_PRIMARY): remote-curl.o http.o http-walker.o GIT-LDFLAGS $(GITLIBS
$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \
$(CURL_LIBCURL) $(EXPAT_LIBEXPAT) $(LIBS)

contrib/scalar/scalar$X: $(SCALAR_OBJECTS) GIT-LDFLAGS $(GITLIBS)
$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \
$(filter %.o,$^) $(LIBS)

$(LIB_FILE): $(LIB_OBJS)
$(QUIET_AR)$(RM) $@ && $(AR) $(ARFLAGS) $@ $^


2
contrib/scalar/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/*.exe
/scalar

45
contrib/scalar/Makefile Normal file
View File

@ -0,0 +1,45 @@
QUIET_SUBDIR0 = +$(MAKE) -C # space to separate -C and subdir
QUIET_SUBDIR1 =

ifneq ($(findstring s,$(MAKEFLAGS)),s)
ifndef V
QUIET_GEN = @echo ' ' GEN $@;
QUIET_SUBDIR0 = +@subdir=
QUIET_SUBDIR1 = ;$(NO_SUBDIR) echo ' ' SUBDIR $$subdir; \
$(MAKE) $(PRINT_DIR) -C $$subdir
else
export V
endif
endif

all:

include ../../config.mak.uname
-include ../../config.mak.autogen
-include ../../config.mak

TARGETS = scalar$(X) scalar.o
GITLIBS = ../../common-main.o ../../libgit.a ../../xdiff/lib.a

all: scalar$(X) ../../bin-wrappers/scalar

$(GITLIBS):
$(QUIET_SUBDIR0)../.. $(QUIET_SUBDIR1) $(subst ../../,,$@)

$(TARGETS): $(GITLIBS) scalar.c
$(QUIET_SUBDIR0)../.. $(QUIET_SUBDIR1) $(patsubst %,contrib/scalar/%,$@)

clean:
$(RM) $(TARGETS) ../../bin-wrappers/scalar

../../bin-wrappers/scalar: ../../wrap-for-bin.sh Makefile
@mkdir -p ../../bin-wrappers
$(QUIET_GEN)sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \
-e 's|@@BUILD_DIR@@|$(shell cd ../.. && pwd)|' \
-e 's|@@PROG@@|contrib/scalar/scalar$(X)|' < $< > $@ && \
chmod +x $@

test: all
$(MAKE) -C t

.PHONY: $(GITLIBS) all clean test FORCE

82
contrib/scalar/README.md Normal file
View File

@ -0,0 +1,82 @@
# Scalar - an opinionated repository management tool

Scalar is an add-on to Git that helps users take advantage of advanced
performance features in Git. Originally implemented in C# using .NET Core,
based on the learnings from the VFS for Git project, most of the techniques
developed by the Scalar project have been integrated into core Git already:

* partial clone,
* commit graphs,
* multi-pack index,
* sparse checkout (cone mode),
* scheduled background maintenance,
* etc

This directory contains the remaining parts of Scalar that are not (yet) in
core Git.

## Roadmap

The idea is to populate this directory via incremental patch series and
eventually move to a top-level directory next to `gitk-git/` and to `git-gui/`. The
current plan involves the following patch series:

- `scalar-the-beginning`: The initial patch series which sets up
`contrib/scalar/` and populates it with a minimal `scalar` command that
demonstrates the fundamental ideas.

- `scalar-c-and-C`: The `scalar` command learns about two options that can be
specified before the command, `-c <key>=<value>` and `-C <directory>`.

- `scalar-diagnose`: The `scalar` command is taught the `diagnose` subcommand.

- `scalar-and-builtin-fsmonitor`: The built-in FSMonitor is enabled in `scalar
register` and in `scalar clone`, for an enormous performance boost when
working in large worktrees. This patch series necessarily depends on Jeff
Hostetler's FSMonitor patch series to be integrated into Git.

- `scalar-gentler-config-locking`: Scalar enlistments are registered in the
user's Git config. This usually does not represent any problem because it is
rare for a user to register an enlistment. However, in Scalar's functional
tests, Scalar enlistments are created galore, and in parallel, which can lead
to lock contention. This patch series works around that problem by re-trying
to lock the config file in a gentle fashion.

- `scalar-extra-docs`: Add some extensive documentation that has been written
in the original Scalar project (all subject to discussion, of course).

- `optionally-install-scalar`: Now that Scalar is feature (and documentation)
complete and is verified in CI builds, let's offer to install it.

- `move-scalar-to-toplevel`: Now that Scalar is complete, let's move it next to
`gitk-git/` and to `git-gui/`, making it a top-level command.

The following two patch series exist in Microsoft's fork of Git and are
publicly available. There is no current plan to upstream them, not because I
want to withhold these patches, but because I don't think the Git community is
interested in these patches.

There are some interesting ideas there, but the implementation is too specific
to Azure Repos and/or VFS for Git to be of much help in general (and also: my
colleagues tried to upstream some patches already and the enthusiasm for
integrating things related to Azure Repos and VFS for Git can be summarized in
very, very few words).

These still exist mainly because the GVFS protocol is what Azure Repos has
instead of partial clone, while Git is focused on improving partial clone:

- `scalar-with-gvfs`: The primary purpose of this patch series is to support
existing Scalar users whose repositories are hosted in Azure Repos (which
does not support Git's partial clones, but supports its predecessor, the GVFS
protocol, which is used by Scalar to emulate the partial clone).

Since the GVFS protocol will never be supported by core Git, this patch
series will remain in Microsoft's fork of Git.

- `run-scalar-functional-tests`: The Scalar project developed a quite
comprehensive set of integration tests (or, "Functional Tests"). They are the
sole remaining part of the original C#-based Scalar project, and this patch
adds a GitHub workflow that runs them all.

Since the tests partially depend on features that are only provided in the
`scalar-with-gvfs` patch series, this patch cannot be upstreamed.

826
contrib/scalar/scalar.c Normal file
View File

@ -0,0 +1,826 @@
/*
* The Scalar command-line interface.
*/

#include "cache.h"
#include "gettext.h"
#include "parse-options.h"
#include "config.h"
#include "run-command.h"
#include "refs.h"
#include "dir.h"
#include "packfile.h"
#include "help.h"

/*
* Remove the deepest subdirectory in the provided path string. Path must not
* include a trailing path separator. Returns 1 if parent directory found,
* otherwise 0.
*/
static int strbuf_parent_directory(struct strbuf *buf)
{
size_t len = buf->len;
size_t offset = offset_1st_component(buf->buf);
char *path_sep = find_last_dir_sep(buf->buf + offset);
strbuf_setlen(buf, path_sep ? path_sep - buf->buf : offset);

return buf->len < len;
}

static void setup_enlistment_directory(int argc, const char **argv,
const char * const *usagestr,
const struct option *options,
struct strbuf *enlistment_root)
{
struct strbuf path = STRBUF_INIT;
char *root;
int enlistment_found = 0;

if (startup_info->have_repository)
BUG("gitdir already set up?!?");

if (argc > 1)
usage_with_options(usagestr, options);

/* find the worktree, determine its corresponding root */
if (argc == 1)
strbuf_add_absolute_path(&path, argv[0]);
else if (strbuf_getcwd(&path) < 0)
die(_("need a working directory"));

strbuf_trim_trailing_dir_sep(&path);
do {
const size_t len = path.len;

/* check if currently in enlistment root with src/ workdir */
strbuf_addstr(&path, "/src");
if (is_nonbare_repository_dir(&path)) {
if (enlistment_root)
strbuf_add(enlistment_root, path.buf, len);

enlistment_found = 1;
break;
}

/* reset to original path */
strbuf_setlen(&path, len);

/* check if currently in workdir */
if (is_nonbare_repository_dir(&path)) {
if (enlistment_root) {
/*
* If the worktree's directory's name is `src`, the enlistment is the
* parent directory, otherwise it is identical to the worktree.
*/
root = strip_path_suffix(path.buf, "src");
strbuf_addstr(enlistment_root, root ? root : path.buf);
free(root);
}

enlistment_found = 1;
break;
}
} while (strbuf_parent_directory(&path));

if (!enlistment_found)
die(_("could not find enlistment root"));

if (chdir(path.buf) < 0)
die_errno(_("could not switch to '%s'"), path.buf);

strbuf_release(&path);
setup_git_directory();
}

static int run_git(const char *arg, ...)
{
struct strvec argv = STRVEC_INIT;
va_list args;
const char *p;
int res;

va_start(args, arg);
strvec_push(&argv, arg);
while ((p = va_arg(args, const char *)))
strvec_push(&argv, p);
va_end(args);

res = run_command_v_opt(argv.v, RUN_GIT_CMD);

strvec_clear(&argv);
return res;
}

static int set_recommended_config(int reconfigure)
{
struct {
const char *key;
const char *value;
int overwrite_on_reconfigure;
} config[] = {
/* Required */
{ "am.keepCR", "true", 1 },
{ "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 */
{ "gc.auto", "0", 1 },
{ "gui.GCWarning", "false", 1 },
{ "index.threads", "true", 1 },
{ "index.version", "4", 1 },
{ "merge.stat", "false", 1 },
{ "merge.renames", "true", 1 },
{ "pack.useBitmaps", "false", 1 },
{ "pack.useSparse", "true", 1 },
{ "receive.autoGC", "false", 1 },
{ "reset.quiet", "true", 1 },
{ "feature.manyFiles", "false", 1 },
{ "feature.experimental", "false", 1 },
{ "fetch.unpackLimit", "1", 1 },
{ "fetch.writeCommitGraph", "false", 1 },
#ifdef WIN32
{ "http.sslBackend", "schannel", 1 },
#endif
/* Optional */
{ "status.aheadBehind", "false" },
{ "commitGraph.generationVersion", "1" },
{ "core.autoCRLF", "false" },
{ "core.safeCRLF", "false" },
{ "fetch.showForcedUpdates", "false" },
{ NULL, NULL },
};
int i;
char *value;

for (i = 0; config[i].key; i++) {
if ((reconfigure && config[i].overwrite_on_reconfigure) ||
git_config_get_string(config[i].key, &value)) {
trace2_data_string("scalar", the_repository, config[i].key, "created");
if (git_config_set_gently(config[i].key,
config[i].value) < 0)
return error(_("could not configure %s=%s"),
config[i].key, config[i].value);
} else {
trace2_data_string("scalar", the_repository, config[i].key, "exists");
free(value);
}
}

/*
* The `log.excludeDecoration` setting is special because it allows
* for multiple values.
*/
if (git_config_get_string("log.excludeDecoration", &value)) {
trace2_data_string("scalar", the_repository,
"log.excludeDecoration", "created");
if (git_config_set_multivar_gently("log.excludeDecoration",
"refs/prefetch/*",
CONFIG_REGEX_NONE, 0))
return error(_("could not configure "
"log.excludeDecoration"));
} else {
trace2_data_string("scalar", the_repository,
"log.excludeDecoration", "exists");
free(value);
}

return 0;
}

static int toggle_maintenance(int enable)
{
return run_git("maintenance", enable ? "start" : "unregister", NULL);
}

static int add_or_remove_enlistment(int add)
{
int res;

if (!the_repository->worktree)
die(_("Scalar enlistments require a worktree"));

res = run_git("config", "--global", "--get", "--fixed-value",
"scalar.repo", the_repository->worktree, NULL);

/*
* If we want to add and the setting is already there, then do nothing.
* If we want to remove and the setting is not there, then do nothing.
*/
if ((add && !res) || (!add && res))
return 0;

return run_git("config", "--global", add ? "--add" : "--unset",
add ? "--no-fixed-value" : "--fixed-value",
"scalar.repo", the_repository->worktree, NULL);
}

static int register_dir(void)
{
int res = add_or_remove_enlistment(1);

if (!res)
res = set_recommended_config(0);

if (!res)
res = toggle_maintenance(1);

return res;
}

static int unregister_dir(void)
{
int res = 0;

if (toggle_maintenance(0) < 0)
res = -1;

if (add_or_remove_enlistment(0) < 0)
res = -1;

return res;
}

/* printf-style interface, expects `<key>=<value>` argument */
static int set_config(const char *fmt, ...)
{
struct strbuf buf = STRBUF_INIT;
char *value;
int res;
va_list args;

va_start(args, fmt);
strbuf_vaddf(&buf, fmt, args);
va_end(args);

value = strchr(buf.buf, '=');
if (value)
*(value++) = '\0';
res = git_config_set_gently(buf.buf, value);
strbuf_release(&buf);

return res;
}

static char *remote_default_branch(const char *url)
{
struct child_process cp = CHILD_PROCESS_INIT;
struct strbuf out = STRBUF_INIT;

cp.git_cmd = 1;
strvec_pushl(&cp.args, "ls-remote", "--symref", url, "HEAD", NULL);
if (!pipe_command(&cp, NULL, 0, &out, 0, NULL, 0)) {
const char *line = out.buf;

while (*line) {
const char *eol = strchrnul(line, '\n'), *p;
size_t len = eol - line;
char *branch;

if (!skip_prefix(line, "ref: ", &p) ||
!strip_suffix_mem(line, &len, "\tHEAD")) {
line = eol + (*eol == '\n');
continue;
}

eol = line + len;
if (skip_prefix(p, "refs/heads/", &p)) {
branch = xstrndup(p, eol - p);
strbuf_release(&out);
return branch;
}

error(_("remote HEAD is not a branch: '%.*s'"),
(int)(eol - p), p);
strbuf_release(&out);
return NULL;
}
}
warning(_("failed to get default branch name from remote; "
"using local default"));
strbuf_reset(&out);

child_process_init(&cp);
cp.git_cmd = 1;
strvec_pushl(&cp.args, "symbolic-ref", "--short", "HEAD", NULL);
if (!pipe_command(&cp, NULL, 0, &out, 0, NULL, 0)) {
strbuf_trim(&out);
return strbuf_detach(&out, NULL);
}

strbuf_release(&out);
error(_("failed to get default branch name"));
return NULL;
}

static int delete_enlistment(struct strbuf *enlistment)
{
#ifdef WIN32
struct strbuf parent = STRBUF_INIT;
#endif

if (unregister_dir())
die(_("failed to unregister repository"));

#ifdef WIN32
/*
* Change the current directory to one outside of the enlistment so
* that we may delete everything underneath it.
*/
strbuf_addbuf(&parent, enlistment);
strbuf_parent_directory(&parent);
if (chdir(parent.buf) < 0)
die_errno(_("could not switch to '%s'"), parent.buf);
strbuf_release(&parent);
#endif

if (remove_dir_recursively(enlistment, 0))
die(_("failed to delete enlistment directory"));

return 0;
}

/*
* Dummy implementation; Using `get_version_info()` would cause a link error
* without this.
*/
void load_builtin_commands(const char *prefix, struct cmdnames *cmds)
{
die("not implemented");
}

static int cmd_clone(int argc, const char **argv)
{
const char *branch = NULL;
int full_clone = 0, single_branch = 0;
struct option clone_options[] = {
OPT_STRING('b', "branch", &branch, N_("<branch>"),
N_("branch to checkout after clone")),
OPT_BOOL(0, "full-clone", &full_clone,
N_("when cloning, create full working directory")),
OPT_BOOL(0, "single-branch", &single_branch,
N_("only download metadata for the branch that will "
"be checked out")),
OPT_END(),
};
const char * const clone_usage[] = {
N_("scalar clone [<options>] [--] <repo> [<dir>]"),
NULL
};
const char *url;
char *enlistment = NULL, *dir = NULL;
struct strbuf buf = STRBUF_INIT;
int res;

argc = parse_options(argc, argv, NULL, clone_options, clone_usage, 0);

if (argc == 2) {
url = argv[0];
enlistment = xstrdup(argv[1]);
} else if (argc == 1) {
url = argv[0];

strbuf_addstr(&buf, url);
/* Strip trailing slashes, if any */
while (buf.len > 0 && is_dir_sep(buf.buf[buf.len - 1]))
strbuf_setlen(&buf, buf.len - 1);
/* Strip suffix `.git`, if any */
strbuf_strip_suffix(&buf, ".git");

enlistment = find_last_dir_sep(buf.buf);
if (!enlistment) {
die(_("cannot deduce worktree name from '%s'"), url);
}
enlistment = xstrdup(enlistment + 1);
} else {
usage_msg_opt(_("You must specify a repository to clone."),
clone_usage, clone_options);
}

if (is_directory(enlistment))
die(_("directory '%s' exists already"), enlistment);

dir = xstrfmt("%s/src", enlistment);

strbuf_reset(&buf);
if (branch)
strbuf_addf(&buf, "init.defaultBranch=%s", branch);
else {
char *b = repo_default_branch_name(the_repository, 1);
strbuf_addf(&buf, "init.defaultBranch=%s", b);
free(b);
}

if ((res = run_git("-c", buf.buf, "init", "--", dir, NULL)))
goto cleanup;

if (chdir(dir) < 0) {
res = error_errno(_("could not switch to '%s'"), dir);
goto cleanup;
}

setup_git_directory();

/* common-main already logs `argv` */
trace2_def_repo(the_repository);

if (!branch && !(branch = remote_default_branch(url))) {
res = error(_("failed to get default branch for '%s'"), url);
goto cleanup;
}

if (set_config("remote.origin.url=%s", url) ||
set_config("remote.origin.fetch="
"+refs/heads/%s:refs/remotes/origin/%s",
single_branch ? branch : "*",
single_branch ? branch : "*") ||
set_config("remote.origin.promisor=true") ||
set_config("remote.origin.partialCloneFilter=blob:none")) {
res = error(_("could not configure remote in '%s'"), dir);
goto cleanup;
}

if (!full_clone &&
(res = run_git("sparse-checkout", "init", "--cone", NULL)))
goto cleanup;

if (set_recommended_config(0))
return error(_("could not configure '%s'"), dir);

if ((res = run_git("fetch", "--quiet", "origin", NULL))) {
warning(_("partial clone failed; attempting full clone"));

if (set_config("remote.origin.promisor") ||
set_config("remote.origin.partialCloneFilter")) {
res = error(_("could not configure for full clone"));
goto cleanup;
}

if ((res = run_git("fetch", "--quiet", "origin", NULL)))
goto cleanup;
}

if ((res = set_config("branch.%s.remote=origin", branch)))
goto cleanup;
if ((res = set_config("branch.%s.merge=refs/heads/%s",
branch, branch)))
goto cleanup;

strbuf_reset(&buf);
strbuf_addf(&buf, "origin/%s", branch);
res = run_git("checkout", "-f", "-t", buf.buf, NULL);
if (res)
goto cleanup;

res = register_dir();

cleanup:
free(enlistment);
free(dir);
strbuf_release(&buf);
return res;
}

static int cmd_list(int argc, const char **argv)
{
if (argc != 1)
die(_("`scalar list` does not take arguments"));

if (run_git("config", "--global", "--get-all", "scalar.repo", NULL) < 0)
return -1;
return 0;
}

static int cmd_register(int argc, const char **argv)
{
struct option options[] = {
OPT_END(),
};
const char * const usage[] = {
N_("scalar register [<enlistment>]"),
NULL
};

argc = parse_options(argc, argv, NULL, options,
usage, 0);

setup_enlistment_directory(argc, argv, usage, options, NULL);

return register_dir();
}

static int get_scalar_repos(const char *key, const char *value, void *data)
{
struct string_list *list = data;

if (!strcmp(key, "scalar.repo"))
string_list_append(list, value);

return 0;
}

static int cmd_reconfigure(int argc, const char **argv)
{
int all = 0;
struct option options[] = {
OPT_BOOL('a', "all", &all,
N_("reconfigure all registered enlistments")),
OPT_END(),
};
const char * const usage[] = {
N_("scalar reconfigure [--all | <enlistment>]"),
NULL
};
struct string_list scalar_repos = STRING_LIST_INIT_DUP;
int i, res = 0;
struct repository r = { NULL };
struct strbuf commondir = STRBUF_INIT, gitdir = STRBUF_INIT;

argc = parse_options(argc, argv, NULL, options,
usage, 0);

if (!all) {
setup_enlistment_directory(argc, argv, usage, options, NULL);

return set_recommended_config(1);
}

if (argc > 0)
usage_msg_opt(_("--all or <enlistment>, but not both"),
usage, options);

git_config(get_scalar_repos, &scalar_repos);

for (i = 0; i < scalar_repos.nr; i++) {
const char *dir = scalar_repos.items[i].string;

strbuf_reset(&commondir);
strbuf_reset(&gitdir);

if (chdir(dir) < 0) {
warning_errno(_("could not switch to '%s'"), dir);
res = -1;
} else if (discover_git_directory(&commondir, &gitdir) < 0) {
warning_errno(_("git repository gone in '%s'"), dir);
res = -1;
} else {
git_config_clear();

the_repository = &r;
r.commondir = commondir.buf;
r.gitdir = gitdir.buf;

if (set_recommended_config(1) < 0)
res = -1;
}
}

string_list_clear(&scalar_repos, 1);
strbuf_release(&commondir);
strbuf_release(&gitdir);

return res;
}

static int cmd_run(int argc, const char **argv)
{
struct option options[] = {
OPT_END(),
};
struct {
const char *arg, *task;
} tasks[] = {
{ "config", NULL },
{ "commit-graph", "commit-graph" },
{ "fetch", "prefetch" },
{ "loose-objects", "loose-objects" },
{ "pack-files", "incremental-repack" },
{ NULL, NULL }
};
struct strbuf buf = STRBUF_INIT;
const char *usagestr[] = { NULL, NULL };
int i;

strbuf_addstr(&buf, N_("scalar run <task> [<enlistment>]\nTasks:\n"));
for (i = 0; tasks[i].arg; i++)
strbuf_addf(&buf, "\t%s\n", tasks[i].arg);
usagestr[0] = buf.buf;

argc = parse_options(argc, argv, NULL, options,
usagestr, 0);

if (!argc)
usage_with_options(usagestr, options);

if (!strcmp("all", argv[0])) {
i = -1;
} else {
for (i = 0; tasks[i].arg && strcmp(tasks[i].arg, argv[0]); i++)
; /* keep looking for the task */

if (i > 0 && !tasks[i].arg) {
error(_("no such task: '%s'"), argv[0]);
usage_with_options(usagestr, options);
}
}

argc--;
argv++;
setup_enlistment_directory(argc, argv, usagestr, options, NULL);
strbuf_release(&buf);

if (i == 0)
return register_dir();

if (i > 0)
return run_git("maintenance", "run",
"--task", tasks[i].task, NULL);

if (register_dir())
return -1;
for (i = 1; tasks[i].arg; i++)
if (run_git("maintenance", "run",
"--task", tasks[i].task, NULL))
return -1;
return 0;
}

static int remove_deleted_enlistment(struct strbuf *path)
{
int res = 0;
strbuf_realpath_forgiving(path, path->buf, 1);

if (run_git("config", "--global",
"--unset", "--fixed-value",
"scalar.repo", path->buf, NULL) < 0)
res = -1;

if (run_git("config", "--global",
"--unset", "--fixed-value",
"maintenance.repo", path->buf, NULL) < 0)
res = -1;

return res;
}

static int cmd_unregister(int argc, const char **argv)
{
struct option options[] = {
OPT_END(),
};
const char * const usage[] = {
N_("scalar unregister [<enlistment>]"),
NULL
};

argc = parse_options(argc, argv, NULL, options,
usage, 0);

/*
* Be forgiving when the enlistment or worktree does not even exist any
* longer; This can be the case if a user deleted the worktree by
* mistake and _still_ wants to unregister the thing.
*/
if (argc == 1) {
struct strbuf src_path = STRBUF_INIT, workdir_path = STRBUF_INIT;

strbuf_addf(&src_path, "%s/src/.git", argv[0]);
strbuf_addf(&workdir_path, "%s/.git", argv[0]);
if (!is_directory(src_path.buf) && !is_directory(workdir_path.buf)) {
/* remove possible matching registrations */
int res = -1;

strbuf_strip_suffix(&src_path, "/.git");
res = remove_deleted_enlistment(&src_path) && res;

strbuf_strip_suffix(&workdir_path, "/.git");
res = remove_deleted_enlistment(&workdir_path) && res;

strbuf_release(&src_path);
strbuf_release(&workdir_path);
return res;
}
strbuf_release(&src_path);
strbuf_release(&workdir_path);
}

setup_enlistment_directory(argc, argv, usage, options, NULL);

return unregister_dir();
}

static int cmd_delete(int argc, const char **argv)
{
char *cwd = xgetcwd();
struct option options[] = {
OPT_END(),
};
const char * const usage[] = {
N_("scalar delete <enlistment>"),
NULL
};
struct strbuf enlistment = STRBUF_INIT;
int res = 0;

argc = parse_options(argc, argv, NULL, options,
usage, 0);

if (argc != 1)
usage_with_options(usage, options);

setup_enlistment_directory(argc, argv, usage, options, &enlistment);

if (dir_inside_of(cwd, enlistment.buf) >= 0)
res = error(_("refusing to delete current working directory"));
else {
close_object_store(the_repository->objects);
res = delete_enlistment(&enlistment);
}
strbuf_release(&enlistment);
free(cwd);

return res;
}

static int cmd_version(int argc, const char **argv)
{
int verbose = 0, build_options = 0;
struct option options[] = {
OPT__VERBOSE(&verbose, N_("include Git version")),
OPT_BOOL(0, "build-options", &build_options,
N_("include Git's build options")),
OPT_END(),
};
const char * const usage[] = {
N_("scalar verbose [-v | --verbose] [--build-options]"),
NULL
};
struct strbuf buf = STRBUF_INIT;

argc = parse_options(argc, argv, NULL, options,
usage, 0);

if (argc != 0)
usage_with_options(usage, options);

get_version_info(&buf, build_options);
fprintf(stderr, "%s\n", buf.buf);
strbuf_release(&buf);

return 0;
}

static struct {
const char *name;
int (*fn)(int, const char **);
} builtins[] = {
{ "clone", cmd_clone },
{ "list", cmd_list },
{ "register", cmd_register },
{ "unregister", cmd_unregister },
{ "run", cmd_run },
{ "reconfigure", cmd_reconfigure },
{ "delete", cmd_delete },
{ "version", cmd_version },
{ NULL, NULL},
};

int cmd_main(int argc, const char **argv)
{
struct strbuf scalar_usage = STRBUF_INIT;
int i;

if (argc > 1) {
argv++;
argc--;

for (i = 0; builtins[i].name; i++)
if (!strcmp(builtins[i].name, argv[0]))
return !!builtins[i].fn(argc, argv);
}

strbuf_addstr(&scalar_usage,
N_("scalar <command> [<options>]\n\nCommands:\n"));
for (i = 0; builtins[i].name; i++)
strbuf_addf(&scalar_usage, "\t%s\n", builtins[i].name);

usage(scalar_usage.buf);
}

145
contrib/scalar/scalar.txt Normal file
View File

@ -0,0 +1,145 @@
scalar(1)
=========

NAME
----
scalar - an opinionated repository management tool

SYNOPSIS
--------
[verse]
scalar clone [--single-branch] [--branch <main-branch>] [--full-clone] <url> [<enlistment>]
scalar list
scalar register [<enlistment>]
scalar unregister [<enlistment>]
scalar run ( all | config | commit-graph | fetch | loose-objects | pack-files ) [<enlistment>]
scalar reconfigure [ --all | <enlistment> ]
scalar delete <enlistment>

DESCRIPTION
-----------

Scalar is an opinionated repository management tool. By creating new
repositories or registering existing repositories with Scalar, your Git
experience will speed up. Scalar sets advanced Git config settings,
maintains your repositories in the background, and helps reduce data sent
across the network.

An important Scalar concept is the enlistment: this is the top-level directory
of the project. It usually contains the subdirectory `src/` which is a Git
worktree. This encourages the separation between tracked files (inside `src/`)
and untracked files, such as build artifacts (outside `src/`). When registering
an existing Git worktree with Scalar whose name is not `src`, the enlistment
will be identical to the worktree.

The `scalar` command implements various subcommands, and different options
depending on the subcommand. With the exception of `clone`, `list` and
`reconfigure --all`, all subcommands expect to be run in an enlistment.

COMMANDS
--------

Clone
~~~~~

clone [<options>] <url> [<enlistment>]::
Clones the specified repository, similar to linkgit:git-clone[1]. By
default, only commit and tree objects are cloned. Once finished, the
worktree is located at `<enlistment>/src`.
+
The sparse-checkout feature is enabled (except when run with `--full-clone`)
and the only files present are those in the top-level directory. Use
`git sparse-checkout set` to expand the set of directories you want to see,
or `git sparse-checkout disable` to expand to all files (see
linkgit:git-sparse-checkout[1] for more details). You can explore the
subdirectories outside your sparse-checkout by using `git ls-tree
HEAD[:<directory>]`.

-b <name>::
--branch <name>::
Instead of checking out the branch pointed to by the cloned
repository's HEAD, check out the `<name>` branch instead.

--[no-]single-branch::
Clone only the history leading to the tip of a single branch, either
specified by the `--branch` option or the primary branch remote's
`HEAD` points at.
+
Further fetches into the resulting repository will only update the
remote-tracking branch for the branch this option was used for the initial
cloning. If the HEAD at the remote did not point at any branch when
`--single-branch` clone was made, no remote-tracking branch is created.

--[no-]full-clone::
A sparse-checkout is initialized by default. This behavior can be
turned off via `--full-clone`.

List
~~~~

list::
List enlistments that are currently registered by Scalar. This
subcommand does not need to be run inside an enlistment.

Register
~~~~~~~~

register [<enlistment>]::
Adds the enlistment's repository to the list of registered repositories
and starts background maintenance. If `<enlistment>` is not provided,
then the enlistment associated with the current working directory is
registered.
+
Note: when this subcommand is called in a worktree that is called `src/`, its
parent directory is considered to be the Scalar enlistment. If the worktree is
_not_ called `src/`, it itself will be considered to be the Scalar enlistment.

Unregister
~~~~~~~~~~

unregister [<enlistment>]::
Remove the specified repository from the list of repositories
registered with Scalar and stop the scheduled background maintenance.

Run
~~~

scalar run ( all | config | commit-graph | fetch | loose-objects | pack-files ) [<enlistment>]::
Run the given maintenance task (or all tasks, if `all` was specified).
Except for `all` and `config`, this subcommand simply hands off to
linkgit:git-maintenance[1] (mapping `fetch` to `prefetch` and
`pack-files` to `incremental-repack`).
+
These tasks are run automatically as part of the scheduled maintenance,
as soon as the repository is registered with Scalar. It should therefore
not be necessary to run this subcommand manually.
+
The `config` task is specific to Scalar and configures all those
opinionated default settings that make Git work more efficiently with
large repositories. As this task is run as part of `scalar clone`
automatically, explicit invocations of this task are rarely needed.

Reconfigure
~~~~~~~~~~~

After a Scalar upgrade, or when the configuration of a Scalar enlistment
was somehow corrupted or changed by mistake, this subcommand allows to
reconfigure the enlistment.

With the `--all` option, all enlistments currently registered with Scalar
will be reconfigured. Use this option after each Scalar upgrade.

Delete
~~~~~~

delete <enlistment>::
This subcommand lets you delete an existing Scalar enlistment from your
local file system, unregistering the repository.

SEE ALSO
--------
linkgit:git-clone[1], linkgit:git-maintenance[1].

Scalar
---
Associated with the linkgit:git[1] suite

78
contrib/scalar/t/Makefile Normal file
View File

@ -0,0 +1,78 @@
# Run scalar tests
#
# Copyright (c) 2005,2021 Junio C Hamano, Johannes Schindelin
#

-include ../../../config.mak.autogen
-include ../../../config.mak

SHELL_PATH ?= $(SHELL)
PERL_PATH ?= /usr/bin/perl
RM ?= rm -f
PROVE ?= prove
DEFAULT_TEST_TARGET ?= test
TEST_LINT ?= test-lint

ifdef TEST_OUTPUT_DIRECTORY
TEST_RESULTS_DIRECTORY = $(TEST_OUTPUT_DIRECTORY)/test-results
else
TEST_RESULTS_DIRECTORY = ../../../t/test-results
endif

# Shell quote;
SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH))
PERL_PATH_SQ = $(subst ','\'',$(PERL_PATH))
TEST_RESULTS_DIRECTORY_SQ = $(subst ','\'',$(TEST_RESULTS_DIRECTORY))

T = $(sort $(wildcard t[0-9][0-9][0-9][0-9]-*.sh))

all: $(DEFAULT_TEST_TARGET)

test: $(TEST_LINT)
$(MAKE) aggregate-results-and-cleanup

prove: $(TEST_LINT)
@echo "*** prove ***"; GIT_CONFIG=.git/config $(PROVE) --exec '$(SHELL_PATH_SQ)' $(GIT_PROVE_OPTS) $(T) :: $(GIT_TEST_OPTS)
$(MAKE) clean-except-prove-cache

$(T):
@echo "*** $@ ***"; GIT_CONFIG=.git/config '$(SHELL_PATH_SQ)' $@ $(GIT_TEST_OPTS)

clean-except-prove-cache:
$(RM) -r 'trash directory'.* '$(TEST_RESULTS_DIRECTORY_SQ)'
$(RM) -r valgrind/bin

clean: clean-except-prove-cache
$(RM) .prove

test-lint: test-lint-duplicates test-lint-executable test-lint-shell-syntax

test-lint-duplicates:
@dups=`echo $(T) | tr ' ' '\n' | sed 's/-.*//' | sort | uniq -d` && \
test -z "$$dups" || { \
echo >&2 "duplicate test numbers:" $$dups; exit 1; }

test-lint-executable:
@bad=`for i in $(T); do test -x "$$i" || echo $$i; done` && \
test -z "$$bad" || { \
echo >&2 "non-executable tests:" $$bad; exit 1; }

test-lint-shell-syntax:
@'$(PERL_PATH_SQ)' ../../../t/check-non-portable-shell.pl $(T)

aggregate-results-and-cleanup: $(T)
$(MAKE) aggregate-results
$(MAKE) clean

aggregate-results:
for f in '$(TEST_RESULTS_DIRECTORY_SQ)'/t*-*.counts; do \
echo "$$f"; \
done | '$(SHELL_PATH_SQ)' ../../../t/aggregate-results.sh

valgrind:
$(MAKE) GIT_TEST_OPTS="$(GIT_TEST_OPTS) --valgrind"

test-results:
mkdir -p test-results

.PHONY: $(T) aggregate-results clean valgrind

View File

@ -0,0 +1,88 @@
#!/bin/sh

test_description='test the `scalar` command'

TEST_DIRECTORY=$PWD/../../../t
export TEST_DIRECTORY

# Make it work with --no-bin-wrappers
PATH=$PWD/..:$PATH

. ../../../t/test-lib.sh

GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab ../cron.txt,launchctl:true,schtasks:true"
export GIT_TEST_MAINT_SCHEDULER

test_expect_success 'scalar shows a usage' '
test_expect_code 129 scalar -h
'

test_expect_success 'scalar unregister' '
git init vanish/src &&
scalar register vanish/src &&
git config --get --global --fixed-value \
maintenance.repo "$(pwd)/vanish/src" &&
scalar list >scalar.repos &&
grep -F "$(pwd)/vanish/src" scalar.repos &&
rm -rf vanish/src/.git &&
scalar unregister vanish &&
test_must_fail git config --get --global --fixed-value \
maintenance.repo "$(pwd)/vanish/src" &&
scalar list >scalar.repos &&
! grep -F "$(pwd)/vanish/src" scalar.repos
'

test_expect_success 'set up repository to clone' '
test_commit first &&
test_commit second &&
test_commit third &&
git switch -c parallel first &&
mkdir -p 1/2 &&
test_commit 1/2/3 &&
git config uploadPack.allowFilter true &&
git config uploadPack.allowAnySHA1InWant true
'

test_expect_success 'scalar clone' '
second=$(git rev-parse --verify second:second.t) &&
scalar clone "file://$(pwd)" cloned --single-branch &&
(
cd cloned/src &&

git config --get --global --fixed-value maintenance.repo \
"$(pwd)" &&

git for-each-ref --format="%(refname)" refs/remotes/origin/ >actual &&
echo "refs/remotes/origin/parallel" >expect &&
test_cmp expect actual &&

test_path_is_missing 1/2 &&
test_must_fail git rev-list --missing=print $second &&
git rev-list $second &&
git cat-file blob $second >actual &&
echo "second" >expect &&
test_cmp expect actual
)
'

test_expect_success 'scalar reconfigure' '
git init one/src &&
scalar register one &&
git -C one/src config core.preloadIndex false &&
scalar reconfigure one &&
test true = "$(git -C one/src config core.preloadIndex)" &&
git -C one/src config core.preloadIndex false &&
scalar reconfigure -a &&
test true = "$(git -C one/src config core.preloadIndex)"
'

test_expect_success 'scalar delete without enlistment shows a usage' '
test_expect_code 129 scalar delete
'

test_expect_success 'scalar delete with enlistment' '
scalar delete cloned &&
test_path_is_missing cloned
'

test_done