summaryrefslogtreecommitdiff
path: root/builtin/gc.c
diff options
context:
space:
mode:
Diffstat (limited to 'builtin/gc.c')
-rw-r--r--builtin/gc.c826
1 files changed, 673 insertions, 153 deletions
diff --git a/builtin/gc.c b/builtin/gc.c
index ae34362e93..bcef6a4c8d 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -54,7 +54,6 @@ static const char *prune_worktrees_expire = "3.months.ago";
static unsigned long big_pack_threshold;
static unsigned long max_delta_cache_size = DEFAULT_DELTA_CACHE_SIZE;
-static struct strvec pack_refs_cmd = STRVEC_INIT;
static struct strvec reflog = STRVEC_INIT;
static struct strvec repack = STRVEC_INIT;
static struct strvec prune = STRVEC_INIT;
@@ -163,6 +162,15 @@ static void gc_config(void)
git_config(git_default_config, NULL);
}
+struct maintenance_run_opts;
+static int maintenance_task_pack_refs(MAYBE_UNUSED struct maintenance_run_opts *opts)
+{
+ struct strvec pack_refs_cmd = STRVEC_INIT;
+ strvec_pushl(&pack_refs_cmd, "pack-refs", "--all", "--prune", NULL);
+
+ return run_command_v_opt(pack_refs_cmd.v, RUN_GIT_CMD);
+}
+
static int too_many_loose_objects(void)
{
/*
@@ -494,7 +502,7 @@ static int report_last_gc_error(void)
*/
warning(_("The last gc run reported the following. "
"Please correct the root cause\n"
- "and remove %s.\n"
+ "and remove %s\n"
"Automatic cleanup will not be performed "
"until the file is removed.\n\n"
"%s"),
@@ -518,8 +526,8 @@ static void gc_before_repack(void)
if (done++)
return;
- if (pack_refs && run_command_v_opt(pack_refs_cmd.v, RUN_GIT_CMD))
- die(FAILED_RUN, pack_refs_cmd.v[0]);
+ if (pack_refs && maintenance_task_pack_refs(NULL))
+ die(FAILED_RUN, "pack-refs");
if (prune_reflogs && run_command_v_opt(reflog.v, RUN_GIT_CMD))
die(FAILED_RUN, reflog.v[0]);
@@ -556,7 +564,6 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
if (argc == 2 && !strcmp(argv[1], "-h"))
usage_with_options(builtin_gc_usage, builtin_gc_options);
- strvec_pushl(&pack_refs_cmd, "pack-refs", "--all", "--prune", NULL);
strvec_pushl(&reflog, "reflog", "expire", "--all", NULL);
strvec_pushl(&repack, "repack", "-d", "-l", NULL);
strvec_pushl(&prune, "prune", "--expire", NULL);
@@ -656,8 +663,8 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
gc_before_repack();
if (!repository_format_precious_objects) {
- close_object_store(the_repository->objects);
- if (run_command_v_opt(repack.v, RUN_GIT_CMD))
+ if (run_command_v_opt(repack.v,
+ RUN_GIT_CMD | RUN_CLOSE_OBJECT_STORE))
die(FAILED_RUN, repack.v[0]);
if (prune_expire) {
@@ -769,7 +776,7 @@ static int dfs_on_ref(const char *refname,
struct commit_list *stack = NULL;
struct commit *commit;
- if (!peel_ref(refname, &peeled))
+ if (!peel_iterated_oid(oid, &peeled))
oid = &peeled;
if (oid_object_info(the_repository, oid, NULL) != OBJ_COMMIT)
return 0;
@@ -841,7 +848,7 @@ static int run_write_commit_graph(struct maintenance_run_opts *opts)
{
struct child_process child = CHILD_PROCESS_INIT;
- child.git_cmd = 1;
+ child.git_cmd = child.close_object_store = 1;
strvec_pushl(&child.args, "commit-graph", "write",
"--split", "--reachable", NULL);
@@ -857,7 +864,6 @@ static int maintenance_task_commit_graph(struct maintenance_run_opts *opts)
if (!the_repository->settings.core_commit_graph)
return 0;
- close_object_store(the_repository->objects);
if (run_write_commit_graph(opts)) {
error(_("failed to write commit-graph"));
return 1;
@@ -866,56 +872,47 @@ static int maintenance_task_commit_graph(struct maintenance_run_opts *opts)
return 0;
}
-static int fetch_remote(const char *remote, struct maintenance_run_opts *opts)
+static int fetch_remote(struct remote *remote, void *cbdata)
{
+ struct maintenance_run_opts *opts = cbdata;
struct child_process child = CHILD_PROCESS_INIT;
+ if (remote->skip_default_update)
+ return 0;
+
child.git_cmd = 1;
- strvec_pushl(&child.args, "fetch", remote, "--prune", "--no-tags",
+ strvec_pushl(&child.args, "fetch", remote->name,
+ "--prefetch", "--prune", "--no-tags",
"--no-write-fetch-head", "--recurse-submodules=no",
- "--refmap=", NULL);
+ NULL);
if (opts->quiet)
strvec_push(&child.args, "--quiet");
- strvec_pushf(&child.args, "+refs/heads/*:refs/prefetch/%s/*", remote);
-
return !!run_command(&child);
}
-static int append_remote(struct remote *remote, void *cbdata)
-{
- struct string_list *remotes = (struct string_list *)cbdata;
-
- string_list_append(remotes, remote->name);
- return 0;
-}
-
static int maintenance_task_prefetch(struct maintenance_run_opts *opts)
{
- int result = 0;
- struct string_list_item *item;
- struct string_list remotes = STRING_LIST_INIT_DUP;
+ git_config_set_multivar_gently("log.excludedecoration",
+ "refs/prefetch/",
+ "refs/prefetch/",
+ CONFIG_FLAGS_FIXED_VALUE |
+ CONFIG_FLAGS_MULTI_REPLACE);
- if (for_each_remote(append_remote, &remotes)) {
- error(_("failed to fill remotes"));
- result = 1;
- goto cleanup;
+ if (for_each_remote(fetch_remote, opts)) {
+ error(_("failed to prefetch remotes"));
+ return 1;
}
- for_each_string_list_item(item, &remotes)
- result |= fetch_remote(item->string, opts);
-
-cleanup:
- string_list_clear(&remotes, 0);
- return result;
+ return 0;
}
static int maintenance_task_gc(struct maintenance_run_opts *opts)
{
struct child_process child = CHILD_PROCESS_INIT;
- child.git_cmd = 1;
+ child.git_cmd = child.close_object_store = 1;
strvec_push(&child.args, "gc");
if (opts->auto_flag)
@@ -925,7 +922,6 @@ static int maintenance_task_gc(struct maintenance_run_opts *opts)
else
strvec_push(&child.args, "--no-quiet");
- close_object_store(the_repository->objects);
return run_command(&child);
}
@@ -1053,12 +1049,11 @@ static int maintenance_task_loose_objects(struct maintenance_run_opts *opts)
static int incremental_repack_auto_condition(void)
{
struct packed_git *p;
- int enabled;
int incremental_repack_auto_limit = 10;
int count = 0;
- if (git_config_get_bool("core.multiPackIndex", &enabled) ||
- !enabled)
+ prepare_repo_settings(the_repository);
+ if (!the_repository->settings.core_multi_pack_index)
return 0;
git_config_get_int("maintenance.incremental-repack.auto",
@@ -1099,14 +1094,12 @@ static int multi_pack_index_expire(struct maintenance_run_opts *opts)
{
struct child_process child = CHILD_PROCESS_INIT;
- child.git_cmd = 1;
+ child.git_cmd = child.close_object_store = 1;
strvec_pushl(&child.args, "multi-pack-index", "expire", NULL);
if (opts->quiet)
strvec_push(&child.args, "--no-progress");
- close_object_store(the_repository->objects);
-
if (run_command(&child))
return error(_("'git multi-pack-index expire' failed"));
@@ -1157,7 +1150,7 @@ static int multi_pack_index_repack(struct maintenance_run_opts *opts)
{
struct child_process child = CHILD_PROCESS_INIT;
- child.git_cmd = 1;
+ child.git_cmd = child.close_object_store = 1;
strvec_pushl(&child.args, "multi-pack-index", "repack", NULL);
if (opts->quiet)
@@ -1166,8 +1159,6 @@ static int multi_pack_index_repack(struct maintenance_run_opts *opts)
strvec_pushf(&child.args, "--batch-size=%"PRIuMAX,
(uintmax_t)get_auto_pack_size());
- close_object_store(the_repository->objects);
-
if (run_command(&child))
return error(_("'git multi-pack-index repack' failed"));
@@ -1218,6 +1209,7 @@ enum maintenance_task_label {
TASK_INCREMENTAL_REPACK,
TASK_GC,
TASK_COMMIT_GRAPH,
+ TASK_PACK_REFS,
/* Leave as final value */
TASK__COUNT
@@ -1249,6 +1241,11 @@ static struct maintenance_task tasks[] = {
maintenance_task_commit_graph,
should_write_commit_graph,
},
+ [TASK_PACK_REFS] = {
+ "pack-refs",
+ maintenance_task_pack_refs,
+ NULL,
+ },
};
static int compare_tasks_by_selection(const void *a_, const void *b_)
@@ -1333,6 +1330,8 @@ static void initialize_maintenance_strategy(void)
tasks[TASK_INCREMENTAL_REPACK].schedule = SCHEDULE_DAILY;
tasks[TASK_LOOSE_OBJECTS].enabled = 1;
tasks[TASK_LOOSE_OBJECTS].schedule = SCHEDULE_DAILY;
+ tasks[TASK_PACK_REFS].enabled = 1;
+ tasks[TASK_PACK_REFS].schedule = SCHEDULE_WEEKLY;
}
}
@@ -1440,11 +1439,23 @@ static int maintenance_run(int argc, const char **argv, const char *prefix)
return maintenance_run_tasks(&opts);
}
+static char *get_maintpath(void)
+{
+ struct strbuf sb = STRBUF_INIT;
+ const char *p = the_repository->worktree ?
+ the_repository->worktree : the_repository->gitdir;
+
+ strbuf_realpath(&sb, p, 1);
+ return strbuf_detach(&sb, NULL);
+}
+
static int maintenance_register(void)
{
+ int rc;
char *config_value;
struct child_process config_set = CHILD_PROCESS_INIT;
struct child_process config_get = CHILD_PROCESS_INIT;
+ char *maintpath = get_maintpath();
/* Disable foreground maintenance */
git_config_set("maintenance.auto", "false");
@@ -1457,40 +1468,44 @@ static int maintenance_register(void)
config_get.git_cmd = 1;
strvec_pushl(&config_get.args, "config", "--global", "--get",
- "--fixed-value", "maintenance.repo",
- the_repository->worktree ? the_repository->worktree
- : the_repository->gitdir,
- NULL);
+ "--fixed-value", "maintenance.repo", maintpath, NULL);
config_get.out = -1;
- if (start_command(&config_get))
- return error(_("failed to run 'git config'"));
+ if (start_command(&config_get)) {
+ rc = error(_("failed to run 'git config'"));
+ goto done;
+ }
/* We already have this value in our config! */
- if (!finish_command(&config_get))
- return 0;
+ if (!finish_command(&config_get)) {
+ rc = 0;
+ goto done;
+ }
config_set.git_cmd = 1;
strvec_pushl(&config_set.args, "config", "--add", "--global", "maintenance.repo",
- the_repository->worktree ? the_repository->worktree
- : the_repository->gitdir,
- NULL);
+ maintpath, NULL);
+
+ rc = run_command(&config_set);
- return run_command(&config_set);
+done:
+ free(maintpath);
+ return rc;
}
static int maintenance_unregister(void)
{
+ int rc;
struct child_process config_unset = CHILD_PROCESS_INIT;
+ char *maintpath = get_maintpath();
config_unset.git_cmd = 1;
strvec_pushl(&config_unset.args, "config", "--global", "--unset",
- "--fixed-value", "maintenance.repo",
- the_repository->worktree ? the_repository->worktree
- : the_repository->gitdir,
- NULL);
+ "--fixed-value", "maintenance.repo", maintpath, NULL);
- return run_command(&config_unset);
+ rc = run_command(&config_unset);
+ free(maintpath);
+ return rc;
}
static const char *get_frequency(enum schedule_priority schedule)
@@ -1507,6 +1522,93 @@ static const char *get_frequency(enum schedule_priority schedule)
}
}
+/*
+ * get_schedule_cmd` reads the GIT_TEST_MAINT_SCHEDULER environment variable
+ * to mock the schedulers that `git maintenance start` rely on.
+ *
+ * For test purpose, GIT_TEST_MAINT_SCHEDULER can be set to a comma-separated
+ * list of colon-separated key/value pairs where each pair contains a scheduler
+ * and its corresponding mock.
+ *
+ * * If $GIT_TEST_MAINT_SCHEDULER is not set, return false and leave the
+ * arguments unmodified.
+ *
+ * * If $GIT_TEST_MAINT_SCHEDULER is set, return true.
+ * In this case, the *cmd value is read as input.
+ *
+ * * if the input value *cmd is the key of one of the comma-separated list
+ * item, then *is_available is set to true and *cmd is modified and becomes
+ * the mock command.
+ *
+ * * if the input value *cmd isn’t the key of any of the comma-separated list
+ * item, then *is_available is set to false.
+ *
+ * Ex.:
+ * GIT_TEST_MAINT_SCHEDULER not set
+ * +-------+-------------------------------------------------+
+ * | Input | Output |
+ * | *cmd | return code | *cmd | *is_available |
+ * +-------+-------------+-------------------+---------------+
+ * | "foo" | false | "foo" (unchanged) | (unchanged) |
+ * +-------+-------------+-------------------+---------------+
+ *
+ * GIT_TEST_MAINT_SCHEDULER set to “foo:./mock_foo.sh,bar:./mock_bar.sh”
+ * +-------+-------------------------------------------------+
+ * | Input | Output |
+ * | *cmd | return code | *cmd | *is_available |
+ * +-------+-------------+-------------------+---------------+
+ * | "foo" | true | "./mock.foo.sh" | true |
+ * | "qux" | true | "qux" (unchanged) | false |
+ * +-------+-------------+-------------------+---------------+
+ */
+static int get_schedule_cmd(const char **cmd, int *is_available)
+{
+ char *testing = xstrdup_or_null(getenv("GIT_TEST_MAINT_SCHEDULER"));
+ struct string_list_item *item;
+ struct string_list list = STRING_LIST_INIT_NODUP;
+
+ if (!testing)
+ return 0;
+
+ if (is_available)
+ *is_available = 0;
+
+ string_list_split_in_place(&list, testing, ',', -1);
+ for_each_string_list_item(item, &list) {
+ struct string_list pair = STRING_LIST_INIT_NODUP;
+
+ if (string_list_split_in_place(&pair, item->string, ':', 2) != 2)
+ continue;
+
+ if (!strcmp(*cmd, pair.items[0].string)) {
+ *cmd = pair.items[1].string;
+ if (is_available)
+ *is_available = 1;
+ string_list_clear(&list, 0);
+ UNLEAK(testing);
+ return 1;
+ }
+ }
+
+ string_list_clear(&list, 0);
+ free(testing);
+ return 1;
+}
+
+static int is_launchctl_available(void)
+{
+ const char *cmd = "launchctl";
+ int is_available;
+ if (get_schedule_cmd(&cmd, &is_available))
+ return is_available;
+
+#ifdef __APPLE__
+ return 1;
+#else
+ return 0;
+#endif
+}
+
static char *launchctl_service_name(const char *frequency)
{
struct strbuf label = STRBUF_INIT;
@@ -1520,7 +1622,7 @@ static char *launchctl_service_filename(const char *name)
struct strbuf filename = STRBUF_INIT;
strbuf_addf(&filename, "~/Library/LaunchAgents/%s.plist", name);
- expanded = expand_user_path(filename.buf, 1);
+ expanded = interpolate_path(filename.buf, 1);
if (!expanded)
die(_("failed to expand path '%s'"), filename.buf);
@@ -1533,19 +1635,17 @@ static char *launchctl_get_uid(void)
return xstrfmt("gui/%d", getuid());
}
-static int launchctl_boot_plist(int enable, const char *filename, const char *cmd)
+static int launchctl_boot_plist(int enable, const char *filename)
{
+ const char *cmd = "launchctl";
int result;
struct child_process child = CHILD_PROCESS_INIT;
char *uid = launchctl_get_uid();
+ get_schedule_cmd(&cmd, NULL);
strvec_split(&child.args, cmd);
- if (enable)
- strvec_push(&child.args, "bootstrap");
- else
- strvec_push(&child.args, "bootout");
- strvec_push(&child.args, uid);
- strvec_push(&child.args, filename);
+ strvec_pushl(&child.args, enable ? "bootstrap" : "bootout", uid,
+ filename, NULL);
child.no_stderr = 1;
child.no_stdout = 1;
@@ -1559,38 +1659,56 @@ static int launchctl_boot_plist(int enable, const char *filename, const char *cm
return result;
}
-static int launchctl_remove_plist(enum schedule_priority schedule, const char *cmd)
+static int launchctl_remove_plist(enum schedule_priority schedule)
{
const char *frequency = get_frequency(schedule);
char *name = launchctl_service_name(frequency);
char *filename = launchctl_service_filename(name);
- int result = launchctl_boot_plist(0, filename, cmd);
+ int result = launchctl_boot_plist(0, filename);
unlink(filename);
free(filename);
free(name);
return result;
}
-static int launchctl_remove_plists(const char *cmd)
+static int launchctl_remove_plists(void)
{
- return launchctl_remove_plist(SCHEDULE_HOURLY, cmd) ||
- launchctl_remove_plist(SCHEDULE_DAILY, cmd) ||
- launchctl_remove_plist(SCHEDULE_WEEKLY, cmd);
+ return launchctl_remove_plist(SCHEDULE_HOURLY) ||
+ launchctl_remove_plist(SCHEDULE_DAILY) ||
+ launchctl_remove_plist(SCHEDULE_WEEKLY);
}
-static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule, const char *cmd)
+static int launchctl_list_contains_plist(const char *name, const char *cmd)
{
- FILE *plist;
- int i;
+ struct child_process child = CHILD_PROCESS_INIT;
+
+ strvec_split(&child.args, cmd);
+ strvec_pushl(&child.args, "list", name, NULL);
+
+ child.no_stderr = 1;
+ child.no_stdout = 1;
+
+ if (start_command(&child))
+ die(_("failed to start launchctl"));
+
+ /* Returns failure if 'name' doesn't exist. */
+ return !finish_command(&child);
+}
+
+static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule)
+{
+ int i, fd;
const char *preamble, *repeat;
const char *frequency = get_frequency(schedule);
char *name = launchctl_service_name(frequency);
char *filename = launchctl_service_filename(name);
+ struct lock_file lk = LOCK_INIT;
+ static unsigned long lock_file_timeout_ms = ULONG_MAX;
+ struct strbuf plist = STRBUF_INIT, plist2 = STRBUF_INIT;
+ struct stat st;
+ const char *cmd = "launchctl";
- if (safe_create_leading_directories(filename))
- die(_("failed to create directories for '%s'"), filename);
- plist = xfopen(filename, "w");
-
+ get_schedule_cmd(&cmd, NULL);
preamble = "<?xml version=\"1.0\"?>\n"
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
"<plist version=\"1.0\">"
@@ -1608,7 +1726,7 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit
"</array>\n"
"<key>StartCalendarInterval</key>\n"
"<array>\n";
- fprintf(plist, preamble, name, exec_path, exec_path, frequency);
+ strbuf_addf(&plist, preamble, name, exec_path, exec_path, frequency);
switch (schedule) {
case SCHEDULE_HOURLY:
@@ -1617,7 +1735,7 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit
"<key>Minute</key><integer>0</integer>\n"
"</dict>\n";
for (i = 1; i <= 23; i++)
- fprintf(plist, repeat, i);
+ strbuf_addf(&plist, repeat, i);
break;
case SCHEDULE_DAILY:
@@ -1627,50 +1745,91 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit
"<key>Minute</key><integer>0</integer>\n"
"</dict>\n";
for (i = 1; i <= 6; i++)
- fprintf(plist, repeat, i);
+ strbuf_addf(&plist, repeat, i);
break;
case SCHEDULE_WEEKLY:
- fprintf(plist,
- "<dict>\n"
- "<key>Day</key><integer>0</integer>\n"
- "<key>Hour</key><integer>0</integer>\n"
- "<key>Minute</key><integer>0</integer>\n"
- "</dict>\n");
+ strbuf_addstr(&plist,
+ "<dict>\n"
+ "<key>Day</key><integer>0</integer>\n"
+ "<key>Hour</key><integer>0</integer>\n"
+ "<key>Minute</key><integer>0</integer>\n"
+ "</dict>\n");
break;
default:
/* unreachable */
break;
}
- fprintf(plist, "</array>\n</dict>\n</plist>\n");
- fclose(plist);
+ strbuf_addstr(&plist, "</array>\n</dict>\n</plist>\n");
+
+ if (safe_create_leading_directories(filename))
+ die(_("failed to create directories for '%s'"), filename);
+
+ if ((long)lock_file_timeout_ms < 0 &&
+ git_config_get_ulong("gc.launchctlplistlocktimeoutms",
+ &lock_file_timeout_ms))
+ lock_file_timeout_ms = 150;
- /* bootout might fail if not already running, so ignore */
- launchctl_boot_plist(0, filename, cmd);
- if (launchctl_boot_plist(1, filename, cmd))
- die(_("failed to bootstrap service %s"), filename);
+ fd = hold_lock_file_for_update_timeout(&lk, filename, LOCK_DIE_ON_ERROR,
+ lock_file_timeout_ms);
+
+ /*
+ * Does this file already exist? With the intended contents? Is it
+ * registered already? Then it does not need to be re-registered.
+ */
+ if (!stat(filename, &st) && st.st_size == plist.len &&
+ strbuf_read_file(&plist2, filename, plist.len) == plist.len &&
+ !strbuf_cmp(&plist, &plist2) &&
+ launchctl_list_contains_plist(name, cmd))
+ rollback_lock_file(&lk);
+ else {
+ if (write_in_full(fd, plist.buf, plist.len) < 0 ||
+ commit_lock_file(&lk))
+ die_errno(_("could not write '%s'"), filename);
+
+ /* bootout might fail if not already running, so ignore */
+ launchctl_boot_plist(0, filename);
+ if (launchctl_boot_plist(1, filename))
+ die(_("failed to bootstrap service %s"), filename);
+ }
free(filename);
free(name);
+ strbuf_release(&plist);
+ strbuf_release(&plist2);
return 0;
}
-static int launchctl_add_plists(const char *cmd)
+static int launchctl_add_plists(void)
{
const char *exec_path = git_exec_path();
- return launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY, cmd) ||
- launchctl_schedule_plist(exec_path, SCHEDULE_DAILY, cmd) ||
- launchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY, cmd);
+ return launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY) ||
+ launchctl_schedule_plist(exec_path, SCHEDULE_DAILY) ||
+ launchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY);
}
-static int launchctl_update_schedule(int run_maintenance, int fd, const char *cmd)
+static int launchctl_update_schedule(int run_maintenance, int fd)
{
if (run_maintenance)
- return launchctl_add_plists(cmd);
+ return launchctl_add_plists();
else
- return launchctl_remove_plists(cmd);
+ return launchctl_remove_plists();
+}
+
+static int is_schtasks_available(void)
+{
+ const char *cmd = "schtasks";
+ int is_available;
+ if (get_schedule_cmd(&cmd, &is_available))
+ return is_available;
+
+#ifdef GIT_WINDOWS_NATIVE
+ return 1;
+#else
+ return 0;
+#endif
}
static char *schtasks_task_name(const char *frequency)
@@ -1680,13 +1839,15 @@ static char *schtasks_task_name(const char *frequency)
return strbuf_detach(&label, NULL);
}
-static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd)
+static int schtasks_remove_task(enum schedule_priority schedule)
{
+ const char *cmd = "schtasks";
int result;
struct strvec args = STRVEC_INIT;
const char *frequency = get_frequency(schedule);
char *name = schtasks_task_name(frequency);
+ get_schedule_cmd(&cmd, NULL);
strvec_split(&args, cmd);
strvec_pushl(&args, "/delete", "/tn", name, "/f", NULL);
@@ -1697,15 +1858,16 @@ static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd
return result;
}
-static int schtasks_remove_tasks(const char *cmd)
+static int schtasks_remove_tasks(void)
{
- return schtasks_remove_task(SCHEDULE_HOURLY, cmd) ||
- schtasks_remove_task(SCHEDULE_DAILY, cmd) ||
- schtasks_remove_task(SCHEDULE_WEEKLY, cmd);
+ return schtasks_remove_task(SCHEDULE_HOURLY) ||
+ schtasks_remove_task(SCHEDULE_DAILY) ||
+ schtasks_remove_task(SCHEDULE_WEEKLY);
}
-static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule, const char *cmd)
+static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule)
{
+ const char *cmd = "schtasks";
int result;
struct child_process child = CHILD_PROCESS_INIT;
const char *xml;
@@ -1714,6 +1876,8 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority
char *name = schtasks_task_name(frequency);
struct strbuf tfilename = STRBUF_INIT;
+ get_schedule_cmd(&cmd, NULL);
+
strbuf_addf(&tfilename, "%s/schedule_%s_XXXXXX",
get_git_common_dir(), frequency);
tfile = xmks_tempfile(tfilename.buf);
@@ -1818,28 +1982,67 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority
return result;
}
-static int schtasks_schedule_tasks(const char *cmd)
+static int schtasks_schedule_tasks(void)
{
const char *exec_path = git_exec_path();
- return schtasks_schedule_task(exec_path, SCHEDULE_HOURLY, cmd) ||
- schtasks_schedule_task(exec_path, SCHEDULE_DAILY, cmd) ||
- schtasks_schedule_task(exec_path, SCHEDULE_WEEKLY, cmd);
+ return schtasks_schedule_task(exec_path, SCHEDULE_HOURLY) ||
+ schtasks_schedule_task(exec_path, SCHEDULE_DAILY) ||
+ schtasks_schedule_task(exec_path, SCHEDULE_WEEKLY);
}
-static int schtasks_update_schedule(int run_maintenance, int fd, const char *cmd)
+static int schtasks_update_schedule(int run_maintenance, int fd)
{
if (run_maintenance)
- return schtasks_schedule_tasks(cmd);
+ return schtasks_schedule_tasks();
else
- return schtasks_remove_tasks(cmd);
+ return schtasks_remove_tasks();
+}
+
+MAYBE_UNUSED
+static int check_crontab_process(const char *cmd)
+{
+ struct child_process child = CHILD_PROCESS_INIT;
+
+ strvec_split(&child.args, cmd);
+ strvec_push(&child.args, "-l");
+ child.no_stdin = 1;
+ child.no_stdout = 1;
+ child.no_stderr = 1;
+ child.silent_exec_failure = 1;
+
+ if (start_command(&child))
+ return 0;
+ /* Ignore exit code, as an empty crontab will return error. */
+ finish_command(&child);
+ return 1;
+}
+
+static int is_crontab_available(void)
+{
+ const char *cmd = "crontab";
+ int is_available;
+
+ if (get_schedule_cmd(&cmd, &is_available))
+ return is_available;
+
+#ifdef __APPLE__
+ /*
+ * macOS has cron, but it requires special permissions and will
+ * create a UI alert when attempting to run this command.
+ */
+ return 0;
+#else
+ return check_crontab_process(cmd);
+#endif
}
#define BEGIN_LINE "# BEGIN GIT MAINTENANCE SCHEDULE"
#define END_LINE "# END GIT MAINTENANCE SCHEDULE"
-static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)
+static int crontab_update_schedule(int run_maintenance, int fd)
{
+ const char *cmd = "crontab";
int result = 0;
int in_old_region = 0;
struct child_process crontab_list = CHILD_PROCESS_INIT;
@@ -1847,6 +2050,7 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)
FILE *cron_list, *cron_in;
struct strbuf line = STRBUF_INIT;
+ get_schedule_cmd(&cmd, NULL);
strvec_split(&crontab_list.args, cmd);
strvec_push(&crontab_list.args, "-l");
crontab_list.in = -1;
@@ -1887,6 +2091,7 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)
else if (!in_old_region)
fprintf(cron_in, "%s\n", line.buf);
}
+ strbuf_release(&line);
if (run_maintenance) {
struct strbuf line_format = STRBUF_INIT;
@@ -1922,61 +2127,376 @@ done_editing:
return result;
}
+static int real_is_systemd_timer_available(void)
+{
+ struct child_process child = CHILD_PROCESS_INIT;
+
+ strvec_pushl(&child.args, "systemctl", "--user", "list-timers", NULL);
+ child.no_stdin = 1;
+ child.no_stdout = 1;
+ child.no_stderr = 1;
+ child.silent_exec_failure = 1;
+
+ if (start_command(&child))
+ return 0;
+ if (finish_command(&child))
+ return 0;
+ return 1;
+}
+
+static int is_systemd_timer_available(void)
+{
+ const char *cmd = "systemctl";
+ int is_available;
+
+ if (get_schedule_cmd(&cmd, &is_available))
+ return is_available;
+
+ return real_is_systemd_timer_available();
+}
+
+static char *xdg_config_home_systemd(const char *filename)
+{
+ return xdg_config_home_for("systemd/user", filename);
+}
+
+static int systemd_timer_enable_unit(int enable,
+ enum schedule_priority schedule)
+{
+ const char *cmd = "systemctl";
+ struct child_process child = CHILD_PROCESS_INIT;
+ const char *frequency = get_frequency(schedule);
+
+ /*
+ * Disabling the systemd unit while it is already disabled makes
+ * systemctl print an error.
+ * Let's ignore it since it means we already are in the expected state:
+ * the unit is disabled.
+ *
+ * On the other hand, enabling a systemd unit which is already enabled
+ * produces no error.
+ */
+ if (!enable)
+ child.no_stderr = 1;
+
+ get_schedule_cmd(&cmd, NULL);
+ strvec_split(&child.args, cmd);
+ strvec_pushl(&child.args, "--user", enable ? "enable" : "disable",
+ "--now", NULL);
+ strvec_pushf(&child.args, "git-maintenance@%s.timer", frequency);
+
+ if (start_command(&child))
+ return error(_("failed to start systemctl"));
+ if (finish_command(&child))
+ /*
+ * Disabling an already disabled systemd unit makes
+ * systemctl fail.
+ * Let's ignore this failure.
+ *
+ * Enabling an enabled systemd unit doesn't fail.
+ */
+ if (enable)
+ return error(_("failed to run systemctl"));
+ return 0;
+}
+
+static int systemd_timer_delete_unit_templates(void)
+{
+ int ret = 0;
+ char *filename = xdg_config_home_systemd("git-maintenance@.timer");
+ if (unlink(filename) && !is_missing_file_error(errno))
+ ret = error_errno(_("failed to delete '%s'"), filename);
+ FREE_AND_NULL(filename);
+
+ filename = xdg_config_home_systemd("git-maintenance@.service");
+ if (unlink(filename) && !is_missing_file_error(errno))
+ ret = error_errno(_("failed to delete '%s'"), filename);
+
+ free(filename);
+ return ret;
+}
+
+static int systemd_timer_delete_units(void)
+{
+ return systemd_timer_enable_unit(0, SCHEDULE_HOURLY) ||
+ systemd_timer_enable_unit(0, SCHEDULE_DAILY) ||
+ systemd_timer_enable_unit(0, SCHEDULE_WEEKLY) ||
+ systemd_timer_delete_unit_templates();
+}
+
+static int systemd_timer_write_unit_templates(const char *exec_path)
+{
+ char *filename;
+ FILE *file;
+ const char *unit;
+
+ filename = xdg_config_home_systemd("git-maintenance@.timer");
+ if (safe_create_leading_directories(filename)) {
+ error(_("failed to create directories for '%s'"), filename);
+ goto error;
+ }
+ file = fopen_or_warn(filename, "w");
+ if (file == NULL)
+ goto error;
+
+ unit = "# This file was created and is maintained by Git.\n"
+ "# Any edits made in this file might be replaced in the future\n"
+ "# by a Git command.\n"
+ "\n"
+ "[Unit]\n"
+ "Description=Optimize Git repositories data\n"
+ "\n"
+ "[Timer]\n"
+ "OnCalendar=%i\n"
+ "Persistent=true\n"
+ "\n"
+ "[Install]\n"
+ "WantedBy=timers.target\n";
+ if (fputs(unit, file) == EOF) {
+ error(_("failed to write to '%s'"), filename);
+ fclose(file);
+ goto error;
+ }
+ if (fclose(file) == EOF) {
+ error_errno(_("failed to flush '%s'"), filename);
+ goto error;
+ }
+ free(filename);
+
+ filename = xdg_config_home_systemd("git-maintenance@.service");
+ file = fopen_or_warn(filename, "w");
+ if (file == NULL)
+ goto error;
+
+ unit = "# This file was created and is maintained by Git.\n"
+ "# Any edits made in this file might be replaced in the future\n"
+ "# by a Git command.\n"
+ "\n"
+ "[Unit]\n"
+ "Description=Optimize Git repositories data\n"
+ "\n"
+ "[Service]\n"
+ "Type=oneshot\n"
+ "ExecStart=\"%s/git\" --exec-path=\"%s\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\n"
+ "LockPersonality=yes\n"
+ "MemoryDenyWriteExecute=yes\n"
+ "NoNewPrivileges=yes\n"
+ "RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\n"
+ "RestrictNamespaces=yes\n"
+ "RestrictRealtime=yes\n"
+ "RestrictSUIDSGID=yes\n"
+ "SystemCallArchitectures=native\n"
+ "SystemCallFilter=@system-service\n";
+ if (fprintf(file, unit, exec_path, exec_path) < 0) {
+ error(_("failed to write to '%s'"), filename);
+ fclose(file);
+ goto error;
+ }
+ if (fclose(file) == EOF) {
+ error_errno(_("failed to flush '%s'"), filename);
+ goto error;
+ }
+ free(filename);
+ return 0;
+
+error:
+ free(filename);
+ systemd_timer_delete_unit_templates();
+ return -1;
+}
+
+static int systemd_timer_setup_units(void)
+{
+ const char *exec_path = git_exec_path();
+
+ int ret = systemd_timer_write_unit_templates(exec_path) ||
+ systemd_timer_enable_unit(1, SCHEDULE_HOURLY) ||
+ systemd_timer_enable_unit(1, SCHEDULE_DAILY) ||
+ systemd_timer_enable_unit(1, SCHEDULE_WEEKLY);
+ if (ret)
+ systemd_timer_delete_units();
+ return ret;
+}
+
+static int systemd_timer_update_schedule(int run_maintenance, int fd)
+{
+ if (run_maintenance)
+ return systemd_timer_setup_units();
+ else
+ return systemd_timer_delete_units();
+}
+
+enum scheduler {
+ SCHEDULER_INVALID = -1,
+ SCHEDULER_AUTO,
+ SCHEDULER_CRON,
+ SCHEDULER_SYSTEMD,
+ SCHEDULER_LAUNCHCTL,
+ SCHEDULER_SCHTASKS,
+};
+
+static const struct {
+ const char *name;
+ int (*is_available)(void);
+ int (*update_schedule)(int run_maintenance, int fd);
+} scheduler_fn[] = {
+ [SCHEDULER_CRON] = {
+ .name = "crontab",
+ .is_available = is_crontab_available,
+ .update_schedule = crontab_update_schedule,
+ },
+ [SCHEDULER_SYSTEMD] = {
+ .name = "systemctl",
+ .is_available = is_systemd_timer_available,
+ .update_schedule = systemd_timer_update_schedule,
+ },
+ [SCHEDULER_LAUNCHCTL] = {
+ .name = "launchctl",
+ .is_available = is_launchctl_available,
+ .update_schedule = launchctl_update_schedule,
+ },
+ [SCHEDULER_SCHTASKS] = {
+ .name = "schtasks",
+ .is_available = is_schtasks_available,
+ .update_schedule = schtasks_update_schedule,
+ },
+};
+
+static enum scheduler parse_scheduler(const char *value)
+{
+ if (!value)
+ return SCHEDULER_INVALID;
+ else if (!strcasecmp(value, "auto"))
+ return SCHEDULER_AUTO;
+ else if (!strcasecmp(value, "cron") || !strcasecmp(value, "crontab"))
+ return SCHEDULER_CRON;
+ else if (!strcasecmp(value, "systemd") ||
+ !strcasecmp(value, "systemd-timer"))
+ return SCHEDULER_SYSTEMD;
+ else if (!strcasecmp(value, "launchctl"))
+ return SCHEDULER_LAUNCHCTL;
+ else if (!strcasecmp(value, "schtasks"))
+ return SCHEDULER_SCHTASKS;
+ else
+ return SCHEDULER_INVALID;
+}
+
+static int maintenance_opt_scheduler(const struct option *opt, const char *arg,
+ int unset)
+{
+ enum scheduler *scheduler = opt->value;
+
+ BUG_ON_OPT_NEG(unset);
+
+ *scheduler = parse_scheduler(arg);
+ if (*scheduler == SCHEDULER_INVALID)
+ return error(_("unrecognized --scheduler argument '%s'"), arg);
+ return 0;
+}
+
+struct maintenance_start_opts {
+ enum scheduler scheduler;
+};
+
+static enum scheduler resolve_scheduler(enum scheduler scheduler)
+{
+ if (scheduler != SCHEDULER_AUTO)
+ return scheduler;
+
#if defined(__APPLE__)
-static const char platform_scheduler[] = "launchctl";
+ return SCHEDULER_LAUNCHCTL;
+
#elif defined(GIT_WINDOWS_NATIVE)
-static const char platform_scheduler[] = "schtasks";
+ return SCHEDULER_SCHTASKS;
+
+#elif defined(__linux__)
+ if (is_systemd_timer_available())
+ return SCHEDULER_SYSTEMD;
+ else if (is_crontab_available())
+ return SCHEDULER_CRON;
+ else
+ die(_("neither systemd timers nor crontab are available"));
+
#else
-static const char platform_scheduler[] = "crontab";
+ return SCHEDULER_CRON;
#endif
+}
-static int update_background_schedule(int enable)
+static void validate_scheduler(enum scheduler scheduler)
{
- int result;
- const char *scheduler = platform_scheduler;
- const char *cmd = scheduler;
- char *testing;
+ if (scheduler == SCHEDULER_INVALID)
+ BUG("invalid scheduler");
+ if (scheduler == SCHEDULER_AUTO)
+ BUG("resolve_scheduler should have been called before");
+
+ if (!scheduler_fn[scheduler].is_available())
+ die(_("%s scheduler is not available"),
+ scheduler_fn[scheduler].name);
+}
+
+static int update_background_schedule(const struct maintenance_start_opts *opts,
+ int enable)
+{
+ unsigned int i;
+ int result = 0;
struct lock_file lk;
char *lock_path = xstrfmt("%s/schedule", the_repository->objects->odb->path);
- testing = xstrdup_or_null(getenv("GIT_TEST_MAINT_SCHEDULER"));
- if (testing) {
- char *sep = strchr(testing, ':');
- if (!sep)
- die("GIT_TEST_MAINT_SCHEDULER unparseable: %s", testing);
- *sep = '\0';
- scheduler = testing;
- cmd = sep + 1;
+ if (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {
+ free(lock_path);
+ return error(_("another process is scheduling background maintenance"));
}
- if (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0)
- return error(_("another process is scheduling background maintenance"));
+ for (i = 1; i < ARRAY_SIZE(scheduler_fn); i++) {
+ if (enable && opts->scheduler == i)
+ continue;
+ if (!scheduler_fn[i].is_available())
+ continue;
+ scheduler_fn[i].update_schedule(0, get_lock_file_fd(&lk));
+ }
- if (!strcmp(scheduler, "launchctl"))
- result = launchctl_update_schedule(enable, get_lock_file_fd(&lk), cmd);
- else if (!strcmp(scheduler, "schtasks"))
- result = schtasks_update_schedule(enable, get_lock_file_fd(&lk), cmd);
- else if (!strcmp(scheduler, "crontab"))
- result = crontab_update_schedule(enable, get_lock_file_fd(&lk), cmd);
- else
- die("unknown background scheduler: %s", scheduler);
+ if (enable)
+ result = scheduler_fn[opts->scheduler].update_schedule(
+ 1, get_lock_file_fd(&lk));
rollback_lock_file(&lk);
- free(testing);
+
+ free(lock_path);
return result;
}
-static int maintenance_start(void)
+static const char *const builtin_maintenance_start_usage[] = {
+ N_("git maintenance start [--scheduler=<scheduler>]"),
+ NULL
+};
+
+static int maintenance_start(int argc, const char **argv, const char *prefix)
{
+ struct maintenance_start_opts opts = { 0 };
+ struct option options[] = {
+ OPT_CALLBACK_F(
+ 0, "scheduler", &opts.scheduler, N_("scheduler"),
+ N_("scheduler to trigger git maintenance run"),
+ PARSE_OPT_NONEG, maintenance_opt_scheduler),
+ OPT_END()
+ };
+
+ argc = parse_options(argc, argv, prefix, options,
+ builtin_maintenance_start_usage, 0);
+ if (argc)
+ usage_with_options(builtin_maintenance_start_usage, options);
+
+ opts.scheduler = resolve_scheduler(opts.scheduler);
+ validate_scheduler(opts.scheduler);
+
if (maintenance_register())
warning(_("failed to add repo to global config"));
-
- return update_background_schedule(1);
+ return update_background_schedule(&opts, 1);
}
static int maintenance_stop(void)
{
- return update_background_schedule(0);
+ return update_background_schedule(NULL, 0);
}
static const char builtin_maintenance_usage[] = N_("git maintenance <subcommand> [<options>]");
@@ -1990,7 +2510,7 @@ int cmd_maintenance(int argc, const char **argv, const char *prefix)
if (!strcmp(argv[1], "run"))
return maintenance_run(argc - 1, argv + 1, prefix);
if (!strcmp(argv[1], "start"))
- return maintenance_start();
+ return maintenance_start(argc - 1, argv + 1, prefix);
if (!strcmp(argv[1], "stop"))
return maintenance_stop();
if (!strcmp(argv[1], "register"))