Skip to content

Commit 546f822

Browse files
dschogitster
authored andcommitted
scalar: implement the clone subcommand
This implements Scalar's opinionated `clone` command: it tries to use a partial clone and sets up a sparse checkout by default. In contrast to `git clone`, `scalar clone` sets up the worktree in the `src/` subdirectory, to encourage a separation between the source files and the build output (which helps Git tremendously because it avoids untracked files that have to be specifically ignored when refreshing the index). Also, it registers the repository for regular, scheduled maintenance, and configures a flurry of configuration settings based on the experience and experiments of the Microsoft Windows and the Microsoft Office development teams. Note: since the `scalar clone` command is by far the most commonly called `scalar` subcommand, we document it at the top of the manual page. Signed-off-by: Johannes Schindelin <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 2b71045 commit 546f822

File tree

3 files changed

+262
-3
lines changed

3 files changed

+262
-3
lines changed

contrib/scalar/scalar.c

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "parse-options.h"
88
#include "config.h"
99
#include "run-command.h"
10+
#include "refs.h"
1011

1112
/*
1213
* Remove the deepest subdirectory in the provided path string. Path must not
@@ -251,6 +252,205 @@ static int unregister_dir(void)
251252
return res;
252253
}
253254

255+
/* printf-style interface, expects `<key>=<value>` argument */
256+
static int set_config(const char *fmt, ...)
257+
{
258+
struct strbuf buf = STRBUF_INIT;
259+
char *value;
260+
int res;
261+
va_list args;
262+
263+
va_start(args, fmt);
264+
strbuf_vaddf(&buf, fmt, args);
265+
va_end(args);
266+
267+
value = strchr(buf.buf, '=');
268+
if (value)
269+
*(value++) = '\0';
270+
res = git_config_set_gently(buf.buf, value);
271+
strbuf_release(&buf);
272+
273+
return res;
274+
}
275+
276+
static char *remote_default_branch(const char *url)
277+
{
278+
struct child_process cp = CHILD_PROCESS_INIT;
279+
struct strbuf out = STRBUF_INIT;
280+
281+
cp.git_cmd = 1;
282+
strvec_pushl(&cp.args, "ls-remote", "--symref", url, "HEAD", NULL);
283+
if (!pipe_command(&cp, NULL, 0, &out, 0, NULL, 0)) {
284+
const char *line = out.buf;
285+
286+
while (*line) {
287+
const char *eol = strchrnul(line, '\n'), *p;
288+
size_t len = eol - line;
289+
char *branch;
290+
291+
if (!skip_prefix(line, "ref: ", &p) ||
292+
!strip_suffix_mem(line, &len, "\tHEAD")) {
293+
line = eol + (*eol == '\n');
294+
continue;
295+
}
296+
297+
eol = line + len;
298+
if (skip_prefix(p, "refs/heads/", &p)) {
299+
branch = xstrndup(p, eol - p);
300+
strbuf_release(&out);
301+
return branch;
302+
}
303+
304+
error(_("remote HEAD is not a branch: '%.*s'"),
305+
(int)(eol - p), p);
306+
strbuf_release(&out);
307+
return NULL;
308+
}
309+
}
310+
warning(_("failed to get default branch name from remote; "
311+
"using local default"));
312+
strbuf_reset(&out);
313+
314+
child_process_init(&cp);
315+
cp.git_cmd = 1;
316+
strvec_pushl(&cp.args, "symbolic-ref", "--short", "HEAD", NULL);
317+
if (!pipe_command(&cp, NULL, 0, &out, 0, NULL, 0)) {
318+
strbuf_trim(&out);
319+
return strbuf_detach(&out, NULL);
320+
}
321+
322+
strbuf_release(&out);
323+
error(_("failed to get default branch name"));
324+
return NULL;
325+
}
326+
327+
static int cmd_clone(int argc, const char **argv)
328+
{
329+
const char *branch = NULL;
330+
int full_clone = 0;
331+
struct option clone_options[] = {
332+
OPT_STRING('b', "branch", &branch, N_("<branch>"),
333+
N_("branch to checkout after clone")),
334+
OPT_BOOL(0, "full-clone", &full_clone,
335+
N_("when cloning, create full working directory")),
336+
OPT_END(),
337+
};
338+
const char * const clone_usage[] = {
339+
N_("scalar clone [<options>] [--] <repo> [<dir>]"),
340+
NULL
341+
};
342+
const char *url;
343+
char *enlistment = NULL, *dir = NULL;
344+
struct strbuf buf = STRBUF_INIT;
345+
int res;
346+
347+
argc = parse_options(argc, argv, NULL, clone_options, clone_usage, 0);
348+
349+
if (argc == 2) {
350+
url = argv[0];
351+
enlistment = xstrdup(argv[1]);
352+
} else if (argc == 1) {
353+
url = argv[0];
354+
355+
strbuf_addstr(&buf, url);
356+
/* Strip trailing slashes, if any */
357+
while (buf.len > 0 && is_dir_sep(buf.buf[buf.len - 1]))
358+
strbuf_setlen(&buf, buf.len - 1);
359+
/* Strip suffix `.git`, if any */
360+
strbuf_strip_suffix(&buf, ".git");
361+
362+
enlistment = find_last_dir_sep(buf.buf);
363+
if (!enlistment) {
364+
die(_("cannot deduce worktree name from '%s'"), url);
365+
}
366+
enlistment = xstrdup(enlistment + 1);
367+
} else {
368+
usage_msg_opt(_("You must specify a repository to clone."),
369+
clone_usage, clone_options);
370+
}
371+
372+
if (is_directory(enlistment))
373+
die(_("directory '%s' exists already"), enlistment);
374+
375+
dir = xstrfmt("%s/src", enlistment);
376+
377+
strbuf_reset(&buf);
378+
if (branch)
379+
strbuf_addf(&buf, "init.defaultBranch=%s", branch);
380+
else {
381+
char *b = repo_default_branch_name(the_repository, 1);
382+
strbuf_addf(&buf, "init.defaultBranch=%s", b);
383+
free(b);
384+
}
385+
386+
if ((res = run_git("-c", buf.buf, "init", "--", dir, NULL)))
387+
goto cleanup;
388+
389+
if (chdir(dir) < 0) {
390+
res = error_errno(_("could not switch to '%s'"), dir);
391+
goto cleanup;
392+
}
393+
394+
setup_git_directory();
395+
396+
/* common-main already logs `argv` */
397+
trace2_def_repo(the_repository);
398+
399+
if (!branch && !(branch = remote_default_branch(url))) {
400+
res = error(_("failed to get default branch for '%s'"), url);
401+
goto cleanup;
402+
}
403+
404+
if (set_config("remote.origin.url=%s", url) ||
405+
set_config("remote.origin.fetch="
406+
"+refs/heads/*:refs/remotes/origin/*") ||
407+
set_config("remote.origin.promisor=true") ||
408+
set_config("remote.origin.partialCloneFilter=blob:none")) {
409+
res = error(_("could not configure remote in '%s'"), dir);
410+
goto cleanup;
411+
}
412+
413+
if (!full_clone &&
414+
(res = run_git("sparse-checkout", "init", "--cone", NULL)))
415+
goto cleanup;
416+
417+
if (set_recommended_config())
418+
return error(_("could not configure '%s'"), dir);
419+
420+
if ((res = run_git("fetch", "--quiet", "origin", NULL))) {
421+
warning(_("partial clone failed; attempting full clone"));
422+
423+
if (set_config("remote.origin.promisor") ||
424+
set_config("remote.origin.partialCloneFilter")) {
425+
res = error(_("could not configure for full clone"));
426+
goto cleanup;
427+
}
428+
429+
if ((res = run_git("fetch", "--quiet", "origin", NULL)))
430+
goto cleanup;
431+
}
432+
433+
if ((res = set_config("branch.%s.remote=origin", branch)))
434+
goto cleanup;
435+
if ((res = set_config("branch.%s.merge=refs/heads/%s",
436+
branch, branch)))
437+
goto cleanup;
438+
439+
strbuf_reset(&buf);
440+
strbuf_addf(&buf, "origin/%s", branch);
441+
res = run_git("checkout", "-f", "-t", buf.buf, NULL);
442+
if (res)
443+
goto cleanup;
444+
445+
res = register_dir();
446+
447+
cleanup:
448+
free(enlistment);
449+
free(dir);
450+
strbuf_release(&buf);
451+
return res;
452+
}
453+
254454
static int cmd_list(int argc, const char **argv)
255455
{
256456
if (argc != 1)
@@ -347,6 +547,7 @@ static struct {
347547
const char *name;
348548
int (*fn)(int, const char **);
349549
} builtins[] = {
550+
{ "clone", cmd_clone },
350551
{ "list", cmd_list },
351552
{ "register", cmd_register },
352553
{ "unregister", cmd_unregister },

contrib/scalar/scalar.txt

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ scalar - an opinionated repository management tool
88
SYNOPSIS
99
--------
1010
[verse]
11+
scalar clone [--branch <main-branch>] [--full-clone] <url> [<enlistment>]
1112
scalar list
1213
scalar register [<enlistment>]
1314
scalar unregister [<enlistment>]
@@ -29,12 +30,37 @@ an existing Git worktree with Scalar whose name is not `src`, the enlistment
2930
will be identical to the worktree.
3031

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

3536
COMMANDS
3637
--------
3738

39+
Clone
40+
~~~~~
41+
42+
clone [<options>] <url> [<enlistment>]::
43+
Clones the specified repository, similar to linkgit:git-clone[1]. By
44+
default, only commit and tree objects are cloned. Once finished, the
45+
worktree is located at `<enlistment>/src`.
46+
+
47+
The sparse-checkout feature is enabled (except when run with `--full-clone`)
48+
and the only files present are those in the top-level directory. Use
49+
`git sparse-checkout set` to expand the set of directories you want to see,
50+
or `git sparse-checkout disable` to expand to all files (see
51+
linkgit:git-sparse-checkout[1] for more details). You can explore the
52+
subdirectories outside your sparse-checkout by using `git ls-tree
53+
HEAD[:<directory>]`.
54+
55+
-b <name>::
56+
--branch <name>::
57+
Instead of checking out the branch pointed to by the cloned
58+
repository's HEAD, check out the `<name>` branch instead.
59+
60+
--[no-]full-clone::
61+
A sparse-checkout is initialized by default. This behavior can be
62+
turned off via `--full-clone`.
63+
3864
List
3965
~~~~
4066

@@ -64,7 +90,7 @@ unregister [<enlistment>]::
6490

6591
SEE ALSO
6692
--------
67-
linkgit:git-maintenance[1].
93+
linkgit:git-clone[1], linkgit:git-maintenance[1].
6894

6995
Scalar
7096
---

contrib/scalar/t/t9099-scalar.sh

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ PATH=$PWD/..:$PATH
1010

1111
. ../../../t/test-lib.sh
1212

13+
GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab ../cron.txt,launchctl:true,schtasks:true"
14+
export GIT_TEST_MAINT_SCHEDULER
15+
1316
test_expect_success 'scalar shows a usage' '
1417
test_expect_code 129 scalar -h
1518
'
@@ -29,4 +32,33 @@ test_expect_success 'scalar unregister' '
2932
! grep -F "$(pwd)/vanish/src" scalar.repos
3033
'
3134

35+
test_expect_success 'set up repository to clone' '
36+
test_commit first &&
37+
test_commit second &&
38+
test_commit third &&
39+
git switch -c parallel first &&
40+
mkdir -p 1/2 &&
41+
test_commit 1/2/3 &&
42+
git config uploadPack.allowFilter true &&
43+
git config uploadPack.allowAnySHA1InWant true
44+
'
45+
46+
test_expect_success 'scalar clone' '
47+
second=$(git rev-parse --verify second:second.t) &&
48+
scalar clone "file://$(pwd)" cloned &&
49+
(
50+
cd cloned/src &&
51+
52+
git config --get --global --fixed-value maintenance.repo \
53+
"$(pwd)" &&
54+
55+
test_path_is_missing 1/2 &&
56+
test_must_fail git rev-list --missing=print $second &&
57+
git rev-list $second &&
58+
git cat-file blob $second >actual &&
59+
echo "second" >expect &&
60+
test_cmp expect actual
61+
)
62+
'
63+
3264
test_done

0 commit comments

Comments
 (0)