maintenance: `git maintenance run` learned `--scheduler=<scheduler>`

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> Acked-by: Derrick Stolee <dstolee@microsoft.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Lénaïc Huard committed Sep 4, 2021 at 22:54 UTC eba1ba9d32f14dd9c052b1e2eb50e5718421d10e
3 files changed +354 -75
Documentation/git-maintenance.txt
+9
@@ -179,6 +179,15 @@ OPTIONS
179 `maintenance.<task>.enabled` configured as `true` are considered.
180 See the 'TASKS' section for the list of accepted `<task>` values.
181
182 +--scheduler=auto|crontab|launchctl|schtasks::
183 + When combined with the `start` subcommand, specify the scheduler
184 + for running the hourly, daily and weekly executions of
185 + `git maintenance run`.
186 + Possible values for `<scheduler>` are `auto`, `crontab` (POSIX),
187 + `launchctl` (macOS), and `schtasks` (Windows).
188 + When `auto` is specified, the appropriate platform-specific
189 + scheduler is used. Default is `auto`.
190 +
191
192 TROUBLESHOOTING
193 ---------------
builtin/gc.c
+294 -71
@@ -1529,6 +1529,93 @@ static const char *get_frequency(enum schedule_priority schedule)
1529 }
1530 }
1531
1532 +/*
1533 + * get_schedule_cmd` reads the GIT_TEST_MAINT_SCHEDULER environment variable
1534 + * to mock the schedulers that `git maintenance start` rely on.
1535 + *
1536 + * For test purpose, GIT_TEST_MAINT_SCHEDULER can be set to a comma-separated
1537 + * list of colon-separated key/value pairs where each pair contains a scheduler
1538 + * and its corresponding mock.
1539 + *
1540 + * * If $GIT_TEST_MAINT_SCHEDULER is not set, return false and leave the
1541 + * arguments unmodified.
1542 + *
1543 + * * If $GIT_TEST_MAINT_SCHEDULER is set, return true.
1544 + * In this case, the *cmd value is read as input.
1545 + *
1546 + * * if the input value *cmd is the key of one of the comma-separated list
1547 + * item, then *is_available is set to true and *cmd is modified and becomes
1548 + * the mock command.
1549 + *
1550 + * * if the input value *cmd isn’t the key of any of the comma-separated list
1551 + * item, then *is_available is set to false.
1552 + *
1553 + * Ex.:
1554 + * GIT_TEST_MAINT_SCHEDULER not set
1555 + * +-------+-------------------------------------------------+
1556 + * | Input | Output |
1557 + * | *cmd | return code | *cmd | *is_available |
1558 + * +-------+-------------+-------------------+---------------+
1559 + * | "foo" | false | "foo" (unchanged) | (unchanged) |
1560 + * +-------+-------------+-------------------+---------------+
1561 + *
1562 + * GIT_TEST_MAINT_SCHEDULER set to “foo:./mock_foo.sh,bar:./mock_bar.sh”
1563 + * +-------+-------------------------------------------------+
1564 + * | Input | Output |
1565 + * | *cmd | return code | *cmd | *is_available |
1566 + * +-------+-------------+-------------------+---------------+
1567 + * | "foo" | true | "./mock.foo.sh" | true |
1568 + * | "qux" | true | "qux" (unchanged) | false |
1569 + * +-------+-------------+-------------------+---------------+
1570 + */
1571 +static int get_schedule_cmd(const char **cmd, int *is_available)
1572 +{
1573 + char *testing = xstrdup_or_null(getenv("GIT_TEST_MAINT_SCHEDULER"));
1574 + struct string_list_item *item;
1575 + struct string_list list = STRING_LIST_INIT_NODUP;
1576 +
1577 + if (!testing)
1578 + return 0;
1579 +
1580 + if (is_available)
1581 + *is_available = 0;
1582 +
1583 + string_list_split_in_place(&list, testing, ',', -1);
1584 + for_each_string_list_item(item, &list) {
1585 + struct string_list pair = STRING_LIST_INIT_NODUP;
1586 +
1587 + if (string_list_split_in_place(&pair, item->string, ':', 2) != 2)
1588 + continue;
1589 +
1590 + if (!strcmp(*cmd, pair.items[0].string)) {
1591 + *cmd = pair.items[1].string;
1592 + if (is_available)
1593 + *is_available = 1;
1594 + string_list_clear(&list, 0);
1595 + UNLEAK(testing);
1596 + return 1;
1597 + }
1598 + }
1599 +
1600 + string_list_clear(&list, 0);
1601 + free(testing);
1602 + return 1;
1603 +}
1604 +
1605 +static int is_launchctl_available(void)
1606 +{
1607 + const char *cmd = "launchctl";
1608 + int is_available;
1609 + if (get_schedule_cmd(&cmd, &is_available))
1610 + return is_available;
1611 +
1612 +#ifdef __APPLE__
1613 + return 1;
1614 +#else
1615 + return 0;
1616 +#endif
1617 +}
1618 +
1619 static char *launchctl_service_name(const char *frequency)
1620 {
1621 struct strbuf label = STRBUF_INIT;
@@ -1555,19 +1642,17 @@ static char *launchctl_get_uid(void)
1642 return xstrfmt("gui/%d", getuid());
1643 }
1644
1558 -static int launchctl_boot_plist(int enable, const char *filename, const char *cmd)
1645 +static int launchctl_boot_plist(int enable, const char *filename)
1646 {
1647 + const char *cmd = "launchctl";
1648 int result;
1649 struct child_process child = CHILD_PROCESS_INIT;
1650 char *uid = launchctl_get_uid();
1651
1652 + get_schedule_cmd(&cmd, NULL);
1653 strvec_split(&child.args, cmd);
1565 - if (enable)
1566 - strvec_push(&child.args, "bootstrap");
1567 - else
1568 - strvec_push(&child.args, "bootout");
1569 - strvec_push(&child.args, uid);
1570 - strvec_push(&child.args, filename);
1654 + strvec_pushl(&child.args, enable ? "bootstrap" : "bootout", uid,
1655 + filename, NULL);
1656
1657 child.no_stderr = 1;
1658 child.no_stdout = 1;
@@ -1581,26 +1666,26 @@ static int launchctl_boot_plist(int enable, const char *filename, const char *cm
1666 return result;
1667 }
1668
1584 -static int launchctl_remove_plist(enum schedule_priority schedule, const char *cmd)
1669 +static int launchctl_remove_plist(enum schedule_priority schedule)
1670 {
1671 const char *frequency = get_frequency(schedule);
1672 char *name = launchctl_service_name(frequency);
1673 char *filename = launchctl_service_filename(name);
1589 - int result = launchctl_boot_plist(0, filename, cmd);
1674 + int result = launchctl_boot_plist(0, filename);
1675 unlink(filename);
1676 free(filename);
1677 free(name);
1678 return result;
1679 }
1680
1596 -static int launchctl_remove_plists(const char *cmd)
1681 +static int launchctl_remove_plists(void)
1682 {
1598 - return launchctl_remove_plist(SCHEDULE_HOURLY, cmd) ||
1599 - launchctl_remove_plist(SCHEDULE_DAILY, cmd) ||
1600 - launchctl_remove_plist(SCHEDULE_WEEKLY, cmd);
1683 + return launchctl_remove_plist(SCHEDULE_HOURLY) ||
1684 + launchctl_remove_plist(SCHEDULE_DAILY) ||
1685 + launchctl_remove_plist(SCHEDULE_WEEKLY);
1686 }
1687
1603 -static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule, const char *cmd)
1688 +static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule)
1689 {
1690 FILE *plist;
1691 int i;
@@ -1669,8 +1754,8 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit
1754 fclose(plist);
1755
1756 /* bootout might fail if not already running, so ignore */
1672 - launchctl_boot_plist(0, filename, cmd);
1673 - if (launchctl_boot_plist(1, filename, cmd))
1757 + launchctl_boot_plist(0, filename);
1758 + if (launchctl_boot_plist(1, filename))
1759 die(_("failed to bootstrap service %s"), filename);
1760
1761 free(filename);
@@ -1678,21 +1763,35 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit
1763 return 0;
1764 }
1765
1681 -static int launchctl_add_plists(const char *cmd)
1766 +static int launchctl_add_plists(void)
1767 {
1768 const char *exec_path = git_exec_path();
1769
1685 - return launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY, cmd) ||
1686 - launchctl_schedule_plist(exec_path, SCHEDULE_DAILY, cmd) ||
1687 - launchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY, cmd);
1770 + return launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY) ||
1771 + launchctl_schedule_plist(exec_path, SCHEDULE_DAILY) ||
1772 + launchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY);
1773 }
1774
1690 -static int launchctl_update_schedule(int run_maintenance, int fd, const char *cmd)
1775 +static int launchctl_update_schedule(int run_maintenance, int fd)
1776 {
1777 if (run_maintenance)
1693 - return launchctl_add_plists(cmd);
1778 + return launchctl_add_plists();
1779 else
1695 - return launchctl_remove_plists(cmd);
1780 + return launchctl_remove_plists();
1781 +}
1782 +
1783 +static int is_schtasks_available(void)
1784 +{
1785 + const char *cmd = "schtasks";
1786 + int is_available;
1787 + if (get_schedule_cmd(&cmd, &is_available))
1788 + return is_available;
1789 +
1790 +#ifdef GIT_WINDOWS_NATIVE
1791 + return 1;
1792 +#else
1793 + return 0;
1794 +#endif
1795 }
1796
1797 static char *schtasks_task_name(const char *frequency)
@@ -1702,13 +1801,15 @@ static char *schtasks_task_name(const char *frequency)
1801 return strbuf_detach(&label, NULL);
1802 }
1803
1705 -static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd)
1804 +static int schtasks_remove_task(enum schedule_priority schedule)
1805 {
1806 + const char *cmd = "schtasks";
1807 int result;
1808 struct strvec args = STRVEC_INIT;
1809 const char *frequency = get_frequency(schedule);
1810 char *name = schtasks_task_name(frequency);
1811
1812 + get_schedule_cmd(&cmd, NULL);
1813 strvec_split(&args, cmd);
1814 strvec_pushl(&args, "/delete", "/tn", name, "/f", NULL);
1815
@@ -1719,15 +1820,16 @@ static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd
1820 return result;
1821 }
1822
1722 -static int schtasks_remove_tasks(const char *cmd)
1823 +static int schtasks_remove_tasks(void)
1824 {
1724 - return schtasks_remove_task(SCHEDULE_HOURLY, cmd) ||
1725 - schtasks_remove_task(SCHEDULE_DAILY, cmd) ||
1726 - schtasks_remove_task(SCHEDULE_WEEKLY, cmd);
1825 + return schtasks_remove_task(SCHEDULE_HOURLY) ||
1826 + schtasks_remove_task(SCHEDULE_DAILY) ||
1827 + schtasks_remove_task(SCHEDULE_WEEKLY);
1828 }
1829
1729 -static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule, const char *cmd)
1830 +static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule)
1831 {
1832 + const char *cmd = "schtasks";
1833 int result;
1834 struct child_process child = CHILD_PROCESS_INIT;
1835 const char *xml;
@@ -1736,6 +1838,8 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority
1838 char *name = schtasks_task_name(frequency);
1839 struct strbuf tfilename = STRBUF_INIT;
1840
1841 + get_schedule_cmd(&cmd, NULL);
1842 +
1843 strbuf_addf(&tfilename, "%s/schedule_%s_XXXXXX",
1844 get_git_common_dir(), frequency);
1845 tfile = xmks_tempfile(tfilename.buf);
@@ -1840,28 +1944,52 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority
1944 return result;
1945 }
1946
1843 -static int schtasks_schedule_tasks(const char *cmd)
1947 +static int schtasks_schedule_tasks(void)
1948 {
1949 const char *exec_path = git_exec_path();
1950
1847 - return schtasks_schedule_task(exec_path, SCHEDULE_HOURLY, cmd) ||
1848 - schtasks_schedule_task(exec_path, SCHEDULE_DAILY, cmd) ||
1849 - schtasks_schedule_task(exec_path, SCHEDULE_WEEKLY, cmd);
1951 + return schtasks_schedule_task(exec_path, SCHEDULE_HOURLY) ||
1952 + schtasks_schedule_task(exec_path, SCHEDULE_DAILY) ||
1953 + schtasks_schedule_task(exec_path, SCHEDULE_WEEKLY);
1954 }
1955
1852 -static int schtasks_update_schedule(int run_maintenance, int fd, const char *cmd)
1956 +static int schtasks_update_schedule(int run_maintenance, int fd)
1957 {
1958 if (run_maintenance)
1855 - return schtasks_schedule_tasks(cmd);
1959 + return schtasks_schedule_tasks();
1960 else
1857 - return schtasks_remove_tasks(cmd);
1961 + return schtasks_remove_tasks();
1962 +}
1963 +
1964 +static int is_crontab_available(void)
1965 +{
1966 + const char *cmd = "crontab";
1967 + int is_available;
1968 + struct child_process child = CHILD_PROCESS_INIT;
1969 +
1970 + if (get_schedule_cmd(&cmd, &is_available))
1971 + return is_available;
1972 +
1973 + strvec_split(&child.args, cmd);
1974 + strvec_push(&child.args, "-l");
1975 + child.no_stdin = 1;
1976 + child.no_stdout = 1;
1977 + child.no_stderr = 1;
1978 + child.silent_exec_failure = 1;
1979 +
1980 + if (start_command(&child))
1981 + return 0;
1982 + /* Ignore exit code, as an empty crontab will return error. */
1983 + finish_command(&child);
1984 + return 1;
1985 }
1986
1987 #define BEGIN_LINE "# BEGIN GIT MAINTENANCE SCHEDULE"
1988 #define END_LINE "# END GIT MAINTENANCE SCHEDULE"
1989
1863 -static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)
1990 +static int crontab_update_schedule(int run_maintenance, int fd)
1991 {
1992 + const char *cmd = "crontab";
1993 int result = 0;
1994 int in_old_region = 0;
1995 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)
1997 FILE *cron_list, *cron_in;
1998 struct strbuf line = STRBUF_INIT;
1999
2000 + get_schedule_cmd(&cmd, NULL);
2001 strvec_split(&crontab_list.args, cmd);
2002 strvec_push(&crontab_list.args, "-l");
2003 crontab_list.in = -1;
@@ -1945,66 +2074,160 @@ done_editing:
2074 return result;
2075 }
2076
2077 +enum scheduler {
2078 + SCHEDULER_INVALID = -1,
2079 + SCHEDULER_AUTO,
2080 + SCHEDULER_CRON,
2081 + SCHEDULER_LAUNCHCTL,
2082 + SCHEDULER_SCHTASKS,
2083 +};
2084 +
2085 +static const struct {
2086 + const char *name;
2087 + int (*is_available)(void);
2088 + int (*update_schedule)(int run_maintenance, int fd);
2089 +} scheduler_fn[] = {
2090 + [SCHEDULER_CRON] = {
2091 + .name = "crontab",
2092 + .is_available = is_crontab_available,
2093 + .update_schedule = crontab_update_schedule,
2094 + },
2095 + [SCHEDULER_LAUNCHCTL] = {
2096 + .name = "launchctl",
2097 + .is_available = is_launchctl_available,
2098 + .update_schedule = launchctl_update_schedule,
2099 + },
2100 + [SCHEDULER_SCHTASKS] = {
2101 + .name = "schtasks",
2102 + .is_available = is_schtasks_available,
2103 + .update_schedule = schtasks_update_schedule,
2104 + },
2105 +};
2106 +
2107 +static enum scheduler parse_scheduler(const char *value)
2108 +{
2109 + if (!value)
2110 + return SCHEDULER_INVALID;
2111 + else if (!strcasecmp(value, "auto"))
2112 + return SCHEDULER_AUTO;
2113 + else if (!strcasecmp(value, "cron") || !strcasecmp(value, "crontab"))
2114 + return SCHEDULER_CRON;
2115 + else if (!strcasecmp(value, "launchctl"))
2116 + return SCHEDULER_LAUNCHCTL;
2117 + else if (!strcasecmp(value, "schtasks"))
2118 + return SCHEDULER_SCHTASKS;
2119 + else
2120 + return SCHEDULER_INVALID;
2121 +}
2122 +
2123 +static int maintenance_opt_scheduler(const struct option *opt, const char *arg,
2124 + int unset)
2125 +{
2126 + enum scheduler *scheduler = opt->value;
2127 +
2128 + BUG_ON_OPT_NEG(unset);
2129 +
2130 + *scheduler = parse_scheduler(arg);
2131 + if (*scheduler == SCHEDULER_INVALID)
2132 + return error(_("unrecognized --scheduler argument '%s'"), arg);
2133 + return 0;
2134 +}
2135 +
2136 +struct maintenance_start_opts {
2137 + enum scheduler scheduler;
2138 +};
2139 +
2140 +static enum scheduler resolve_scheduler(enum scheduler scheduler)
2141 +{
2142 + if (scheduler != SCHEDULER_AUTO)
2143 + return scheduler;
2144 +
2145 #if defined(__APPLE__)
1949 -static const char platform_scheduler[] = "launchctl";
2146 + return SCHEDULER_LAUNCHCTL;
2147 +
2148 #elif defined(GIT_WINDOWS_NATIVE)
1951 -static const char platform_scheduler[] = "schtasks";
2149 + return SCHEDULER_SCHTASKS;
2150 +
2151 #else
1953 -static const char platform_scheduler[] = "crontab";
2152 + return SCHEDULER_CRON;
2153 #endif
2154 +}
2155
1956 -static int update_background_schedule(int enable)
2156 +static void validate_scheduler(enum scheduler scheduler)
2157 {
1958 - int result;
1959 - const char *scheduler = platform_scheduler;
1960 - const char *cmd = scheduler;
1961 - char *testing;
2158 + if (scheduler == SCHEDULER_INVALID)
2159 + BUG("invalid scheduler");
2160 + if (scheduler == SCHEDULER_AUTO)
2161 + BUG("resolve_scheduler should have been called before");
2162 +
2163 + if (!scheduler_fn[scheduler].is_available())
2164 + die(_("%s scheduler is not available"),
2165 + scheduler_fn[scheduler].name);
2166 +}
2167 +
2168 +static int update_background_schedule(const struct maintenance_start_opts *opts,
2169 + int enable)
2170 +{
2171 + unsigned int i;
2172 + int result = 0;
2173 struct lock_file lk;
2174 char *lock_path = xstrfmt("%s/schedule", the_repository->objects->odb->path);
2175
1965 - testing = xstrdup_or_null(getenv("GIT_TEST_MAINT_SCHEDULER"));
1966 - if (testing) {
1967 - char *sep = strchr(testing, ':');
1968 - if (!sep)
1969 - die("GIT_TEST_MAINT_SCHEDULER unparseable: %s", testing);
1970 - *sep = '\0';
1971 - scheduler = testing;
1972 - cmd = sep + 1;
2176 + if (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {
2177 + free(lock_path);
2178 + return error(_("another process is scheduling background maintenance"));
2179 }
2180
1975 - if (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {
1976 - result = error(_("another process is scheduling background maintenance"));
1977 - goto cleanup;
2181 + for (i = 1; i < ARRAY_SIZE(scheduler_fn); i++) {
2182 + if (enable && opts->scheduler == i)
2183 + continue;
2184 + if (!scheduler_fn[i].is_available())
2185 + continue;
2186 + scheduler_fn[i].update_schedule(0, get_lock_file_fd(&lk));
2187 }
2188
1980 - if (!strcmp(scheduler, "launchctl"))
1981 - result = launchctl_update_schedule(enable, get_lock_file_fd(&lk), cmd);
1982 - else if (!strcmp(scheduler, "schtasks"))
1983 - result = schtasks_update_schedule(enable, get_lock_file_fd(&lk), cmd);
1984 - else if (!strcmp(scheduler, "crontab"))
1985 - result = crontab_update_schedule(enable, get_lock_file_fd(&lk), cmd);
1986 - else
1987 - die("unknown background scheduler: %s", scheduler);
2189 + if (enable)
2190 + result = scheduler_fn[opts->scheduler].update_schedule(
2191 + 1, get_lock_file_fd(&lk));
2192
2193 rollback_lock_file(&lk);
2194
1991 -cleanup:
2195 free(lock_path);
1993 - free(testing);
2196 return result;
2197 }
2198
1997 -static int maintenance_start(void)
2199 +static const char *const builtin_maintenance_start_usage[] = {
2200 + N_("git maintenance start [--scheduler=<scheduler>]"),
2201 + NULL
2202 +};
2203 +
2204 +static int maintenance_start(int argc, const char **argv, const char *prefix)
2205 {
2206 + struct maintenance_start_opts opts = { 0 };
2207 + struct option options[] = {
2208 + OPT_CALLBACK_F(
2209 + 0, "scheduler", &opts.scheduler, N_("scheduler"),
2210 + N_("scheduler to trigger git maintenance run"),
2211 + PARSE_OPT_NONEG, maintenance_opt_scheduler),
2212 + OPT_END()
2213 + };
2214 +
2215 + argc = parse_options(argc, argv, prefix, options,
2216 + builtin_maintenance_start_usage, 0);
2217 + if (argc)
2218 + usage_with_options(builtin_maintenance_start_usage, options);
2219 +
2220 + opts.scheduler = resolve_scheduler(opts.scheduler);
2221 + validate_scheduler(opts.scheduler);
2222 +
2223 if (maintenance_register())
2224 warning(_("failed to add repo to global config"));
2001 -
2002 - return update_background_schedule(1);
2225 + return update_background_schedule(&opts, 1);
2226 }
2227
2228 static int maintenance_stop(void)
2229 {
2007 - return update_background_schedule(0);
2230 + return update_background_schedule(NULL, 0);
2231 }
2232
2233 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)
2241 if (!strcmp(argv[1], "run"))
2242 return maintenance_run(argc - 1, argv + 1, prefix);
2243 if (!strcmp(argv[1], "start"))
2021 - return maintenance_start();
2244 + return maintenance_start(argc - 1, argv + 1, prefix);
2245 if (!strcmp(argv[1], "stop"))
2246 return maintenance_stop();
2247 if (!strcmp(argv[1], "register"))
t/t7900-maintenance.sh
+51 -4
@@ -494,8 +494,21 @@ test_expect_success !MINGW 'register and unregister with regex metacharacters' '
494 maintenance.repo "$(pwd)/$META"
495 '
496
497 +test_expect_success 'start --scheduler=<scheduler>' '
498 + test_expect_code 129 git maintenance start --scheduler=foo 2>err &&
499 + test_i18ngrep "unrecognized --scheduler argument" err &&
500 +
501 + test_expect_code 129 git maintenance start --no-scheduler 2>err &&
502 + test_i18ngrep "unknown option" err &&
503 +
504 + test_expect_code 128 \
505 + env GIT_TEST_MAINT_SCHEDULER="launchctl:true,schtasks:true" \
506 + git maintenance start --scheduler=crontab 2>err &&
507 + test_i18ngrep "fatal: crontab scheduler is not available" err
508 +'
509 +
510 test_expect_success 'start from empty cron table' '
498 - GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab cron.txt" git maintenance start &&
511 + GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab cron.txt" git maintenance start --scheduler=crontab &&
512
513 # start registers the repo
514 git config --get --global --fixed-value maintenance.repo "$(pwd)" &&
@@ -518,7 +531,7 @@ test_expect_success 'stop from existing schedule' '
531
532 test_expect_success 'start preserves existing schedule' '
533 echo "Important information!" >cron.txt &&
521 - GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab cron.txt" git maintenance start &&
534 + GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab cron.txt" git maintenance start --scheduler=crontab &&
535 grep "Important information!" cron.txt
536 '
537
@@ -547,7 +560,7 @@ test_expect_success 'start and stop macOS maintenance' '
560 EOF
561
562 rm -f args &&
550 - GIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start &&
563 + GIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start --scheduler=launchctl &&
564
565 # start registers the repo
566 git config --get --global --fixed-value maintenance.repo "$(pwd)" &&
@@ -598,7 +611,7 @@ test_expect_success 'start and stop Windows maintenance' '
611 EOF
612
613 rm -f args &&
601 - GIT_TEST_MAINT_SCHEDULER="schtasks:./print-args" git maintenance start &&
614 + GIT_TEST_MAINT_SCHEDULER="schtasks:./print-args" git maintenance start --scheduler=schtasks &&
615
616 # start registers the repo
617 git config --get --global --fixed-value maintenance.repo "$(pwd)" &&
@@ -621,6 +634,40 @@ test_expect_success 'start and stop Windows maintenance' '
634 test_cmp expect args
635 '
636
637 +test_expect_success 'start and stop when several schedulers are available' '
638 + write_script print-args <<-\EOF &&
639 + printf "%s\n" "$*" | sed "s:gui/[0-9][0-9]*:gui/[UID]:; s:\(schtasks /create .* /xml\).*:\1:;" >>args
640 + EOF
641 +
642 + rm -f args &&
643 + GIT_TEST_MAINT_SCHEDULER="launchctl:./print-args launchctl,schtasks:./print-args schtasks" git maintenance start --scheduler=launchctl &&
644 + printf "schtasks /delete /tn Git Maintenance (%s) /f\n" \
645 + hourly daily weekly >expect &&
646 + for frequency in hourly daily weekly
647 + do
648 + PLIST="$pfx/Library/LaunchAgents/org.git-scm.git.$frequency.plist" &&
649 + echo "launchctl bootout gui/[UID] $PLIST" >>expect &&
650 + echo "launchctl bootstrap gui/[UID] $PLIST" >>expect || return 1
651 + done &&
652 + test_cmp expect args &&
653 +
654 + rm -f args &&
655 + GIT_TEST_MAINT_SCHEDULER="launchctl:./print-args launchctl,schtasks:./print-args schtasks" git maintenance start --scheduler=schtasks &&
656 + printf "launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\n" \
657 + hourly daily weekly >expect &&
658 + printf "schtasks /create /tn Git Maintenance (%s) /f /xml\n" \
659 + hourly daily weekly >>expect &&
660 + test_cmp expect args &&
661 +
662 + rm -f args &&
663 + GIT_TEST_MAINT_SCHEDULER="launchctl:./print-args launchctl,schtasks:./print-args schtasks" git maintenance stop &&
664 + printf "launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\n" \
665 + hourly daily weekly >expect &&
666 + printf "schtasks /delete /tn Git Maintenance (%s) /f\n" \
667 + hourly daily weekly >>expect &&
668 + test_cmp expect args
669 +'
670 +
671 test_expect_success 'register preserves existing strategy' '
672 git config maintenance.strategy none &&
673 git maintenance register &&