Message ID | 20210827210255.12565-3-lenaic@lhuard.fr (mailing list archive) |
---|---|
State | Superseded |
Headers | show |
Series | maintenance: add support for systemd timers on Linux | expand |
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@lhuard.fr> > --- > 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 && >
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 + * arguments unmodified. + * + * * If $GET_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,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 &&
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@lhuard.fr> --- Documentation/git-maintenance.txt | 9 + builtin/gc.c | 365 ++++++++++++++++++++++++------ t/t7900-maintenance.sh | 55 ++++- 3 files changed, 354 insertions(+), 75 deletions(-)