Merge branch 'lh/systemd-timers'
"git maintenance" scheduler learned to use systemd timers as a possible backend. * lh/systemd-timers: maintenance: add support for systemd timers on Linux maintenance: `git maintenance run` learned `--scheduler=<scheduler>` cache.h: Introduce a generic "xdg_config_home_for(…)" function
This commit is contained in:
commit
ed8794ef7a
@ -179,6 +179,17 @@ OPTIONS
|
||||
`maintenance.<task>.enabled` configured as `true` are considered.
|
||||
See the 'TASKS' section for the list of accepted `<task>` values.
|
||||
|
||||
--scheduler=auto|crontab|systemd-timer|launchctl|schtasks::
|
||||
When combined with the `start` subcommand, specify the scheduler
|
||||
for running the hourly, daily and weekly executions of
|
||||
`git maintenance run`.
|
||||
Possible values for `<scheduler>` are `auto`, `crontab`
|
||||
(POSIX), `systemd-timer` (Linux), `launchctl` (macOS), and
|
||||
`schtasks` (Windows). When `auto` is specified, the
|
||||
appropriate platform-specific scheduler is used; on Linux,
|
||||
`systemd-timer` is used if available, otherwise
|
||||
`crontab`. Default is `auto`.
|
||||
|
||||
|
||||
TROUBLESHOOTING
|
||||
---------------
|
||||
@ -277,6 +288,52 @@ schedule to ensure you are executing the correct binaries in your
|
||||
schedule.
|
||||
|
||||
|
||||
BACKGROUND MAINTENANCE ON LINUX SYSTEMD SYSTEMS
|
||||
-----------------------------------------------
|
||||
|
||||
While Linux supports `cron`, depending on the distribution, `cron` may
|
||||
be an optional package not necessarily installed. On modern Linux
|
||||
distributions, systemd timers are superseding it.
|
||||
|
||||
If user systemd timers are available, they will be used as a replacement
|
||||
of `cron`.
|
||||
|
||||
In this case, `git maintenance start` will create user systemd timer units
|
||||
and start the timers. The current list of user-scheduled tasks can be found
|
||||
by running `systemctl --user list-timers`. The timers written by `git
|
||||
maintenance start` are similar to this:
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
$ systemctl --user list-timers
|
||||
NEXT LEFT LAST PASSED UNIT ACTIVATES
|
||||
Thu 2021-04-29 19:00:00 CEST 42min left Thu 2021-04-29 18:00:11 CEST 17min ago git-maintenance@hourly.timer git-maintenance@hourly.service
|
||||
Fri 2021-04-30 00:00:00 CEST 5h 42min left Thu 2021-04-29 00:00:11 CEST 18h ago git-maintenance@daily.timer git-maintenance@daily.service
|
||||
Mon 2021-05-03 00:00:00 CEST 3 days left Mon 2021-04-26 00:00:11 CEST 3 days ago git-maintenance@weekly.timer git-maintenance@weekly.service
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
One timer is registered for each `--schedule=<frequency>` option.
|
||||
|
||||
The definition of the systemd units can be inspected in the following files:
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
~/.config/systemd/user/git-maintenance@.timer
|
||||
~/.config/systemd/user/git-maintenance@.service
|
||||
~/.config/systemd/user/timers.target.wants/git-maintenance@hourly.timer
|
||||
~/.config/systemd/user/timers.target.wants/git-maintenance@daily.timer
|
||||
~/.config/systemd/user/timers.target.wants/git-maintenance@weekly.timer
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
`git maintenance start` will overwrite these files and start the timer
|
||||
again with `systemctl --user`, so any customization should be done by
|
||||
creating a drop-in file, i.e. a `.conf` suffixed file in the
|
||||
`~/.config/systemd/user/git-maintenance@.service.d` directory.
|
||||
|
||||
`git maintenance stop` will stop the user systemd timers and delete
|
||||
the above mentioned files.
|
||||
|
||||
For more details, see `systemd.timer(5)`.
|
||||
|
||||
|
||||
BACKGROUND MAINTENANCE ON MACOS SYSTEMS
|
||||
---------------------------------------
|
||||
|
||||
|
595
builtin/gc.c
595
builtin/gc.c
@ -1529,6 +1529,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;
|
||||
@ -1555,19 +1642,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;
|
||||
@ -1581,23 +1666,23 @@ 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_list_contains_plist(const char *name, const char *cmd)
|
||||
@ -1623,7 +1708,7 @@ static int launchctl_list_contains_plist(const char *name, const char *cmd)
|
||||
return !result;
|
||||
}
|
||||
|
||||
static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule, const char *cmd)
|
||||
static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule)
|
||||
{
|
||||
int i, fd;
|
||||
const char *preamble, *repeat;
|
||||
@ -1634,7 +1719,9 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit
|
||||
static unsigned long lock_file_timeout_ms = ULONG_MAX;
|
||||
struct strbuf plist = STRBUF_INIT, plist2 = STRBUF_INIT;
|
||||
struct stat st;
|
||||
const char *cmd = "launchctl";
|
||||
|
||||
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\">"
|
||||
@ -1715,8 +1802,8 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit
|
||||
die_errno(_("could not write '%s'"), filename);
|
||||
|
||||
/* bootout might fail if not already running, so ignore */
|
||||
launchctl_boot_plist(0, filename, cmd);
|
||||
if (launchctl_boot_plist(1, filename, cmd))
|
||||
launchctl_boot_plist(0, filename);
|
||||
if (launchctl_boot_plist(1, filename))
|
||||
die(_("failed to bootstrap service %s"), filename);
|
||||
}
|
||||
|
||||
@ -1727,21 +1814,35 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit
|
||||
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)
|
||||
@ -1751,13 +1852,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);
|
||||
|
||||
@ -1768,15 +1871,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;
|
||||
@ -1785,6 +1889,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);
|
||||
@ -1889,28 +1995,52 @@ 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();
|
||||
}
|
||||
|
||||
static int is_crontab_available(void)
|
||||
{
|
||||
const char *cmd = "crontab";
|
||||
int is_available;
|
||||
struct child_process child = CHILD_PROCESS_INIT;
|
||||
|
||||
if (get_schedule_cmd(&cmd, &is_available))
|
||||
return is_available;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
#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;
|
||||
@ -1918,6 +2048,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;
|
||||
@ -1994,66 +2125,376 @@ done_editing:
|
||||
return result;
|
||||
}
|
||||
|
||||
#if defined(__APPLE__)
|
||||
static const char platform_scheduler[] = "launchctl";
|
||||
#elif defined(GIT_WINDOWS_NATIVE)
|
||||
static const char platform_scheduler[] = "schtasks";
|
||||
#else
|
||||
static const char platform_scheduler[] = "crontab";
|
||||
#endif
|
||||
|
||||
static int update_background_schedule(int enable)
|
||||
static int real_is_systemd_timer_available(void)
|
||||
{
|
||||
int result;
|
||||
const char *scheduler = platform_scheduler;
|
||||
const char *cmd = scheduler;
|
||||
char *testing;
|
||||
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__)
|
||||
return SCHEDULER_LAUNCHCTL;
|
||||
|
||||
#elif defined(GIT_WINDOWS_NATIVE)
|
||||
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
|
||||
return SCHEDULER_CRON;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void validate_scheduler(enum scheduler scheduler)
|
||||
{
|
||||
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) {
|
||||
result = error(_("another process is scheduling background maintenance"));
|
||||
goto cleanup;
|
||||
free(lock_path);
|
||||
return error(_("another process is scheduling background maintenance"));
|
||||
}
|
||||
|
||||
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);
|
||||
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 (enable)
|
||||
result = scheduler_fn[opts->scheduler].update_schedule(
|
||||
1, get_lock_file_fd(&lk));
|
||||
|
||||
rollback_lock_file(&lk);
|
||||
|
||||
cleanup:
|
||||
free(lock_path);
|
||||
free(testing);
|
||||
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>]");
|
||||
@ -2067,7 +2508,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"))
|
||||
|
7
cache.h
7
cache.h
@ -1297,6 +1297,13 @@ int is_ntfs_dotmailmap(const char *name);
|
||||
*/
|
||||
int looks_like_command_line_option(const char *str);
|
||||
|
||||
/**
|
||||
* Return a newly allocated string with the evaluation of
|
||||
* "$XDG_CONFIG_HOME/$subdir/$filename" if $XDG_CONFIG_HOME is non-empty, otherwise
|
||||
* "$HOME/.config/$subdir/$filename". Return NULL upon error.
|
||||
*/
|
||||
char *xdg_config_home_for(const char *subdir, const char *filename);
|
||||
|
||||
/**
|
||||
* Return a newly allocated string with the evaluation of
|
||||
* "$XDG_CONFIG_HOME/git/$filename" if $XDG_CONFIG_HOME is non-empty, otherwise
|
||||
|
13
path.c
13
path.c
@ -1510,21 +1510,28 @@ int looks_like_command_line_option(const char *str)
|
||||
return str && str[0] == '-';
|
||||
}
|
||||
|
||||
char *xdg_config_home(const char *filename)
|
||||
char *xdg_config_home_for(const char *subdir, const char *filename)
|
||||
{
|
||||
const char *home, *config_home;
|
||||
|
||||
assert(subdir);
|
||||
assert(filename);
|
||||
config_home = getenv("XDG_CONFIG_HOME");
|
||||
if (config_home && *config_home)
|
||||
return mkpathdup("%s/git/%s", config_home, filename);
|
||||
return mkpathdup("%s/%s/%s", config_home, subdir, filename);
|
||||
|
||||
home = getenv("HOME");
|
||||
if (home)
|
||||
return mkpathdup("%s/.config/git/%s", home, filename);
|
||||
return mkpathdup("%s/.config/%s/%s", home, subdir, filename);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char *xdg_config_home(const char *filename)
|
||||
{
|
||||
return xdg_config_home_for("git", filename);
|
||||
}
|
||||
|
||||
char *xdg_cache_home(const char *filename)
|
||||
{
|
||||
const char *home, *cache_home;
|
||||
|
@ -20,6 +20,18 @@ test_xmllint () {
|
||||
fi
|
||||
}
|
||||
|
||||
test_lazy_prereq SYSTEMD_ANALYZE '
|
||||
systemd-analyze --help >out &&
|
||||
grep verify out
|
||||
'
|
||||
|
||||
test_systemd_analyze_verify () {
|
||||
if test_have_prereq SYSTEMD_ANALYZE
|
||||
then
|
||||
systemd-analyze verify "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
test_expect_success 'help text' '
|
||||
test_expect_code 129 git maintenance -h 2>err &&
|
||||
test_i18ngrep "usage: git maintenance <subcommand>" err &&
|
||||
@ -492,8 +504,21 @@ test_expect_success !MINGW 'register and unregister with regex metacharacters' '
|
||||
maintenance.repo "$(pwd)/$META"
|
||||
'
|
||||
|
||||
test_expect_success 'start --scheduler=<scheduler>' '
|
||||
test_expect_code 129 git maintenance start --scheduler=foo 2>err &&
|
||||
test_i18ngrep "unrecognized --scheduler argument" err &&
|
||||
|
||||
test_expect_code 129 git maintenance start --no-scheduler 2>err &&
|
||||
test_i18ngrep "unknown option" err &&
|
||||
|
||||
test_expect_code 128 \
|
||||
env GIT_TEST_MAINT_SCHEDULER="launchctl:true,schtasks:true" \
|
||||
git maintenance start --scheduler=crontab 2>err &&
|
||||
test_i18ngrep "fatal: crontab scheduler is not available" err
|
||||
'
|
||||
|
||||
test_expect_success 'start from empty cron table' '
|
||||
GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab cron.txt" git maintenance start &&
|
||||
GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab cron.txt" git maintenance start --scheduler=crontab &&
|
||||
|
||||
# start registers the repo
|
||||
git config --get --global --fixed-value maintenance.repo "$(pwd)" &&
|
||||
@ -516,7 +541,7 @@ test_expect_success 'stop from existing schedule' '
|
||||
|
||||
test_expect_success 'start preserves existing schedule' '
|
||||
echo "Important information!" >cron.txt &&
|
||||
GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab cron.txt" git maintenance start &&
|
||||
GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab cron.txt" git maintenance start --scheduler=crontab &&
|
||||
grep "Important information!" cron.txt
|
||||
'
|
||||
|
||||
@ -545,7 +570,7 @@ test_expect_success 'start and stop macOS maintenance' '
|
||||
EOF
|
||||
|
||||
rm -f args &&
|
||||
GIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start &&
|
||||
GIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start --scheduler=launchctl &&
|
||||
|
||||
# start registers the repo
|
||||
git config --get --global --fixed-value maintenance.repo "$(pwd)" &&
|
||||
@ -584,11 +609,11 @@ test_expect_success 'start and stop macOS maintenance' '
|
||||
|
||||
test_expect_success 'use launchctl list to prevent extra work' '
|
||||
# ensure we are registered
|
||||
GIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start &&
|
||||
GIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start --scheduler=launchctl &&
|
||||
|
||||
# do it again on a fresh args file
|
||||
rm -f args &&
|
||||
GIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start &&
|
||||
GIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start --scheduler=launchctl &&
|
||||
|
||||
ls "$HOME/Library/LaunchAgents" >actual &&
|
||||
cat >expect <<-\EOF &&
|
||||
@ -613,7 +638,7 @@ test_expect_success 'start and stop Windows maintenance' '
|
||||
EOF
|
||||
|
||||
rm -f args &&
|
||||
GIT_TEST_MAINT_SCHEDULER="schtasks:./print-args" git maintenance start &&
|
||||
GIT_TEST_MAINT_SCHEDULER="schtasks:./print-args" git maintenance start --scheduler=schtasks &&
|
||||
|
||||
# start registers the repo
|
||||
git config --get --global --fixed-value maintenance.repo "$(pwd)" &&
|
||||
@ -636,6 +661,83 @@ test_expect_success 'start and stop Windows maintenance' '
|
||||
test_cmp expect args
|
||||
'
|
||||
|
||||
test_expect_success 'start and stop Linux/systemd maintenance' '
|
||||
write_script print-args <<-\EOF &&
|
||||
printf "%s\n" "$*" >>args
|
||||
EOF
|
||||
|
||||
XDG_CONFIG_HOME="$PWD" &&
|
||||
export XDG_CONFIG_HOME &&
|
||||
rm -f args &&
|
||||
GIT_TEST_MAINT_SCHEDULER="systemctl:./print-args" git maintenance start --scheduler=systemd-timer &&
|
||||
|
||||
# start registers the repo
|
||||
git config --get --global --fixed-value maintenance.repo "$(pwd)" &&
|
||||
|
||||
test_systemd_analyze_verify "systemd/user/git-maintenance@.service" &&
|
||||
|
||||
printf -- "--user enable --now git-maintenance@%s.timer\n" hourly daily weekly >expect &&
|
||||
test_cmp expect args &&
|
||||
|
||||
rm -f args &&
|
||||
GIT_TEST_MAINT_SCHEDULER="systemctl:./print-args" git maintenance stop &&
|
||||
|
||||
# stop does not unregister the repo
|
||||
git config --get --global --fixed-value maintenance.repo "$(pwd)" &&
|
||||
|
||||
test_path_is_missing "systemd/user/git-maintenance@.timer" &&
|
||||
test_path_is_missing "systemd/user/git-maintenance@.service" &&
|
||||
|
||||
printf -- "--user disable --now git-maintenance@%s.timer\n" hourly daily weekly >expect &&
|
||||
test_cmp expect args
|
||||
'
|
||||
|
||||
test_expect_success 'start and stop when several schedulers are available' '
|
||||
write_script print-args <<-\EOF &&
|
||||
printf "%s\n" "$*" | sed "s:gui/[0-9][0-9]*:gui/[UID]:; s:\(schtasks /create .* /xml\).*:\1:;" >>args
|
||||
EOF
|
||||
|
||||
rm -f args &&
|
||||
GIT_TEST_MAINT_SCHEDULER="systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks" git maintenance start --scheduler=systemd-timer &&
|
||||
printf "launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\n" \
|
||||
hourly daily weekly >expect &&
|
||||
printf "schtasks /delete /tn Git Maintenance (%s) /f\n" \
|
||||
hourly daily weekly >>expect &&
|
||||
printf -- "systemctl --user enable --now git-maintenance@%s.timer\n" hourly daily weekly >>expect &&
|
||||
test_cmp expect args &&
|
||||
|
||||
rm -f args &&
|
||||
GIT_TEST_MAINT_SCHEDULER="systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks" git maintenance start --scheduler=launchctl &&
|
||||
printf -- "systemctl --user disable --now git-maintenance@%s.timer\n" hourly daily weekly >expect &&
|
||||
printf "schtasks /delete /tn Git Maintenance (%s) /f\n" \
|
||||
hourly daily weekly >>expect &&
|
||||
for frequency in hourly daily weekly
|
||||
do
|
||||
PLIST="$pfx/Library/LaunchAgents/org.git-scm.git.$frequency.plist" &&
|
||||
echo "launchctl bootout gui/[UID] $PLIST" >>expect &&
|
||||
echo "launchctl bootstrap gui/[UID] $PLIST" >>expect || return 1
|
||||
done &&
|
||||
test_cmp expect args &&
|
||||
|
||||
rm -f args &&
|
||||
GIT_TEST_MAINT_SCHEDULER="systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks" git maintenance start --scheduler=schtasks &&
|
||||
printf -- "systemctl --user disable --now git-maintenance@%s.timer\n" hourly daily weekly >expect &&
|
||||
printf "launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\n" \
|
||||
hourly daily weekly >>expect &&
|
||||
printf "schtasks /create /tn Git Maintenance (%s) /f /xml\n" \
|
||||
hourly daily weekly >>expect &&
|
||||
test_cmp expect args &&
|
||||
|
||||
rm -f args &&
|
||||
GIT_TEST_MAINT_SCHEDULER="systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks" git maintenance stop &&
|
||||
printf -- "systemctl --user disable --now git-maintenance@%s.timer\n" hourly daily weekly >expect &&
|
||||
printf "launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\n" \
|
||||
hourly daily weekly >>expect &&
|
||||
printf "schtasks /delete /tn Git Maintenance (%s) /f\n" \
|
||||
hourly daily weekly >>expect &&
|
||||
test_cmp expect args
|
||||
'
|
||||
|
||||
test_expect_success 'register preserves existing strategy' '
|
||||
git config maintenance.strategy none &&
|
||||
git maintenance register &&
|
||||
|
Loading…
Reference in New Issue
Block a user