2 * The Scalar command-line interface.
7 #include "parse-options.h"
9 #include "run-command.h"
16 * Remove the deepest subdirectory in the provided path string. Path must not
17 * include a trailing path separator. Returns 1 if parent directory found,
20 static int strbuf_parent_directory(struct strbuf
*buf
)
22 size_t len
= buf
->len
;
23 size_t offset
= offset_1st_component(buf
->buf
);
24 char *path_sep
= find_last_dir_sep(buf
->buf
+ offset
);
25 strbuf_setlen(buf
, path_sep
? path_sep
- buf
->buf
: offset
);
27 return buf
->len
< len
;
30 static void setup_enlistment_directory(int argc
, const char **argv
,
31 const char * const *usagestr
,
32 const struct option
*options
,
33 struct strbuf
*enlistment_root
)
35 struct strbuf path
= STRBUF_INIT
;
37 int enlistment_found
= 0;
39 if (startup_info
->have_repository
)
40 BUG("gitdir already set up?!?");
43 usage_with_options(usagestr
, options
);
45 /* find the worktree, determine its corresponding root */
47 strbuf_add_absolute_path(&path
, argv
[0]);
48 else if (strbuf_getcwd(&path
) < 0)
49 die(_("need a working directory"));
51 strbuf_trim_trailing_dir_sep(&path
);
53 const size_t len
= path
.len
;
55 /* check if currently in enlistment root with src/ workdir */
56 strbuf_addstr(&path
, "/src");
57 if (is_nonbare_repository_dir(&path
)) {
59 strbuf_add(enlistment_root
, path
.buf
, len
);
65 /* reset to original path */
66 strbuf_setlen(&path
, len
);
68 /* check if currently in workdir */
69 if (is_nonbare_repository_dir(&path
)) {
70 if (enlistment_root
) {
72 * If the worktree's directory's name is `src`, the enlistment is the
73 * parent directory, otherwise it is identical to the worktree.
75 root
= strip_path_suffix(path
.buf
, "src");
76 strbuf_addstr(enlistment_root
, root
? root
: path
.buf
);
83 } while (strbuf_parent_directory(&path
));
85 if (!enlistment_found
)
86 die(_("could not find enlistment root"));
88 if (chdir(path
.buf
) < 0)
89 die_errno(_("could not switch to '%s'"), path
.buf
);
91 strbuf_release(&path
);
92 setup_git_directory();
95 static int run_git(const char *arg
, ...)
97 struct strvec argv
= STRVEC_INIT
;
103 strvec_push(&argv
, arg
);
104 while ((p
= va_arg(args
, const char *)))
105 strvec_push(&argv
, p
);
108 res
= run_command_v_opt(argv
.v
, RUN_GIT_CMD
);
114 static int set_recommended_config(int reconfigure
)
119 int overwrite_on_reconfigure
;
122 { "am.keepCR", "true", 1 },
123 { "core.FSCache", "true", 1 },
124 { "core.multiPackIndex", "true", 1 },
125 { "core.preloadIndex", "true", 1 },
127 { "core.untrackedCache", "true", 1 },
130 * Unfortunately, Scalar's Functional Tests demonstrated
131 * that the untracked cache feature is unreliable on Windows
132 * (which is a bummer because that platform would benefit the
133 * most from it). For some reason, freshly created files seem
134 * not to update the directory's `lastModified` time
135 * immediately, but the untracked cache would need to rely on
138 * Therefore, with a sad heart, we disable this very useful
139 * feature on Windows.
141 { "core.untrackedCache", "false", 1 },
143 { "core.logAllRefUpdates", "true", 1 },
144 { "credential.https://dev.azure.com.useHttpPath", "true", 1 },
145 { "credential.validate", "false", 1 }, /* GCM4W-only */
146 { "gc.auto", "0", 1 },
147 { "gui.GCWarning", "false", 1 },
148 { "index.threads", "true", 1 },
149 { "index.version", "4", 1 },
150 { "merge.stat", "false", 1 },
151 { "merge.renames", "true", 1 },
152 { "pack.useBitmaps", "false", 1 },
153 { "pack.useSparse", "true", 1 },
154 { "receive.autoGC", "false", 1 },
155 { "reset.quiet", "true", 1 },
156 { "feature.manyFiles", "false", 1 },
157 { "feature.experimental", "false", 1 },
158 { "fetch.unpackLimit", "1", 1 },
159 { "fetch.writeCommitGraph", "false", 1 },
161 { "http.sslBackend", "schannel", 1 },
164 { "status.aheadBehind", "false" },
165 { "commitGraph.generationVersion", "1" },
166 { "core.autoCRLF", "false" },
167 { "core.safeCRLF", "false" },
168 { "fetch.showForcedUpdates", "false" },
174 for (i
= 0; config
[i
].key
; i
++) {
175 if ((reconfigure
&& config
[i
].overwrite_on_reconfigure
) ||
176 git_config_get_string(config
[i
].key
, &value
)) {
177 trace2_data_string("scalar", the_repository
, config
[i
].key
, "created");
178 if (git_config_set_gently(config
[i
].key
,
179 config
[i
].value
) < 0)
180 return error(_("could not configure %s=%s"),
181 config
[i
].key
, config
[i
].value
);
183 trace2_data_string("scalar", the_repository
, config
[i
].key
, "exists");
189 * The `log.excludeDecoration` setting is special because it allows
190 * for multiple values.
192 if (git_config_get_string("log.excludeDecoration", &value
)) {
193 trace2_data_string("scalar", the_repository
,
194 "log.excludeDecoration", "created");
195 if (git_config_set_multivar_gently("log.excludeDecoration",
197 CONFIG_REGEX_NONE
, 0))
198 return error(_("could not configure "
199 "log.excludeDecoration"));
201 trace2_data_string("scalar", the_repository
,
202 "log.excludeDecoration", "exists");
209 static int toggle_maintenance(int enable
)
211 return run_git("maintenance", enable
? "start" : "unregister", NULL
);
214 static int add_or_remove_enlistment(int add
)
218 if (!the_repository
->worktree
)
219 die(_("Scalar enlistments require a worktree"));
221 res
= run_git("config", "--global", "--get", "--fixed-value",
222 "scalar.repo", the_repository
->worktree
, NULL
);
225 * If we want to add and the setting is already there, then do nothing.
226 * If we want to remove and the setting is not there, then do nothing.
228 if ((add
&& !res
) || (!add
&& res
))
231 return run_git("config", "--global", add
? "--add" : "--unset",
232 add
? "--no-fixed-value" : "--fixed-value",
233 "scalar.repo", the_repository
->worktree
, NULL
);
236 static int register_dir(void)
238 int res
= add_or_remove_enlistment(1);
241 res
= set_recommended_config(0);
244 res
= toggle_maintenance(1);
249 static int unregister_dir(void)
253 if (toggle_maintenance(0) < 0)
256 if (add_or_remove_enlistment(0) < 0)
262 /* printf-style interface, expects `<key>=<value>` argument */
263 static int set_config(const char *fmt
, ...)
265 struct strbuf buf
= STRBUF_INIT
;
271 strbuf_vaddf(&buf
, fmt
, args
);
274 value
= strchr(buf
.buf
, '=');
277 res
= git_config_set_gently(buf
.buf
, value
);
278 strbuf_release(&buf
);
283 static char *remote_default_branch(const char *url
)
285 struct child_process cp
= CHILD_PROCESS_INIT
;
286 struct strbuf out
= STRBUF_INIT
;
289 strvec_pushl(&cp
.args
, "ls-remote", "--symref", url
, "HEAD", NULL
);
290 if (!pipe_command(&cp
, NULL
, 0, &out
, 0, NULL
, 0)) {
291 const char *line
= out
.buf
;
294 const char *eol
= strchrnul(line
, '\n'), *p
;
295 size_t len
= eol
- line
;
298 if (!skip_prefix(line
, "ref: ", &p
) ||
299 !strip_suffix_mem(line
, &len
, "\tHEAD")) {
300 line
= eol
+ (*eol
== '\n');
305 if (skip_prefix(p
, "refs/heads/", &p
)) {
306 branch
= xstrndup(p
, eol
- p
);
307 strbuf_release(&out
);
311 error(_("remote HEAD is not a branch: '%.*s'"),
313 strbuf_release(&out
);
317 warning(_("failed to get default branch name from remote; "
318 "using local default"));
321 child_process_init(&cp
);
323 strvec_pushl(&cp
.args
, "symbolic-ref", "--short", "HEAD", NULL
);
324 if (!pipe_command(&cp
, NULL
, 0, &out
, 0, NULL
, 0)) {
326 return strbuf_detach(&out
, NULL
);
329 strbuf_release(&out
);
330 error(_("failed to get default branch name"));
334 static int delete_enlistment(struct strbuf
*enlistment
)
337 struct strbuf parent
= STRBUF_INIT
;
340 if (unregister_dir())
341 die(_("failed to unregister repository"));
345 * Change the current directory to one outside of the enlistment so
346 * that we may delete everything underneath it.
348 strbuf_addbuf(&parent
, enlistment
);
349 strbuf_parent_directory(&parent
);
350 if (chdir(parent
.buf
) < 0)
351 die_errno(_("could not switch to '%s'"), parent
.buf
);
352 strbuf_release(&parent
);
355 if (remove_dir_recursively(enlistment
, 0))
356 die(_("failed to delete enlistment directory"));
362 * Dummy implementation; Using `get_version_info()` would cause a link error
365 void load_builtin_commands(const char *prefix
, struct cmdnames
*cmds
)
367 die("not implemented");
370 static int cmd_clone(int argc
, const char **argv
)
372 const char *branch
= NULL
;
373 int full_clone
= 0, single_branch
= 0;
374 struct option clone_options
[] = {
375 OPT_STRING('b', "branch", &branch
, N_("<branch>"),
376 N_("branch to checkout after clone")),
377 OPT_BOOL(0, "full-clone", &full_clone
,
378 N_("when cloning, create full working directory")),
379 OPT_BOOL(0, "single-branch", &single_branch
,
380 N_("only download metadata for the branch that will "
384 const char * const clone_usage
[] = {
385 N_("scalar clone [<options>] [--] <repo> [<dir>]"),
389 char *enlistment
= NULL
, *dir
= NULL
;
390 struct strbuf buf
= STRBUF_INIT
;
393 argc
= parse_options(argc
, argv
, NULL
, clone_options
, clone_usage
, 0);
397 enlistment
= xstrdup(argv
[1]);
398 } else if (argc
== 1) {
401 strbuf_addstr(&buf
, url
);
402 /* Strip trailing slashes, if any */
403 while (buf
.len
> 0 && is_dir_sep(buf
.buf
[buf
.len
- 1]))
404 strbuf_setlen(&buf
, buf
.len
- 1);
405 /* Strip suffix `.git`, if any */
406 strbuf_strip_suffix(&buf
, ".git");
408 enlistment
= find_last_dir_sep(buf
.buf
);
410 die(_("cannot deduce worktree name from '%s'"), url
);
412 enlistment
= xstrdup(enlistment
+ 1);
414 usage_msg_opt(_("You must specify a repository to clone."),
415 clone_usage
, clone_options
);
418 if (is_directory(enlistment
))
419 die(_("directory '%s' exists already"), enlistment
);
421 dir
= xstrfmt("%s/src", enlistment
);
425 strbuf_addf(&buf
, "init.defaultBranch=%s", branch
);
427 char *b
= repo_default_branch_name(the_repository
, 1);
428 strbuf_addf(&buf
, "init.defaultBranch=%s", b
);
432 if ((res
= run_git("-c", buf
.buf
, "init", "--", dir
, NULL
)))
435 if (chdir(dir
) < 0) {
436 res
= error_errno(_("could not switch to '%s'"), dir
);
440 setup_git_directory();
442 /* common-main already logs `argv` */
443 trace2_def_repo(the_repository
);
445 if (!branch
&& !(branch
= remote_default_branch(url
))) {
446 res
= error(_("failed to get default branch for '%s'"), url
);
450 if (set_config("remote.origin.url=%s", url
) ||
451 set_config("remote.origin.fetch="
452 "+refs/heads/%s:refs/remotes/origin/%s",
453 single_branch
? branch
: "*",
454 single_branch
? branch
: "*") ||
455 set_config("remote.origin.promisor=true") ||
456 set_config("remote.origin.partialCloneFilter=blob:none")) {
457 res
= error(_("could not configure remote in '%s'"), dir
);
462 (res
= run_git("sparse-checkout", "init", "--cone", NULL
)))
465 if (set_recommended_config(0))
466 return error(_("could not configure '%s'"), dir
);
468 if ((res
= run_git("fetch", "--quiet", "origin", NULL
))) {
469 warning(_("partial clone failed; attempting full clone"));
471 if (set_config("remote.origin.promisor") ||
472 set_config("remote.origin.partialCloneFilter")) {
473 res
= error(_("could not configure for full clone"));
477 if ((res
= run_git("fetch", "--quiet", "origin", NULL
)))
481 if ((res
= set_config("branch.%s.remote=origin", branch
)))
483 if ((res
= set_config("branch.%s.merge=refs/heads/%s",
488 strbuf_addf(&buf
, "origin/%s", branch
);
489 res
= run_git("checkout", "-f", "-t", buf
.buf
, NULL
);
493 res
= register_dir();
498 strbuf_release(&buf
);
502 static int cmd_list(int argc
, const char **argv
)
505 die(_("`scalar list` does not take arguments"));
507 if (run_git("config", "--global", "--get-all", "scalar.repo", NULL
) < 0)
512 static int cmd_register(int argc
, const char **argv
)
514 struct option options
[] = {
517 const char * const usage
[] = {
518 N_("scalar register [<enlistment>]"),
522 argc
= parse_options(argc
, argv
, NULL
, options
,
525 setup_enlistment_directory(argc
, argv
, usage
, options
, NULL
);
527 return register_dir();
530 static int get_scalar_repos(const char *key
, const char *value
, void *data
)
532 struct string_list
*list
= data
;
534 if (!strcmp(key
, "scalar.repo"))
535 string_list_append(list
, value
);
540 static int cmd_reconfigure(int argc
, const char **argv
)
543 struct option options
[] = {
544 OPT_BOOL('a', "all", &all
,
545 N_("reconfigure all registered enlistments")),
548 const char * const usage
[] = {
549 N_("scalar reconfigure [--all | <enlistment>]"),
552 struct string_list scalar_repos
= STRING_LIST_INIT_DUP
;
554 struct repository r
= { NULL
};
555 struct strbuf commondir
= STRBUF_INIT
, gitdir
= STRBUF_INIT
;
557 argc
= parse_options(argc
, argv
, NULL
, options
,
561 setup_enlistment_directory(argc
, argv
, usage
, options
, NULL
);
563 return set_recommended_config(1);
567 usage_msg_opt(_("--all or <enlistment>, but not both"),
570 git_config(get_scalar_repos
, &scalar_repos
);
572 for (i
= 0; i
< scalar_repos
.nr
; i
++) {
573 const char *dir
= scalar_repos
.items
[i
].string
;
575 strbuf_reset(&commondir
);
576 strbuf_reset(&gitdir
);
578 if (chdir(dir
) < 0) {
579 warning_errno(_("could not switch to '%s'"), dir
);
581 } else if (discover_git_directory(&commondir
, &gitdir
) < 0) {
582 warning_errno(_("git repository gone in '%s'"), dir
);
588 r
.commondir
= commondir
.buf
;
589 r
.gitdir
= gitdir
.buf
;
591 if (set_recommended_config(1) < 0)
596 string_list_clear(&scalar_repos
, 1);
597 strbuf_release(&commondir
);
598 strbuf_release(&gitdir
);
603 static int cmd_run(int argc
, const char **argv
)
605 struct option options
[] = {
609 const char *arg
, *task
;
612 { "commit-graph", "commit-graph" },
613 { "fetch", "prefetch" },
614 { "loose-objects", "loose-objects" },
615 { "pack-files", "incremental-repack" },
618 struct strbuf buf
= STRBUF_INIT
;
619 const char *usagestr
[] = { NULL
, NULL
};
622 strbuf_addstr(&buf
, N_("scalar run <task> [<enlistment>]\nTasks:\n"));
623 for (i
= 0; tasks
[i
].arg
; i
++)
624 strbuf_addf(&buf
, "\t%s\n", tasks
[i
].arg
);
625 usagestr
[0] = buf
.buf
;
627 argc
= parse_options(argc
, argv
, NULL
, options
,
631 usage_with_options(usagestr
, options
);
633 if (!strcmp("all", argv
[0])) {
636 for (i
= 0; tasks
[i
].arg
&& strcmp(tasks
[i
].arg
, argv
[0]); i
++)
637 ; /* keep looking for the task */
639 if (i
> 0 && !tasks
[i
].arg
) {
640 error(_("no such task: '%s'"), argv
[0]);
641 usage_with_options(usagestr
, options
);
647 setup_enlistment_directory(argc
, argv
, usagestr
, options
, NULL
);
648 strbuf_release(&buf
);
651 return register_dir();
654 return run_git("maintenance", "run",
655 "--task", tasks
[i
].task
, NULL
);
659 for (i
= 1; tasks
[i
].arg
; i
++)
660 if (run_git("maintenance", "run",
661 "--task", tasks
[i
].task
, NULL
))
666 static int remove_deleted_enlistment(struct strbuf
*path
)
669 strbuf_realpath_forgiving(path
, path
->buf
, 1);
671 if (run_git("config", "--global",
672 "--unset", "--fixed-value",
673 "scalar.repo", path
->buf
, NULL
) < 0)
676 if (run_git("config", "--global",
677 "--unset", "--fixed-value",
678 "maintenance.repo", path
->buf
, NULL
) < 0)
684 static int cmd_unregister(int argc
, const char **argv
)
686 struct option options
[] = {
689 const char * const usage
[] = {
690 N_("scalar unregister [<enlistment>]"),
694 argc
= parse_options(argc
, argv
, NULL
, options
,
698 * Be forgiving when the enlistment or worktree does not even exist any
699 * longer; This can be the case if a user deleted the worktree by
700 * mistake and _still_ wants to unregister the thing.
703 struct strbuf src_path
= STRBUF_INIT
, workdir_path
= STRBUF_INIT
;
705 strbuf_addf(&src_path
, "%s/src/.git", argv
[0]);
706 strbuf_addf(&workdir_path
, "%s/.git", argv
[0]);
707 if (!is_directory(src_path
.buf
) && !is_directory(workdir_path
.buf
)) {
708 /* remove possible matching registrations */
711 strbuf_strip_suffix(&src_path
, "/.git");
712 res
= remove_deleted_enlistment(&src_path
) && res
;
714 strbuf_strip_suffix(&workdir_path
, "/.git");
715 res
= remove_deleted_enlistment(&workdir_path
) && res
;
717 strbuf_release(&src_path
);
718 strbuf_release(&workdir_path
);
721 strbuf_release(&src_path
);
722 strbuf_release(&workdir_path
);
725 setup_enlistment_directory(argc
, argv
, usage
, options
, NULL
);
727 return unregister_dir();
730 static int cmd_delete(int argc
, const char **argv
)
732 char *cwd
= xgetcwd();
733 struct option options
[] = {
736 const char * const usage
[] = {
737 N_("scalar delete <enlistment>"),
740 struct strbuf enlistment
= STRBUF_INIT
;
743 argc
= parse_options(argc
, argv
, NULL
, options
,
747 usage_with_options(usage
, options
);
749 setup_enlistment_directory(argc
, argv
, usage
, options
, &enlistment
);
751 if (dir_inside_of(cwd
, enlistment
.buf
) >= 0)
752 res
= error(_("refusing to delete current working directory"));
754 close_object_store(the_repository
->objects
);
755 res
= delete_enlistment(&enlistment
);
757 strbuf_release(&enlistment
);
763 static int cmd_version(int argc
, const char **argv
)
765 int verbose
= 0, build_options
= 0;
766 struct option options
[] = {
767 OPT__VERBOSE(&verbose
, N_("include Git version")),
768 OPT_BOOL(0, "build-options", &build_options
,
769 N_("include Git's build options")),
772 const char * const usage
[] = {
773 N_("scalar verbose [-v | --verbose] [--build-options]"),
776 struct strbuf buf
= STRBUF_INIT
;
778 argc
= parse_options(argc
, argv
, NULL
, options
,
782 usage_with_options(usage
, options
);
784 get_version_info(&buf
, build_options
);
785 fprintf(stderr
, "%s\n", buf
.buf
);
786 strbuf_release(&buf
);
793 int (*fn
)(int, const char **);
795 { "clone", cmd_clone
},
796 { "list", cmd_list
},
797 { "register", cmd_register
},
798 { "unregister", cmd_unregister
},
800 { "reconfigure", cmd_reconfigure
},
801 { "delete", cmd_delete
},
802 { "version", cmd_version
},
806 int cmd_main(int argc
, const char **argv
)
808 struct strbuf scalar_usage
= STRBUF_INIT
;
815 for (i
= 0; builtins
[i
].name
; i
++)
816 if (!strcmp(builtins
[i
].name
, argv
[0]))
817 return !!builtins
[i
].fn(argc
, argv
);
820 strbuf_addstr(&scalar_usage
,
821 N_("scalar <command> [<options>]\n\nCommands:\n"));
822 for (i
= 0; builtins
[i
].name
; i
++)
823 strbuf_addf(&scalar_usage
, "\t%s\n", builtins
[i
].name
);
825 usage(scalar_usage
.buf
);