Hi Lénaïc, I noticed a couple of typos while tracking down a test failure on the previous version of this series: On 27/08/2021 22:02, Lénaïc Huard wrote: > Depending on the system, different schedulers can be used to schedule > the hourly, daily and weekly executions of `git maintenance run`: > * `launchctl` for MacOS, > * `schtasks` for Windows and > * `crontab` for everything else. > > `git maintenance run` now has an option to let the end-user explicitly > choose which scheduler he wants to use: > `--scheduler=auto|crontab|launchctl|schtasks`. > > When `git maintenance start --scheduler=XXX` is run, it not only > registers `git maintenance run` tasks in the scheduler XXX, it also > removes the `git maintenance run` tasks from all the other schedulers to > ensure we cannot have two schedulers launching concurrent identical > tasks. > > The default value is `auto` which chooses a suitable scheduler for the > system. > > `git maintenance stop` doesn't have any `--scheduler` parameter because > this command will try to remove the `git maintenance run` tasks from all > the available schedulers. > > Signed-off-by: Lénaïc Huard <lenaic@xxxxxxxxx> > --- > Documentation/git-maintenance.txt | 9 + > builtin/gc.c | 365 ++++++++++++++++++++++++------ > t/t7900-maintenance.sh | 55 ++++- > 3 files changed, 354 insertions(+), 75 deletions(-) > > diff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt > index 1e738ad398..576290b5c6 100644 > --- a/Documentation/git-maintenance.txt > +++ b/Documentation/git-maintenance.txt > @@ -179,6 +179,15 @@ OPTIONS > `maintenance.<task>.enabled` configured as `true` are considered. > See the 'TASKS' section for the list of accepted `<task>` values. > > +--scheduler=auto|crontab|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), > + `launchctl` (macOS), and `schtasks` (Windows). > + When `auto` is specified, the appropriate platform-specific > + scheduler is used. Default is `auto`. > + > > TROUBLESHOOTING > --------------- > diff --git a/builtin/gc.c b/builtin/gc.c > index f05d2f0a1a..9e464d4a10 100644 > --- a/builtin/gc.c > +++ b/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 $GET_TEST_MAINT_SCHEDULER is not set, return false and leave the s/GET/GIT/ > + * arguments unmodified. > + * > + * * If $GET_TEST_MAINT_SCHEDULER is set, return true. s/GET/GIT/ ATB, Ramsay Jones > + * 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,26 +1666,26 @@ 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_schedule_plist(const char *exec_path, enum schedule_priority schedule) > { > FILE *plist; > int i; > @@ -1669,8 +1754,8 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit > fclose(plist); > > /* 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); > > free(filename); > @@ -1678,21 +1763,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) > @@ -1702,13 +1801,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); > > @@ -1719,15 +1820,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; > @@ -1736,6 +1838,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); > @@ -1840,28 +1944,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; > @@ -1869,6 +1997,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; > @@ -1945,66 +2074,160 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd) > return result; > } > > +enum scheduler { > + SCHEDULER_INVALID = -1, > + SCHEDULER_AUTO, > + SCHEDULER_CRON, > + 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_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, "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; > + > #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) { > - result = error(_("another process is scheduling background maintenance")); > - goto cleanup; > + 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); > > -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>]"); > @@ -2018,7 +2241,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")) > diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh > index 58f46c77e6..27bce7992c 100755 > --- a/t/t7900-maintenance.sh > +++ b/t/t7900-maintenance.sh > @@ -492,8 +492,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 +529,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 +558,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)" && > @@ -596,7 +609,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)" && > @@ -619,6 +632,40 @@ test_expect_success 'start and stop Windows maintenance' ' > 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="launchctl:./print-args launchctl,schtasks:./print-args schtasks" git maintenance start --scheduler=launchctl && > + 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="launchctl:./print-args launchctl,schtasks:./print-args schtasks" git maintenance start --scheduler=schtasks && > + 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="launchctl:./print-args launchctl,schtasks:./print-args schtasks" git maintenance stop && > + 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 && >