]>
git.ipfire.org Git - thirdparty/git.git/blob - contrib/scalar/scalar.c
2 * The Scalar command-line interface.
7 #include "parse-options.h"
9 #include "run-command.h"
17 * Remove the deepest subdirectory in the provided path string. Path must not
18 * include a trailing path separator. Returns 1 if parent directory found,
21 static int strbuf_parent_directory(struct strbuf
*buf
)
23 size_t len
= buf
->len
;
24 size_t offset
= offset_1st_component(buf
->buf
);
25 char *path_sep
= find_last_dir_sep(buf
->buf
+ offset
);
26 strbuf_setlen(buf
, path_sep
? path_sep
- buf
->buf
: offset
);
28 return buf
->len
< len
;
31 static void setup_enlistment_directory(int argc
, const char **argv
,
32 const char * const *usagestr
,
33 const struct option
*options
,
34 struct strbuf
*enlistment_root
)
36 struct strbuf path
= STRBUF_INIT
;
38 int enlistment_found
= 0;
40 if (startup_info
->have_repository
)
41 BUG("gitdir already set up?!?");
44 usage_with_options(usagestr
, options
);
46 /* find the worktree, determine its corresponding root */
48 strbuf_add_absolute_path(&path
, argv
[0]);
49 if (!is_directory(path
.buf
))
50 die(_("'%s' does not exist"), path
.buf
);
51 } else if (strbuf_getcwd(&path
) < 0)
52 die(_("need a working directory"));
54 strbuf_trim_trailing_dir_sep(&path
);
56 const size_t len
= path
.len
;
58 /* check if currently in enlistment root with src/ workdir */
59 strbuf_addstr(&path
, "/src");
60 if (is_nonbare_repository_dir(&path
)) {
62 strbuf_add(enlistment_root
, path
.buf
, len
);
68 /* reset to original path */
69 strbuf_setlen(&path
, len
);
71 /* check if currently in workdir */
72 if (is_nonbare_repository_dir(&path
)) {
73 if (enlistment_root
) {
75 * If the worktree's directory's name is `src`, the enlistment is the
76 * parent directory, otherwise it is identical to the worktree.
78 root
= strip_path_suffix(path
.buf
, "src");
79 strbuf_addstr(enlistment_root
, root
? root
: path
.buf
);
86 } while (strbuf_parent_directory(&path
));
88 if (!enlistment_found
)
89 die(_("could not find enlistment root"));
91 if (chdir(path
.buf
) < 0)
92 die_errno(_("could not switch to '%s'"), path
.buf
);
94 strbuf_release(&path
);
95 setup_git_directory();
98 static int run_git(const char *arg
, ...)
100 struct strvec argv
= STRVEC_INIT
;
106 strvec_push(&argv
, arg
);
107 while ((p
= va_arg(args
, const char *)))
108 strvec_push(&argv
, p
);
111 res
= run_command_v_opt(argv
.v
, RUN_GIT_CMD
);
117 static int set_recommended_config(int reconfigure
)
122 int overwrite_on_reconfigure
;
125 { "am.keepCR", "true", 1 },
126 { "core.FSCache", "true", 1 },
127 { "core.multiPackIndex", "true", 1 },
128 { "core.preloadIndex", "true", 1 },
130 { "core.untrackedCache", "true", 1 },
133 * Unfortunately, Scalar's Functional Tests demonstrated
134 * that the untracked cache feature is unreliable on Windows
135 * (which is a bummer because that platform would benefit the
136 * most from it). For some reason, freshly created files seem
137 * not to update the directory's `lastModified` time
138 * immediately, but the untracked cache would need to rely on
141 * Therefore, with a sad heart, we disable this very useful
142 * feature on Windows.
144 { "core.untrackedCache", "false", 1 },
146 { "core.logAllRefUpdates", "true", 1 },
147 { "credential.https://dev.azure.com.useHttpPath", "true", 1 },
148 { "credential.validate", "false", 1 }, /* GCM4W-only */
149 { "gc.auto", "0", 1 },
150 { "gui.GCWarning", "false", 1 },
151 { "index.threads", "true", 1 },
152 { "index.version", "4", 1 },
153 { "merge.stat", "false", 1 },
154 { "merge.renames", "true", 1 },
155 { "pack.useBitmaps", "false", 1 },
156 { "pack.useSparse", "true", 1 },
157 { "receive.autoGC", "false", 1 },
158 { "feature.manyFiles", "false", 1 },
159 { "feature.experimental", "false", 1 },
160 { "fetch.unpackLimit", "1", 1 },
161 { "fetch.writeCommitGraph", "false", 1 },
163 { "http.sslBackend", "schannel", 1 },
166 { "status.aheadBehind", "false" },
167 { "commitGraph.generationVersion", "1" },
168 { "core.autoCRLF", "false" },
169 { "core.safeCRLF", "false" },
170 { "fetch.showForcedUpdates", "false" },
176 for (i
= 0; config
[i
].key
; i
++) {
177 if ((reconfigure
&& config
[i
].overwrite_on_reconfigure
) ||
178 git_config_get_string(config
[i
].key
, &value
)) {
179 trace2_data_string("scalar", the_repository
, config
[i
].key
, "created");
180 if (git_config_set_gently(config
[i
].key
,
181 config
[i
].value
) < 0)
182 return error(_("could not configure %s=%s"),
183 config
[i
].key
, config
[i
].value
);
185 trace2_data_string("scalar", the_repository
, config
[i
].key
, "exists");
191 * The `log.excludeDecoration` setting is special because it allows
192 * for multiple values.
194 if (git_config_get_string("log.excludeDecoration", &value
)) {
195 trace2_data_string("scalar", the_repository
,
196 "log.excludeDecoration", "created");
197 if (git_config_set_multivar_gently("log.excludeDecoration",
199 CONFIG_REGEX_NONE
, 0))
200 return error(_("could not configure "
201 "log.excludeDecoration"));
203 trace2_data_string("scalar", the_repository
,
204 "log.excludeDecoration", "exists");
211 static int toggle_maintenance(int enable
)
213 return run_git("maintenance", enable
? "start" : "unregister", NULL
);
216 static int add_or_remove_enlistment(int add
)
220 if (!the_repository
->worktree
)
221 die(_("Scalar enlistments require a worktree"));
223 res
= run_git("config", "--global", "--get", "--fixed-value",
224 "scalar.repo", the_repository
->worktree
, NULL
);
227 * If we want to add and the setting is already there, then do nothing.
228 * If we want to remove and the setting is not there, then do nothing.
230 if ((add
&& !res
) || (!add
&& res
))
233 return run_git("config", "--global", add
? "--add" : "--unset",
234 add
? "--no-fixed-value" : "--fixed-value",
235 "scalar.repo", the_repository
->worktree
, NULL
);
238 static int register_dir(void)
240 int res
= add_or_remove_enlistment(1);
243 res
= set_recommended_config(0);
246 res
= toggle_maintenance(1);
251 static int unregister_dir(void)
255 if (toggle_maintenance(0) < 0)
258 if (add_or_remove_enlistment(0) < 0)
264 /* printf-style interface, expects `<key>=<value>` argument */
265 static int set_config(const char *fmt
, ...)
267 struct strbuf buf
= STRBUF_INIT
;
273 strbuf_vaddf(&buf
, fmt
, args
);
276 value
= strchr(buf
.buf
, '=');
279 res
= git_config_set_gently(buf
.buf
, value
);
280 strbuf_release(&buf
);
285 static char *remote_default_branch(const char *url
)
287 struct child_process cp
= CHILD_PROCESS_INIT
;
288 struct strbuf out
= STRBUF_INIT
;
291 strvec_pushl(&cp
.args
, "ls-remote", "--symref", url
, "HEAD", NULL
);
292 if (!pipe_command(&cp
, NULL
, 0, &out
, 0, NULL
, 0)) {
293 const char *line
= out
.buf
;
296 const char *eol
= strchrnul(line
, '\n'), *p
;
297 size_t len
= eol
- line
;
300 if (!skip_prefix(line
, "ref: ", &p
) ||
301 !strip_suffix_mem(line
, &len
, "\tHEAD")) {
302 line
= eol
+ (*eol
== '\n');
307 if (skip_prefix(p
, "refs/heads/", &p
)) {
308 branch
= xstrndup(p
, eol
- p
);
309 strbuf_release(&out
);
313 error(_("remote HEAD is not a branch: '%.*s'"),
315 strbuf_release(&out
);
319 warning(_("failed to get default branch name from remote; "
320 "using local default"));
323 child_process_init(&cp
);
325 strvec_pushl(&cp
.args
, "symbolic-ref", "--short", "HEAD", NULL
);
326 if (!pipe_command(&cp
, NULL
, 0, &out
, 0, NULL
, 0)) {
328 return strbuf_detach(&out
, NULL
);
331 strbuf_release(&out
);
332 error(_("failed to get default branch name"));
336 static int delete_enlistment(struct strbuf
*enlistment
)
339 struct strbuf parent
= STRBUF_INIT
;
342 if (unregister_dir())
343 die(_("failed to unregister repository"));
347 * Change the current directory to one outside of the enlistment so
348 * that we may delete everything underneath it.
350 strbuf_addbuf(&parent
, enlistment
);
351 strbuf_parent_directory(&parent
);
352 if (chdir(parent
.buf
) < 0)
353 die_errno(_("could not switch to '%s'"), parent
.buf
);
354 strbuf_release(&parent
);
357 if (remove_dir_recursively(enlistment
, 0))
358 die(_("failed to delete enlistment directory"));
364 * Dummy implementation; Using `get_version_info()` would cause a link error
367 void load_builtin_commands(const char *prefix
, struct cmdnames
*cmds
)
369 die("not implemented");
372 static int cmd_clone(int argc
, const char **argv
)
374 const char *branch
= NULL
;
375 int full_clone
= 0, single_branch
= 0;
376 struct option clone_options
[] = {
377 OPT_STRING('b', "branch", &branch
, N_("<branch>"),
378 N_("branch to checkout after clone")),
379 OPT_BOOL(0, "full-clone", &full_clone
,
380 N_("when cloning, create full working directory")),
381 OPT_BOOL(0, "single-branch", &single_branch
,
382 N_("only download metadata for the branch that will "
386 const char * const clone_usage
[] = {
387 N_("scalar clone [<options>] [--] <repo> [<dir>]"),
391 char *enlistment
= NULL
, *dir
= NULL
;
392 struct strbuf buf
= STRBUF_INIT
;
395 argc
= parse_options(argc
, argv
, NULL
, clone_options
, clone_usage
, 0);
399 enlistment
= xstrdup(argv
[1]);
400 } else if (argc
== 1) {
403 strbuf_addstr(&buf
, url
);
404 /* Strip trailing slashes, if any */
405 while (buf
.len
> 0 && is_dir_sep(buf
.buf
[buf
.len
- 1]))
406 strbuf_setlen(&buf
, buf
.len
- 1);
407 /* Strip suffix `.git`, if any */
408 strbuf_strip_suffix(&buf
, ".git");
410 enlistment
= find_last_dir_sep(buf
.buf
);
412 die(_("cannot deduce worktree name from '%s'"), url
);
414 enlistment
= xstrdup(enlistment
+ 1);
416 usage_msg_opt(_("You must specify a repository to clone."),
417 clone_usage
, clone_options
);
420 if (is_directory(enlistment
))
421 die(_("directory '%s' exists already"), enlistment
);
423 dir
= xstrfmt("%s/src", enlistment
);
427 strbuf_addf(&buf
, "init.defaultBranch=%s", branch
);
429 char *b
= repo_default_branch_name(the_repository
, 1);
430 strbuf_addf(&buf
, "init.defaultBranch=%s", b
);
434 if ((res
= run_git("-c", buf
.buf
, "init", "--", dir
, NULL
)))
437 if (chdir(dir
) < 0) {
438 res
= error_errno(_("could not switch to '%s'"), dir
);
442 setup_git_directory();
444 /* common-main already logs `argv` */
445 trace2_def_repo(the_repository
);
447 if (!branch
&& !(branch
= remote_default_branch(url
))) {
448 res
= error(_("failed to get default branch for '%s'"), url
);
452 if (set_config("remote.origin.url=%s", url
) ||
453 set_config("remote.origin.fetch="
454 "+refs/heads/%s:refs/remotes/origin/%s",
455 single_branch
? branch
: "*",
456 single_branch
? branch
: "*") ||
457 set_config("remote.origin.promisor=true") ||
458 set_config("remote.origin.partialCloneFilter=blob:none")) {
459 res
= error(_("could not configure remote in '%s'"), dir
);
464 (res
= run_git("sparse-checkout", "init", "--cone", NULL
)))
467 if (set_recommended_config(0))
468 return error(_("could not configure '%s'"), dir
);
470 if ((res
= run_git("fetch", "--quiet", "origin", NULL
))) {
471 warning(_("partial clone failed; attempting full clone"));
473 if (set_config("remote.origin.promisor") ||
474 set_config("remote.origin.partialCloneFilter")) {
475 res
= error(_("could not configure for full clone"));
479 if ((res
= run_git("fetch", "--quiet", "origin", NULL
)))
483 if ((res
= set_config("branch.%s.remote=origin", branch
)))
485 if ((res
= set_config("branch.%s.merge=refs/heads/%s",
490 strbuf_addf(&buf
, "origin/%s", branch
);
491 res
= run_git("checkout", "-f", "-t", buf
.buf
, NULL
);
495 res
= register_dir();
500 strbuf_release(&buf
);
504 static int cmd_diagnose(int argc
, const char **argv
)
506 struct option options
[] = {
509 const char * const usage
[] = {
510 N_("scalar diagnose [<enlistment>]"),
513 struct strbuf zip_path
= STRBUF_INIT
;
514 time_t now
= time(NULL
);
518 argc
= parse_options(argc
, argv
, NULL
, options
,
521 setup_enlistment_directory(argc
, argv
, usage
, options
, &zip_path
);
523 strbuf_addstr(&zip_path
, "/.scalarDiagnostics/scalar_");
524 strbuf_addftime(&zip_path
,
525 "%Y%m%d_%H%M%S", localtime_r(&now
, &tm
), 0, 0);
526 strbuf_addstr(&zip_path
, ".zip");
527 switch (safe_create_leading_directories(zip_path
.buf
)) {
532 error_errno(_("could not create directory for '%s'"),
534 goto diagnose_cleanup
;
537 res
= create_diagnostics_archive(&zip_path
, DIAGNOSE_ALL
);
540 strbuf_release(&zip_path
);
544 static int cmd_list(int argc
, const char **argv
)
547 die(_("`scalar list` does not take arguments"));
549 if (run_git("config", "--global", "--get-all", "scalar.repo", NULL
) < 0)
554 static int cmd_register(int argc
, const char **argv
)
556 struct option options
[] = {
559 const char * const usage
[] = {
560 N_("scalar register [<enlistment>]"),
564 argc
= parse_options(argc
, argv
, NULL
, options
,
567 setup_enlistment_directory(argc
, argv
, usage
, options
, NULL
);
569 return register_dir();
572 static int get_scalar_repos(const char *key
, const char *value
, void *data
)
574 struct string_list
*list
= data
;
576 if (!strcmp(key
, "scalar.repo"))
577 string_list_append(list
, value
);
582 static int cmd_reconfigure(int argc
, const char **argv
)
585 struct option options
[] = {
586 OPT_BOOL('a', "all", &all
,
587 N_("reconfigure all registered enlistments")),
590 const char * const usage
[] = {
591 N_("scalar reconfigure [--all | <enlistment>]"),
594 struct string_list scalar_repos
= STRING_LIST_INIT_DUP
;
596 struct repository r
= { NULL
};
597 struct strbuf commondir
= STRBUF_INIT
, gitdir
= STRBUF_INIT
;
599 argc
= parse_options(argc
, argv
, NULL
, options
,
603 setup_enlistment_directory(argc
, argv
, usage
, options
, NULL
);
605 return set_recommended_config(1);
609 usage_msg_opt(_("--all or <enlistment>, but not both"),
612 git_config(get_scalar_repos
, &scalar_repos
);
614 for (i
= 0; i
< scalar_repos
.nr
; i
++) {
615 const char *dir
= scalar_repos
.items
[i
].string
;
617 strbuf_reset(&commondir
);
618 strbuf_reset(&gitdir
);
620 if (chdir(dir
) < 0) {
621 warning_errno(_("could not switch to '%s'"), dir
);
623 } else if (discover_git_directory(&commondir
, &gitdir
) < 0) {
624 warning_errno(_("git repository gone in '%s'"), dir
);
630 r
.commondir
= commondir
.buf
;
631 r
.gitdir
= gitdir
.buf
;
633 if (set_recommended_config(1) < 0)
638 string_list_clear(&scalar_repos
, 1);
639 strbuf_release(&commondir
);
640 strbuf_release(&gitdir
);
645 static int cmd_run(int argc
, const char **argv
)
647 struct option options
[] = {
651 const char *arg
, *task
;
654 { "commit-graph", "commit-graph" },
655 { "fetch", "prefetch" },
656 { "loose-objects", "loose-objects" },
657 { "pack-files", "incremental-repack" },
660 struct strbuf buf
= STRBUF_INIT
;
661 const char *usagestr
[] = { NULL
, NULL
};
664 strbuf_addstr(&buf
, N_("scalar run <task> [<enlistment>]\nTasks:\n"));
665 for (i
= 0; tasks
[i
].arg
; i
++)
666 strbuf_addf(&buf
, "\t%s\n", tasks
[i
].arg
);
667 usagestr
[0] = buf
.buf
;
669 argc
= parse_options(argc
, argv
, NULL
, options
,
673 usage_with_options(usagestr
, options
);
675 if (!strcmp("all", argv
[0])) {
678 for (i
= 0; tasks
[i
].arg
&& strcmp(tasks
[i
].arg
, argv
[0]); i
++)
679 ; /* keep looking for the task */
681 if (i
> 0 && !tasks
[i
].arg
) {
682 error(_("no such task: '%s'"), argv
[0]);
683 usage_with_options(usagestr
, options
);
689 setup_enlistment_directory(argc
, argv
, usagestr
, options
, NULL
);
690 strbuf_release(&buf
);
693 return register_dir();
696 return run_git("maintenance", "run",
697 "--task", tasks
[i
].task
, NULL
);
701 for (i
= 1; tasks
[i
].arg
; i
++)
702 if (run_git("maintenance", "run",
703 "--task", tasks
[i
].task
, NULL
))
708 static int remove_deleted_enlistment(struct strbuf
*path
)
711 strbuf_realpath_forgiving(path
, path
->buf
, 1);
713 if (run_git("config", "--global",
714 "--unset", "--fixed-value",
715 "scalar.repo", path
->buf
, NULL
) < 0)
718 if (run_git("config", "--global",
719 "--unset", "--fixed-value",
720 "maintenance.repo", path
->buf
, NULL
) < 0)
726 static int cmd_unregister(int argc
, const char **argv
)
728 struct option options
[] = {
731 const char * const usage
[] = {
732 N_("scalar unregister [<enlistment>]"),
736 argc
= parse_options(argc
, argv
, NULL
, options
,
740 * Be forgiving when the enlistment or worktree does not even exist any
741 * longer; This can be the case if a user deleted the worktree by
742 * mistake and _still_ wants to unregister the thing.
745 struct strbuf src_path
= STRBUF_INIT
, workdir_path
= STRBUF_INIT
;
747 strbuf_addf(&src_path
, "%s/src/.git", argv
[0]);
748 strbuf_addf(&workdir_path
, "%s/.git", argv
[0]);
749 if (!is_directory(src_path
.buf
) && !is_directory(workdir_path
.buf
)) {
750 /* remove possible matching registrations */
753 strbuf_strip_suffix(&src_path
, "/.git");
754 res
= remove_deleted_enlistment(&src_path
) && res
;
756 strbuf_strip_suffix(&workdir_path
, "/.git");
757 res
= remove_deleted_enlistment(&workdir_path
) && res
;
759 strbuf_release(&src_path
);
760 strbuf_release(&workdir_path
);
763 strbuf_release(&src_path
);
764 strbuf_release(&workdir_path
);
767 setup_enlistment_directory(argc
, argv
, usage
, options
, NULL
);
769 return unregister_dir();
772 static int cmd_delete(int argc
, const char **argv
)
774 char *cwd
= xgetcwd();
775 struct option options
[] = {
778 const char * const usage
[] = {
779 N_("scalar delete <enlistment>"),
782 struct strbuf enlistment
= STRBUF_INIT
;
785 argc
= parse_options(argc
, argv
, NULL
, options
,
789 usage_with_options(usage
, options
);
791 setup_enlistment_directory(argc
, argv
, usage
, options
, &enlistment
);
793 if (dir_inside_of(cwd
, enlistment
.buf
) >= 0)
794 res
= error(_("refusing to delete current working directory"));
796 close_object_store(the_repository
->objects
);
797 res
= delete_enlistment(&enlistment
);
799 strbuf_release(&enlistment
);
805 static int cmd_version(int argc
, const char **argv
)
807 int verbose
= 0, build_options
= 0;
808 struct option options
[] = {
809 OPT__VERBOSE(&verbose
, N_("include Git version")),
810 OPT_BOOL(0, "build-options", &build_options
,
811 N_("include Git's build options")),
814 const char * const usage
[] = {
815 N_("scalar verbose [-v | --verbose] [--build-options]"),
818 struct strbuf buf
= STRBUF_INIT
;
820 argc
= parse_options(argc
, argv
, NULL
, options
,
824 usage_with_options(usage
, options
);
826 get_version_info(&buf
, build_options
);
827 fprintf(stderr
, "%s\n", buf
.buf
);
828 strbuf_release(&buf
);
835 int (*fn
)(int, const char **);
837 { "clone", cmd_clone
},
838 { "list", cmd_list
},
839 { "register", cmd_register
},
840 { "unregister", cmd_unregister
},
842 { "reconfigure", cmd_reconfigure
},
843 { "delete", cmd_delete
},
844 { "version", cmd_version
},
845 { "diagnose", cmd_diagnose
},
849 int cmd_main(int argc
, const char **argv
)
851 struct strbuf scalar_usage
= STRBUF_INIT
;
854 while (argc
> 1 && *argv
[1] == '-') {
855 if (!strcmp(argv
[1], "-C")) {
857 die(_("-C requires a <directory>"));
858 if (chdir(argv
[2]) < 0)
859 die_errno(_("could not change to '%s'"),
863 } else if (!strcmp(argv
[1], "-c")) {
865 die(_("-c requires a <key>=<value> argument"));
866 git_config_push_parameter(argv
[2]);
877 for (i
= 0; builtins
[i
].name
; i
++)
878 if (!strcmp(builtins
[i
].name
, argv
[0]))
879 return !!builtins
[i
].fn(argc
, argv
);
882 strbuf_addstr(&scalar_usage
,
883 N_("scalar [-C <directory>] [-c <key>=<value>] "
884 "<command> [<options>]\n\nCommands:\n"));
885 for (i
= 0; builtins
[i
].name
; i
++)
886 strbuf_addf(&scalar_usage
, "\t%s\n", builtins
[i
].name
);
888 usage(scalar_usage
.buf
);