{"thread":{"id":"55603","subject":"[PATCH] maintenance: use systemd timers on Linux","startedAt":"2021-05-01T15:00:04Z","lastAt":"2021-09-27T21:44:49Z","messageCount":138,"participants":["Lénaïc Huard","brian m. carlson","Bagas Sanjaya","Eric Sunshine","Phillip Wood","Derrick Stolee","Ævar Arnfjörð Bjarmason","Đoàn Trần Công Danh","Junio C Hamano","Martin Ågren","Felipe Contreras","Johannes Schindelin","Jeff King","Phillip Susi","Ramsay Jones"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"423406","messageId":"20210501145220.2082670-1-lenaic@lhuard.fr","threadId":"55603","inReplyTo":null,"subject":"[PATCH] maintenance: use systemd timers on Linux","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-05-01T14:52:20Z","receivedAt":"2021-05-01T15:00:04Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"The existing mechanism for scheduling background maintenance is done\nthrough cron. On Linux systems managed by systemd, systemd provides an\nalternative to schedule recurring tasks: systemd timers.\n\nThe main motivations to implement systemd timers in addition to cron\nare:\n* cron is optional and Linux systems running systemd might not have it\n  installed.\n* The execution of `crontab -l` can tell us if cron is installed but not\n  if the daemon is actually running.\n* With systemd, each service is run in its own cgroup and its logs are\n  tagged by the service inside journald. With cron, all scheduled tasks\n  are running in the cron daemon cgroup and all the logs of the\n  user-scheduled tasks are pretended to belong to the system cron\n  service.\n  Concretely, a user that doesn’t have access to the system logs won’t\n  have access to the log of its own tasks scheduled by cron whereas he\n  will have access to the log of its own tasks scheduled by systemd\n  timer.\n\nIn order to schedule git maintenance, we need two unit template files:\n* ~/.config/systemd/user/git-maintenance@.service\n  to define the command to be started by systemd and\n* ~/.config/systemd/user/git-maintenance@.timer\n  to define the schedule at which the command should be run.\n\nThose units are templates that are parametrized by the frequency.\n\nBased on those templates, 3 timers are started:\n* git-maintenance@hourly.timer\n* git-maintenance@daily.timer\n* git-maintenance@weekly.timer\n\nThe command launched by those three timers are the same than with the\nother scheduling methods:\n\ngit for-each-repo --config=maintenance.repo maintenance run\n--schedule=%i\n\nwith the full path for git to ensure that the version of git launched\nfor the scheduled maintenance is the same as the one used to run\n`maintenance start`.\n\nThe timer unit contains `Persistent=true` so that, if the computer is\npowered down when a maintenance task should run, the task will be run\nwhen the computer is back powered on.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n Documentation/git-maintenance.txt |  49 ++++++++\n builtin/gc.c                      | 188 +++++++++++++++++++++++++++++-\n t/t7900-maintenance.sh            |  51 ++++++++\n 3 files changed, 287 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\nindex 80ddd33ceb..30443b417a 100644\n--- a/Documentation/git-maintenance.txt\n+++ b/Documentation/git-maintenance.txt\n@@ -279,6 +279,55 @@ schedule to ensure you are executing the correct binaries in your\n schedule.\n \n \n+BACKGROUND MAINTENANCE ON LINUX SYSTEMD SYSTEMD\n+-----------------------------------------------\n+\n+While Linux supports `cron`, depending on the distribution, `cron` may\n+be an optional package not necessarily installed. On modern Linux\n+distributions, systemd timers are superseding it.\n+\n+If user systemd timers are available, they will be used as a replacement\n+of `cron`.\n+\n+In this case, `git maintenance start` will create user systemd timer units\n+and start the timers. The current list of user-scheduled tasks can be found\n+by running `systemctl --user list-timers`. The timers written by `git\n+maintenance start` are similar to this:\n+\n+-----------------------------------------------------------------------\n+$ systemctl --user list-timers\n+NEXT                         LEFT          LAST                         PASSED     UNIT                         ACTIVATES\n+Thu 2021-04-29 19:00:00 CEST 42min left    Thu 2021-04-29 18:00:11 CEST 17min ago  git-maintenance@hourly.timer git-maintenance@hourly.service\n+Fri 2021-04-30 00:00:00 CEST 5h 42min left Thu 2021-04-29 00:00:11 CEST 18h ago    git-maintenance@daily.timer  git-maintenance@daily.service\n+Mon 2021-05-03 00:00:00 CEST 3 days left   Mon 2021-04-26 00:00:11 CEST 3 days ago git-maintenance@weekly.timer git-maintenance@weekly.service\n+\n+3 timers listed.\n+Pass --all to see loaded but inactive timers, too.\n+-----------------------------------------------------------------------\n+\n+One timer is registered for each `--schedule=<frequency>` option.\n+\n+The definition of the systemd units can be inspected in the following files:\n+\n+-----------------------------------------------------------------------\n+~/.config/systemd/user/git-maintenance@.timer\n+~/.config/systemd/user/git-maintenance@.service\n+~/.config/systemd/user/timers.target.wants/git-maintenance@hourly.timer\n+~/.config/systemd/user/timers.target.wants/git-maintenance@daily.timer\n+~/.config/systemd/user/timers.target.wants/git-maintenance@weekly.timer\n+-----------------------------------------------------------------------\n+\n+`git maintenance start` will overwrite these files and start the timer\n+again with `systemctl --user`, so any customization should be done by\n+creating a drop-in file\n+`~/.config/systemd/user/git-maintenance@.service.d/*.conf`.\n+\n+`git maintenance stop` will stop the user systemd timers and delete\n+the above mentioned files.\n+\n+For more details, see systemd.timer(5)\n+\n+\n BACKGROUND MAINTENANCE ON MACOS SYSTEMS\n ---------------------------------------\n \ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex ef7226d7bc..913fcfc882 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1872,6 +1872,25 @@ static int schtasks_update_schedule(int run_maintenance, int fd, const char *cmd\n \t\treturn schtasks_remove_tasks(cmd);\n }\n \n+static int is_crontab_available(const char *cmd)\n+{\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\n+\tstrvec_split(&child.args, cmd);\n+\tstrvec_push(&child.args, \"-l\");\n+\tchild.no_stdin = 1;\n+\tchild.no_stdout = 1;\n+\tchild.no_stderr = 1;\n+\tchild.silent_exec_failure = 1;\n+\n+\tif (start_command(&child))\n+\t\treturn 0;\n+\t/* Ignore exit code, as an empty crontab will return error. */\n+\tfinish_command(&child);\n+\n+\treturn 1;\n+}\n+\n #define BEGIN_LINE \"# BEGIN GIT MAINTENANCE SCHEDULE\"\n #define END_LINE \"# END GIT MAINTENANCE SCHEDULE\"\n \n@@ -1959,10 +1978,164 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n \treturn result;\n }\n \n+static int is_systemd_timer_available(const char *cmd)\n+{\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\n+\tstrvec_split(&child.args, cmd);\n+\tstrvec_pushl(&child.args, \"--user\", \"list-timers\", NULL);\n+\tchild.no_stdin = 1;\n+\tchild.no_stdout = 1;\n+\tchild.no_stderr = 1;\n+\tchild.silent_exec_failure = 1;\n+\n+\tif (start_command(&child))\n+\t\treturn 0;\n+\tif (finish_command(&child))\n+\t\treturn 0;\n+\n+\treturn 1;\n+}\n+\n+static char *systemd_timer_timer_filename()\n+{\n+\tconst char *filename = \"~/.config/systemd/user/git-maintenance@.timer\";\n+\tchar *expanded = expand_user_path(filename, 0);\n+\tif (!expanded)\n+\t\tdie(_(\"failed to expand path '%s'\"), filename);\n+\n+\treturn expanded;\n+}\n+\n+static char *systemd_timer_service_filename()\n+{\n+\tconst char *filename =\n+\t\t\"~/.config/systemd/user/git-maintenance@.service\";\n+\tchar *expanded = expand_user_path(filename, 0);\n+\tif (!expanded)\n+\t\tdie(_(\"failed to expand path '%s'\"), filename);\n+\n+\treturn expanded;\n+}\n+\n+static int systemd_timer_enable_unit(int enable,\n+\t\t\t\t     enum schedule_priority schedule,\n+\t\t\t\t     const char *cmd)\n+{\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\tconst char *frequency = get_frequency(schedule);\n+\n+\tstrvec_split(&child.args, cmd);\n+\tstrvec_push(&child.args, \"--user\");\n+\tif (enable)\n+\t\tstrvec_push(&child.args, \"enable\");\n+\telse\n+\t\tstrvec_push(&child.args, \"disable\");\n+\tstrvec_push(&child.args, \"--now\");\n+\tstrvec_pushf(&child.args, \"git-maintenance@%s.timer\", frequency);\n+\n+\tif (start_command(&child))\n+\t\tdie(_(\"failed to run systemctl\"));\n+\treturn finish_command(&child);\n+}\n+\n+static int systemd_timer_delete_unit_templates()\n+{\n+\tchar *filename = systemd_timer_timer_filename();\n+\tunlink(filename);\n+\tfree(filename);\n+\n+\tfilename = systemd_timer_service_filename();\n+\tunlink(filename);\n+\tfree(filename);\n+\n+\treturn 0;\n+}\n+\n+static int systemd_timer_delete_units(const char *cmd)\n+{\n+\treturn systemd_timer_enable_unit(0, SCHEDULE_HOURLY, cmd) ||\n+\t       systemd_timer_enable_unit(0, SCHEDULE_DAILY, cmd) ||\n+\t       systemd_timer_enable_unit(0, SCHEDULE_WEEKLY, cmd) ||\n+\t       systemd_timer_delete_unit_templates();\n+}\n+\n+static int systemd_timer_write_unit_templates(const char *exec_path)\n+{\n+\tchar *filename;\n+\tFILE *file;\n+\tconst char *unit;\n+\n+\tfilename = systemd_timer_timer_filename();\n+\tif (safe_create_leading_directories(filename))\n+\t\tdie(_(\"failed to create directories for '%s'\"), filename);\n+\tfile = xfopen(filename, \"w\");\n+\tfree(filename);\n+\n+\tunit = \"[Unit]\\n\"\n+\t       \"Description=Optimize Git repositories data\\n\"\n+\t       \"\\n\"\n+\t       \"[Timer]\\n\"\n+\t       \"OnCalendar=%i\\n\"\n+\t       \"Persistent=true\\n\"\n+\t       \"\\n\"\n+\t       \"[Install]\\n\"\n+\t       \"WantedBy=timers.target\\n\";\n+\tfputs(unit, file);\n+\tfclose(file);\n+\n+\tfilename = systemd_timer_service_filename();\n+\tif (safe_create_leading_directories(filename))\n+\t\tdie(_(\"failed to create directories for '%s'\"), filename);\n+\tfile = xfopen(filename, \"w\");\n+\tfree(filename);\n+\n+\tunit = \"[Unit]\\n\"\n+\t       \"Description=Optimize Git repositories data\\n\"\n+\t       \"\\n\"\n+\t       \"[Service]\\n\"\n+\t       \"Type=oneshot\\n\"\n+\t       \"ExecStart=\\\"%1$s/git\\\" --exec-path=\\\"%1$s\\\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\\n\"\n+\t       \"LockPersonality=yes\\n\"\n+\t       \"MemoryDenyWriteExecute=yes\\n\"\n+\t       \"NoNewPrivileges=yes\\n\"\n+\t       \"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\\n\"\n+\t       \"RestrictNamespaces=yes\\n\"\n+\t       \"RestrictRealtime=yes\\n\"\n+\t       \"RestrictSUIDSGID=yes\\n\"\n+\t       \"SystemCallArchitectures=native\\n\"\n+\t       \"SystemCallFilter=@system-service\\n\";\n+\tfprintf(file, unit, exec_path);\n+\tfclose(file);\n+\n+\treturn 0;\n+}\n+\n+static int systemd_timer_setup_units(const char *cmd)\n+{\n+\tconst char *exec_path = git_exec_path();\n+\n+\treturn systemd_timer_write_unit_templates(exec_path) ||\n+\t       systemd_timer_enable_unit(1, SCHEDULE_HOURLY, cmd) ||\n+\t       systemd_timer_enable_unit(1, SCHEDULE_DAILY, cmd) ||\n+\t       systemd_timer_enable_unit(1, SCHEDULE_WEEKLY, cmd);\n+}\n+\n+static int systemd_timer_update_schedule(int run_maintenance, int fd,\n+\t\t\t\t\t const char *cmd)\n+{\n+\tif (run_maintenance)\n+\t\treturn systemd_timer_setup_units(cmd);\n+\telse\n+\t\treturn systemd_timer_delete_units(cmd);\n+}\n+\n #if defined(__APPLE__)\n static const char platform_scheduler[] = \"launchctl\";\n #elif defined(GIT_WINDOWS_NATIVE)\n static const char platform_scheduler[] = \"schtasks\";\n+#elif defined(__linux__)\n+static const char platform_scheduler[] = \"crontab_or_systemctl\";\n #else\n static const char platform_scheduler[] = \"crontab\";\n #endif\n@@ -1986,6 +2159,15 @@ static int update_background_schedule(int enable)\n \t\tcmd = sep + 1;\n \t}\n \n+\tif (!strcmp(scheduler, \"crontab_or_systemctl\")) {\n+\t\tif (is_systemd_timer_available(\"systemctl\"))\n+\t\t\tscheduler = cmd = \"systemctl\";\n+\t\telse if (is_crontab_available(\"crontab\"))\n+\t\t\tscheduler = cmd = \"crontab\";\n+\t\telse\n+\t\t\tdie(_(\"Neither systemd timers nor crontab are available\"));\n+\t}\n+\n \tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0)\n \t\treturn error(_(\"another process is scheduling background maintenance\"));\n \n@@ -1995,10 +2177,14 @@ static int update_background_schedule(int enable)\n \t\tresult = schtasks_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n \telse if (!strcmp(scheduler, \"crontab\"))\n \t\tresult = crontab_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n+\telse if (!strcmp(scheduler, \"systemctl\"))\n+\t\tresult = systemd_timer_update_schedule(\n+\t\t\tenable, get_lock_file_fd(&lk), cmd);\n \telse\n-\t\tdie(\"unknown background scheduler: %s\", scheduler);\n+\t\tdie(_(\"unknown background scheduler: %s\"), scheduler);\n \n \trollback_lock_file(&lk);\n+\tfree(lock_path);\n \tfree(testing);\n \treturn result;\n }\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 2412d8c5c0..dd281789f4 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -20,6 +20,20 @@ test_xmllint () {\n \tfi\n }\n \n+test_lazy_prereq SYSTEMD_ANALYZE '\n+\tsystemd-analyze --help >out &&\n+\tgrep -w verify out\n+'\n+\n+test_systemd_analyze_verify () {\n+\tif test_have_prereq SYSTEMD_ANALYZE\n+\tthen\n+\t\tsystemd-analyze verify \"$@\"\n+\telse\n+\t\ttrue\n+\tfi\n+}\n+\n test_expect_success 'help text' '\n \ttest_expect_code 129 git maintenance -h 2>err &&\n \ttest_i18ngrep \"usage: git maintenance <subcommand>\" err &&\n@@ -615,6 +629,43 @@ test_expect_success 'start and stop Windows maintenance' '\n \ttest_cmp expect args\n '\n \n+test_expect_success 'start and stop Linux/systemd maintenance' '\n+\twrite_script print-args <<-\\EOF &&\n+\techo $* >>args\n+\tEOF\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args\" git maintenance start &&\n+\n+\t# start registers the repo\n+\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n+\n+\ttest_systemd_analyze_verify \"$HOME/.config/systemd/user/git-maintenance@.service\" &&\n+\n+\trm -f expect &&\n+\tfor frequency in hourly daily weekly\n+\tdo\n+\t\techo \"--user enable --now git-maintenance@${frequency}.timer\" >>expect || return 1\n+\tdone &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args\" git maintenance stop &&\n+\n+\t# stop does not unregister the repo\n+\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n+\n+\ttest_path_is_missing \"$HOME/.config/systemd/user/git-maintenance@.timer\" &&\n+\ttest_path_is_missing \"$HOME/.config/systemd/user/git-maintenance@.service\" &&\n+\n+\trm -f expect &&\n+\tfor frequency in hourly daily weekly\n+\tdo\n+\t\techo \"--user disable --now git-maintenance@${frequency}.timer\" >>expect || return 1\n+\tdone &&\n+\ttest_cmp expect args\n+'\n+\n test_expect_success 'register preserves existing strategy' '\n \tgit config maintenance.strategy none &&\n \tgit maintenance register &&\n-- \n2.31.1\n\n"},{"id":"423425","messageId":"YI2zzKMJ1eDH3U/S@camp.crustytoothpaste.net","threadId":"55603","inReplyTo":"20210501145220.2082670-1-lenaic@lhuard.fr","subject":"Re: [PATCH] maintenance: use systemd timers on Linux","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2021-05-01T20:02:20Z","receivedAt":"2021-05-01T20:02:27Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2021-05-01 at 14:52:20, Lénaïc Huard wrote:\n> The existing mechanism for scheduling background maintenance is done\n> through cron. On Linux systems managed by systemd, systemd provides an\n> alternative to schedule recurring tasks: systemd timers.\n> \n> The main motivations to implement systemd timers in addition to cron\n> are:\n> * cron is optional and Linux systems running systemd might not have it\n>   installed.\n> * The execution of `crontab -l` can tell us if cron is installed but not\n>   if the daemon is actually running.\n> * With systemd, each service is run in its own cgroup and its logs are\n>   tagged by the service inside journald. With cron, all scheduled tasks\n>   are running in the cron daemon cgroup and all the logs of the\n>   user-scheduled tasks are pretended to belong to the system cron\n>   service.\n>   Concretely, a user that doesn’t have access to the system logs won’t\n>   have access to the log of its own tasks scheduled by cron whereas he\n>   will have access to the log of its own tasks scheduled by systemd\n>   timer.\n\nI would prefer to see this as a configurable option.  I have systemd\ninstalled (because it's not really optional to have a functional desktop\non Linux) but I want to restrict it to starting and stopping services,\nnot performing the tasks of cron.  cron is portable across a wide\nvariety of systems, including Linux variants (and WSL) that don't use\nsystemd, and I prefer to use more standard tooling when possible.\n-- \nbrian m. carlson (he/him or they/them)\nHouston, Texas, US\n"},{"id":"423432","messageId":"81e77586-8e9a-44b8-bee6-94ecf1b60d12@gmail.com","threadId":"55603","inReplyTo":"20210501145220.2082670-1-lenaic@lhuard.fr","subject":"Re: [PATCH] maintenance: use systemd timers on Linux","fromName":"Bagas Sanjaya","fromEmail":"bagasdotme@gmail.com","sentAt":"2021-05-02T05:28:31Z","receivedAt":"2021-05-02T05:28:37Z","isPatch":true,"sender":{"key":"bagasdotme@gmail.com","avatar":"https://avatars.githubusercontent.com/u/40219486?v=4"},"body":"On 01/05/21 21.52, Lénaïc Huard wrote:\n> The existing mechanism for scheduling background maintenance is done\n> through cron. On Linux systems managed by systemd, systemd provides an\n> alternative to schedule recurring tasks: systemd timers.\n> \n> The main motivations to implement systemd timers in addition to cron\n> are:\n> * cron is optional and Linux systems running systemd might not have it\n>    installed.\n\nSupposed that I have Linux box with systemd and classical cron. Should\nsystemd timers be preferred over cron?\n\n> * The execution of `crontab -l` can tell us if cron is installed but not\n>    if the daemon is actually running.\n> * With systemd, each service is run in its own cgroup and its logs are\n>    tagged by the service inside journald. With cron, all scheduled tasks\n>    are running in the cron daemon cgroup and all the logs of the\n>    user-scheduled tasks are pretended to belong to the system cron\n>    service.\n>    Concretely, a user that doesn’t have access to the system logs won’t\n>    have access to the log of its own tasks scheduled by cron whereas he\n>    will have access to the log of its own tasks scheduled by systemd\n>    timer.\n> \n> In order to schedule git maintenance, we need two unit template files:\n> * ~/.config/systemd/user/git-maintenance@.service\n>    to define the command to be started by systemd and\n> * ~/.config/systemd/user/git-maintenance@.timer\n>    to define the schedule at which the command should be run.\n> \n> Those units are templates that are parametrized by the frequency.\n> \n> Based on those templates, 3 timers are started:\n> * git-maintenance@hourly.timer\n> * git-maintenance@daily.timer\n> * git-maintenance@weekly.timer\n> \n> The command launched by those three timers are the same than with the\n> other scheduling methods:\n> \n> git for-each-repo --config=maintenance.repo maintenance run\n> --schedule=%i\n> \n> with the full path for git to ensure that the version of git launched\n> for the scheduled maintenance is the same as the one used to run\n> `maintenance start`.\n> \n> The timer unit contains `Persistent=true` so that, if the computer is\n> powered down when a maintenance task should run, the task will be run\n> when the computer is back powered on.\n> \n> Signed-off-by: Lénaïc Huard <lenaic@lhuard.fr>\nNevertheless, because we are dealing with external dependency (systemd), it\nshould makes sense to enforce this dependency requirement when user choose to use\nsystemd timers so that users on non-systemd boxes (such as Gentoo with OpenRC)\ndon't see errors that forcing them to use systemd.\n\n-- \nAn old man doll... just what I always wanted! - Clara\n"},{"id":"423437","messageId":"CAPig+cQks0_nL1J4YUbEUjmWYLKrhuHX-f8PkWM2zFE4gybWMw@mail.gmail.com","threadId":"55603","inReplyTo":"20210501145220.2082670-1-lenaic@lhuard.fr","subject":"Re: [PATCH] maintenance: use systemd timers on Linux","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2021-05-02T06:45:32Z","receivedAt":"2021-05-02T06:45:45Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Sat, May 1, 2021 at 10:59 AM Lénaïc Huard <lenaic@lhuard.fr> wrote:\n> The existing mechanism for scheduling background maintenance is done\n> through cron. On Linux systems managed by systemd, systemd provides an\n> alternative to schedule recurring tasks: systemd timers.\n\nThanks for working on this. While `cron` has been the go-to standard\nfor decades, `systemd` is certainly widespread enough that it makes\nsense to support it, as well.\n\n> The main motivations to implement systemd timers in addition to cron\n> are:\n> * cron is optional and Linux systems running systemd might not have it\n>   installed.\n> * The execution of `crontab -l` can tell us if cron is installed but not\n>   if the daemon is actually running.\n> * With systemd, each service is run in its own cgroup and its logs are\n>   tagged by the service inside journald. With cron, all scheduled tasks\n>   are running in the cron daemon cgroup and all the logs of the\n>   user-scheduled tasks are pretended to belong to the system cron\n>   service.\n>   Concretely, a user that doesn’t have access to the system logs won’t\n>   have access to the log of its own tasks scheduled by cron whereas he\n>   will have access to the log of its own tasks scheduled by systemd\n>   timer.\n\nThe last point is somewhat compelling. A potential counterargument is\nthat `cron` does send email to the user by default if any output is\ngenerated by the cron job. However, it seems quite likely these days\nthat many systems either won't have local mail service enabled or the\nuser won't bother checking the local mailbox. It's a minor point, but\nif you re-roll it might make sense for the commit message to expand\nthe last point by saying that although `cron` attempts to send email,\nthat email may go unseen by the user.\n\n> In order to schedule git maintenance, we need two unit template files:\n> * ~/.config/systemd/user/git-maintenance@.service\n>   to define the command to be started by systemd and\n> * ~/.config/systemd/user/git-maintenance@.timer\n>   to define the schedule at which the command should be run.\n> [...]\n> The timer unit contains `Persistent=true` so that, if the computer is\n> powered down when a maintenance task should run, the task will be run\n> when the computer is back powered on.\n\nIt would be nice for the commit message to also give some high-level\ninformation about how git-maintenance chooses between `cron` and\n`systemd` and whether the user can influence that decision. (I know\nthe answer because I read the patch, but this is the sort of\ninformation which is good to have in the commit message; readers want\nto know why certain choices were made.)\n\nAlthough I avoid Linux distros with `systemd`, my knee-jerk reaction,\nlike brian's upthread, is that there should be some escape hatch or\ndirect mechanism to allow the user to choose between `systemd` and\n`cron`.\n\nThe patch itself is straightforward enough and nicely follows the\npattern established for already-implemented schedulers, so I don't\nhave a lot to say about it. I did leave a few comments below, most of\nwhich are subjective nits and minor observations, though there are two\nor three actionable items.\n\n> Signed-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n> ---\n> diff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\n> @@ -279,6 +279,55 @@ schedule to ensure you are executing the correct binaries in your\n> +BACKGROUND MAINTENANCE ON LINUX SYSTEMD SYSTEMD\n> +-----------------------------------------------\n\nIs there a reason for the duplicated \"SYSTEMD\" that I'm missing? I\nsuppose you probably mean \"SYSTEMD SYSTEMS\".\n\n> +In this case, `git maintenance start` will create user systemd timer units\n> +and start the timers. The current list of user-scheduled tasks can be found\n> +by running `systemctl --user list-timers`. The timers written by `git\n> +maintenance start` are similar to this:\n> +\n> +-----------------------------------------------------------------------\n> +$ systemctl --user list-timers\n> +NEXT                         LEFT          LAST                         PASSED     UNIT                         ACTIVATES\n> +Thu 2021-04-29 19:00:00 CEST 42min left    Thu 2021-04-29 18:00:11 CEST 17min ago  git-maintenance@hourly.timer git-maintenance@hourly.service\n> +Fri 2021-04-30 00:00:00 CEST 5h 42min left Thu 2021-04-29 00:00:11 CEST 18h ago    git-maintenance@daily.timer  git-maintenance@daily.service\n> +Mon 2021-05-03 00:00:00 CEST 3 days left   Mon 2021-04-26 00:00:11 CEST 3 days ago git-maintenance@weekly.timer git-maintenance@weekly.service\n> +\n> +3 timers listed.\n> +Pass --all to see loaded but inactive timers, too.\n> +-----------------------------------------------------------------------\n\nI suspect that the \"3 timers listed\" and \"Pass --all\" lines don't add\nvalue and can be dropped without hurting the example.\n\n> +`git maintenance start` will overwrite these files and start the timer\n> +again with `systemctl --user`, so any customization should be done by\n> +creating a drop-in file\n> +`~/.config/systemd/user/git-maintenance@.service.d/*.conf`.\n\nWill `systemd` users generally understand what filename to create in\nthe \"...@.service.d/\" directory, and will they know what to populate\nthe file with? (Genuine question; I've never dealt with that.)\n\n> diff --git a/builtin/gc.c b/builtin/gc.c\n> @@ -1872,6 +1872,25 @@ static int schtasks_update_schedule(int run_maintenance, int fd, const char *cmd\n> +static int is_crontab_available(const char *cmd)\n> +{\n> +       struct child_process child = CHILD_PROCESS_INIT;\n> +\n> +       strvec_split(&child.args, cmd);\n> +       strvec_push(&child.args, \"-l\");\n> +       child.no_stdin = 1;\n> +       child.no_stdout = 1;\n> +       child.no_stderr = 1;\n> +       child.silent_exec_failure = 1;\n> +\n> +       if (start_command(&child))\n> +               return 0;\n> +       /* Ignore exit code, as an empty crontab will return error. */\n> +       finish_command(&child);\n> +\n> +       return 1;\n> +}\n\nIgnoring the error from `crontab -l` is an already-established idiom\nin this file. Okay.\n\nNit: There doesn't seem to be a need for the blank line before `return\n1`, and other maintenance-related functions don't have such a blank\nline. The same comment about blank lines before `return` applies to\nother newly-added functions, as well. But it's subjective, and not\nnecessarily worth changing.\n\n> +static char *systemd_timer_timer_filename()\n> +{\n> +       const char *filename = \"~/.config/systemd/user/git-maintenance@.timer\";\n> +       char *expanded = expand_user_path(filename, 0);\n> +       if (!expanded)\n> +               die(_(\"failed to expand path '%s'\"), filename);\n> +\n> +       return expanded;\n> +}\n\nI was curious whether this would fail if `.config/systemd/user/`\ndidn't already exist, but looking at the implementation of\nexpand_user_path() , I see that it doesn't require the path to already\nexist if you pass 0 for the second argument as you do here. Okay.\n\n> +static char *systemd_timer_service_filename()\n> +{\n> +       const char *filename =\n> +               \"~/.config/systemd/user/git-maintenance@.service\";\n> +       char *expanded = expand_user_path(filename, 0);\n> +       if (!expanded)\n> +               die(_(\"failed to expand path '%s'\"), filename);\n> +\n> +       return expanded;\n> +}\n\nThe duplication of code between systemd_timer_timer_filename() and\nsystemd_timer_service_filename() is probably too minor to worry about.\nOkay.\n\n> +static int systemd_timer_enable_unit(int enable,\n> +                                    enum schedule_priority schedule,\n> +                                    const char *cmd)\n> +{\n> +       struct child_process child = CHILD_PROCESS_INIT;\n> +       const char *frequency = get_frequency(schedule);\n> +\n> +       strvec_split(&child.args, cmd);\n> +       strvec_push(&child.args, \"--user\");\n> +       if (enable)\n> +               strvec_push(&child.args, \"enable\");\n> +       else\n> +               strvec_push(&child.args, \"disable\");\n\nIt's subjective, but this might be more nicely expressed as:\n\n    strvec_push(&child.args, enable ? \"enable\" : \"disable\");\n\n> +       strvec_push(&child.args, \"--now\");\n> +       strvec_pushf(&child.args, \"git-maintenance@%s.timer\", frequency);\n> +\n> +       if (start_command(&child))\n> +               die(_(\"failed to run systemctl\"));\n> +       return finish_command(&child);\n> +}\n> +static int systemd_timer_write_unit_templates(const char *exec_path)\n> +{\n> +       unit = \"[Unit]\\n\"\n> +              \"Description=Optimize Git repositories data\\n\"\n> +              \"\\n\"\n> +              \"[Service]\\n\"\n> +              \"Type=oneshot\\n\"\n> +              \"ExecStart=\\\"%1$s/git\\\" --exec-path=\\\"%1$s\\\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\\n\"\n\nI see that it's in POSIX, but do we use this `%n$s` directive\nelsewhere in the Git source code? If not, I'd be cautious of\nintroducing it here. Maybe it's better to just use plain `%s` twice...\n\n> +              \"LockPersonality=yes\\n\"\n> +              \"MemoryDenyWriteExecute=yes\\n\"\n> +              \"NoNewPrivileges=yes\\n\"\n> +              \"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\\n\"\n> +              \"RestrictNamespaces=yes\\n\"\n> +              \"RestrictRealtime=yes\\n\"\n> +              \"RestrictSUIDSGID=yes\\n\"\n> +              \"SystemCallArchitectures=native\\n\"\n> +              \"SystemCallFilter=@system-service\\n\";\n> +       fprintf(file, unit, exec_path);\n\n... and then:\n\n    fprintf(file, unit, exec_path, exec_path);\n\n> +       fclose(file);\n> +\n> +       return 0;\n> +}\n> @@ -1986,6 +2159,15 @@ static int update_background_schedule(int enable)\n> +       if (!strcmp(scheduler, \"crontab_or_systemctl\")) {\n> +               if (is_systemd_timer_available(\"systemctl\"))\n> +                       scheduler = cmd = \"systemctl\";\n> +               else if (is_crontab_available(\"crontab\"))\n> +                       scheduler = cmd = \"crontab\";\n> +               else\n> +                       die(_(\"Neither systemd timers nor crontab are available\"));\n> +       }\n\nOther messages emitted by git-maintenance are entirely lowercase, so\ndowncasing \"Neither\" would be appropriate.\n\n> @@ -1995,10 +2177,14 @@ static int update_background_schedule(int enable)\n> -               die(\"unknown background scheduler: %s\", scheduler);\n> +               die(_(\"unknown background scheduler: %s\"), scheduler);\n\nThis change is unrelated to the rest of the patch. Normally, such a\n\"fix\" would be made as a separate patch. This one is somewhat minor,\nso perhaps it doesn't matter whether it's in this patch...\n\n>         rollback_lock_file(&lk);\n> +       free(lock_path);\n>         free(testing);\n>         return result;\n\n... however, this leak fix probably deserves its own patch. Or, at the\nvery least, mention these two fixes in this commit message.\n\n> diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\n> @@ -20,6 +20,20 @@ test_xmllint () {\n> +test_lazy_prereq SYSTEMD_ANALYZE '\n> +       systemd-analyze --help >out &&\n> +       grep -w verify out\n> +'\n\nUnportable use of `grep -w`. It's neither in POSIX nor understood by\nBSD-lineage `grep` (including macOS `grep`).\n"},{"id":"423439","messageId":"CAPig+cSwFwUbeyUZxs5mZ4GpFRqQ51C9xA1uD=VHQB==jkKj0Q@mail.gmail.com","threadId":"55603","inReplyTo":"81e77586-8e9a-44b8-bee6-94ecf1b60d12@gmail.com","subject":"Re: [PATCH] maintenance: use systemd timers on Linux","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2021-05-02T06:49:16Z","receivedAt":"2021-05-02T06:49:31Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Sun, May 2, 2021 at 1:28 AM Bagas Sanjaya <bagasdotme@gmail.com> wrote:\n> On 01/05/21 21.52, Lénaïc Huard wrote:\n> > The existing mechanism for scheduling background maintenance is done\n> > through cron. On Linux systems managed by systemd, systemd provides an\n> > alternative to schedule recurring tasks: systemd timers.\n> >\n> > The main motivations to implement systemd timers in addition to cron\n> > are:\n> > * cron is optional and Linux systems running systemd might not have it\n> >    installed.\n>\n> Supposed that I have Linux box with systemd and classical cron. Should\n> systemd timers be preferred over cron?\n\nThe implementation in this patch unconditionally prefers `systemd`\nover `cron`. Whether that's a good idea is subject to question (as\nboth brian and I mentioned in our reviews).\n\n> Nevertheless, because we are dealing with external dependency (systemd), it\n> should makes sense to enforce this dependency requirement when user choose to use\n> systemd timers so that users on non-systemd boxes (such as Gentoo with OpenRC)\n> don't see errors that forcing them to use systemd.\n\nIf you scan through the patch itself, you will find that it is careful\nto choose the appropriate scheduler and not to spit out errors when\none or the other scheduler is unavailable.\n"},{"id":"423445","messageId":"d27d4832-0dc0-ae01-cb2b-264c938b1649@gmail.com","threadId":"55603","inReplyTo":"20210501145220.2082670-1-lenaic@lhuard.fr","subject":"Re: [PATCH] maintenance: use systemd timers on Linux","fromName":"Bagas Sanjaya","fromEmail":"bagasdotme@gmail.com","sentAt":"2021-05-02T11:12:06Z","receivedAt":"2021-05-02T11:12:12Z","isPatch":true,"sender":{"key":"bagasdotme@gmail.com","avatar":"https://avatars.githubusercontent.com/u/40219486?v=4"},"body":"On 01/05/21 21.52, Lénaïc Huard wrote:\n> +BACKGROUND MAINTENANCE ON LINUX SYSTEMD SYSTEMD\n> +-----------------------------------------------\n> +\n\nsystemd was repeated twice above. It should be\n`BACKGROUND MAINTENANCE WITH SYSTEMD TIMERS`\n\n>   test_expect_success 'help text' '\n>   \ttest_expect_code 129 git maintenance -h 2>err &&\n\nWhy exit code 129 there?\n\n-- \nAn old man doll... just what I always wanted! - Clara\n"},{"id":"423447","messageId":"b1153c47-05cf-877c-587b-8205d8d32816@gmail.com","threadId":"55603","inReplyTo":"CAPig+cQks0_nL1J4YUbEUjmWYLKrhuHX-f8PkWM2zFE4gybWMw@mail.gmail.com","subject":"Re: [PATCH] maintenance: use systemd timers on Linux","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2021-05-02T14:10:05Z","receivedAt":"2021-05-02T14:10:58Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 02/05/2021 07:45, Eric Sunshine wrote:\n> On Sat, May 1, 2021 at 10:59 AM Lénaïc Huard <lenaic@lhuard.fr> wrote:\n>> The existing mechanism for scheduling background maintenance is done\n>> through cron. On Linux systems managed by systemd, systemd provides an\n>> alternative to schedule recurring tasks: systemd timers.\n> \n> Thanks for working on this. While `cron` has been the go-to standard\n> for decades, `systemd` is certainly widespread enough that it makes\n> sense to support it, as well.\n\nYes, thank you for working on this, it will be very useful to users like \nme who use a linux distribution that does not install a cron daemon by \ndefault but relies on systemd instead.\n\n>> The main motivations to implement systemd timers in addition to cron\n>> are:\n>> * cron is optional and Linux systems running systemd might not have it\n>>    installed.\n>> * The execution of `crontab -l` can tell us if cron is installed but not\n>>    if the daemon is actually running.\n\nCan we use systemctl to see if it is running (and enabled so we know it \nwill be restarted after a reboot)?\n\n>> * With systemd, each service is run in its own cgroup and its logs are\n>>    tagged by the service inside journald. With cron, all scheduled tasks\n>>    are running in the cron daemon cgroup and all the logs of the\n>>    user-scheduled tasks are pretended to belong to the system cron\n>>    service.\n>>    Concretely, a user that doesn’t have access to the system logs won’t\n>>    have access to the log of its own tasks scheduled by cron whereas he\n>>    will have access to the log of its own tasks scheduled by systemd\n>>    timer.\n> \n> The last point is somewhat compelling. A potential counterargument is\n> that `cron` does send email to the user by default if any output is\n> generated by the cron job. However, it seems quite likely these days\n> that many systems either won't have local mail service enabled or the\n> user won't bother checking the local mailbox. It's a minor point, but\n> if you re-roll it might make sense for the commit message to expand\n> the last point by saying that although `cron` attempts to send email,\n> that email may go unseen by the user.\n> \n>> In order to schedule git maintenance, we need two unit template files:\n>> * ~/.config/systemd/user/git-maintenance@.service\n>>    to define the command to be started by systemd and\n>> * ~/.config/systemd/user/git-maintenance@.timer\n>>    to define the schedule at which the command should be run.\n>> [...]\n>> The timer unit contains `Persistent=true` so that, if the computer is\n>> powered down when a maintenance task should run, the task will be run\n>> when the computer is back powered on.\n> \n> It would be nice for the commit message to also give some high-level\n> information about how git-maintenance chooses between `cron` and\n> `systemd` and whether the user can influence that decision. (I know\n> the answer because I read the patch, but this is the sort of\n> information which is good to have in the commit message; readers want\n> to know why certain choices were made.)\n> \n> Although I avoid Linux distros with `systemd`, my knee-jerk reaction,\n> like brian's upthread, is that there should be some escape hatch or\n> direct mechanism to allow the user to choose between `systemd` and\n> `cron`.\n\nI agree that if both are present the user should be able to choose one. \nI'm not sure what the default should be in that case - before I read the \ncommit message and Eric's comments I was inclined to say that if the \nuser has cron installed we should take that as a sign they preferred \ncron over systemd timers and use that. However, given the arguments \nabove about not knowing if cron is running and the user not necessarily \ngetting emails from cron or being able to read the logs than maybe \ndefaulting to systemd timers makes sense.\n\n> The patch itself is straightforward enough and nicely follows the\n> pattern established for already-implemented schedulers, so I don't\n> have a lot to say about it. I did leave a few comments below, most of\n> which are subjective nits and minor observations, though there are two\n> or three actionable items.\n> \n>> Signed-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n>> ---\n>> diff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\n>> @@ -279,6 +279,55 @@ schedule to ensure you are executing the correct binaries in your\n>> +BACKGROUND MAINTENANCE ON LINUX SYSTEMD SYSTEMD\n>> +-----------------------------------------------\n> \n> Is there a reason for the duplicated \"SYSTEMD\" that I'm missing? I\n> suppose you probably mean \"SYSTEMD SYSTEMS\".\n> \n>> +In this case, `git maintenance start` will create user systemd timer units\n>> +and start the timers. The current list of user-scheduled tasks can be found\n>> +by running `systemctl --user list-timers`. The timers written by `git\n>> +maintenance start` are similar to this:\n>> +\n>> +-----------------------------------------------------------------------\n>> +$ systemctl --user list-timers\n>> +NEXT                         LEFT          LAST                         PASSED     UNIT                         ACTIVATES\n>> +Thu 2021-04-29 19:00:00 CEST 42min left    Thu 2021-04-29 18:00:11 CEST 17min ago  git-maintenance@hourly.timer git-maintenance@hourly.service\n>> +Fri 2021-04-30 00:00:00 CEST 5h 42min left Thu 2021-04-29 00:00:11 CEST 18h ago    git-maintenance@daily.timer  git-maintenance@daily.service\n>> +Mon 2021-05-03 00:00:00 CEST 3 days left   Mon 2021-04-26 00:00:11 CEST 3 days ago git-maintenance@weekly.timer git-maintenance@weekly.service\n>> +\n>> +3 timers listed.\n>> +Pass --all to see loaded but inactive timers, too.\n>> +-----------------------------------------------------------------------\n> \n> I suspect that the \"3 timers listed\" and \"Pass --all\" lines don't add\n> value and can be dropped without hurting the example.\n\nIf the idea is to show the output of `systemctl --user list-timers` then \nI don't think we should be editing it. I also think having the column \nheaders helps as it shows what the fields are.\n\n>> +`git maintenance start` will overwrite these files and start the timer\n>> +again with `systemctl --user`, so any customization should be done by\n>> +creating a drop-in file\n>> +`~/.config/systemd/user/git-maintenance@.service.d/*.conf`.\n> \n> Will `systemd` users generally understand what filename to create in\n> the \"...@.service.d/\" directory, and will they know what to populate\n> the file with? (Genuine question; I've never dealt with that.)\n\nI think it would be helpful to explicitly mention the file names (I \ndon't think I could tell you what they are without reading the relevant \nsystemd man page)\n\n>> diff --git a/builtin/gc.c b/builtin/gc.c\n>> @@ -1872,6 +1872,25 @@ static int schtasks_update_schedule(int run_maintenance, int fd, const char *cmd\n>> +static int is_crontab_available(const char *cmd)\n>> +{\n>> +       struct child_process child = CHILD_PROCESS_INIT;\n>> +\n>> +       strvec_split(&child.args, cmd);\n>> +       strvec_push(&child.args, \"-l\");\n>> +       child.no_stdin = 1;\n>> +       child.no_stdout = 1;\n>> +       child.no_stderr = 1;\n>> +       child.silent_exec_failure = 1;\n>> +\n>> +       if (start_command(&child))\n>> +               return 0;\n>> +       /* Ignore exit code, as an empty crontab will return error. */\n>> +       finish_command(&child);\n>> +\n>> +       return 1;\n>> +}\n> \n> Ignoring the error from `crontab -l` is an already-established idiom\n> in this file. Okay.\n> \n> Nit: There doesn't seem to be a need for the blank line before `return\n> 1`, and other maintenance-related functions don't have such a blank\n> line. The same comment about blank lines before `return` applies to\n> other newly-added functions, as well. But it's subjective, and not\n> necessarily worth changing.\n> \n>> +static char *systemd_timer_timer_filename()\n>> +{\n>> +       const char *filename = \"~/.config/systemd/user/git-maintenance@.timer\";\n>> +       char *expanded = expand_user_path(filename, 0);\n>> +       if (!expanded)\n>> +               die(_(\"failed to expand path '%s'\"), filename);\n>> +\n>> +       return expanded;\n>> +}\n> \n> I was curious whether this would fail if `.config/systemd/user/`\n> didn't already exist, but looking at the implementation of\n> expand_user_path() , I see that it doesn't require the path to already\n> exist if you pass 0 for the second argument as you do here. Okay.\n\nDo we need to worry about $XDG_CONFIG_HOME rather than hard coding \n\"~/.config/\". There is a function xdg_config_home() that takes care of this.\n\nThanks again for working on this\n\nBest Wishes\n\nPhillip\n"},{"id":"423481","messageId":"faed7793-a011-748f-d5c5-2dc00d0fe52c@gmail.com","threadId":"55603","inReplyTo":"20210501145220.2082670-1-lenaic@lhuard.fr","subject":"Re: [PATCH] maintenance: use systemd timers on Linux","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-05-03T12:04:59Z","receivedAt":"2021-05-03T12:05:27Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 5/1/2021 10:52 AM, Lénaïc Huard wrote:\n> The existing mechanism for scheduling background maintenance is done\n> through cron. On Linux systems managed by systemd, systemd provides an\n> alternative to schedule recurring tasks: systemd timers.\n> \n> The main motivations to implement systemd timers in addition to cron\n> are:\n...\n\nThank you for working on this. Users have questioned \"why cron?\"\nsince the release of background maintenance, so I appreciate you\ntaking the time to port this feature to systemd.\n\nI won't do a deep code review here since that seems to already be\ncovered, and a v2 seems required. Ensuring that users can choose\nwhich of the two backends is a good idea. We might even want to\nstart with 'cron' as the default and 'systemd' as an opt-in.\n\nThe other concern I wanted to discuss was the upgrade scenario.\nIf users have already enabled background maintenance with the\ncron backend, how can we help users disable the cron backend\nbefore they upgrade to the systemd version? I imagine that we\nshould disable cron when enabling systemd, using\ncrontab_update_schedule() with run_maintenance given as 0.\nWe might want to enable quiet errors in that method for the\ncase that cron does not exist.\n\nIt is important to make it clear that we only accept one\nscheduler at a time, since they would be competing to run\n'git for-each-repo' over the same list of repos. A single user\ncannot schedule some repositories in 'cron' and another set in\n'systemd'. This seems like an acceptable technical limitation.\n\nThanks, and I look forward to your v2!\n-Stolee\n"},{"id":"423657","messageId":"871ralwfo8.fsf@evledraar.gmail.com","threadId":"55603","inReplyTo":"CAPig+cQks0_nL1J4YUbEUjmWYLKrhuHX-f8PkWM2zFE4gybWMw@mail.gmail.com","subject":"Re: [PATCH] maintenance: use systemd timers on Linux","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-05T12:01:25Z","receivedAt":"2021-05-05T12:03:59Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Sun, May 02 2021, Eric Sunshine wrote:\n\n> On Sat, May 1, 2021 at 10:59 AM Lénaïc Huard <lenaic@lhuard.fr> wrote:\n>> +       strvec_push(&child.args, \"--now\");\n>> +       strvec_pushf(&child.args, \"git-maintenance@%s.timer\", frequency);\n>> +\n>> +       if (start_command(&child))\n>> +               die(_(\"failed to run systemctl\"));\n>> +       return finish_command(&child);\n>> +}\n>> +static int systemd_timer_write_unit_templates(const char *exec_path)\n>> +{\n>> +       unit = \"[Unit]\\n\"\n>> +              \"Description=Optimize Git repositories data\\n\"\n>> +              \"\\n\"\n>> +              \"[Service]\\n\"\n>> +              \"Type=oneshot\\n\"\n>> +              \"ExecStart=\\\"%1$s/git\\\" --exec-path=\\\"%1$s\\\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\\n\"\n>\n> I see that it's in POSIX, but do we use this `%n$s` directive\n> elsewhere in the Git source code? If not, I'd be cautious of\n> introducing it here. Maybe it's better to just use plain `%s` twice...\n\nWe use it in po/, so for sprintf() on systems that don't have\nNO_GETTEXT=Y we already test it in the wild.\n\nBut no, I don't think anything in the main source uses it, FWIW I have a\nWIP series in my own fork that I've cooked for a while that uses this, I\nhaven't run into any issues with it in either GitHub's CI\n(e.g. Windows), or on the systems I myself test on.\n\nI think it would be a useful canary to just take a change like this, we\ncan always change it to the form you suggest if it doesn't work out.\n"},{"id":"423661","messageId":"YJKNVX5Nq64wAfvA@danh.dev","threadId":"55603","inReplyTo":"b1153c47-05cf-877c-587b-8205d8d32816@gmail.com","subject":"Re: [PATCH] maintenance: use systemd timers on Linux","fromName":"Đoàn Trần Công Danh","fromEmail":"congdanhqx@gmail.com","sentAt":"2021-05-05T12:19:33Z","receivedAt":"2021-05-05T12:19:38Z","isPatch":true,"sender":{"key":"congdanhqx@gmail.com","avatar":"https://avatars.githubusercontent.com/u/42673067?v=4"},"body":"On 2021-05-02 15:10:05+0100, Phillip Wood <phillip.wood123@gmail.com> wrote:\n> On 02/05/2021 07:45, Eric Sunshine wrote:\n> > On Sat, May 1, 2021 at 10:59 AM Lénaïc Huard <lenaic@lhuard.fr> wrote:\n> > > The existing mechanism for scheduling background maintenance is done\n> > > through cron. On Linux systems managed by systemd, systemd provides an\n> > > alternative to schedule recurring tasks: systemd timers.\n> > \n> > Thanks for working on this. While `cron` has been the go-to standard\n> > for decades, `systemd` is certainly widespread enough that it makes\n> > sense to support it, as well.\n> \n> Yes, thank you for working on this, it will be very useful to users like me\n> who use a linux distribution that does not install a cron daemon by default\n> but relies on systemd instead.\n> \n> > > The main motivations to implement systemd timers in addition to cron\n> > > are:\n> > > * cron is optional and Linux systems running systemd might not have it\n> > >    installed.\n> > > * The execution of `crontab -l` can tell us if cron is installed but not\n> > >    if the daemon is actually running.\n> \n> Can we use systemctl to see if it is running (and enabled so we know it will\n> be restarted after a reboot)?\n\nNot sure if I understand this suggestion.\nHowever, non-systemd systems doesn't have systemctl command to begin\nwith.\n\n> > > * With systemd, each service is run in its own cgroup and its logs are\n> > >    tagged by the service inside journald. With cron, all scheduled tasks\n> > >    are running in the cron daemon cgroup and all the logs of the\n> > >    user-scheduled tasks are pretended to belong to the system cron\n> > >    service.\n> > >    Concretely, a user that doesn’t have access to the system logs won’t\n> > >    have access to the log of its own tasks scheduled by cron whereas he\n> > >    will have access to the log of its own tasks scheduled by systemd\n> > >    timer.\n> > \n> > The last point is somewhat compelling. A potential counterargument is\n> > that `cron` does send email to the user by default if any output is\n> > generated by the cron job. However, it seems quite likely these days\n> > that many systems either won't have local mail service enabled or the\n> > user won't bother checking the local mailbox. It's a minor point, but\n> > if you re-roll it might make sense for the commit message to expand\n> > the last point by saying that although `cron` attempts to send email,\n> > that email may go unseen by the user.\n> > \n> > > In order to schedule git maintenance, we need two unit template files:\n> > > * ~/.config/systemd/user/git-maintenance@.service\n> > >    to define the command to be started by systemd and\n> > > * ~/.config/systemd/user/git-maintenance@.timer\n> > >    to define the schedule at which the command should be run.\n\nI think it would be better to change ~/.config here to\n$XDG_CONFIG_HOME, as others also points out in another comments.\n\n[..snip..]\n\n> > > +`git maintenance start` will overwrite these files and start the timer\n> > > +again with `systemctl --user`, so any customization should be done by\n> > > +creating a drop-in file\n> > > +`~/.config/systemd/user/git-maintenance@.service.d/*.conf`.\n\nDitto.\n\n> > Will `systemd` users generally understand what filename to create in\n> > the \"...@.service.d/\" directory, and will they know what to populate\n> > the file with? (Genuine question; I've never dealt with that.)\n> \n> I think it would be helpful to explicitly mention the file names (I don't\n> think I could tell you what they are without reading the relevant systemd\n> man page)\n\n[..snip..]\n\n> > > +static char *systemd_timer_timer_filename()\n> > > +{\n> > > +       const char *filename = \"~/.config/systemd/user/git-maintenance@.timer\";\n> > > +       char *expanded = expand_user_path(filename, 0);\n> > > +       if (!expanded)\n> > > +               die(_(\"failed to expand path '%s'\"), filename);\n> > > +\n> > > +       return expanded;\n> > > +}\n> > \n> > I was curious whether this would fail if `.config/systemd/user/`\n> > didn't already exist, but looking at the implementation of\n> > expand_user_path() , I see that it doesn't require the path to already\n> > exist if you pass 0 for the second argument as you do here. Okay.\n> \n> Do we need to worry about $XDG_CONFIG_HOME rather than hard coding\n> \"~/.config/\". There is a function xdg_config_home() that takes care of this.\n\n-- \nDanh\n"},{"id":"423691","messageId":"24e37e66-1418-4f64-5405-46aaf6449e4a@gmail.com","threadId":"55603","inReplyTo":"YJKNVX5Nq64wAfvA@danh.dev","subject":"Re: [PATCH] maintenance: use systemd timers on Linux","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2021-05-05T14:57:27Z","receivedAt":"2021-05-05T14:57:32Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Đoàn\n\nOn 05/05/2021 13:19, Đoàn Trần Công Danh wrote:\n> On 2021-05-02 15:10:05+0100, Phillip Wood <phillip.wood123@gmail.com> wrote:\n>> On 02/05/2021 07:45, Eric Sunshine wrote:\n>>> On Sat, May 1, 2021 at 10:59 AM Lénaïc Huard <lenaic@lhuard.fr> wrote:\n>>>> The existing mechanism for scheduling background maintenance is done\n>>>> through cron. On Linux systems managed by systemd, systemd provides an\n>>>> alternative to schedule recurring tasks: systemd timers.\n>>>\n>>> Thanks for working on this. While `cron` has been the go-to standard\n>>> for decades, `systemd` is certainly widespread enough that it makes\n>>> sense to support it, as well.\n>>\n>> Yes, thank you for working on this, it will be very useful to users like me\n>> who use a linux distribution that does not install a cron daemon by default\n>> but relies on systemd instead.\n>>\n>>>> The main motivations to implement systemd timers in addition to cron\n>>>> are:\n>>>> * cron is optional and Linux systems running systemd might not have it\n>>>>     installed.\n>>>> * The execution of `crontab -l` can tell us if cron is installed but not\n>>>>     if the daemon is actually running.\n>>\n>> Can we use systemctl to see if it is running (and enabled so we know it will\n>> be restarted after a reboot)?\n> \n> Not sure if I understand this suggestion.\n> However, non-systemd systems doesn't have systemctl command to begin\n> with.\n\nI was wondering if on systems with both cron and systemd installed we \ncould use systemctl to determine if crond is actually running as Lénaïc \npointed out that being able to run `crontab -l` does not tell us if \ncrond is running.\n\nBest Wishes\n\nPhillip\n\n>>>> * With systemd, each service is run in its own cgroup and its logs are\n>>>>     tagged by the service inside journald. With cron, all scheduled tasks\n>>>>     are running in the cron daemon cgroup and all the logs of the\n>>>>     user-scheduled tasks are pretended to belong to the system cron\n>>>>     service.\n>>>>     Concretely, a user that doesn’t have access to the system logs won’t\n>>>>     have access to the log of its own tasks scheduled by cron whereas he\n>>>>     will have access to the log of its own tasks scheduled by systemd\n>>>>     timer.\n>>>\n>>> The last point is somewhat compelling. A potential counterargument is\n>>> that `cron` does send email to the user by default if any output is\n>>> generated by the cron job. However, it seems quite likely these days\n>>> that many systems either won't have local mail service enabled or the\n>>> user won't bother checking the local mailbox. It's a minor point, but\n>>> if you re-roll it might make sense for the commit message to expand\n>>> the last point by saying that although `cron` attempts to send email,\n>>> that email may go unseen by the user.\n>>>\n>>>> In order to schedule git maintenance, we need two unit template files:\n>>>> * ~/.config/systemd/user/git-maintenance@.service\n>>>>     to define the command to be started by systemd and\n>>>> * ~/.config/systemd/user/git-maintenance@.timer\n>>>>     to define the schedule at which the command should be run.\n> \n> I think it would be better to change ~/.config here to\n> $XDG_CONFIG_HOME, as others also points out in another comments.\n> \n> [..snip..]\n> \n>>>> +`git maintenance start` will overwrite these files and start the timer\n>>>> +again with `systemctl --user`, so any customization should be done by\n>>>> +creating a drop-in file\n>>>> +`~/.config/systemd/user/git-maintenance@.service.d/*.conf`.\n> \n> Ditto.\n> \n>>> Will `systemd` users generally understand what filename to create in\n>>> the \"...@.service.d/\" directory, and will they know what to populate\n>>> the file with? (Genuine question; I've never dealt with that.)\n>>\n>> I think it would be helpful to explicitly mention the file names (I don't\n>> think I could tell you what they are without reading the relevant systemd\n>> man page)\n> \n> [..snip..]\n> \n>>>> +static char *systemd_timer_timer_filename()\n>>>> +{\n>>>> +       const char *filename = \"~/.config/systemd/user/git-maintenance@.timer\";\n>>>> +       char *expanded = expand_user_path(filename, 0);\n>>>> +       if (!expanded)\n>>>> +               die(_(\"failed to expand path '%s'\"), filename);\n>>>> +\n>>>> +       return expanded;\n>>>> +}\n>>>\n>>> I was curious whether this would fail if `.config/systemd/user/`\n>>> didn't already exist, but looking at the implementation of\n>>> expand_user_path() , I see that it doesn't require the path to already\n>>> exist if you pass 0 for the second argument as you do here. Okay.\n>>\n>> Do we need to worry about $XDG_CONFIG_HOME rather than hard coding\n>> \"~/.config/\". There is a function xdg_config_home() that takes care of this.\n> \n"},{"id":"423979","messageId":"20210509213217.449489-1-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210501145220.2082670-1-lenaic@lhuard.fr","subject":"[PATCH v2 0/1] maintenance: use systemd timers on Linux","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-05-09T21:32:16Z","receivedAt":"2021-05-09T21:36:03Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Hello,\n\nThank you all for your valuable feedback!\nI tried to address all the discussed points in this new version of the\npatch.\nDo not hesitate to let me know if I forgot anything.\n\nThe main new thing in this version is the `--scheduler=<scheduler>`\nparameter that has been added to `git maintenance start` command. It\nallows the end user to choose between `cron` or user systemd timers\nfor scheduling git maintenance tasks.\n\nI also addressed the migration problematic during an upgrade.\nIf a user invokes `git maintenance start --scheduler=systemd-timer`\nfirst, and then invokes `git maintenance start --scheduler=cron`\nwithout invoking `git maintenance stop` in between, the git\nmaintenance tasks will be removed from `systemd-timer` to be sure that\nthe same tasks won’t be scheduled concurrently twice by both\n`systemd-timer` and `cron`. And the same in the other way round.\n\nOn its side, `git maintenance stop` don’t have any\n`--scheduler=<scheduler>` parameter as it will try to remove the git\nmaintenance tasks from all the schedulers available on the system.\n\nThe default scheduler when `--scheduler=<scheduler>` isn’t specified\nis `auto` which means “choose an appropriate scheduler”.\n\nOn Windows and MacOS, it always chooses the specific scheduler on\nthose platforms, `schtasks` and `launchctl`.\n\nOn Linux, it chooses user systemd timers if they are available and\n`cron` otherwise.\nThis order has been a subject of discussion so, let me explain why I\nchose this order.\n\nOn my system, I uses systemd and all the packages of my distribution\nthat are needing regular scheduled tasks are defining systemd timers\ninstead of cron task.\nSo, I don’t use crontab anymore. However, a cron package is installed\nbecause it is an indirect dependency of a package I installed through\nmy Linux distribution package manager. But the cron daemon isn’t\nstarted.\n\n`systemctl --user list-timers` is the CLI command that actually talks\nto the daemon in charge of scheduling timers. So, if `systemctl --user\nlist-timers` succeed, we can be sure that user systemd timers are\nfunctional.\nConcretely, the `is_systemd_timer_available` function of this patch is\nreliable.\n\n`crontab`, on the other hand, only reads and writes to\n`/var/spool/cron/$USER` but this command can work also if the `cron`\ndaemon isn’t running. In this case, the scheduled tasks will never\nrun.\nConcretely, the `is_crontab_available` function of this patch is less\nreliable.\n\nWe would need to check if the `cron` daemon is really running.\nRelying on `systemctl` to check if the service is enabled and running\nisn’t ideal since we want to support systems that don’t have systemd.\nParsing directly `/proc` is challenging since its content is OS\nspecific. `/proc` content on a Solaris system is different than its\ncontent on a Linux system.\nWe could rely on `ps` but we must keep in mind its interface is very\ndifferent from one system to another. It doesn’t implement both the\nstandard syntax and the BSD syntax for its arguments on all platforms\nfor example.\n\nMoreover, Linux distributions are proposing several different\nimplementations of `cron`: `cronie`, `fcron`, `dcron`, `vixie-cron`,\n`scron`, `bcron`.\n\nSee:\n* https://wiki.archlinux.org/title/Cron#Installation\n* https://wiki.gentoo.org/wiki/Cron#Which_cron_is_right_for_the_job.3F\n\nDepending on the `cron` implementation, the name of the `cron` daemon\nprocess might differ.\nSo, reliably detecting if a `cron` daemon is running may require to\nreview each `cron` implementation.\n\nWith this new version of the patch, advanced users that care about\nsystemd timers versus `cron` can explicitly choose which one they want to\nuse.\nFor less advanced users that don’t care, I prefer to choose the method\nwhich has the higher probability of working.\n\nBut since this order is a subject of debate, what I can propose is a\n`./configure` compile time option to select which `cron` or systemd\ntimers should be chosen in priority if both are available.\n\n\n\nIn addition to this big change, this new version of the patch also honors\nthe `XDG_CONFIG_HOME` environment variable and removes the code\nduplication between `systemd_timer_timer_filename()` and\n`systemd_timer_service_filename()`.\n\nIt also fixes other points raised in the code review.\n\nLénaïc Huard (1):\n  maintenance: use systemd timers on Linux\n\n Documentation/git-maintenance.txt |  60 +++++\n builtin/gc.c                      | 375 ++++++++++++++++++++++++++++--\n t/t7900-maintenance.sh            |  51 ++++\n 3 files changed, 462 insertions(+), 24 deletions(-)\n\n-- \n2.31.1\n\n"},{"id":"423980","messageId":"20210509213217.449489-2-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210509213217.449489-1-lenaic@lhuard.fr","subject":"[PATCH v2 1/1] maintenance: use systemd timers on Linux","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-05-09T21:32:17Z","receivedAt":"2021-05-09T21:36:46Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"The existing mechanism for scheduling background maintenance is done\nthrough cron. On Linux systems managed by systemd, systemd provides an\nalternative to schedule recurring tasks: systemd timers.\n\nThe main motivations to implement systemd timers in addition to cron\nare:\n* cron is optional and Linux systems running systemd might not have it\n  installed.\n* The execution of `crontab -l` can tell us if cron is installed but not\n  if the daemon is actually running.\n* With systemd, each service is run in its own cgroup and its logs are\n  tagged by the service inside journald. With cron, all scheduled tasks\n  are running in the cron daemon cgroup and all the logs of the\n  user-scheduled tasks are pretended to belong to the system cron\n  service.\n  Concretely, a user that doesn’t have access to the system logs won’t\n  have access to the log of its own tasks scheduled by cron whereas he\n  will have access to the log of its own tasks scheduled by systemd\n  timer.\n  Although `cron` attempts to send email, that email may go unseen by\n  the user because these days, local mailboxes are not heavily used\n  anymore.\n\nIn order to choose which scheduler to use between `cron` and user\nsystemd timers, a new option\n`--scheduler=auto|cron|systemd|launchctl|schtasks` has been added to\n`git maintenance start`.\nWhen `git maintenance start --scheduler=XXX` is run, it not only\nregisters `git maintenance run` tasks in the scheduler XXX, it also\nremoves the `git maintenance run` tasks from all the other schedulers to\nensure we cannot have two schedulers launching concurrent identical\ntasks.\n\nThe default value is `auto` which chooses a suitable scheduler for the\nsystem.\nOn Linux, if user systemd timers are available, they will be used as git\nmaintenance scheduler. If not, `cron` will be used if it is available.\nIf none is available, it will fail.\n\n`git maintenance stop` doesn't have any `--scheduler` parameter because\nthis command will try to remove the `git maintenance run` tasks from all\nthe available schedulers.\n\nIn order to schedule git maintenance, we need two unit template files:\n* ~/.config/systemd/user/git-maintenance@.service\n  to define the command to be started by systemd and\n* ~/.config/systemd/user/git-maintenance@.timer\n  to define the schedule at which the command should be run.\n\nThose units are templates that are parametrized by the frequency.\n\nBased on those templates, 3 timers are started:\n* git-maintenance@hourly.timer\n* git-maintenance@daily.timer\n* git-maintenance@weekly.timer\n\nThe command launched by those three timers are the same than with the\nother scheduling methods:\n\ngit for-each-repo --config=maintenance.repo maintenance run\n--schedule=%i\n\nwith the full path for git to ensure that the version of git launched\nfor the scheduled maintenance is the same as the one used to run\n`maintenance start`.\n\nThe timer unit contains `Persistent=true` so that, if the computer is\npowered down when a maintenance task should run, the task will be run\nwhen the computer is back powered on.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n Documentation/git-maintenance.txt |  60 +++++\n builtin/gc.c                      | 375 ++++++++++++++++++++++++++++--\n t/t7900-maintenance.sh            |  51 ++++\n 3 files changed, 462 insertions(+), 24 deletions(-)\n\ndiff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\nindex 80ddd33ceb..f012923333 100644\n--- a/Documentation/git-maintenance.txt\n+++ b/Documentation/git-maintenance.txt\n@@ -181,6 +181,20 @@ OPTIONS\n \t`maintenance.<task>.enabled` configured as `true` are considered.\n \tSee the 'TASKS' section for the list of accepted `<task>` values.\n \n+--scheduler=auto|crontab|systemd-timer|launchctl|schtasks::\n+\tWhen combined with the `start` subcommand, specify the scheduler\n+\tto use to run the hourly, daily and weekly executions of\n+\t`git maintenance run`.\n+\tThe possible values for `<scheduler>` depend on the system: `crontab`\n+\tis available on POSIX systems, `systemd-timer` is available on Linux\n+\tsystems, `launchctl` is available on MacOS and `schtasks` is available\n+\ton Windows.\n+\tBy default or when `auto` is specified, the most appropriate scheduler\n+\tfor the system is used. On MacOS, `launchctl` is used. On Windows,\n+\t`schtasks` is used. On Linux, `systemd-timers` is used if user systemd\n+\ttimers are available, otherwise, `crontab` is used. On all other systems,\n+\t`crontab` is used.\n+\n \n TROUBLESHOOTING\n ---------------\n@@ -279,6 +293,52 @@ schedule to ensure you are executing the correct binaries in your\n schedule.\n \n \n+BACKGROUND MAINTENANCE ON LINUX SYSTEMD SYSTEMS\n+-----------------------------------------------\n+\n+While Linux supports `cron`, depending on the distribution, `cron` may\n+be an optional package not necessarily installed. On modern Linux\n+distributions, systemd timers are superseding it.\n+\n+If user systemd timers are available, they will be used as a replacement\n+of `cron`.\n+\n+In this case, `git maintenance start` will create user systemd timer units\n+and start the timers. The current list of user-scheduled tasks can be found\n+by running `systemctl --user list-timers`. The timers written by `git\n+maintenance start` are similar to this:\n+\n+-----------------------------------------------------------------------\n+$ systemctl --user list-timers\n+NEXT                         LEFT          LAST                         PASSED     UNIT                         ACTIVATES\n+Thu 2021-04-29 19:00:00 CEST 42min left    Thu 2021-04-29 18:00:11 CEST 17min ago  git-maintenance@hourly.timer git-maintenance@hourly.service\n+Fri 2021-04-30 00:00:00 CEST 5h 42min left Thu 2021-04-29 00:00:11 CEST 18h ago    git-maintenance@daily.timer  git-maintenance@daily.service\n+Mon 2021-05-03 00:00:00 CEST 3 days left   Mon 2021-04-26 00:00:11 CEST 3 days ago git-maintenance@weekly.timer git-maintenance@weekly.service\n+-----------------------------------------------------------------------\n+\n+One timer is registered for each `--schedule=<frequency>` option.\n+\n+The definition of the systemd units can be inspected in the following files:\n+\n+-----------------------------------------------------------------------\n+~/.config/systemd/user/git-maintenance@.timer\n+~/.config/systemd/user/git-maintenance@.service\n+~/.config/systemd/user/timers.target.wants/git-maintenance@hourly.timer\n+~/.config/systemd/user/timers.target.wants/git-maintenance@daily.timer\n+~/.config/systemd/user/timers.target.wants/git-maintenance@weekly.timer\n+-----------------------------------------------------------------------\n+\n+`git maintenance start` will overwrite these files and start the timer\n+again with `systemctl --user`, so any customization should be done by\n+creating a drop-in file, i.e. a `.conf` suffixed file in the\n+`~/.config/systemd/user/git-maintenance@.service.d` directory.\n+\n+`git maintenance stop` will stop the user systemd timers and delete\n+the above mentioned files.\n+\n+For more details, see systemd.timer(5)\n+\n+\n BACKGROUND MAINTENANCE ON MACOS SYSTEMS\n ---------------------------------------\n \ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex ef7226d7bc..7c72aa3b99 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1544,6 +1544,15 @@ static const char *get_frequency(enum schedule_priority schedule)\n \t}\n }\n \n+static int is_launchctl_available(const char *cmd)\n+{\n+#ifdef __APPLE__\n+\treturn 1;\n+#else\n+\treturn 0;\n+#endif\n+}\n+\n static char *launchctl_service_name(const char *frequency)\n {\n \tstruct strbuf label = STRBUF_INIT;\n@@ -1710,6 +1719,15 @@ static int launchctl_update_schedule(int run_maintenance, int fd, const char *cm\n \t\treturn launchctl_remove_plists(cmd);\n }\n \n+static int is_schtasks_available(const char *cmd)\n+{\n+#ifdef GIT_WINDOWS_NATIVE\n+\treturn 1;\n+#else\n+\treturn 0;\n+#endif\n+}\n+\n static char *schtasks_task_name(const char *frequency)\n {\n \tstruct strbuf label = STRBUF_INIT;\n@@ -1872,6 +1890,28 @@ static int schtasks_update_schedule(int run_maintenance, int fd, const char *cmd\n \t\treturn schtasks_remove_tasks(cmd);\n }\n \n+static int is_crontab_available(const char *cmd)\n+{\n+\tstatic int cached_result = -1;\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\n+\tif (cached_result != -1)\n+\t\treturn cached_result;\n+\n+\tstrvec_split(&child.args, cmd);\n+\tstrvec_push(&child.args, \"-l\");\n+\tchild.no_stdin = 1;\n+\tchild.no_stdout = 1;\n+\tchild.no_stderr = 1;\n+\tchild.silent_exec_failure = 1;\n+\n+\tif (start_command(&child))\n+\t\treturn cached_result = 0;\n+\t/* Ignore exit code, as an empty crontab will return error. */\n+\tfinish_command(&child);\n+\treturn cached_result = 1;\n+}\n+\n #define BEGIN_LINE \"# BEGIN GIT MAINTENANCE SCHEDULE\"\n #define END_LINE \"# END GIT MAINTENANCE SCHEDULE\"\n \n@@ -1959,61 +1999,348 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n \treturn result;\n }\n \n+static int is_systemd_timer_available(const char *cmd)\n+{\n+#ifdef __linux__\n+\tstatic int cached_result = -1;\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\n+\tif (cached_result != -1)\n+\t\treturn cached_result;\n+\n+\tstrvec_split(&child.args, cmd);\n+\tstrvec_pushl(&child.args, \"--user\", \"list-timers\", NULL);\n+\tchild.no_stdin = 1;\n+\tchild.no_stdout = 1;\n+\tchild.no_stderr = 1;\n+\tchild.silent_exec_failure = 1;\n+\n+\tif (start_command(&child))\n+\t\treturn cached_result = 0;\n+\tif (finish_command(&child))\n+\t\treturn cached_result = 0;\n+\treturn cached_result = 1;\n+#else\n+\treturn 0;\n+#endif\n+}\n+\n+static char *xdg_config_home_systemd(const char *filename)\n+{\n+\tconst char *home, *config_home;\n+\n+\tassert(filename);\n+\tconfig_home = getenv(\"XDG_CONFIG_HOME\");\n+\tif (config_home && *config_home)\n+\t\treturn mkpathdup(\"%s/systemd/user/%s\", config_home, filename);\n+\n+\thome = getenv(\"HOME\");\n+\tif (home)\n+\t\treturn mkpathdup(\"%s/.config/systemd/user/%s\", home, filename);\n+\n+\tdie(_(\"failed to get $XDG_CONFIG_HOME and $HOME\"));\n+}\n+\n+static int systemd_timer_enable_unit(int enable,\n+\t\t\t\t     enum schedule_priority schedule,\n+\t\t\t\t     const char *cmd)\n+{\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\tconst char *frequency = get_frequency(schedule);\n+\n+\tstrvec_split(&child.args, cmd);\n+\tstrvec_pushl(&child.args, \"--user\", enable ? \"enable\" : \"disable\",\n+\t\t     \"--now\", NULL);\n+\tstrvec_pushf(&child.args, \"git-maintenance@%s.timer\", frequency);\n+\n+\tif (start_command(&child))\n+\t\tdie(_(\"failed to run systemctl\"));\n+\treturn finish_command(&child);\n+}\n+\n+static int systemd_timer_delete_unit_templates(void)\n+{\n+\tchar *filename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n+\tunlink(filename);\n+\tfree(filename);\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n+\tunlink(filename);\n+\tfree(filename);\n+\n+\treturn 0;\n+}\n+\n+static int systemd_timer_delete_units(const char *cmd)\n+{\n+\treturn systemd_timer_enable_unit(0, SCHEDULE_HOURLY, cmd) ||\n+\t       systemd_timer_enable_unit(0, SCHEDULE_DAILY, cmd) ||\n+\t       systemd_timer_enable_unit(0, SCHEDULE_WEEKLY, cmd) ||\n+\t       systemd_timer_delete_unit_templates();\n+}\n+\n+static int systemd_timer_write_unit_templates(const char *exec_path)\n+{\n+\tchar *filename;\n+\tFILE *file;\n+\tconst char *unit;\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n+\tif (safe_create_leading_directories(filename))\n+\t\tdie(_(\"failed to create directories for '%s'\"), filename);\n+\tfile = xfopen(filename, \"w\");\n+\tfree(filename);\n+\n+\tunit = \"# This file was created and is maintained by Git.\\n\"\n+\t       \"# Any edits made in this file might be replaced in the future\\n\"\n+\t       \"# by a Git command.\\n\"\n+\t       \"\\n\"\n+\t       \"[Unit]\\n\"\n+\t       \"Description=Optimize Git repositories data\\n\"\n+\t       \"\\n\"\n+\t       \"[Timer]\\n\"\n+\t       \"OnCalendar=%i\\n\"\n+\t       \"Persistent=true\\n\"\n+\t       \"\\n\"\n+\t       \"[Install]\\n\"\n+\t       \"WantedBy=timers.target\\n\";\n+\tfputs(unit, file);\n+\tfclose(file);\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n+\tif (safe_create_leading_directories(filename))\n+\t\tdie(_(\"failed to create directories for '%s'\"), filename);\n+\tfile = xfopen(filename, \"w\");\n+\tfree(filename);\n+\n+\tunit = \"# This file was created and is maintained by Git.\\n\"\n+\t       \"# Any edits made in this file might be replaced in the future\\n\"\n+\t       \"# by a Git command.\\n\"\n+\t       \"\\n\"\n+\t       \"[Unit]\\n\"\n+\t       \"Description=Optimize Git repositories data\\n\"\n+\t       \"\\n\"\n+\t       \"[Service]\\n\"\n+\t       \"Type=oneshot\\n\"\n+\t       \"ExecStart=\\\"%1$s/git\\\" --exec-path=\\\"%1$s\\\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\\n\"\n+\t       \"LockPersonality=yes\\n\"\n+\t       \"MemoryDenyWriteExecute=yes\\n\"\n+\t       \"NoNewPrivileges=yes\\n\"\n+\t       \"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\\n\"\n+\t       \"RestrictNamespaces=yes\\n\"\n+\t       \"RestrictRealtime=yes\\n\"\n+\t       \"RestrictSUIDSGID=yes\\n\"\n+\t       \"SystemCallArchitectures=native\\n\"\n+\t       \"SystemCallFilter=@system-service\\n\";\n+\tfprintf(file, unit, exec_path);\n+\tfclose(file);\n+\n+\treturn 0;\n+}\n+\n+static int systemd_timer_setup_units(const char *cmd)\n+{\n+\tconst char *exec_path = git_exec_path();\n+\n+\treturn systemd_timer_write_unit_templates(exec_path) ||\n+\t       systemd_timer_enable_unit(1, SCHEDULE_HOURLY, cmd) ||\n+\t       systemd_timer_enable_unit(1, SCHEDULE_DAILY, cmd) ||\n+\t       systemd_timer_enable_unit(1, SCHEDULE_WEEKLY, cmd);\n+}\n+\n+static int systemd_timer_update_schedule(int run_maintenance, int fd,\n+\t\t\t\t\t const char *cmd)\n+{\n+\tif (run_maintenance)\n+\t\treturn systemd_timer_setup_units(cmd);\n+\telse\n+\t\treturn systemd_timer_delete_units(cmd);\n+}\n+\n+enum scheduler {\n+\tSCHEDULER_INVALID = -1,\n+\tSCHEDULER_AUTO = 0,\n+\tSCHEDULER_CRON = 1,\n+\tSCHEDULER_SYSTEMD = 2,\n+\tSCHEDULER_LAUNCHCTL = 3,\n+\tSCHEDULER_SCHTASKS = 4,\n+};\n+\n+static const struct {\n+\tint (*is_available)(const char *cmd);\n+\tint (*update_schedule)(int run_maintenance, int fd, const char *cmd);\n+\tconst char *cmd;\n+} scheduler_fn[] = {\n+\t[SCHEDULER_CRON] = { is_crontab_available, crontab_update_schedule,\n+\t\t\t     \"crontab\" },\n+\t[SCHEDULER_SYSTEMD] = { is_systemd_timer_available,\n+\t\t\t\tsystemd_timer_update_schedule, \"systemctl\" },\n+\t[SCHEDULER_LAUNCHCTL] = { is_launchctl_available,\n+\t\t\t\t  launchctl_update_schedule, \"launchctl\" },\n+\t[SCHEDULER_SCHTASKS] = { is_schtasks_available,\n+\t\t\t\t schtasks_update_schedule, \"schtasks\" },\n+};\n+\n+static enum scheduler parse_scheduler(const char *value)\n+{\n+\tif (!value)\n+\t\treturn SCHEDULER_INVALID;\n+\telse if (!strcasecmp(value, \"auto\"))\n+\t\treturn SCHEDULER_AUTO;\n+\telse if (!strcasecmp(value, \"cron\") || !strcasecmp(value, \"crontab\"))\n+\t\treturn SCHEDULER_CRON;\n+\telse if (!strcasecmp(value, \"systemd\") ||\n+\t\t !strcasecmp(value, \"systemd-timer\"))\n+\t\treturn SCHEDULER_SYSTEMD;\n+\telse if (!strcasecmp(value, \"launchctl\"))\n+\t\treturn SCHEDULER_LAUNCHCTL;\n+\telse if (!strcasecmp(value, \"schtasks\"))\n+\t\treturn SCHEDULER_SCHTASKS;\n+\telse\n+\t\treturn SCHEDULER_INVALID;\n+}\n+\n+static int maintenance_opt_scheduler(const struct option *opt, const char *arg,\n+\t\t\t\t     int unset)\n+{\n+\tenum scheduler *scheduler = opt->value;\n+\n+\tif (unset)\n+\t\tdie(_(\"--no-scheduler is not allowed\"));\n+\n+\t*scheduler = parse_scheduler(arg);\n+\n+\tif (*scheduler == SCHEDULER_INVALID)\n+\t\tdie(_(\"unrecognized --scheduler argument '%s'\"), arg);\n+\n+\treturn 0;\n+}\n+\n+struct maintenance_start_opts {\n+\tenum scheduler scheduler;\n+};\n+\n+static void resolve_auto_scheduler(enum scheduler *scheduler)\n+{\n+\tif (*scheduler != SCHEDULER_AUTO)\n+\t\treturn;\n+\n #if defined(__APPLE__)\n-static const char platform_scheduler[] = \"launchctl\";\n+\t*scheduler = SCHEDULER_LAUNCHCTL;\n+\treturn;\n+\n #elif defined(GIT_WINDOWS_NATIVE)\n-static const char platform_scheduler[] = \"schtasks\";\n+\t*scheduler = SCHEDULER_SCHTASKS;\n+\treturn;\n+\n+#elif defined(__linux__)\n+\tif (is_systemd_timer_available(\"systemctl\"))\n+\t\t*scheduler = SCHEDULER_SYSTEMD;\n+\telse if (is_crontab_available(\"crontab\"))\n+\t\t*scheduler = SCHEDULER_CRON;\n+\telse\n+\t\tdie(_(\"neither systemd timers nor crontab are available\"));\n+\treturn;\n+\n #else\n-static const char platform_scheduler[] = \"crontab\";\n+\t*scheduler = SCHEDULER_CRON;\n+\treturn;\n #endif\n+}\n \n-static int update_background_schedule(int enable)\n+static void validate_scheduler(enum scheduler scheduler)\n {\n-\tint result;\n-\tconst char *scheduler = platform_scheduler;\n-\tconst char *cmd = scheduler;\n+\tconst char *cmd;\n+\n+\tif (scheduler == SCHEDULER_INVALID)\n+\t\tBUG(\"invalid scheduler\");\n+\tif (scheduler == SCHEDULER_AUTO)\n+\t\tBUG(\"resolve_auto_scheduler should have been called before\");\n+\n+\tcmd = scheduler_fn[scheduler].cmd;\n+\tif (!scheduler_fn[scheduler].is_available(cmd))\n+\t\tdie(_(\"%s scheduler is not available\"), cmd);\n+}\n+\n+static int update_background_schedule(const struct maintenance_start_opts *opts,\n+\t\t\t\t      int enable)\n+{\n+\tunsigned int i;\n+\tint res, result = 0;\n+\tenum scheduler scheduler;\n+\tconst char *cmd = NULL;\n \tchar *testing;\n \tstruct lock_file lk;\n \tchar *lock_path = xstrfmt(\"%s/schedule\", the_repository->objects->odb->path);\n \n+\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0)\n+\t\treturn error(_(\"another process is scheduling background maintenance\"));\n+\n \ttesting = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n \tif (testing) {\n \t\tchar *sep = strchr(testing, ':');\n \t\tif (!sep)\n \t\t\tdie(\"GIT_TEST_MAINT_SCHEDULER unparseable: %s\", testing);\n \t\t*sep = '\\0';\n-\t\tscheduler = testing;\n+\t\tscheduler = parse_scheduler(testing);\n \t\tcmd = sep + 1;\n+\t\tresult = scheduler_fn[scheduler].update_schedule(\n+\t\t\tenable, get_lock_file_fd(&lk), cmd);\n+\t\tgoto done;\n \t}\n \n-\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0)\n-\t\treturn error(_(\"another process is scheduling background maintenance\"));\n-\n-\tif (!strcmp(scheduler, \"launchctl\"))\n-\t\tresult = launchctl_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse if (!strcmp(scheduler, \"schtasks\"))\n-\t\tresult = schtasks_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse if (!strcmp(scheduler, \"crontab\"))\n-\t\tresult = crontab_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse\n-\t\tdie(\"unknown background scheduler: %s\", scheduler);\n+\tfor (i = 1; i < ARRAY_SIZE(scheduler_fn); i++) {\n+\t\tint enable_scheduler = enable && (opts->scheduler == i);\n+\t\tcmd = scheduler_fn[i].cmd;\n+\t\tif (!scheduler_fn[i].is_available(cmd))\n+\t\t\tcontinue;\n+\t\tres = scheduler_fn[i].update_schedule(\n+\t\t\tenable_scheduler, get_lock_file_fd(&lk), cmd);\n+\t\tif (enable_scheduler)\n+\t\t\tresult = res;\n+\t}\n \n+done:\n \trollback_lock_file(&lk);\n \tfree(testing);\n \treturn result;\n }\n \n-static int maintenance_start(void)\n+static const char *const builtin_maintenance_start_usage[] = {\n+\tN_(\"git maintenance start [--scheduler=<scheduler>]\"), NULL\n+};\n+\n+static int maintenance_start(int argc, const char **argv, const char *prefix)\n {\n+\tstruct maintenance_start_opts opts;\n+\tstruct option builtin_maintenance_start_options[] = {\n+\t\tOPT_CALLBACK(\n+\t\t\t0, \"scheduler\", &opts.scheduler, N_(\"scheduler\"),\n+\t\t\tN_(\"scheduler to use to trigger git maintenance run\"),\n+\t\t\tmaintenance_opt_scheduler),\n+\t\tOPT_END()\n+\t};\n+\tmemset(&opts, 0, sizeof(opts));\n+\n+\targc = parse_options(argc, argv, prefix,\n+\t\t\t     builtin_maintenance_start_options,\n+\t\t\t     builtin_maintenance_start_usage, 0);\n+\n+\tresolve_auto_scheduler(&opts.scheduler);\n+\tvalidate_scheduler(opts.scheduler);\n+\n+\tif (argc > 0)\n+\t\tusage_with_options(builtin_maintenance_start_usage,\n+\t\t\t\t   builtin_maintenance_start_options);\n+\n \tif (maintenance_register())\n \t\twarning(_(\"failed to add repo to global config\"));\n-\n-\treturn update_background_schedule(1);\n+\treturn update_background_schedule(&opts, 1);\n }\n \n static int maintenance_stop(void)\n {\n-\treturn update_background_schedule(0);\n+\treturn update_background_schedule(NULL, 0);\n }\n \n static const char builtin_maintenance_usage[] =\tN_(\"git maintenance <subcommand> [<options>]\");\n@@ -2027,7 +2354,7 @@ int cmd_maintenance(int argc, const char **argv, const char *prefix)\n \tif (!strcmp(argv[1], \"run\"))\n \t\treturn maintenance_run(argc - 1, argv + 1, prefix);\n \tif (!strcmp(argv[1], \"start\"))\n-\t\treturn maintenance_start();\n+\t\treturn maintenance_start(argc - 1, argv + 1, prefix);\n \tif (!strcmp(argv[1], \"stop\"))\n \t\treturn maintenance_stop();\n \tif (!strcmp(argv[1], \"register\"))\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 2412d8c5c0..6e6316cd90 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -20,6 +20,20 @@ test_xmllint () {\n \tfi\n }\n \n+test_lazy_prereq SYSTEMD_ANALYZE '\n+\tsystemd-analyze --help >out &&\n+\tgrep verify out\n+'\n+\n+test_systemd_analyze_verify () {\n+\tif test_have_prereq SYSTEMD_ANALYZE\n+\tthen\n+\t\tsystemd-analyze verify \"$@\"\n+\telse\n+\t\ttrue\n+\tfi\n+}\n+\n test_expect_success 'help text' '\n \ttest_expect_code 129 git maintenance -h 2>err &&\n \ttest_i18ngrep \"usage: git maintenance <subcommand>\" err &&\n@@ -615,6 +629,43 @@ test_expect_success 'start and stop Windows maintenance' '\n \ttest_cmp expect args\n '\n \n+test_expect_success 'start and stop Linux/systemd maintenance' '\n+\twrite_script print-args <<-\\EOF &&\n+\techo $* >>args\n+\tEOF\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemd:./print-args\" git maintenance start &&\n+\n+\t# start registers the repo\n+\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n+\n+\ttest_systemd_analyze_verify \"$HOME/.config/systemd/user/git-maintenance@.service\" &&\n+\n+\trm -f expect &&\n+\tfor frequency in hourly daily weekly\n+\tdo\n+\t\techo \"--user enable --now git-maintenance@${frequency}.timer\" >>expect || return 1\n+\tdone &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemd:./print-args\" git maintenance stop &&\n+\n+\t# stop does not unregister the repo\n+\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n+\n+\ttest_path_is_missing \"$HOME/.config/systemd/user/git-maintenance@.timer\" &&\n+\ttest_path_is_missing \"$HOME/.config/systemd/user/git-maintenance@.service\" &&\n+\n+\trm -f expect &&\n+\tfor frequency in hourly daily weekly\n+\tdo\n+\t\techo \"--user disable --now git-maintenance@${frequency}.timer\" >>expect || return 1\n+\tdone &&\n+\ttest_cmp expect args\n+'\n+\n test_expect_success 'register preserves existing strategy' '\n \tgit config maintenance.strategy none &&\n \tgit maintenance register &&\n-- \n2.31.1\n\n"},{"id":"423984","messageId":"2224656.qFoTrlakfJ@coruscant.lhuard.fr","threadId":"55603","inReplyTo":"871ralwfo8.fsf@evledraar.gmail.com","subject":"Re: [PATCH] maintenance: use systemd timers on Linux","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-05-09T22:34:24Z","receivedAt":"2021-05-09T22:34:44Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Le mercredi 5 mai 2021, 14:01:25 CEST Ævar Arnfjörð Bjarmason a écrit :\n> On Sun, May 02 2021, Eric Sunshine wrote:\n> > On Sat, May 1, 2021 at 10:59 AM Lénaïc Huard <lenaic@lhuard.fr> wrote:\n> >> +       strvec_push(&child.args, \"--now\");\n> >> +       strvec_pushf(&child.args, \"git-maintenance@%s.timer\", frequency);\n> >> +\n> >> +       if (start_command(&child))\n> >> +               die(_(\"failed to run systemctl\"));\n> >> +       return finish_command(&child);\n> >> +}\n> >> +static int systemd_timer_write_unit_templates(const char *exec_path)\n> >> +{\n> >> +       unit = \"[Unit]\\n\"\n> >> +              \"Description=Optimize Git repositories data\\n\"\n> >> +              \"\\n\"\n> >> +              \"[Service]\\n\"\n> >> +              \"Type=oneshot\\n\"\n> >> +              \"ExecStart=\\\"%1$s/git\\\" --exec-path=\\\"%1$s\\\" for-each-repo\n> >> --config=maintenance.repo maintenance run --schedule=%%i\\n\"> \n> > I see that it's in POSIX, but do we use this `%n$s` directive\n> > elsewhere in the Git source code? If not, I'd be cautious of\n> > introducing it here. Maybe it's better to just use plain `%s` twice...\n> \n> We use it in po/, so for sprintf() on systems that don't have\n> NO_GETTEXT=Y we already test it in the wild.\n> \n> But no, I don't think anything in the main source uses it, FWIW I have a\n> WIP series in my own fork that I've cooked for a while that uses this, I\n> haven't run into any issues with it in either GitHub's CI\n> (e.g. Windows), or on the systems I myself test on.\n> \n> I think it would be a useful canary to just take a change like this, we\n> can always change it to the form you suggest if it doesn't work out.\n\nBased on this latest comment, I left the `%n$s` directive in the v2 of the \npatch.\n\nLet me know if that’s still OK. Otherwise, I’d be happy to implement Eric’s \nsuggestion.\n\nNote however that this would be a “poor” canary to check if that directive is \nsupported on all the platforms on which git has been ported.\nIndeed, this code is executed only on systemd platforms, which means quite \nrecent Linux systems.\nShould this directive not be supported, I suppose it would be on more exotic \nsystems.\n\n\n"},{"id":"423987","messageId":"YJiKXRywBhhGXC5Q@danh.dev","threadId":"55603","inReplyTo":"20210509213217.449489-2-lenaic@lhuard.fr","subject":"Re: [PATCH v2 1/1] maintenance: use systemd timers on Linux","fromName":"Đoàn Trần Công Danh","fromEmail":"congdanhqx@gmail.com","sentAt":"2021-05-10T01:20:29Z","receivedAt":"2021-05-10T01:20:34Z","isPatch":true,"sender":{"key":"congdanhqx@gmail.com","avatar":"https://avatars.githubusercontent.com/u/42673067?v=4"},"body":"On 2021-05-09 23:32:17+0200, Lénaïc Huard <lenaic@lhuard.fr> wrote:\n> The existing mechanism for scheduling background maintenance is done\n> through cron. On Linux systems managed by systemd, systemd provides an\n> alternative to schedule recurring tasks: systemd timers.\n> \n> +static int systemd_timer_enable_unit(int enable,\n> +\t\t\t\t     enum schedule_priority schedule,\n> +\t\t\t\t     const char *cmd)\n> +{\n> +\tstruct child_process child = CHILD_PROCESS_INIT;\n> +\tconst char *frequency = get_frequency(schedule);\n> +\n> +\tstrvec_split(&child.args, cmd);\n> +\tstrvec_pushl(&child.args, \"--user\", enable ? \"enable\" : \"disable\",\n> +\t\t     \"--now\", NULL);\n> +\tstrvec_pushf(&child.args, \"git-maintenance@%s.timer\", frequency);\n> +\n> +\tif (start_command(&child))\n> +\t\tdie(_(\"failed to run systemctl\"));\n> +\treturn finish_command(&child);\n> +}\n> +\n> +static int systemd_timer_delete_unit_templates(void)\n> +{\n> +\tchar *filename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n> +\tunlink(filename);\n> +\tfree(filename);\n> +\n> +\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n> +\tunlink(filename);\n> +\tfree(filename);\n> +\n> +\treturn 0;\n> +}\n> +\n> +static int systemd_timer_delete_units(const char *cmd)\n> +{\n> +\treturn systemd_timer_enable_unit(0, SCHEDULE_HOURLY, cmd) ||\n> +\t       systemd_timer_enable_unit(0, SCHEDULE_DAILY, cmd) ||\n> +\t       systemd_timer_enable_unit(0, SCHEDULE_WEEKLY, cmd) ||\n> +\t       systemd_timer_delete_unit_templates();\n> +}\n\nI'm not using any systemd-based distros. However, isn't this try to\nenable all systemd's {hourly,daily,weekly} user's timer, then delete\nthe templates?^W^W^W^W^W^W^W^W^W^W^W^W^W^W^W^W^W^W^W^W^W^W^W^W^W^W^W\n\nArgh, we're disabling those systemd timer units first, by passing 0 as\nfirst argument of systemd_timer_delete_units.\n\nThe fact that I read that twice, and still wrote down above reply\nmakes me think that above code is not self-explanatory enough.\nMay we switch to something else? Let's say using enum?\n\n\n> +static int systemd_timer_write_unit_templates(const char *exec_path)\n> +{\n> +\tchar *filename;\n> +\tFILE *file;\n> +\tconst char *unit;\n> +\n> +\tfilename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n> +\tif (safe_create_leading_directories(filename))\n> +\t\tdie(_(\"failed to create directories for '%s'\"), filename);\n\nThis message is used by other codes, less works for translator, nice!\n\n> +\tfile = xfopen(filename, \"w\");\n> +\tfree(filename);\n\nI'm sure if we should use FREE_AND_NULL(filename) instead?\nSince, filename will be reused later.\n\n> +\n> +\tunit = \"# This file was created and is maintained by Git.\\n\"\n> +\t       \"# Any edits made in this file might be replaced in the future\\n\"\n> +\t       \"# by a Git command.\\n\"\n> +\t       \"\\n\"\n> +\t       \"[Unit]\\n\"\n> +\t       \"Description=Optimize Git repositories data\\n\"\n> +\t       \"\\n\"\n> +\t       \"[Timer]\\n\"\n> +\t       \"OnCalendar=%i\\n\"\n> +\t       \"Persistent=true\\n\"\n> +\t       \"\\n\"\n> +\t       \"[Install]\\n\"\n> +\t       \"WantedBy=timers.target\\n\";\n> +\tfputs(unit, file);\n> +\tfclose(file);\n> +\n> +\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n> +\tif (safe_create_leading_directories(filename))\n> +\t\tdie(_(\"failed to create directories for '%s'\"), filename);\n> +\tfile = xfopen(filename, \"w\");\n> +\tfree(filename);\n> +\n> +\tunit = \"# This file was created and is maintained by Git.\\n\"\n> +\t       \"# Any edits made in this file might be replaced in the future\\n\"\n> +\t       \"# by a Git command.\\n\"\n> +\t       \"\\n\"\n> +\t       \"[Unit]\\n\"\n> +\t       \"Description=Optimize Git repositories data\\n\"\n> +\t       \"\\n\"\n> +\t       \"[Service]\\n\"\n> +\t       \"Type=oneshot\\n\"\n> +\t       \"ExecStart=\\\"%1$s/git\\\" --exec-path=\\\"%1$s\\\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\\n\"\n> +\t       \"LockPersonality=yes\\n\"\n> +\t       \"MemoryDenyWriteExecute=yes\\n\"\n> +\t       \"NoNewPrivileges=yes\\n\"\n> +\t       \"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\\n\"\n> +\t       \"RestrictNamespaces=yes\\n\"\n> +\t       \"RestrictRealtime=yes\\n\"\n> +\t       \"RestrictSUIDSGID=yes\\n\"\n> +\t       \"SystemCallArchitectures=native\\n\"\n> +\t       \"SystemCallFilter=@system-service\\n\";\n> +\tfprintf(file, unit, exec_path);\n\nI think others have strong opinion on not using \"%1$s\",\nand prefer simple \"%s\" and using \"exec_path\" twice instead.\n\n> +\tfclose(file);\n> +\n> +\treturn 0;\n> +}\n> +\n> +static int systemd_timer_setup_units(const char *cmd)\n> +{\n> +\tconst char *exec_path = git_exec_path();\n> +\n> +\treturn systemd_timer_write_unit_templates(exec_path) ||\n> +\t       systemd_timer_enable_unit(1, SCHEDULE_HOURLY, cmd) ||\n> +\t       systemd_timer_enable_unit(1, SCHEDULE_DAILY, cmd) ||\n> +\t       systemd_timer_enable_unit(1, SCHEDULE_WEEKLY, cmd);\n> +}\n> +\n> +static int systemd_timer_update_schedule(int run_maintenance, int fd,\n> +\t\t\t\t\t const char *cmd)\n> +{\n> +\tif (run_maintenance)\n> +\t\treturn systemd_timer_setup_units(cmd);\n> +\telse\n> +\t\treturn systemd_timer_delete_units(cmd);\n> +}\n> +\n> +enum scheduler {\n> +\tSCHEDULER_INVALID = -1,\n> +\tSCHEDULER_AUTO = 0,\n> +\tSCHEDULER_CRON = 1,\n> +\tSCHEDULER_SYSTEMD = 2,\n> +\tSCHEDULER_LAUNCHCTL = 3,\n> +\tSCHEDULER_SCHTASKS = 4,\n\nI think explicitly writing down values doesn't make things clearer,\n-1 would be nice, not a strong opinion, though.\n\nAnyway, would it be better to move those type declaration to top of\nfile?\n\n> +};\n> +\n> +static const struct {\n> +\tint (*is_available)(const char *cmd);\n> +\tint (*update_schedule)(int run_maintenance, int fd, const char *cmd);\n> +\tconst char *cmd;\n> +} scheduler_fn[] = {\n> +\t[SCHEDULER_CRON] = { is_crontab_available, crontab_update_schedule,\n> +\t\t\t     \"crontab\" },\n> +\t[SCHEDULER_SYSTEMD] = { is_systemd_timer_available,\n> +\t\t\t\tsystemd_timer_update_schedule, \"systemctl\" },\n> +\t[SCHEDULER_LAUNCHCTL] = { is_launchctl_available,\n> +\t\t\t\t  launchctl_update_schedule, \"launchctl\" },\n> +\t[SCHEDULER_SCHTASKS] = { is_schtasks_available,\n> +\t\t\t\t schtasks_update_schedule, \"schtasks\" },\n> +};\n> +\n> +static enum scheduler parse_scheduler(const char *value)\n> +{\n> +\tif (!value)\n> +\t\treturn SCHEDULER_INVALID;\n> +\telse if (!strcasecmp(value, \"auto\"))\n> +\t\treturn SCHEDULER_AUTO;\n> +\telse if (!strcasecmp(value, \"cron\") || !strcasecmp(value, \"crontab\"))\n> +\t\treturn SCHEDULER_CRON;\n> +\telse if (!strcasecmp(value, \"systemd\") ||\n> +\t\t !strcasecmp(value, \"systemd-timer\"))\n> +\t\treturn SCHEDULER_SYSTEMD;\n> +\telse if (!strcasecmp(value, \"launchctl\"))\n> +\t\treturn SCHEDULER_LAUNCHCTL;\n> +\telse if (!strcasecmp(value, \"schtasks\"))\n> +\t\treturn SCHEDULER_SCHTASKS;\n> +\telse\n> +\t\treturn SCHEDULER_INVALID;\n> +}\n> +\n> +static int maintenance_opt_scheduler(const struct option *opt, const char *arg,\n> +\t\t\t\t     int unset)\n> +{\n> +\tenum scheduler *scheduler = opt->value;\n> +\n> +\tif (unset)\n> +\t\tdie(_(\"--no-scheduler is not allowed\"));\n\nI think it's better to use OPT_CALLBACK_F in the options list and\nwe will write below code instead:\n\n\tBUG_ON_OPT_NEG(unset)\n\n> +\n> +\t*scheduler = parse_scheduler(arg);\n> +\n> +\tif (*scheduler == SCHEDULER_INVALID)\n> +\t\tdie(_(\"unrecognized --scheduler argument '%s'\"), arg);\n\nMost of other callbacks do this instead:\n\n\treturn error(_(\"messsage.... '%s'\"), arg);\n\n> +\n> +\treturn 0;\n> +}\n> +\n> +struct maintenance_start_opts {\n> +\tenum scheduler scheduler;\n> +};\n> +\n> +static void resolve_auto_scheduler(enum scheduler *scheduler)\n> +{\n> +\tif (*scheduler != SCHEDULER_AUTO)\n> +\t\treturn;\n> +\n>  #if defined(__APPLE__)\n> -static const char platform_scheduler[] = \"launchctl\";\n> +\t*scheduler = SCHEDULER_LAUNCHCTL;\n> +\treturn;\n> +\n>  #elif defined(GIT_WINDOWS_NATIVE)\n> -static const char platform_scheduler[] = \"schtasks\";\n> +\t*scheduler = SCHEDULER_SCHTASKS;\n> +\treturn;\n> +\n> +#elif defined(__linux__)\n> +\tif (is_systemd_timer_available(\"systemctl\"))\n> +\t\t*scheduler = SCHEDULER_SYSTEMD;\n> +\telse if (is_crontab_available(\"crontab\"))\n> +\t\t*scheduler = SCHEDULER_CRON;\n> +\telse\n> +\t\tdie(_(\"neither systemd timers nor crontab are available\"));\n> +\treturn;\n> +\n>  #else\n> -static const char platform_scheduler[] = \"crontab\";\n> +\t*scheduler = SCHEDULER_CRON;\n> +\treturn;\n>  #endif\n> +}\n>  \n> -static int update_background_schedule(int enable)\n> +static void validate_scheduler(enum scheduler scheduler)\n>  {\n> -\tint result;\n> -\tconst char *scheduler = platform_scheduler;\n> -\tconst char *cmd = scheduler;\n> +\tconst char *cmd;\n> +\n> +\tif (scheduler == SCHEDULER_INVALID)\n> +\t\tBUG(\"invalid scheduler\");\n> +\tif (scheduler == SCHEDULER_AUTO)\n> +\t\tBUG(\"resolve_auto_scheduler should have been called before\");\n> +\n> +\tcmd = scheduler_fn[scheduler].cmd;\n> +\tif (!scheduler_fn[scheduler].is_available(cmd))\n> +\t\tdie(_(\"%s scheduler is not available\"), cmd);\n> +}\n> +\n> +static int update_background_schedule(const struct maintenance_start_opts *opts,\n> +\t\t\t\t      int enable)\n> +{\n> +\tunsigned int i;\n> +\tint res, result = 0;\n> +\tenum scheduler scheduler;\n> +\tconst char *cmd = NULL;\n>  \tchar *testing;\n>  \tstruct lock_file lk;\n>  \tchar *lock_path = xstrfmt(\"%s/schedule\", the_repository->objects->odb->path);\n>  \n> +\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0)\n> +\t\treturn error(_(\"another process is scheduling background maintenance\"));\n> +\n>  \ttesting = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n>  \tif (testing) {\n>  \t\tchar *sep = strchr(testing, ':');\n>  \t\tif (!sep)\n>  \t\t\tdie(\"GIT_TEST_MAINT_SCHEDULER unparseable: %s\", testing);\n>  \t\t*sep = '\\0';\n> -\t\tscheduler = testing;\n> +\t\tscheduler = parse_scheduler(testing);\n>  \t\tcmd = sep + 1;\n> +\t\tresult = scheduler_fn[scheduler].update_schedule(\n> +\t\t\tenable, get_lock_file_fd(&lk), cmd);\n> +\t\tgoto done;\n>  \t}\n>  \n> -\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0)\n> -\t\treturn error(_(\"another process is scheduling background maintenance\"));\n> -\n> -\tif (!strcmp(scheduler, \"launchctl\"))\n> -\t\tresult = launchctl_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n> -\telse if (!strcmp(scheduler, \"schtasks\"))\n> -\t\tresult = schtasks_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n> -\telse if (!strcmp(scheduler, \"crontab\"))\n> -\t\tresult = crontab_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n> -\telse\n> -\t\tdie(\"unknown background scheduler: %s\", scheduler);\n> +\tfor (i = 1; i < ARRAY_SIZE(scheduler_fn); i++) {\n> +\t\tint enable_scheduler = enable && (opts->scheduler == i);\n> +\t\tcmd = scheduler_fn[i].cmd;\n> +\t\tif (!scheduler_fn[i].is_available(cmd))\n> +\t\t\tcontinue;\n> +\t\tres = scheduler_fn[i].update_schedule(\n> +\t\t\tenable_scheduler, get_lock_file_fd(&lk), cmd);\n> +\t\tif (enable_scheduler)\n> +\t\t\tresult = res;\n> +\t}\n>  \n> +done:\n>  \trollback_lock_file(&lk);\n>  \tfree(testing);\n>  \treturn result;\n>  }\n>  \n> -static int maintenance_start(void)\n> +static const char *const builtin_maintenance_start_usage[] = {\n> +\tN_(\"git maintenance start [--scheduler=<scheduler>]\"), NULL\n> +};\n> +\n> +static int maintenance_start(int argc, const char **argv, const char *prefix)\n>  {\n> +\tstruct maintenance_start_opts opts;\n> +\tstruct option builtin_maintenance_start_options[] = {\n> +\t\tOPT_CALLBACK(\n> +\t\t\t0, \"scheduler\", &opts.scheduler, N_(\"scheduler\"),\n> +\t\t\tN_(\"scheduler to use to trigger git maintenance run\"),\n> +\t\t\tmaintenance_opt_scheduler),\n\nFollowing up my comment above, we're better to use:\n\n\t\tOPT_CALLBACK_F(0, \"scheduler\", &opts.scheduler, N_(\"scheduler\"),\n\t\t\tN_(\"............\"),\n\t\t\tPARSE_OPT_NONEG, maintenance_opt_scheduler),\n\n> +\t\tOPT_END()\n> +\t};\n> +\tmemset(&opts, 0, sizeof(opts));\n> +\n> +\targc = parse_options(argc, argv, prefix,\n> +\t\t\t     builtin_maintenance_start_options,\n> +\t\t\t     builtin_maintenance_start_usage, 0);\n> +\n> +\tresolve_auto_scheduler(&opts.scheduler);\n> +\tvalidate_scheduler(opts.scheduler);\n> +\n> +\tif (argc > 0)\n> +\t\tusage_with_options(builtin_maintenance_start_usage,\n> +\t\t\t\t   builtin_maintenance_start_options);\n> +\n>  \tif (maintenance_register())\n>  \t\twarning(_(\"failed to add repo to global config\"));\n> -\n> -\treturn update_background_schedule(1);\n> +\treturn update_background_schedule(&opts, 1);\n>  }\n>  \n>  static int maintenance_stop(void)\n>  {\n> -\treturn update_background_schedule(0);\n> +\treturn update_background_schedule(NULL, 0);\n>  }\n>  \n>  static const char builtin_maintenance_usage[] =\tN_(\"git maintenance <subcommand> [<options>]\");\n> @@ -2027,7 +2354,7 @@ int cmd_maintenance(int argc, const char **argv, const char *prefix)\n>  \tif (!strcmp(argv[1], \"run\"))\n>  \t\treturn maintenance_run(argc - 1, argv + 1, prefix);\n>  \tif (!strcmp(argv[1], \"start\"))\n> -\t\treturn maintenance_start();\n> +\t\treturn maintenance_start(argc - 1, argv + 1, prefix);\n>  \tif (!strcmp(argv[1], \"stop\"))\n>  \t\treturn maintenance_stop();\n>  \tif (!strcmp(argv[1], \"register\"))\n> diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\n> index 2412d8c5c0..6e6316cd90 100755\n> --- a/t/t7900-maintenance.sh\n> +++ b/t/t7900-maintenance.sh\n> @@ -20,6 +20,20 @@ test_xmllint () {\n>  \tfi\n>  }\n>  \n> +test_lazy_prereq SYSTEMD_ANALYZE '\n> +\tsystemd-analyze --help >out &&\n> +\tgrep verify out\n> +'\n> +\n> +test_systemd_analyze_verify () {\n> +\tif test_have_prereq SYSTEMD_ANALYZE\n> +\tthen\n> +\t\tsystemd-analyze verify \"$@\"\n> +\telse\n> +\t\ttrue\n\nThe \"else\" leg is not necessary.\n\n> +\tfi\n> +}\n> +\n>  test_expect_success 'help text' '\n>  \ttest_expect_code 129 git maintenance -h 2>err &&\n>  \ttest_i18ngrep \"usage: git maintenance <subcommand>\" err &&\n> @@ -615,6 +629,43 @@ test_expect_success 'start and stop Windows maintenance' '\n>  \ttest_cmp expect args\n>  '\n>  \n> +test_expect_success 'start and stop Linux/systemd maintenance' '\n> +\twrite_script print-args <<-\\EOF &&\n> +\techo $* >>args\n\nTo avoid any possible incompatibility with zillion echo implementation\nout there. printf should be prefered over echo. Not a in this test\ncase, however, it costs us nothing anyway.\n\n\tprintf \"%s\\n\" \"$*\"\n\n> +\tEOF\n> +\n> +\trm -f args &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"systemd:./print-args\" git maintenance start &&\n> +\n> +\t# start registers the repo\n> +\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n> +\n> +\ttest_systemd_analyze_verify \"$HOME/.config/systemd/user/git-maintenance@.service\" &&\n> +\n> +\trm -f expect &&\n> +\tfor frequency in hourly daily weekly\n> +\tdo\n> +\t\techo \"--user enable --now git-maintenance@${frequency}.timer\" >>expect || return 1\n> +\tdone &&\n\nAnd here, we can have a nicer syntax with printf:\n\n\tprintf \"--user enable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n\nWith printf, we don't even need \"rm -f expect\" above.\n\n> +\ttest_cmp expect args &&\n> +\n> +\trm -f args &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"systemd:./print-args\" git maintenance stop &&\n> +\n> +\t# stop does not unregister the repo\n> +\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n> +\n> +\ttest_path_is_missing \"$HOME/.config/systemd/user/git-maintenance@.timer\" &&\n> +\ttest_path_is_missing \"$HOME/.config/systemd/user/git-maintenance@.service\" &&\n> +\n> +\trm -f expect &&\n> +\tfor frequency in hourly daily weekly\n> +\tdo\n> +\t\techo \"--user disable --now git-maintenance@${frequency}.timer\" >>expect || return 1\n> +\tdone &&\n\nDitto.\n\nAll of this was written without testing, because I don't have any\nsystemd based system near my hand, right now.\n\nSo, please take it with a grain of salt.\n\n\n-- \nDanh\n"},{"id":"423988","messageId":"CAPig+cQdU_0uPKDkbuz3XqdYePAMNPcF_u+Enx+qfgRGvtiseA@mail.gmail.com","threadId":"55603","inReplyTo":"YJiKXRywBhhGXC5Q@danh.dev","subject":"Re: [PATCH v2 1/1] maintenance: use systemd timers on Linux","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2021-05-10T02:48:17Z","receivedAt":"2021-05-10T02:48:31Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Sun, May 9, 2021 at 9:20 PM Đoàn Trần Công Danh <congdanhqx@gmail.com> wrote:\n> On 2021-05-09 23:32:17+0200, Lénaïc Huard <lenaic@lhuard.fr> wrote:\n> > +static int systemd_timer_delete_units(const char *cmd)\n> > +{\n> > +     return systemd_timer_enable_unit(0, SCHEDULE_HOURLY, cmd) ||\n> > +            systemd_timer_enable_unit(0, SCHEDULE_DAILY, cmd) ||\n> > +            systemd_timer_enable_unit(0, SCHEDULE_WEEKLY, cmd) ||\n> > +            systemd_timer_delete_unit_templates();\n> > +}\n>\n> Argh, we're disabling those systemd timer units first, by passing 0 as\n> first argument of systemd_timer_delete_units.\n>\n> The fact that I read that twice, and still wrote down above reply\n> makes me think that above code is not self-explanatory enough.\n> May we switch to something else? Let's say using enum?\n\nThis is modeled after existing scheduler functions in this file, in\nwhich the `enable` argument is a simple 0 or 1, so changing this to an\nenum just for this function would be inconsistent. Changing all the\nfunctions to `enum` in a preparatory patch could indeed improve\nreadability, however, that's tangential cleanup which may be outside\nthe scope of this submission.\n\n> > +     file = xfopen(filename, \"w\");\n> > +     free(filename);\n>\n> I'm sure if we should use FREE_AND_NULL(filename) instead?\n> Since, filename will be reused later.\n\nIndeed, probably a good idea, as it would make catching mistakes easier.\n\n> > +            \"ExecStart=\\\"%1$s/git\\\" --exec-path=\\\"%1$s\\\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\\n\"\n>\n> I think others have strong opinion on not using \"%1$s\",\n> and prefer simple \"%s\" and using \"exec_path\" twice instead.\n\nI brought it up only because I hadn't seen it in Git sources, and\nwasn't sure if we'd want to start using it. Aside from Ævar, who\nseemed reasonably in favor of it, nobody else chimed in, so it could\ngo either way, I suppose.\n\n> > +test_systemd_analyze_verify () {\n> > +     if test_have_prereq SYSTEMD_ANALYZE\n> > +     then\n> > +             systemd-analyze verify \"$@\"\n> > +     else\n> > +             true\n>\n> The \"else\" leg is not necessary.\n\nThis was patterned after the existing test_xmllint() function in this\nfile which has the `else true` leg. I wrote test_xmllint(), so I'll\ntake blame. But you're right, the `if` will return success if the\nprerequisite is not set, so the `else` leg is indeed not needed.\n(Cleaning up the test_xmllint() function is outside the scope of this\npatch.)\n\n> > +test_expect_success 'start and stop Linux/systemd maintenance' '\n> > +     write_script print-args <<-\\EOF &&\n> > +     echo $* >>args\n>\n> To avoid any possible incompatibility with zillion echo implementation\n> out there. printf should be prefered over echo. Not a in this test\n> case, however, it costs us nothing anyway.\n>\n>         printf \"%s\\n\" \"$*\"\n\nThis, too, is patterned after existing auxiliary scripts created by\nthese test functions, and you're correct that it's potentially\ndangerous. A manual inspection of all the existing instances shows\nthat `echo $*` happens to be safe for those cases but that doesn't\nexcuse being sloppy about it, so the existing cases probably ought to\nbe cleaned up. But, again, that is outside the scope of this series.\nFor this particular case, though...\n\n> > +     for frequency in hourly daily weekly\n> > +     do\n> > +             echo \"--user enable --now git-maintenance@${frequency}.timer\" >>expect || return 1\n> > +     done &&\n>\n> And here, we can have a nicer syntax with printf:\n>\n>         printf \"--user enable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n>\n> With printf, we don't even need \"rm -f expect\" above.\n\n... you're quite correct that `printf` is the way to go both here and\nin the generated `print-args` script since these arguments start with\nhyphen.\n"},{"id":"424002","messageId":"xmqqtunbqf5o.fsf@gitster.g","threadId":"55603","inReplyTo":"CAPig+cQdU_0uPKDkbuz3XqdYePAMNPcF_u+Enx+qfgRGvtiseA@mail.gmail.com","subject":"Re: [PATCH v2 1/1] maintenance: use systemd timers on Linux","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-05-10T06:25:07Z","receivedAt":"2021-05-10T06:25:13Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Eric Sunshine <sunshine@sunshineco.com> writes:\n\n>> I think others have strong opinion on not using \"%1$s\",\n>> and prefer simple \"%s\" and using \"exec_path\" twice instead.\n>\n> I brought it up only because I hadn't seen it in Git sources, and\n> wasn't sure if we'd want to start using it. Aside from Ævar, who\n> seemed reasonably in favor of it, nobody else chimed in, so it could\n> go either way, I suppose.\n\nIf this were a piece of code that _everybody_ would use on _all_ the\nsupported platforms, I would suggest declaring that this is a\nweather-balloon to see if some platforms have trouble using it.  But\nunfortunately this is not such a piece of code.  Dependence on\nsystemd should strictly be opt-in.\n\nSo my preference is\n\n - here, just do it in the dumb and simple way\n\n - somewhere else, find code that is compiled and run for everybody\n   on all platforms that feeds two same arguments to printf format,\n   and update it to use \"%1$x\" twice, mark it clearly as a weather\n   balloon, and document it (see how what 512f41cf did is documented\n   in Documentation/CodingGuidelines and mimick, but tone it down as\n   we haven't declared it safe to use (yet).\n\nIt is likely that we need rearrangement of argument order for po/\nfiles anyway, but a misimplementation might not handle using the\nsame placeholder twice, and that is why I'd like to be a bit extra\ncareful.\n"},{"id":"424031","messageId":"87fsyuu4dj.fsf@evledraar.gmail.com","threadId":"55603","inReplyTo":"2224656.qFoTrlakfJ@coruscant.lhuard.fr","subject":"Re: [PATCH] maintenance: use systemd timers on Linux","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-10T13:03:50Z","receivedAt":"2021-05-10T13:41:17Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Mon, May 10 2021, Lénaïc Huard wrote:\n\n> Le mercredi 5 mai 2021, 14:01:25 CEST Ævar Arnfjörð Bjarmason a écrit :\n>> On Sun, May 02 2021, Eric Sunshine wrote:\n>> > On Sat, May 1, 2021 at 10:59 AM Lénaïc Huard <lenaic@lhuard.fr> wrote:\n>> >> +       strvec_push(&child.args, \"--now\");\n>> >> +       strvec_pushf(&child.args, \"git-maintenance@%s.timer\", frequency);\n>> >> +\n>> >> +       if (start_command(&child))\n>> >> +               die(_(\"failed to run systemctl\"));\n>> >> +       return finish_command(&child);\n>> >> +}\n>> >> +static int systemd_timer_write_unit_templates(const char *exec_path)\n>> >> +{\n>> >> +       unit = \"[Unit]\\n\"\n>> >> +              \"Description=Optimize Git repositories data\\n\"\n>> >> +              \"\\n\"\n>> >> +              \"[Service]\\n\"\n>> >> +              \"Type=oneshot\\n\"\n>> >> +              \"ExecStart=\\\"%1$s/git\\\" --exec-path=\\\"%1$s\\\" for-each-repo\n>> >> --config=maintenance.repo maintenance run --schedule=%%i\\n\"> \n>> > I see that it's in POSIX, but do we use this `%n$s` directive\n>> > elsewhere in the Git source code? If not, I'd be cautious of\n>> > introducing it here. Maybe it's better to just use plain `%s` twice...\n>> \n>> We use it in po/, so for sprintf() on systems that don't have\n>> NO_GETTEXT=Y we already test it in the wild.\n>> \n>> But no, I don't think anything in the main source uses it, FWIW I have a\n>> WIP series in my own fork that I've cooked for a while that uses this, I\n>> haven't run into any issues with it in either GitHub's CI\n>> (e.g. Windows), or on the systems I myself test on.\n>> \n>> I think it would be a useful canary to just take a change like this, we\n>> can always change it to the form you suggest if it doesn't work out.\n>\n> Based on this latest comment, I left the `%n$s` directive in the v2 of the \n> patch.\n>\n> Let me know if that’s still OK. Otherwise, I’d be happy to implement Eric’s \n> suggestion.\n>\n> Note however that this would be a “poor” canary to check if that directive is \n> supported on all the platforms on which git has been ported.\n> Indeed, this code is executed only on systemd platforms, which means quite \n> recent Linux systems.\n> Should this directive not be supported, I suppose it would be on more exotic \n> systems.\n\nIndeed, although we compile it on non-Linux platforms, so we'd expect to\nget complaints from smarter non-Linux compilers if fprintf() is given an\nunknown formatting directive.\n"},{"id":"424062","messageId":"3fd17223-8667-24be-2e65-f1970d411bdf@gmail.com","threadId":"55603","inReplyTo":"20210509213217.449489-2-lenaic@lhuard.fr","subject":"Re: [PATCH v2 1/1] maintenance: use systemd timers on Linux","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2021-05-10T18:03:58Z","receivedAt":"2021-05-10T18:04:04Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Lénaïc\n\nThanks for updating the patch, I've left some comments below. Aside from \none possible bug, they mostly revolve around not passing the name of \nscheduler command around when that name is fixed and functions which do \nnot check for errors but return a success/failure value (either they \nshould check the return values of the system calls they make or be \ndeclared as void if it is safe to ignore the failures)\n\nOn 09/05/2021 22:32, Lénaïc Huard wrote:\n> The existing mechanism for scheduling background maintenance is done\n> through cron. On Linux systems managed by systemd, systemd provides an\n> alternative to schedule recurring tasks: systemd timers.\n> \n> The main motivations to implement systemd timers in addition to cron\n> are:\n> * cron is optional and Linux systems running systemd might not have it\n>    installed.\n> * The execution of `crontab -l` can tell us if cron is installed but not\n>    if the daemon is actually running.\n> * With systemd, each service is run in its own cgroup and its logs are\n>    tagged by the service inside journald. With cron, all scheduled tasks\n>    are running in the cron daemon cgroup and all the logs of the\n>    user-scheduled tasks are pretended to belong to the system cron\n>    service.\n>    Concretely, a user that doesn’t have access to the system logs won’t\n>    have access to the log of its own tasks scheduled by cron whereas he\n>    will have access to the log of its own tasks scheduled by systemd\n>    timer.\n>    Although `cron` attempts to send email, that email may go unseen by\n>    the user because these days, local mailboxes are not heavily used\n>    anymore.\n> \n> In order to choose which scheduler to use between `cron` and user\n> systemd timers, a new option\n> `--scheduler=auto|cron|systemd|launchctl|schtasks` has been added to\n> `git maintenance start`.\n> When `git maintenance start --scheduler=XXX` is run, it not only\n> registers `git maintenance run` tasks in the scheduler XXX, it also\n> removes the `git maintenance run` tasks from all the other schedulers\n\nI'm not sure it is actually doing that at the moment - see my comment in \nupdate_background_schedule()\n\n> to\n> ensure we cannot have two schedulers launching concurrent identical\n> tasks.\n> \n> The default value is `auto` which chooses a suitable scheduler for the\n> system.\n> On Linux, if user systemd timers are available, they will be used as git\n> maintenance scheduler. If not, `cron` will be used if it is available.\n> If none is available, it will fail.\n\nI think defaulting to systemd timers when both systemd and cron are \ninstalled is sensible given the problems associated with testing whether \ncrond is actually running in that scenario.\n\n> `git maintenance stop` doesn't have any `--scheduler` parameter because\n> this command will try to remove the `git maintenance run` tasks from all\n> the available schedulers.\n> \n> In order to schedule git maintenance, we need two unit template files:\n> * ~/.config/systemd/user/git-maintenance@.service\n>    to define the command to be started by systemd and\n> * ~/.config/systemd/user/git-maintenance@.timer\n>    to define the schedule at which the command should be run.\n> \n> Those units are templates that are parametrized by the frequency.\n> \n> Based on those templates, 3 timers are started:\n> * git-maintenance@hourly.timer\n> * git-maintenance@daily.timer\n> * git-maintenance@weekly.timer\n> \n> The command launched by those three timers are the same than with the\n> other scheduling methods:\n> \n> git for-each-repo --config=maintenance.repo maintenance run\n> --schedule=%i\n> \n> with the full path for git to ensure that the version of git launched\n> for the scheduled maintenance is the same as the one used to run\n> `maintenance start`.\n> \n> The timer unit contains `Persistent=true` so that, if the computer is\n> powered down when a maintenance task should run, the task will be run\n> when the computer is back powered on.\n> \n> Signed-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n> ---\n>   Documentation/git-maintenance.txt |  60 +++++\n>   builtin/gc.c                      | 375 ++++++++++++++++++++++++++++--\n>   t/t7900-maintenance.sh            |  51 ++++\n>   3 files changed, 462 insertions(+), 24 deletions(-)\n> \n> diff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\n> index 80ddd33ceb..f012923333 100644\n> --- a/Documentation/git-maintenance.txt\n> +++ b/Documentation/git-maintenance.txt\n> @@ -181,6 +181,20 @@ OPTIONS\n>   \t`maintenance.<task>.enabled` configured as `true` are considered.\n>   \tSee the 'TASKS' section for the list of accepted `<task>` values.\n>   \n> +--scheduler=auto|crontab|systemd-timer|launchctl|schtasks::\n> +\tWhen combined with the `start` subcommand, specify the scheduler\n> +\tto use to run the hourly, daily and weekly executions of\n> +\t`git maintenance run`.\n> +\tThe possible values for `<scheduler>` depend on the system: `crontab`\n> +\tis available on POSIX systems, `systemd-timer` is available on Linux\n> +\tsystems, `launchctl` is available on MacOS and `schtasks` is available\n> +\ton Windows.\n> +\tBy default or when `auto` is specified, the most appropriate scheduler\n> +\tfor the system is used. On MacOS, `launchctl` is used. On Windows,\n> +\t`schtasks` is used. On Linux, `systemd-timers` is used if user systemd\n> +\ttimers are available, otherwise, `crontab` is used. On all other systems,\n> +\t`crontab` is used.\n> +\n>   \n>   TROUBLESHOOTING\n>   ---------------\n> @@ -279,6 +293,52 @@ schedule to ensure you are executing the correct binaries in your\n>   schedule.\n>   \n>   \n> +BACKGROUND MAINTENANCE ON LINUX SYSTEMD SYSTEMS\n> +-----------------------------------------------\n> +\n> +While Linux supports `cron`, depending on the distribution, `cron` may\n> +be an optional package not necessarily installed. On modern Linux\n> +distributions, systemd timers are superseding it.\n> +\n> +If user systemd timers are available, they will be used as a replacement\n> +of `cron`.\n> +\n> +In this case, `git maintenance start` will create user systemd timer units\n> +and start the timers. The current list of user-scheduled tasks can be found\n> +by running `systemctl --user list-timers`. The timers written by `git\n> +maintenance start` are similar to this:\n> +\n> +-----------------------------------------------------------------------\n> +$ systemctl --user list-timers\n> +NEXT                         LEFT          LAST                         PASSED     UNIT                         ACTIVATES\n> +Thu 2021-04-29 19:00:00 CEST 42min left    Thu 2021-04-29 18:00:11 CEST 17min ago  git-maintenance@hourly.timer git-maintenance@hourly.service\n> +Fri 2021-04-30 00:00:00 CEST 5h 42min left Thu 2021-04-29 00:00:11 CEST 18h ago    git-maintenance@daily.timer  git-maintenance@daily.service\n> +Mon 2021-05-03 00:00:00 CEST 3 days left   Mon 2021-04-26 00:00:11 CEST 3 days ago git-maintenance@weekly.timer git-maintenance@weekly.service\n> +-----------------------------------------------------------------------\n> +\n> +One timer is registered for each `--schedule=<frequency>` option.\n> +\n> +The definition of the systemd units can be inspected in the following files:\n> +\n> +-----------------------------------------------------------------------\n> +~/.config/systemd/user/git-maintenance@.timer\n> +~/.config/systemd/user/git-maintenance@.service\n> +~/.config/systemd/user/timers.target.wants/git-maintenance@hourly.timer\n> +~/.config/systemd/user/timers.target.wants/git-maintenance@daily.timer\n> +~/.config/systemd/user/timers.target.wants/git-maintenance@weekly.timer\n> +-----------------------------------------------------------------------\n> +\n> +`git maintenance start` will overwrite these files and start the timer\n> +again with `systemctl --user`, so any customization should be done by\n> +creating a drop-in file, i.e. a `.conf` suffixed file in the\n> +`~/.config/systemd/user/git-maintenance@.service.d` directory.\n> +\n> +`git maintenance stop` will stop the user systemd timers and delete\n> +the above mentioned files.\n> +\n> +For more details, see systemd.timer(5)\n> +\n> +\n>   BACKGROUND MAINTENANCE ON MACOS SYSTEMS\n>   ---------------------------------------\n>   \n> diff --git a/builtin/gc.c b/builtin/gc.c\n> index ef7226d7bc..7c72aa3b99 100644\n> --- a/builtin/gc.c\n> +++ b/builtin/gc.c\n> @@ -1544,6 +1544,15 @@ static const char *get_frequency(enum schedule_priority schedule)\n>   \t}\n>   }\n>   \n> +static int is_launchctl_available(const char *cmd)\n\nNone of these is_..._available() function needs the cmd parameter. It \nmatches the existing pattern of the existing ..._update_schedule() \nfunctions but they don't really need the cmd argument either so I would \ndrop the argument for the new functions.\n\n> +{\n> +#ifdef __APPLE__\n> +\treturn 1;\n> +#else\n> +\treturn 0;\n> +#endif\n> +}\n> +\n>   static char *launchctl_service_name(const char *frequency)\n>   {\n>   \tstruct strbuf label = STRBUF_INIT;\n> @@ -1710,6 +1719,15 @@ static int launchctl_update_schedule(int run_maintenance, int fd, const char *cm\n>   \t\treturn launchctl_remove_plists(cmd);\n>   }\n>   \n> +static int is_schtasks_available(const char *cmd)\n> +{\n> +#ifdef GIT_WINDOWS_NATIVE\n> +\treturn 1;\n> +#else\n> +\treturn 0;\n> +#endif\n> +}\n> +\n>   static char *schtasks_task_name(const char *frequency)\n>   {\n>   \tstruct strbuf label = STRBUF_INIT;\n> @@ -1872,6 +1890,28 @@ static int schtasks_update_schedule(int run_maintenance, int fd, const char *cmd\n>   \t\treturn schtasks_remove_tasks(cmd);\n>   }\n>   \n> +static int is_crontab_available(const char *cmd)\n> +{\n> +\tstatic int cached_result = -1;\n> +\tstruct child_process child = CHILD_PROCESS_INIT;\n> +\n> +\tif (cached_result != -1)\n> +\t\treturn cached_result;\n> +\n> +\tstrvec_split(&child.args, cmd);\n> +\tstrvec_push(&child.args, \"-l\");\n> +\tchild.no_stdin = 1;\n> +\tchild.no_stdout = 1;\n> +\tchild.no_stderr = 1;\n> +\tchild.silent_exec_failure = 1;\n> +\n> +\tif (start_command(&child))\n> +\t\treturn cached_result = 0;\n> +\t/* Ignore exit code, as an empty crontab will return error. */\n> +\tfinish_command(&child);\n> +\treturn cached_result = 1;\n> +}\n> +\n>   #define BEGIN_LINE \"# BEGIN GIT MAINTENANCE SCHEDULE\"\n>   #define END_LINE \"# END GIT MAINTENANCE SCHEDULE\"\n>   \n> @@ -1959,61 +1999,348 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n>   \treturn result;\n>   }\n>   \n> +static int is_systemd_timer_available(const char *cmd)\n> +{\n> +#ifdef __linux__\n> +\tstatic int cached_result = -1;\n> +\tstruct child_process child = CHILD_PROCESS_INIT;\n> +\n> +\tif (cached_result != -1)\n> +\t\treturn cached_result;\n> +\n> +\tstrvec_split(&child.args, cmd);\n> +\tstrvec_pushl(&child.args, \"--user\", \"list-timers\", NULL);\n> +\tchild.no_stdin = 1;\n> +\tchild.no_stdout = 1;\n> +\tchild.no_stderr = 1;\n> +\tchild.silent_exec_failure = 1;\n> +\n> +\tif (start_command(&child))\n> +\t\treturn cached_result = 0;\n\nThis is maybe a bit too clever. It would be clearer to separate the \nassignment of the cached result and the return statement.\n\n> +\tif (finish_command(&child))\n> +\t\treturn cached_result = 0;\n> +\treturn cached_result = 1;\n> +#else\n> +\treturn 0;\n> +#endif\n> +}\n> +\n> +static char *xdg_config_home_systemd(const char *filename)\n> +{\n> +\tconst char *home, *config_home;\n> +\n> +\tassert(filename);\n> +\tconfig_home = getenv(\"XDG_CONFIG_HOME\");\n> +\tif (config_home && *config_home)\n> +\t\treturn mkpathdup(\"%s/systemd/user/%s\", config_home, filename);\n> +\n> +\thome = getenv(\"HOME\");\n> +\tif (home)\n> +\t\treturn mkpathdup(\"%s/.config/systemd/user/%s\", home, filename);\n> +\n> +\tdie(_(\"failed to get $XDG_CONFIG_HOME and $HOME\"));\n> +}\n\nThis is largely a copy of xdg_config_home(), I would prefer to see this \nfunction calling that rather than duplicating it.\n\nstatic char *xdg_config_home_systemd(const char *filename)\n{\n     struct strbuf buf = STRBUF_INIT;\n     char *path;\n\n     strbuf_addf(&buf, \"systemd/user/%s\", filename);\n     path = xdg_config_home(buf.buf);\n     strbuf_release(&buf);\n     return path;\n}\n\nEven better would be to modify xdg_config_home() to take a format string.\n\n> +static int systemd_timer_enable_unit(int enable,\n> +\t\t\t\t     enum schedule_priority schedule,\n> +\t\t\t\t     const char *cmd)\n\nThe cmd argument is pointless, it will always be \"systemctl\" and you \nhave even hard coded that value into the error message below.\n\n> +{\n> +\tstruct child_process child = CHILD_PROCESS_INIT;\n> +\tconst char *frequency = get_frequency(schedule);\n> +\n> +\tstrvec_split(&child.args, cmd);\n> +\tstrvec_pushl(&child.args, \"--user\", enable ? \"enable\" : \"disable\",\n> +\t\t     \"--now\", NULL);\n> +\tstrvec_pushf(&child.args, \"git-maintenance@%s.timer\", frequency);\n> +\n> +\tif (start_command(&child))\n> +\t\tdie(_(\"failed to run systemctl\"));\n> +\treturn finish_command(&child);\n\nThere is a strange mix of dying and returning an error here. Also should \nthis be printing a message if finish_command() returns a non-zero value? \nIf systemctl fails then there is not much we can do to recover so dying \non any error might be ok unless we need to perform some cleanup if it \nfails like removing the timer unit files.\n\nWhat is the exit code of systemctl if a unit is already enabled and we \ntry to enbale it again (and the same for disabling a disabled unit)?\n\n> +}\n> +\n> +static int systemd_timer_delete_unit_templates(void)\n> +{\n> +\tchar *filename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n> +\tunlink(filename);\n\nThis is missing error handling (and below). If there is a good reason to \nignore the errors then there is no point in returning a value from this \nfunction\n\n> +\tfree(filename);\n> +\n> +\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n> +\tunlink(filename);\n> +\tfree(filename);\n> +\n> +\treturn 0;\n> +}\n> +\n> +static int systemd_timer_delete_units(const char *cmd)\n> +{\n> +\treturn systemd_timer_enable_unit(0, SCHEDULE_HOURLY, cmd) ||\n> +\t       systemd_timer_enable_unit(0, SCHEDULE_DAILY, cmd) ||\n> +\t       systemd_timer_enable_unit(0, SCHEDULE_WEEKLY, cmd) ||\n> +\t       systemd_timer_delete_unit_templates();\n> +}\n> +\n> +static int systemd_timer_write_unit_templates(const char *exec_path)\n> +{\n> +\tchar *filename;\n> +\tFILE *file;\n> +\tconst char *unit;\n> +\n> +\tfilename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n> +\tif (safe_create_leading_directories(filename))\n> +\t\tdie(_(\"failed to create directories for '%s'\"), filename);\n> +\tfile = xfopen(filename, \"w\");\n> +\tfree(filename);\n> +\n> +\tunit = \"# This file was created and is maintained by Git.\\n\"\n> +\t       \"# Any edits made in this file might be replaced in the future\\n\"\n> +\t       \"# by a Git command.\\n\"\n> +\t       \"\\n\"\n> +\t       \"[Unit]\\n\"\n> +\t       \"Description=Optimize Git repositories data\\n\"\n> +\t       \"\\n\"\n> +\t       \"[Timer]\\n\"\n> +\t       \"OnCalendar=%i\\n\"\n> +\t       \"Persistent=true\\n\"\n> +\t       \"\\n\"\n> +\t       \"[Install]\\n\"\n> +\t       \"WantedBy=timers.target\\n\";\n> +\tfputs(unit, file);\n> +\tfclose(file);\n> +\n> +\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n> +\tif (safe_create_leading_directories(filename))\n\nHaven't we already created this path if it was missing above for the \nfirst file?\n\n> +\t\tdie(_(\"failed to create directories for '%s'\"), filename);\n> +\tfile = xfopen(filename, \"w\");\n\nDo we want to try and remove the first file if we cannot write the \nsecond file to leave the file system in a consitent state? If so you'll \nneed to use something like fopen_or_warn() and check the return value.\n\n> +\tfree(filename);\n> +\n> +\tunit = \"# This file was created and is maintained by Git.\\n\"\n> +\t       \"# Any edits made in this file might be replaced in the future\\n\"\n> +\t       \"# by a Git command.\\n\"\n> +\t       \"\\n\"\n> +\t       \"[Unit]\\n\"\n> +\t       \"Description=Optimize Git repositories data\\n\"\n> +\t       \"\\n\"\n> +\t       \"[Service]\\n\"\n> +\t       \"Type=oneshot\\n\"\n> +\t       \"ExecStart=\\\"%1$s/git\\\" --exec-path=\\\"%1$s\\\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\\n\"\n> +\t       \"LockPersonality=yes\\n\"\n> +\t       \"MemoryDenyWriteExecute=yes\\n\"\n> +\t       \"NoNewPrivileges=yes\\n\"\n> +\t       \"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\\n\"\n> +\t       \"RestrictNamespaces=yes\\n\"\n> +\t       \"RestrictRealtime=yes\\n\"\n> +\t       \"RestrictSUIDSGID=yes\\n\"\n\nAfter a quick read of the systemd.exec man page it is unclear to me if \nthese Restrict... lines are needed as we already have \nNoNewPrivileges=yes - maybe they have some effect if `git maintence` is \nrun as root?\n\n> +\t       \"SystemCallArchitectures=native\\n\"\n> +\t       \"SystemCallFilter=@system-service\\n\";\n> +\tfprintf(file, unit, exec_path);\n> +\tfclose(file);\n> +\n> +\treturn 0;\n\nAs we don't return any errors but die instead there is no need to return \na value. On the other hand maybe we should be checking the return values \nof fclose() and possibly fputs() / fprint().\n\n> +}\n> +\n> +static int systemd_timer_setup_units(const char *cmd)\n> +{\n> +\tconst char *exec_path = git_exec_path();\n> +\n> +\treturn systemd_timer_write_unit_templates(exec_path) ||\n> +\t       systemd_timer_enable_unit(1, SCHEDULE_HOURLY, cmd) ||\n> +\t       systemd_timer_enable_unit(1, SCHEDULE_DAILY, cmd) ||\n> +\t       systemd_timer_enable_unit(1, SCHEDULE_WEEKLY, cmd);\n\nIf any step fails do we need to try and reverse the preceding steps to \navoid leaving so units enabled but not others. In practice this is \nprobably pretty unlikely so maybe we don't need to worry.\n\n> +}\n> +\n> +static int systemd_timer_update_schedule(int run_maintenance, int fd,\n> +\t\t\t\t\t const char *cmd)\n\nNo need for the cmd argument\n\n> +{\n> +\tif (run_maintenance)\n> +\t\treturn systemd_timer_setup_units(cmd);\n> +\telse\n> +\t\treturn systemd_timer_delete_units(cmd);\n> +}\n> +\n> +enum scheduler {\n> +\tSCHEDULER_INVALID = -1,\n> +\tSCHEDULER_AUTO = 0,\n> +\tSCHEDULER_CRON = 1,\n> +\tSCHEDULER_SYSTEMD = 2,\n> +\tSCHEDULER_LAUNCHCTL = 3,\n> +\tSCHEDULER_SCHTASKS = 4,\n> +};\n> +\n> +static const struct {\n> +\tint (*is_available)(const char *cmd);\n> +\tint (*update_schedule)(int run_maintenance, int fd, const char *cmd);\n> +\tconst char *cmd;\n> +} scheduler_fn[] = {\n> +\t[SCHEDULER_CRON] = { is_crontab_available, crontab_update_schedule,\n> +\t\t\t     \"crontab\" },\n> +\t[SCHEDULER_SYSTEMD] = { is_systemd_timer_available,\n> +\t\t\t\tsystemd_timer_update_schedule, \"systemctl\" },\n> +\t[SCHEDULER_LAUNCHCTL] = { is_launchctl_available,\n> +\t\t\t\t  launchctl_update_schedule, \"launchctl\" },\n> +\t[SCHEDULER_SCHTASKS] = { is_schtasks_available,\n> +\t\t\t\t schtasks_update_schedule, \"schtasks\" },\n> +};\n> +\n> +static enum scheduler parse_scheduler(const char *value)\n> +{\n> +\tif (!value)\n> +\t\treturn SCHEDULER_INVALID;\n> +\telse if (!strcasecmp(value, \"auto\"))\n> +\t\treturn SCHEDULER_AUTO;\n> +\telse if (!strcasecmp(value, \"cron\") || !strcasecmp(value, \"crontab\"))\n> +\t\treturn SCHEDULER_CRON;\n> +\telse if (!strcasecmp(value, \"systemd\") ||\n> +\t\t !strcasecmp(value, \"systemd-timer\"))\n> +\t\treturn SCHEDULER_SYSTEMD;\n> +\telse if (!strcasecmp(value, \"launchctl\"))\n> +\t\treturn SCHEDULER_LAUNCHCTL;\n> +\telse if (!strcasecmp(value, \"schtasks\"))\n> +\t\treturn SCHEDULER_SCHTASKS;\n> +\telse\n> +\t\treturn SCHEDULER_INVALID;\n> +}\n> +\n> +static int maintenance_opt_scheduler(const struct option *opt, const char *arg,\n> +\t\t\t\t     int unset)\n> +{\n> +\tenum scheduler *scheduler = opt->value;\n> +\n> +\tif (unset)\n> +\t\tdie(_(\"--no-scheduler is not allowed\"));\n\nAs others have said it would be better to BUG() here and pass \nPARSE_OPT_NONEG when defining the option below.\n\n> +\n> +\t*scheduler = parse_scheduler(arg);\n> +\n> +\tif (*scheduler == SCHEDULER_INVALID)\n> +\t\tdie(_(\"unrecognized --scheduler argument '%s'\"), arg);\n> +\n> +\treturn 0;\n> +}\n> +\n> +struct maintenance_start_opts {\n> +\tenum scheduler scheduler;\n> +};\n> +\n> +static void resolve_auto_scheduler(enum scheduler *scheduler)\n> +{\n> +\tif (*scheduler != SCHEDULER_AUTO)\n> +\t\treturn;\n> +\n>   #if defined(__APPLE__)\n> -static const char platform_scheduler[] = \"launchctl\";\n> +\t*scheduler = SCHEDULER_LAUNCHCTL;\n> +\treturn;\n> +\n>   #elif defined(GIT_WINDOWS_NATIVE)\n> -static const char platform_scheduler[] = \"schtasks\";\n> +\t*scheduler = SCHEDULER_SCHTASKS;\n> +\treturn;\n> +\n> +#elif defined(__linux__)\n> +\tif (is_systemd_timer_available(\"systemctl\"))\n> +\t\t*scheduler = SCHEDULER_SYSTEMD;\n> +\telse if (is_crontab_available(\"crontab\"))\n> +\t\t*scheduler = SCHEDULER_CRON;\n> +\telse\n> +\t\tdie(_(\"neither systemd timers nor crontab are available\"));\n> +\treturn;\n> +\n>   #else\n> -static const char platform_scheduler[] = \"crontab\";\n> +\t*scheduler = SCHEDULER_CRON;\n> +\treturn;\n>   #endif\n> +}\n>   \n> -static int update_background_schedule(int enable)\n> +static void validate_scheduler(enum scheduler scheduler)\n>   {\n> -\tint result;\n> -\tconst char *scheduler = platform_scheduler;\n> -\tconst char *cmd = scheduler;\n> +\tconst char *cmd;\n> +\n> +\tif (scheduler == SCHEDULER_INVALID)\n> +\t\tBUG(\"invalid scheduler\");\n> +\tif (scheduler == SCHEDULER_AUTO)\n> +\t\tBUG(\"resolve_auto_scheduler should have been called before\");\n> +\n> +\tcmd = scheduler_fn[scheduler].cmd;\n> +\tif (!scheduler_fn[scheduler].is_available(cmd))\n> +\t\tdie(_(\"%s scheduler is not available\"), cmd);\n> +}\n> +\n> +static int update_background_schedule(const struct maintenance_start_opts *opts,\n> +\t\t\t\t      int enable)\n> +{\n> +\tunsigned int i;\n> +\tint res, result = 0;\n> +\tenum scheduler scheduler;\n> +\tconst char *cmd = NULL;\n>   \tchar *testing;\n>   \tstruct lock_file lk;\n>   \tchar *lock_path = xstrfmt(\"%s/schedule\", the_repository->objects->odb->path);\n>   \n> +\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0)\n> +\t\treturn error(_(\"another process is scheduling background maintenance\"));\n> +\n>   \ttesting = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n>   \tif (testing) {\n>   \t\tchar *sep = strchr(testing, ':');\n>   \t\tif (!sep)\n>   \t\t\tdie(\"GIT_TEST_MAINT_SCHEDULER unparseable: %s\", testing);\n>   \t\t*sep = '\\0';\n> -\t\tscheduler = testing;\n> +\t\tscheduler = parse_scheduler(testing);\n>   \t\tcmd = sep + 1;\n> +\t\tresult = scheduler_fn[scheduler].update_schedule(\n> +\t\t\tenable, get_lock_file_fd(&lk), cmd);\n> +\t\tgoto done;\n>   \t}\n>   \n> -\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0)\n> -\t\treturn error(_(\"another process is scheduling background maintenance\"));\n> -\n> -\tif (!strcmp(scheduler, \"launchctl\"))\n> -\t\tresult = launchctl_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n> -\telse if (!strcmp(scheduler, \"schtasks\"))\n> -\t\tresult = schtasks_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n> -\telse if (!strcmp(scheduler, \"crontab\"))\n> -\t\tresult = crontab_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n> -\telse\n> -\t\tdie(\"unknown background scheduler: %s\", scheduler);\n> +\tfor (i = 1; i < ARRAY_SIZE(scheduler_fn); i++) {\n> +\t\tint enable_scheduler = enable && (opts->scheduler == i);\n> +\t\tcmd = scheduler_fn[i].cmd;\n> +\t\tif (!scheduler_fn[i].is_available(cmd))\n> +\t\t\tcontinue;\n> +\t\tres = scheduler_fn[i].update_schedule(\n> +\t\t\tenable_scheduler, get_lock_file_fd(&lk), cmd);\n> +\t\tif (enable_scheduler)\n> +\t\t\tresult = res;\n\nIf I have understood the code correctly then if systemd timers are \ncurrently enabled and the user runs `git maintenance --scheduler=cron` \nthen cron will be enabled and the loop will quit before we get the the \nentry to disable systemd timers leaving both running. I think it would \nbe cleaner and clearer to loop over the schedulers disabling them first \nand then enable the one the user has selected.\n\n> +\t}\n>   \n> +done:\n>   \trollback_lock_file(&lk);\n>   \tfree(testing);\n>   \treturn result;\n>   }\n>   \n> -static int maintenance_start(void)\n> +static const char *const builtin_maintenance_start_usage[] = {\n> +\tN_(\"git maintenance start [--scheduler=<scheduler>]\"), NULL\n> +};\n> +\n> +static int maintenance_start(int argc, const char **argv, const char *prefix)\n>   {\n> +\tstruct maintenance_start_opts opts;\n\nstruct maintenance_start_opts opts = { 0 };\n\nwould avoid the need for the memset() call below\n\nThanks again for working on this. Best Wishes\n\nPhillip\n\n> +\tstruct option builtin_maintenance_start_options[] = {\n> +\t\tOPT_CALLBACK(\n> +\t\t\t0, \"scheduler\", &opts.scheduler, N_(\"scheduler\"),\n> +\t\t\tN_(\"scheduler to use to trigger git maintenance run\"),\n> +\t\t\tmaintenance_opt_scheduler),\n> +\t\tOPT_END()\n> +\t};\n> +\tmemset(&opts, 0, sizeof(opts));\n> +\n> +\targc = parse_options(argc, argv, prefix,\n> +\t\t\t     builtin_maintenance_start_options,\n> +\t\t\t     builtin_maintenance_start_usage, 0);\n> +\n> +\tresolve_auto_scheduler(&opts.scheduler);\n> +\tvalidate_scheduler(opts.scheduler);\n> +\n> +\tif (argc > 0)\n> +\t\tusage_with_options(builtin_maintenance_start_usage,\n> +\t\t\t\t   builtin_maintenance_start_options);\n> +\n>   \tif (maintenance_register())\n>   \t\twarning(_(\"failed to add repo to global config\"));\n> -\n> -\treturn update_background_schedule(1);\n> +\treturn update_background_schedule(&opts, 1);\n>   }\n>   \n>   static int maintenance_stop(void)\n>   {\n> -\treturn update_background_schedule(0);\n> +\treturn update_background_schedule(NULL, 0);\n>   }\n>   \n>   static const char builtin_maintenance_usage[] =\tN_(\"git maintenance <subcommand> [<options>]\");\n> @@ -2027,7 +2354,7 @@ int cmd_maintenance(int argc, const char **argv, const char *prefix)\n>   \tif (!strcmp(argv[1], \"run\"))\n>   \t\treturn maintenance_run(argc - 1, argv + 1, prefix);\n>   \tif (!strcmp(argv[1], \"start\"))\n> -\t\treturn maintenance_start();\n> +\t\treturn maintenance_start(argc - 1, argv + 1, prefix);\n>   \tif (!strcmp(argv[1], \"stop\"))\n>   \t\treturn maintenance_stop();\n>   \tif (!strcmp(argv[1], \"register\"))\n> diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\n> index 2412d8c5c0..6e6316cd90 100755\n> --- a/t/t7900-maintenance.sh\n> +++ b/t/t7900-maintenance.sh\n> @@ -20,6 +20,20 @@ test_xmllint () {\n>   \tfi\n>   }\n>   \n> +test_lazy_prereq SYSTEMD_ANALYZE '\n> +\tsystemd-analyze --help >out &&\n> +\tgrep verify out\n> +'\n> +\n> +test_systemd_analyze_verify () {\n> +\tif test_have_prereq SYSTEMD_ANALYZE\n> +\tthen\n> +\t\tsystemd-analyze verify \"$@\"\n> +\telse\n> +\t\ttrue\n> +\tfi\n> +}\n> +\n>   test_expect_success 'help text' '\n>   \ttest_expect_code 129 git maintenance -h 2>err &&\n>   \ttest_i18ngrep \"usage: git maintenance <subcommand>\" err &&\n> @@ -615,6 +629,43 @@ test_expect_success 'start and stop Windows maintenance' '\n>   \ttest_cmp expect args\n>   '\n>   \n> +test_expect_success 'start and stop Linux/systemd maintenance' '\n> +\twrite_script print-args <<-\\EOF &&\n> +\techo $* >>args\n> +\tEOF\n> +\n> +\trm -f args &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"systemd:./print-args\" git maintenance start &&\n> +\n> +\t# start registers the repo\n> +\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n> +\n> +\ttest_systemd_analyze_verify \"$HOME/.config/systemd/user/git-maintenance@.service\" &&\n> +\n> +\trm -f expect &&\n> +\tfor frequency in hourly daily weekly\n> +\tdo\n> +\t\techo \"--user enable --now git-maintenance@${frequency}.timer\" >>expect || return 1\n> +\tdone &&\n> +\ttest_cmp expect args &&\n> +\n> +\trm -f args &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"systemd:./print-args\" git maintenance stop &&\n> +\n> +\t# stop does not unregister the repo\n> +\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n> +\n> +\ttest_path_is_missing \"$HOME/.config/systemd/user/git-maintenance@.timer\" &&\n> +\ttest_path_is_missing \"$HOME/.config/systemd/user/git-maintenance@.service\" &&\n> +\n> +\trm -f expect &&\n> +\tfor frequency in hourly daily weekly\n> +\tdo\n> +\t\techo \"--user disable --now git-maintenance@${frequency}.timer\" >>expect || return 1\n> +\tdone &&\n> +\ttest_cmp expect args\n> +'\n> +\n>   test_expect_success 'register preserves existing strategy' '\n>   \tgit config maintenance.strategy none &&\n>   \tgit maintenance register &&\n> \n"},{"id":"424065","messageId":"CAPig+cRvs+d-Tv4WyuD967v5Pbfhmwj-ruwyBXhwcR=5chQAmQ@mail.gmail.com","threadId":"55603","inReplyTo":"3fd17223-8667-24be-2e65-f1970d411bdf@gmail.com","subject":"Re: [PATCH v2 1/1] maintenance: use systemd timers on Linux","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2021-05-10T18:25:23Z","receivedAt":"2021-05-10T18:25:39Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Mon, May 10, 2021 at 2:04 PM Phillip Wood <phillip.wood123@gmail.com> wrote:\n> On 09/05/2021 22:32, Lénaïc Huard wrote:\n> > +static int systemd_timer_enable_unit(int enable,\n> > +                                  enum schedule_priority schedule,\n> > +                                  const char *cmd)\n>\n> The cmd argument is pointless, it will always be \"systemctl\" and you\n> have even hard coded that value into the error message below.\n\nThe reason that `cmd` is passed around everywhere is that the actual\ncommand can be overridden by GIT_TEST_MAINT_SCHEDULER which allows the\ntest script to mock up a scheduler command rather than running the\nreal scheduler command. I haven't read the new version of the patch\nclosely yet, but after a quick scan, I'm pretty confident that this is\nstill the case (despite the aggressive changes the patch makes to the\nareas around GIT_TEST_MAINT_SCHEDULER).\n\nAs for hardcoding the command name in the error message, that seems\nperfectly fine since, under normal circumstances, it _will_ be that\ncommand (it's only different when testing).\n"},{"id":"424070","messageId":"CAN0heSqN6Pdxap7JVN_L-BKJmbKhLiuzNVvmdC87vZjitv6eWg@mail.gmail.com","threadId":"55603","inReplyTo":"20210509213217.449489-2-lenaic@lhuard.fr","subject":"Re: [PATCH v2 1/1] maintenance: use systemd timers on Linux","fromName":"Martin Ågren","fromEmail":"martin.agren@gmail.com","sentAt":"2021-05-10T19:15:15Z","receivedAt":"2021-05-10T19:15:31Z","isPatch":true,"sender":{"key":"martin.agren@gmail.com","avatar":null},"body":"Hi Lénaïc,\n\nOn Sun, 9 May 2021 at 23:37, Lénaïc Huard <lenaic@lhuard.fr> wrote:\n> The default value is `auto` which chooses a suitable scheduler for the\n> system.\n> On Linux, if user systemd timers are available, they will be used as git\n> maintenance scheduler. If not, `cron` will be used if it is available.\n> If none is available, it will fail.\n\nI understand your reasoning for going with systemd-timer over cron,\nespecially the part about knowing that the thing is actually running.\n\n> +--scheduler=auto|crontab|systemd-timer|launchctl|schtasks::\n\nThis says \"systemd-timer\"...\n\n> +       By default or when `auto` is specified, the most appropriate scheduler\n> +       for the system is used. On MacOS, `launchctl` is used. On Windows,\n> +       `schtasks` is used. On Linux, `systemd-timers` is used if user systemd\n\n... this says \"systemd-timers\". Should those two be the same? (Which?)\n\n> +       timers are available, otherwise, `crontab` is used. On all other systems,\n> +       `crontab` is used.\n\nSo to be clear, I don't have a horse in this race. A few years ago I\nwould have foreseen all kinds of reactions to the implication that\nsystemd-timers would be \"the most appropriate scheduler [...] on Linux\".\nMaybe those times are behind us now. In the commit message, you say \"a\nsuitable\", which reads a little bit less opinionated (to me).\n\nThat's just a minor point; feel free to disregard.\n\n> +For more details, see systemd.timer(5)\n\nMissing trailing \".\".\n\nA cursory grepping of our docs suggests this should be monospace\n(`systemd.timer(5)`). There aren't that many places where we refer to\nnon-git manpages, thanks for doing so.\n\nThat's the only nit I found to make about the markup in the\ndocumentation. Thanks for your attention to details. :-)\n\nMartin\n"},{"id":"424076","messageId":"72be08d4-1b2d-f400-f99d-c348ae667bdb@gmail.com","threadId":"55603","inReplyTo":"CAPig+cRvs+d-Tv4WyuD967v5Pbfhmwj-ruwyBXhwcR=5chQAmQ@mail.gmail.com","subject":"Re: [PATCH v2 1/1] maintenance: use systemd timers on Linux","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2021-05-10T20:09:16Z","receivedAt":"2021-05-10T20:09:21Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Eric\n\nOn 10/05/2021 19:25, Eric Sunshine wrote:\n> On Mon, May 10, 2021 at 2:04 PM Phillip Wood <phillip.wood123@gmail.com> wrote:\n>> On 09/05/2021 22:32, Lénaïc Huard wrote:\n>>> +static int systemd_timer_enable_unit(int enable,\n>>> +                                  enum schedule_priority schedule,\n>>> +                                  const char *cmd)\n>>\n>> The cmd argument is pointless, it will always be \"systemctl\" and you\n>> have even hard coded that value into the error message below.\n> \n> The reason that `cmd` is passed around everywhere is that the actual\n> command can be overridden by GIT_TEST_MAINT_SCHEDULER which allows the\n> test script to mock up a scheduler command rather than running the\n> real scheduler command. I haven't read the new version of the patch\n> closely yet, but after a quick scan, I'm pretty confident that this is\n> still the case (despite the aggressive changes the patch makes to the\n> areas around GIT_TEST_MAINT_SCHEDULER).\n\nThanks for pointing that out I'd somehow glossed over the \nGIT_TEST_MAINT_SCHEDULER code, I agree it looks like the patch takes \ncare to keep it working.\n\nIt is outside the scope of this patch but a possibly nicer pattern would \nbe to have a function get_command_name(const char *default) that checks \nGIT_TEST_MAINT_SCHEDULER and returns the command name from that or the \ndefault if it is not set. We would then call that function to get the \ncommand name when we want to run a command. That way all the extra \ncomplexity is localized around the command call (and consists of a \nsingle function call), the usual command name is visible in the function \ncalling the command and we'd avoid littering all the function signatures \nwith a argument that is only relevant for testing.\n\n> As for hardcoding the command name in the error message, that seems\n> perfectly fine since, under normal circumstances, it _will_ be that\n> command (it's only different when testing).\n\nI agree, thanks\n\nPhillip\n"},{"id":"424077","messageId":"CAPig+cTmWBMJctFpfWe_Fw5w5oo_jMuLOSSXGs_o=tD89uAzkw@mail.gmail.com","threadId":"55603","inReplyTo":"72be08d4-1b2d-f400-f99d-c348ae667bdb@gmail.com","subject":"Re: [PATCH v2 1/1] maintenance: use systemd timers on Linux","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2021-05-10T20:52:49Z","receivedAt":"2021-05-10T20:53:07Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Mon, May 10, 2021 at 4:09 PM Phillip Wood <phillip.wood123@gmail.com> wrote:\n> It is outside the scope of this patch but a possibly nicer pattern would\n> be to have a function get_command_name(const char *default) that checks\n> GIT_TEST_MAINT_SCHEDULER and returns the command name from that or the\n> default if it is not set. We would then call that function to get the\n> command name when we want to run a command. That way all the extra\n> complexity is localized around the command call (and consists of a\n> single function call), the usual command name is visible in the function\n> calling the command and we'd avoid littering all the function signatures\n> with a argument that is only relevant for testing.\n\nYup, that would be a nice eventual cleanup. I agree that it is outside\nthe scope of this submission.\n"},{"id":"424137","messageId":"ba9ba56f-9253-afe5-aab5-514ce8ddb706@gmail.com","threadId":"55603","inReplyTo":"20210509213217.449489-2-lenaic@lhuard.fr","subject":"Re: [PATCH v2 1/1] maintenance: use systemd timers on Linux","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2021-05-11T14:50:56Z","receivedAt":"2021-05-11T14:51:05Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Lénaïc\n\nI've added some comments about testing\n\nOn 09/05/2021 22:32, Lénaïc Huard wrote:\n>[...]\n> +struct maintenance_start_opts {\n> +\tenum scheduler scheduler;\n> +};\n> +\n> +static void resolve_auto_scheduler(enum scheduler *scheduler)\n> +{\n> +\tif (*scheduler != SCHEDULER_AUTO)\n> +\t\treturn;\n> +\n>   #if defined(__APPLE__)\n> -static const char platform_scheduler[] = \"launchctl\";\n> +\t*scheduler = SCHEDULER_LAUNCHCTL;\n> +\treturn;\n> +\n>   #elif defined(GIT_WINDOWS_NATIVE)\n> -static const char platform_scheduler[] = \"schtasks\";\n> +\t*scheduler = SCHEDULER_SCHTASKS;\n> +\treturn;\n> +\n> +#elif defined(__linux__)\n> +\tif (is_systemd_timer_available(\"systemctl\"))\n> +\t\t*scheduler = SCHEDULER_SYSTEMD;\n> +\telse if (is_crontab_available(\"crontab\"))\n> +\t\t*scheduler = SCHEDULER_CRON;\n> +\telse\n> +\t\tdie(_(\"neither systemd timers nor crontab are available\"));\n> +\treturn;\n> +\n>   #else\n> -static const char platform_scheduler[] = \"crontab\";\n> +\t*scheduler = SCHEDULER_CRON;\n> +\treturn;\n>   #endif\n> +}\n\nAs it stands this function is untested and there is no way to test it \nwith the current setup. There are two difficulties with testing it (i) \nit uses conditional compilation and (ii) there is no way to fake crontab \nand systemctl with the current test setup. I think we can address the \nlatter (see below) and handle the condition compilation issues using \ntest prerequisites if necessary.\n\n> -static int update_background_schedule(int enable)\n> +static void validate_scheduler(enum scheduler scheduler)\n>   {\n> -\tint result;\n> -\tconst char *scheduler = platform_scheduler;\n> -\tconst char *cmd = scheduler;\n> +\tconst char *cmd;\n> +\n> +\tif (scheduler == SCHEDULER_INVALID)\n> +\t\tBUG(\"invalid scheduler\");\n> +\tif (scheduler == SCHEDULER_AUTO)\n> +\t\tBUG(\"resolve_auto_scheduler should have been called before\");\n> +\n> +\tcmd = scheduler_fn[scheduler].cmd;\n> +\tif (!scheduler_fn[scheduler].is_available(cmd))\n> +\t\tdie(_(\"%s scheduler is not available\"), cmd);\n> +}\n> +\n> +static int update_background_schedule(const struct maintenance_start_opts *opts,\n> +\t\t\t\t      int enable)\n> +{\n> +\tunsigned int i;\n> +\tint res, result = 0;\n> +\tenum scheduler scheduler;\n> +\tconst char *cmd = NULL;\n>   \tchar *testing;\n>   \tstruct lock_file lk;\n>   \tchar *lock_path = xstrfmt(\"%s/schedule\", the_repository->objects->odb->path);\n>   \n> +\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0)\n> +\t\treturn error(_(\"another process is scheduling background maintenance\"));\n> +\n>   \ttesting = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n>   \tif (testing) {\n>   \t\tchar *sep = strchr(testing, ':');\n>   \t\tif (!sep)\n>   \t\t\tdie(\"GIT_TEST_MAINT_SCHEDULER unparseable: %s\", testing);\n>   \t\t*sep = '\\0';\n> -\t\tscheduler = testing;\n> +\t\tscheduler = parse_scheduler(testing);\n>   \t\tcmd = sep + 1;\n> +\t\tresult = scheduler_fn[scheduler].update_schedule(\n> +\t\t\tenable, get_lock_file_fd(&lk), cmd);\n> +\t\tgoto done;\n>   \t}\n>   \n> -\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0)\n> -\t\treturn error(_(\"another process is scheduling background maintenance\"));\n> -\n> -\tif (!strcmp(scheduler, \"launchctl\"))\n> -\t\tresult = launchctl_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n> -\telse if (!strcmp(scheduler, \"schtasks\"))\n> -\t\tresult = schtasks_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n> -\telse if (!strcmp(scheduler, \"crontab\"))\n> -\t\tresult = crontab_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n> -\telse\n> -\t\tdie(\"unknown background scheduler: %s\", scheduler);\n> +\tfor (i = 1; i < ARRAY_SIZE(scheduler_fn); i++) {\n> +\t\tint enable_scheduler = enable && (opts->scheduler == i);\n> +\t\tcmd = scheduler_fn[i].cmd;\n> +\t\tif (!scheduler_fn[i].is_available(cmd))\n> +\t\t\tcontinue;\n> +\t\tres = scheduler_fn[i].update_schedule(\n> +\t\t\tenable_scheduler, get_lock_file_fd(&lk), cmd);\n> +\t\tif (enable_scheduler)\n> +\t\t\tresult = res;\n> +\t}\n\nThis loop which is responsible for disabling the existing scheduler and \nenabling a new one is completely untested. I think that before this \npatch special casing the testing above still ran the important code, \nhowever now we fail to test an important aspect of the business logic. \nAs we can only fake one command name the current test setup is \ninsufficient to fix this. I think the best solution would be to mock \nsystemctl, crontab, and the other commands in the test environment.\n\n\tmkdir bin &&\n\tPATH=\"$(pwd)/bin:$PATH\" &&\n\ttest_write_script systemctl <<-\\EOF &&\n\t# Mock systemctl, set FAKE_SYSTEMD=0 in the\n\t# environment to fake systemctl missing\n\tcase \"$*\" in\n\t\t\"--user list-timers\") test ${FAKE_SYSTEMD:-1} = 1;\n\t\t\t\t      exit $?;;\n\t\t*) printf \"%s\\n\" \"$*\";;\n\tesac\t\n\tEOF\n\t# and so on for crontab etc\n\nThen it would be possible to test the various values \n--scheduler=<scheduler> including 'auto' (by setting FAKE_SYSTEMD=0 or \nFAKE_CRONTAB=0) and that we disable cron when enabling systemd and vice \nversa. Mocking the commands would also allow us to cleanup the code in \ngc.c as it would no longer need to pass command names around.\n\n> +done:\n>   \trollback_lock_file(&lk);\n>   \tfree(testing);\n>   \treturn result;\n>   }\n>   \n> -static int maintenance_start(void)\n> +static const char *const builtin_maintenance_start_usage[] = {\n> +\tN_(\"git maintenance start [--scheduler=<scheduler>]\"), NULL\n> +};\n> +\n> +static int maintenance_start(int argc, const char **argv, const char *prefix)\n>   {\n> +\tstruct maintenance_start_opts opts;\n> +\tstruct option builtin_maintenance_start_options[] = {\n> +\t\tOPT_CALLBACK(\n> +\t\t\t0, \"scheduler\", &opts.scheduler, N_(\"scheduler\"),\n> +\t\t\tN_(\"scheduler to use to trigger git maintenance run\"),\n> +\t\t\tmaintenance_opt_scheduler),\n> +\t\tOPT_END()\n> +\t};\n> +\tmemset(&opts, 0, sizeof(opts));\n> +\n> +\targc = parse_options(argc, argv, prefix,\n> +\t\t\t     builtin_maintenance_start_options,\n> +\t\t\t     builtin_maintenance_start_usage, 0);\n\nThis new command line option is completely untested\n\nBest Wishes\n\nPhillip\n\n> +\n> +\tresolve_auto_scheduler(&opts.scheduler);\n> +\tvalidate_scheduler(opts.scheduler);\n> +\n> +\tif (argc > 0)\n> +\t\tusage_with_options(builtin_maintenance_start_usage,\n> +\t\t\t\t   builtin_maintenance_start_options);\n> +\n>   \tif (maintenance_register())\n>   \t\twarning(_(\"failed to add repo to global config\"));\n> -\n> -\treturn update_background_schedule(1);\n> +\treturn update_background_schedule(&opts, 1);\n>   }\n>   \n>   static int maintenance_stop(void)\n>   {\n> -\treturn update_background_schedule(0);\n> +\treturn update_background_schedule(NULL, 0);\n>   }\n>   \n>   static const char builtin_maintenance_usage[] =\tN_(\"git maintenance <subcommand> [<options>]\");\n> @@ -2027,7 +2354,7 @@ int cmd_maintenance(int argc, const char **argv, const char *prefix)\n>   \tif (!strcmp(argv[1], \"run\"))\n>   \t\treturn maintenance_run(argc - 1, argv + 1, prefix);\n>   \tif (!strcmp(argv[1], \"start\"))\n> -\t\treturn maintenance_start();\n> +\t\treturn maintenance_start(argc - 1, argv + 1, prefix);\n>   \tif (!strcmp(argv[1], \"stop\"))\n>   \t\treturn maintenance_stop();\n>   \tif (!strcmp(argv[1], \"register\"))\n> diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\n> index 2412d8c5c0..6e6316cd90 100755\n> --- a/t/t7900-maintenance.sh\n> +++ b/t/t7900-maintenance.sh\n> @@ -20,6 +20,20 @@ test_xmllint () {\n>   \tfi\n>   }\n>   \n> +test_lazy_prereq SYSTEMD_ANALYZE '\n> +\tsystemd-analyze --help >out &&\n> +\tgrep verify out\n> +'\n> +\n> +test_systemd_analyze_verify () {\n> +\tif test_have_prereq SYSTEMD_ANALYZE\n> +\tthen\n> +\t\tsystemd-analyze verify \"$@\"\n> +\telse\n> +\t\ttrue\n> +\tfi\n> +}\n> +\n>   test_expect_success 'help text' '\n>   \ttest_expect_code 129 git maintenance -h 2>err &&\n>   \ttest_i18ngrep \"usage: git maintenance <subcommand>\" err &&\n> @@ -615,6 +629,43 @@ test_expect_success 'start and stop Windows maintenance' '\n>   \ttest_cmp expect args\n>   '\n>   \n> +test_expect_success 'start and stop Linux/systemd maintenance' '\n> +\twrite_script print-args <<-\\EOF &&\n> +\techo $* >>args\n> +\tEOF\n> +\n> +\trm -f args &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"systemd:./print-args\" git maintenance start &&\n> +\n> +\t# start registers the repo\n> +\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n> +\n> +\ttest_systemd_analyze_verify \"$HOME/.config/systemd/user/git-maintenance@.service\" &&\n> +\n> +\trm -f expect &&\n> +\tfor frequency in hourly daily weekly\n> +\tdo\n> +\t\techo \"--user enable --now git-maintenance@${frequency}.timer\" >>expect || return 1\n> +\tdone &&\n> +\ttest_cmp expect args &&\n> +\n> +\trm -f args &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"systemd:./print-args\" git maintenance stop &&\n> +\n> +\t# stop does not unregister the repo\n> +\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n> +\n> +\ttest_path_is_missing \"$HOME/.config/systemd/user/git-maintenance@.timer\" &&\n> +\ttest_path_is_missing \"$HOME/.config/systemd/user/git-maintenance@.service\" &&\n> +\n> +\trm -f expect &&\n> +\tfor frequency in hourly daily weekly\n> +\tdo\n> +\t\techo \"--user disable --now git-maintenance@${frequency}.timer\" >>expect || return 1\n> +\tdone &&\n> +\ttest_cmp expect args\n> +'\n> +\n>   test_expect_success 'register preserves existing strategy' '\n>   \tgit config maintenance.strategy none &&\n>   \tgit maintenance register &&\n> \n"},{"id":"424148","messageId":"1873c650-e9b3-b6d2-b67b-bd09a90e6782@gmail.com","threadId":"55603","inReplyTo":"20210509213217.449489-2-lenaic@lhuard.fr","subject":"Re: [PATCH v2 1/1] maintenance: use systemd timers on Linux","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-05-11T17:31:45Z","receivedAt":"2021-05-11T17:31:50Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 5/9/2021 5:32 PM, Lénaïc Huard wrote:\n> The existing mechanism for scheduling background maintenance is done\n> through cron. On Linux systems managed by systemd, systemd provides an\n> alternative to schedule recurring tasks: systemd timers.\n\nThank you for working so hard on this systemd integration. I see you\nhave already received significant feedback on that portion, so I\nwanted to focus my review on the other piece that exists in this patch.\n\n> In order to choose which scheduler to use between `cron` and user\n> systemd timers, a new option\n> `--scheduler=auto|cron|systemd|launchctl|schtasks` has been added to\n> `git maintenance start`.\n> When `git maintenance start --scheduler=XXX` is run, it not only\n> registers `git maintenance run` tasks in the scheduler XXX, it also\n> removes the `git maintenance run` tasks from all the other schedulers to\n> ensure we cannot have two schedulers launching concurrent identical\n> tasks.\n> \n> The default value is `auto` which chooses a suitable scheduler for the\n> system.\n\nThis addition of the --scheduler option should be split into a patch\non its own. It requires significant refactoring of the existing code\nin a way that distracts from your systemd work.\n\nI'll highlight the portions of the diff that you could include in\na preliminary patch and save the systemd stuff for an addition on top\nof that.\n\n> diff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\n> index 80ddd33ceb..f012923333 100644\n> --- a/Documentation/git-maintenance.txt\n> +++ b/Documentation/git-maintenance.txt\n> @@ -181,6 +181,20 @@ OPTIONS\n>  \t`maintenance.<task>.enabled` configured as `true` are considered.\n>  \tSee the 'TASKS' section for the list of accepted `<task>` values.\n>  \n> +--scheduler=auto|crontab|systemd-timer|launchctl|schtasks::\n> +\tWhen combined with the `start` subcommand, specify the scheduler\n> +\tto use to run the hourly, daily and weekly executions of\n> +\t`git maintenance run`.\n> +\tThe possible values for `<scheduler>` depend on the system: `crontab`\n> +\tis available on POSIX systems, `systemd-timer` is available on Linux\n> +\tsystems, `launchctl` is available on MacOS and `schtasks` is available\n> +\ton Windows.\n> +\tBy default or when `auto` is specified, the most appropriate scheduler\n> +\tfor the system is used. On MacOS, `launchctl` is used. On Windows,\n> +\t`schtasks` is used. On Linux, `systemd-timers` is used if user systemd\n> +\ttimers are available, otherwise, `crontab` is used. On all other systems,\n> +\t`crontab` is used.\n> +\n\nThis portion of the docs can be updated on its own (minus the systemd bits).\n> diff --git a/builtin/gc.c b/builtin/gc.c\n> index ef7226d7bc..7c72aa3b99 100644\n> --- a/builtin/gc.c\n> +++ b/builtin/gc.c\n> @@ -1544,6 +1544,15 @@ static const char *get_frequency(enum schedule_priority schedule)\n>  \t}\n>  }\n>  \n> +static int is_launchctl_available(const char *cmd)\n> +{\n> +#ifdef __APPLE__\n> +\treturn 1;\n> +#else\n> +\treturn 0;\n> +#endif\n> +}\n> +\n>  static char *launchctl_service_name(const char *frequency)\n>  {\n>  \tstruct strbuf label = STRBUF_INIT;\n> @@ -1710,6 +1719,15 @@ static int launchctl_update_schedule(int run_maintenance, int fd, const char *cm\n>  \t\treturn launchctl_remove_plists(cmd);\n>  }\n>  \n> +static int is_schtasks_available(const char *cmd)\n> +{\n> +#ifdef GIT_WINDOWS_NATIVE\n> +\treturn 1;\n> +#else\n> +\treturn 0;\n> +#endif\n> +}\n> +\n>  static char *schtasks_task_name(const char *frequency)\n>  {\n>  \tstruct strbuf label = STRBUF_INIT;\n> @@ -1872,6 +1890,28 @@ static int schtasks_update_schedule(int run_maintenance, int fd, const char *cmd\n>  \t\treturn schtasks_remove_tasks(cmd);\n>  }\n>  \n> +static int is_crontab_available(const char *cmd)\n> +{\n> +\tstatic int cached_result = -1;\n> +\tstruct child_process child = CHILD_PROCESS_INIT;\n> +\n> +\tif (cached_result != -1)\n> +\t\treturn cached_result;\n> +\n> +\tstrvec_split(&child.args, cmd);\n> +\tstrvec_push(&child.args, \"-l\");\n> +\tchild.no_stdin = 1;\n> +\tchild.no_stdout = 1;\n> +\tchild.no_stderr = 1;\n> +\tchild.silent_exec_failure = 1;\n> +\n> +\tif (start_command(&child))\n> +\t\treturn cached_result = 0;\n> +\t/* Ignore exit code, as an empty crontab will return error. */\n> +\tfinish_command(&child);\n> +\treturn cached_result = 1;\n> +}\n> +\n\nThese is_X_available() methods are valuable helpers. Adding\nis_systemd_timer_available() in the second patch will be\nsimpler with this framework in place.\n\n> +enum scheduler {\n> +\tSCHEDULER_INVALID = -1,\n> +\tSCHEDULER_AUTO = 0,\n> +\tSCHEDULER_CRON = 1,\n> +\tSCHEDULER_SYSTEMD = 2,\n> +\tSCHEDULER_LAUNCHCTL = 3,\n> +\tSCHEDULER_SCHTASKS = 4,\n> +};\n> +\n> +static const struct {\n> +\tint (*is_available)(const char *cmd);\n> +\tint (*update_schedule)(int run_maintenance, int fd, const char *cmd);\n> +\tconst char *cmd;\n> +} scheduler_fn[] = {\n> +\t[SCHEDULER_CRON] = { is_crontab_available, crontab_update_schedule,\n> +\t\t\t     \"crontab\" },\n> +\t[SCHEDULER_SYSTEMD] = { is_systemd_timer_available,\n> +\t\t\t\tsystemd_timer_update_schedule, \"systemctl\" },\n> +\t[SCHEDULER_LAUNCHCTL] = { is_launchctl_available,\n> +\t\t\t\t  launchctl_update_schedule, \"launchctl\" },\n> +\t[SCHEDULER_SCHTASKS] = { is_schtasks_available,\n> +\t\t\t\t schtasks_update_schedule, \"schtasks\" },\n> +};\n\nThis is also good to include, minus the systemd lines.\n\nI would also like to see this declaration reformatted.\nSomething like this would be good:\n\nstatic const struct {\n\tint (*is_available)(const char *cmd);\n\tint (*update_schedule)(int run_maintenance, int fd, const char *cmd);\n\tconst char *cmd;\n} scheduler_fn[] = {\n\t[SCHEDULER_CRON] = {\n\t\t.is_available = is_crontab_available,\n\t\t.update_schedule = crontab_update_schedule,\n\t\t.cmd = \"crontab\",\n\t},\n\t[SCHEDULER_LAUNCHCTL] = {\n\t\t.is_available = is_launchctl_available,\n\t\t.update_schedule = launchctl_update_schedule,\n\t\t.cmd = \"launchctl\",\n\t},\n\t[SCHEDULER_SCHTASKS] = {\n\t\t.is_available = is_schtasks_available,\n\t\t.update_schedule = schtasks_update_schedule,\n\t\t.cmd = \"schtasks\",\n\t},\n};\n\nThe use of member names can help if we need to augment this\nstruct later, and the use of commas after the final terms of\neach block helps the future diff if we add items to the end.\n\n> +\n> +static enum scheduler parse_scheduler(const char *value)\n> +{\n> +\tif (!value)\n> +\t\treturn SCHEDULER_INVALID;\n> +\telse if (!strcasecmp(value, \"auto\"))\n> +\t\treturn SCHEDULER_AUTO;\n> +\telse if (!strcasecmp(value, \"cron\") || !strcasecmp(value, \"crontab\"))\n> +\t\treturn SCHEDULER_CRON;\n> +\telse if (!strcasecmp(value, \"systemd\") ||\n> +\t\t !strcasecmp(value, \"systemd-timer\"))\n> +\t\treturn SCHEDULER_SYSTEMD;\n> +\telse if (!strcasecmp(value, \"launchctl\"))\n> +\t\treturn SCHEDULER_LAUNCHCTL;\n> +\telse if (!strcasecmp(value, \"schtasks\"))\n> +\t\treturn SCHEDULER_SCHTASKS;\n> +\telse\n> +\t\treturn SCHEDULER_INVALID;\n> +}\n\nGood. The systemd stuff can be added in the second patch,\nmaking a clear integration point.\n\n> +static int maintenance_opt_scheduler(const struct option *opt, const char *arg,\n> +\t\t\t\t     int unset)\n> +{\n> +\tenum scheduler *scheduler = opt->value;\n> +\n> +\tif (unset)\n> +\t\tdie(_(\"--no-scheduler is not allowed\"));\n> +\n> +\t*scheduler = parse_scheduler(arg);\n> +\n> +\tif (*scheduler == SCHEDULER_INVALID)\n> +\t\tdie(_(\"unrecognized --scheduler argument '%s'\"), arg);\n> +\n> +\treturn 0;\n> +}\n> +\n> +struct maintenance_start_opts {\n> +\tenum scheduler scheduler;\n> +};\n\nThis struct that contains only the enum seems confusing to me.\nMaybe it will make sense later.\n\n> +static void resolve_auto_scheduler(enum scheduler *scheduler)\n> +{\n> +\tif (*scheduler != SCHEDULER_AUTO)\n> +\t\treturn;\n> +\n>  #if defined(__APPLE__)\n> -static const char platform_scheduler[] = \"launchctl\";\n> +\t*scheduler = SCHEDULER_LAUNCHCTL;\n> +\treturn;\n> +\n>  #elif defined(GIT_WINDOWS_NATIVE)\n> -static const char platform_scheduler[] = \"schtasks\";\n> +\t*scheduler = SCHEDULER_SCHTASKS;\n> +\treturn;\n> +\n> +#elif defined(__linux__)\n> +\tif (is_systemd_timer_available(\"systemctl\"))\n> +\t\t*scheduler = SCHEDULER_SYSTEMD;\n> +\telse if (is_crontab_available(\"crontab\"))\n> +\t\t*scheduler = SCHEDULER_CRON;\n> +\telse\n> +\t\tdie(_(\"neither systemd timers nor crontab are available\"));\n> +\treturn;\n> +\n>  #else\n> -static const char platform_scheduler[] = \"crontab\";\n> +\t*scheduler = SCHEDULER_CRON;\n> +\treturn;\n>  #endif\n> +}\n\nThis diff looks pretty rough. I see that you are making\nsystemctl the default for Linux. Ok. This also seems like\nit will not be testable in the test suite.\n\n>  \n> -static int update_background_schedule(int enable)\n> +static void validate_scheduler(enum scheduler scheduler)\n>  {\n> -\tint result;\n> -\tconst char *scheduler = platform_scheduler;\n> -\tconst char *cmd = scheduler;\n> +\tconst char *cmd;\n> +\n> +\tif (scheduler == SCHEDULER_INVALID)\n> +\t\tBUG(\"invalid scheduler\");\n> +\tif (scheduler == SCHEDULER_AUTO)\n> +\t\tBUG(\"resolve_auto_scheduler should have been called before\");\n> +\n> +\tcmd = scheduler_fn[scheduler].cmd;\n> +\tif (!scheduler_fn[scheduler].is_available(cmd))\n> +\t\tdie(_(\"%s scheduler is not available\"), cmd);\n> +}\n> +\n> +static int update_background_schedule(const struct maintenance_start_opts *opts,\n> +\t\t\t\t      int enable)\n> +{\n> +\tunsigned int i;\n> +\tint res, result = 0;\n> +\tenum scheduler scheduler;\n> +\tconst char *cmd = NULL;\n>  \tchar *testing;\n>  \tstruct lock_file lk;\n>  \tchar *lock_path = xstrfmt(\"%s/schedule\", the_repository->objects->odb->path);\n>  \n> +\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0)\n> +\t\treturn error(_(\"another process is scheduling background maintenance\"));\n> +\n>  \ttesting = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n>  \tif (testing) {\n>  \t\tchar *sep = strchr(testing, ':');\n>  \t\tif (!sep)\n>  \t\t\tdie(\"GIT_TEST_MAINT_SCHEDULER unparseable: %s\", testing);\n>  \t\t*sep = '\\0';\n> -\t\tscheduler = testing;\n> +\t\tscheduler = parse_scheduler(testing);\n>  \t\tcmd = sep + 1;\n> +\t\tresult = scheduler_fn[scheduler].update_schedule(\n> +\t\t\tenable, get_lock_file_fd(&lk), cmd);\n> +\t\tgoto done;\n\nI see this 'goto done' is the reason we need to take the lock earlier. The\nother option would be to put the 'goto done' after the rollback_lock_file(),\nbut this is fine, too.\n\n>  \t}\n>  \n> -\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0)\n> -\t\treturn error(_(\"another process is scheduling background maintenance\"));\n> -\n> -\tif (!strcmp(scheduler, \"launchctl\"))\n> -\t\tresult = launchctl_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n> -\telse if (!strcmp(scheduler, \"schtasks\"))\n> -\t\tresult = schtasks_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n> -\telse if (!strcmp(scheduler, \"crontab\"))\n> -\t\tresult = crontab_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n> -\telse\n> -\t\tdie(\"unknown background scheduler: %s\", scheduler);\n> +\tfor (i = 1; i < ARRAY_SIZE(scheduler_fn); i++) {\n> +\t\tint enable_scheduler = enable && (opts->scheduler == i);\n> +\t\tcmd = scheduler_fn[i].cmd;\n> +\t\tif (!scheduler_fn[i].is_available(cmd))\n> +\t\t\tcontinue;\n> +\t\tres = scheduler_fn[i].update_schedule(\n> +\t\t\tenable_scheduler, get_lock_file_fd(&lk), cmd);\n> +\t\tif (enable_scheduler)\n> +\t\t\tresult = res;\n> +\t}\n\nThis loop is cleaner than the list of else-ifs.\n\n>  \n> +done:\n>  \trollback_lock_file(&lk);\n>  \tfree(testing);\n>  \treturn result;\n>  }\n>  \n> -static int maintenance_start(void)\n> +static const char *const builtin_maintenance_start_usage[] = {\n> +\tN_(\"git maintenance start [--scheduler=<scheduler>]\"), NULL\n> +};\n> +\n> +static int maintenance_start(int argc, const char **argv, const char *prefix)\n>  {\n> +\tstruct maintenance_start_opts opts;\n\nI see you using the struct now and in the helper methods above.\nWhile the creation of the struct looks a little strange now, this\nuse matches other patterns in this file and is more flexible to\nadditional options in the future. Thanks.\n\n> +\tstruct option builtin_maintenance_start_options[] = {\n> +\t\tOPT_CALLBACK(\n> +\t\t\t0, \"scheduler\", &opts.scheduler, N_(\"scheduler\"),\n> +\t\t\tN_(\"scheduler to use to trigger git maintenance run\"),\n> +\t\t\tmaintenance_opt_scheduler),\n\nnit: these would typically be aligned with the end of the\nOPT_CALLBACK( opener, with the first set of parameters being on\nthe same line:\n\n\t\tOPT_CALLBACK(0, \"scheduler\", &opts.scheduler, N_(\"scheduler\"),\n\t\t\t     N_(\"scheduler to use to trigger 'git maintenance run'\"),\n\t\t\t     maintenance_opt_scheduler),\n\nThese options frequently run a little long on the line width,\nwhich might have been your motivation in adding an extra line.\n\n> +\t\tOPT_END()\n> +\t};\n> +\tmemset(&opts, 0, sizeof(opts));\n> +\n> +\targc = parse_options(argc, argv, prefix,\n> +\t\t\t     builtin_maintenance_start_options,\n> +\t\t\t     builtin_maintenance_start_usage, 0);\n> +\n> +\tresolve_auto_scheduler(&opts.scheduler);\n> +\tvalidate_scheduler(opts.scheduler);\n> +\n> +\tif (argc > 0)\n> +\t\tusage_with_options(builtin_maintenance_start_usage,\n> +\t\t\t\t   builtin_maintenance_start_options);\n\nnit: \"if (argc)\" is the more typical pattern in the Git codebase.\n\nAlso, this check should come right after parse_options().\n\n>  \tif (maintenance_register())\n>  \t\twarning(_(\"failed to add repo to global config\"));\n> -\n> -\treturn update_background_schedule(1);\n> +\treturn update_background_schedule(&opts, 1);\n>  }\n>  \n>  static int maintenance_stop(void)\n>  {\n> -\treturn update_background_schedule(0);\n> +\treturn update_background_schedule(NULL, 0);\n>  }\n>  \n>  static const char builtin_maintenance_usage[] =\tN_(\"git maintenance <subcommand> [<options>]\");\n> @@ -2027,7 +2354,7 @@ int cmd_maintenance(int argc, const char **argv, const char *prefix)\n>  \tif (!strcmp(argv[1], \"run\"))\n>  \t\treturn maintenance_run(argc - 1, argv + 1, prefix);\n>  \tif (!strcmp(argv[1], \"start\"))\n> -\t\treturn maintenance_start();\n> +\t\treturn maintenance_start(argc - 1, argv + 1, prefix);\n>  \tif (!strcmp(argv[1], \"stop\"))\n>  \t\treturn maintenance_stop();\n>  \tif (!strcmp(argv[1], \"register\"))\n> diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\n> index 2412d8c5c0..6e6316cd90 100755\n> --- a/t/t7900-maintenance.sh\n> +++ b/t/t7900-maintenance.sh\n\nI'm not sure how we could achieve it, but it might be good to demonstrate\na use of the --scheduler option here in the test script.\n\nThanks,\n-Stolee\n"},{"id":"424233","messageId":"YJsheKt/UBJNtgv1@danh.dev","threadId":"55603","inReplyTo":"xmqqtunbqf5o.fsf@gitster.g","subject":"Re: [PATCH v2 1/1] maintenance: use systemd timers on Linux","fromName":"Đoàn Trần Công Danh","fromEmail":"congdanhqx@gmail.com","sentAt":"2021-05-12T00:29:44Z","receivedAt":"2021-05-12T00:29:48Z","isPatch":true,"sender":{"key":"congdanhqx@gmail.com","avatar":"https://avatars.githubusercontent.com/u/42673067?v=4"},"body":"On 2021-05-10 15:25:07+0900, Junio C Hamano <gitster@pobox.com> wrote:\n> Eric Sunshine <sunshine@sunshineco.com> writes:\n> \n> >> I think others have strong opinion on not using \"%1$s\",\n> >> and prefer simple \"%s\" and using \"exec_path\" twice instead.\n> >\n> > I brought it up only because I hadn't seen it in Git sources, and\n> > wasn't sure if we'd want to start using it. Aside from Ævar, who\n> > seemed reasonably in favor of it, nobody else chimed in, so it could\n> > go either way, I suppose.\n> \n> If this were a piece of code that _everybody_ would use on _all_ the\n> supported platforms, I would suggest declaring that this is a\n> weather-balloon to see if some platforms have trouble using it.  But\n> unfortunately this is not such a piece of code.  Dependence on\n> systemd should strictly be opt-in.\n\nYes, dependence on systemd should be strictly opt-in.\nAlthough, I don't use systemd-based distro, so it is irrelevant to me.\nI think it's none of Git (the project) business to decide which\nscheduler should be given higher priority. It's crontab when\nmaintenance was introduced, it should be crontab, now.\n\nAnother point for eternal bikeshedding: why do we limit ourselves in\ncrontab and systemd, how about other homebrew schedulers? What should\nwe do if another scheduler raise to be the big star in the scheduler\nworld?\n\nI guess we should take some templates for running on {,un}register\ninstead? However, I think such design may open another can of worms.\nSo, I don't know.\n\n\n-- \nDanh\n"},{"id":"424293","messageId":"609b7cd397063_6e0fc2082c@natae.notmuch","threadId":"55603","inReplyTo":"YJsheKt/UBJNtgv1@danh.dev","subject":"Re: [PATCH v2 1/1] maintenance: use systemd timers on Linux","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2021-05-12T06:59:31Z","receivedAt":"2021-05-12T06:59:40Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Đoàn Trần Công Danh wrote:\n> Yes, dependence on systemd should be strictly opt-in.\n> Although, I don't use systemd-based distro, so it is irrelevant to me.\n> I think it's none of Git (the project) business to decide which\n> scheduler should be given higher priority. It's crontab when\n> maintenance was introduced, it should be crontab, now.\n\nI do use a systemd-based distro, and I like the option to use systemd\nunits, but let's be honest...\n\n100% of systems with systemd have cron... So...\n\n-- \nFelipe Contreras"},{"id":"424329","messageId":"c676b6e7-e152-ef8b-c29d-3835a3899ffa@gmail.com","threadId":"55603","inReplyTo":"609b7cd397063_6e0fc2082c@natae.notmuch","subject":"Re: [PATCH v2 1/1] maintenance: use systemd timers on Linux","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2021-05-12T13:26:22Z","receivedAt":"2021-05-12T13:26:28Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 12/05/2021 07:59, Felipe Contreras wrote:\n> Đoàn Trần Công Danh wrote:\n>> Yes, dependence on systemd should be strictly opt-in.\n>> Although, I don't use systemd-based distro, so it is irrelevant to me.\n>> I think it's none of Git (the project) business to decide which\n>> scheduler should be given higher priority. It's crontab when\n>> maintenance was introduced, it should be crontab, now.\n> \n> I do use a systemd-based distro, and I like the option to use systemd\n> units, but let's be honest...\n> \n> 100% of systems with systemd have cron...\n\nThis is untrue, as the commit message points out cron is optional on \nsystems running systemd and there are distributions such as Arch Linux \nthat do not install a cron daemon without explicit user intervention.\n\n  So...\n> \n"},{"id":"424330","messageId":"fbfd503b-d822-e9df-f766-cc70941fdec1@gmail.com","threadId":"55603","inReplyTo":"YJsheKt/UBJNtgv1@danh.dev","subject":"Re: [PATCH v2 1/1] maintenance: use systemd timers on Linux","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2021-05-12T13:38:26Z","receivedAt":"2021-05-12T13:38:33Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Đoàn\n\nOn 12/05/2021 01:29, Đoàn Trần Công Danh wrote:\n> On 2021-05-10 15:25:07+0900, Junio C Hamano <gitster@pobox.com> wrote:\n>> Eric Sunshine <sunshine@sunshineco.com> writes:\n>>\n>>>> I think others have strong opinion on not using \"%1$s\",\n>>>> and prefer simple \"%s\" and using \"exec_path\" twice instead.\n>>>\n>>> I brought it up only because I hadn't seen it in Git sources, and\n>>> wasn't sure if we'd want to start using it. Aside from Ævar, who\n>>> seemed reasonably in favor of it, nobody else chimed in, so it could\n>>> go either way, I suppose.\n>>\n>> If this were a piece of code that _everybody_ would use on _all_ the\n>> supported platforms, I would suggest declaring that this is a\n>> weather-balloon to see if some platforms have trouble using it.  But\n>> unfortunately this is not such a piece of code.  Dependence on\n>> systemd should strictly be opt-in.\n> \n> Yes, dependence on systemd should be strictly opt-in.\n> Although, I don't use systemd-based distro, so it is irrelevant to me.\n> I think it's none of Git (the project) business to decide which\n> scheduler should be given higher priority. It's crontab when\n> maintenance was introduced, it should be crontab, now.\n\nYou seem to be simultaneously arguing that git should be neutral on the \nchoice of scheduler while saying it should prioritize crontab. The \ncommit message and cover letter list a number of difficulties with the \nstrategy of prioritizing crontab over systemd when both are installed. I \nthink we should aim for the solution that has the most chance of working \nwithout user intervention.\n\n> Another point for eternal bikeshedding: why do we limit ourselves in\n> crontab and systemd, how about other homebrew schedulers? What should\n> we do if another scheduler raise to be the big star in the scheduler\n> world?\n\nWe should support the default scheduler on each platform - that was the \nrod we made for our own back when we decided to use the platform's \nscheduler rather than having a cross platform git maintenance daemon. It \njust happens that there are two possible default schedulers on linux so \nwe need to support both of them.\n\nBest Wishes\n\nPhillip\n\n> I guess we should take some templates for running on {,un}register\n> instead? However, I think such design may open another can of worms.\n> So, I don't know.\n> \n> \n"},{"id":"424346","messageId":"YJv3Ojt+4ZElN6JG@danh.dev","threadId":"55603","inReplyTo":"fbfd503b-d822-e9df-f766-cc70941fdec1@gmail.com","subject":"Re: [PATCH v2 1/1] maintenance: use systemd timers on Linux","fromName":"Đoàn Trần Công Danh","fromEmail":"congdanhqx@gmail.com","sentAt":"2021-05-12T15:41:46Z","receivedAt":"2021-05-12T16:45:23Z","isPatch":true,"sender":{"key":"congdanhqx@gmail.com","avatar":"https://avatars.githubusercontent.com/u/42673067?v=4"},"body":"On 2021-05-12 14:38:26+0100, Phillip Wood <phillip.wood123@gmail.com> wrote:\n> > Yes, dependence on systemd should be strictly opt-in.\n> > Although, I don't use systemd-based distro, so it is irrelevant to me.\n> > I think it's none of Git (the project) business to decide which\n> > scheduler should be given higher priority. It's crontab when\n> > maintenance was introduced, it should be crontab, now.\n> \n> You seem to be simultaneously arguing that git should be neutral on the\n> choice of scheduler while saying it should prioritize crontab.\n\nYes, I'm arguing for git should be neutral on the choice of scheduler.\n\nNo, I'm not arguing for git should be prioritize crontab, I'm arguing\nfor \"princible of least surprise\" for no known break-through advantage.\n\nFWIW, whatever default scheduler chosen won't affect me, since I don't\nhave systemd-timers to begin with. So ...\n\nIn addition, I was one of those people pointed out that beside\ncrontab, Linux users nowaday employed different schedulers [1],\nand the consensus, some how, settled on crontab.\n\nI think  we shouldn't switch away from crontab if we don't have any\ncompelling reasons.\n\n> The commit\n> message and cover letter list a number of difficulties with the strategy of\n> prioritizing crontab over systemd when both are installed. I think we should\n> aim for the solution that has the most chance of working without user\n> intervention.\n\nThe solution that has the most chance of working without user\nintervention is the solution that is the status quo. Promoting\nsystemd-timers to higher priority is a solution requires either\nuser intervention or our supports (which will be carried over our\nlifetime).\n\n> > Another point for eternal bikeshedding: why do we limit ourselves in\n> > crontab and systemd, how about other homebrew schedulers? What should\n> > we do if another scheduler raise to be the big star in the scheduler\n> > world?\n> \n> We should support the default scheduler on each platform - that was the rod\n> we made for our own back when we decided to use the platform's scheduler\n> rather than having a cross platform git maintenance daemon. It just happens\n> that there are two possible default schedulers on linux so we need to\n> support both of them.\n\nAs noted in [1], some home-brew solutions are very popular solutions\namong those some community.\nI'm not arguing that crontab or systemd-timers aren't popular.\nIn fact, I think they're *very* popular, I listed systemd-timers as\n*first* alternative in the linked email.\nI'm not against supporting both of them, I was arguing about a generic\nsolution.\n\n1: https://lore.kernel.org/git/20200407005828.GC2568@danh.dev/\n\n-- \nDanh\n"},{"id":"425161","messageId":"20210520221359.75615-3-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210520221359.75615-1-lenaic@lhuard.fr","subject":"[PATCH v3 2/4] maintenance: introduce ENABLE/DISABLE for code clarity","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-05-20T22:13:57Z","receivedAt":"2021-05-20T22:23:06Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"The first parameter of `XXX_update_schedule` and alike functions is a\nboolean specifying if the tasks should be scheduled or unscheduled.\n\nUsing an `enum` with `ENABLE` and `DISABLE` values can make the code\nclearer.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n builtin/gc.c | 49 +++++++++++++++++++++++++++++++------------------\n 1 file changed, 31 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex ef7226d7bc..0caf8d45c4 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1570,19 +1570,21 @@ static char *launchctl_get_uid(void)\n \treturn xstrfmt(\"gui/%d\", getuid());\n }\n \n-static int launchctl_boot_plist(int enable, const char *filename, const char *cmd)\n+enum enable_or_disable {\n+\tDISABLE,\n+\tENABLE\n+};\n+\n+static int launchctl_boot_plist(enum enable_or_disable enable,\n+\t\t\t\tconst char *filename, const char *cmd)\n {\n \tint result;\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tchar *uid = launchctl_get_uid();\n \n \tstrvec_split(&child.args, cmd);\n-\tif (enable)\n-\t\tstrvec_push(&child.args, \"bootstrap\");\n-\telse\n-\t\tstrvec_push(&child.args, \"bootout\");\n-\tstrvec_push(&child.args, uid);\n-\tstrvec_push(&child.args, filename);\n+\tstrvec_pushl(&child.args, enable == ENABLE ? \"bootstrap\" : \"bootout\",\n+\t\t     uid, filename, NULL);\n \n \tchild.no_stderr = 1;\n \tchild.no_stdout = 1;\n@@ -1601,7 +1603,7 @@ static int launchctl_remove_plist(enum schedule_priority schedule, const char *c\n \tconst char *frequency = get_frequency(schedule);\n \tchar *name = launchctl_service_name(frequency);\n \tchar *filename = launchctl_service_filename(name);\n-\tint result = launchctl_boot_plist(0, filename, cmd);\n+\tint result = launchctl_boot_plist(DISABLE, filename, cmd);\n \tunlink(filename);\n \tfree(filename);\n \tfree(name);\n@@ -1684,8 +1686,8 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n \tfclose(plist);\n \n \t/* bootout might fail if not already running, so ignore */\n-\tlaunchctl_boot_plist(0, filename, cmd);\n-\tif (launchctl_boot_plist(1, filename, cmd))\n+\tlaunchctl_boot_plist(DISABLE, filename, cmd);\n+\tif (launchctl_boot_plist(ENABLE, filename, cmd))\n \t\tdie(_(\"failed to bootstrap service %s\"), filename);\n \n \tfree(filename);\n@@ -1702,12 +1704,17 @@ static int launchctl_add_plists(const char *cmd)\n \t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY, cmd);\n }\n \n-static int launchctl_update_schedule(int run_maintenance, int fd, const char *cmd)\n+static int launchctl_update_schedule(enum enable_or_disable run_maintenance,\n+\t\t\t\t     int fd, const char *cmd)\n {\n-\tif (run_maintenance)\n+\tswitch (run_maintenance) {\n+\tcase ENABLE:\n \t\treturn launchctl_add_plists(cmd);\n-\telse\n+\tcase DISABLE:\n \t\treturn launchctl_remove_plists(cmd);\n+\tdefault:\n+\t\tBUG(\"invalid enable_or_disable value\");\n+\t}\n }\n \n static char *schtasks_task_name(const char *frequency)\n@@ -1864,18 +1871,24 @@ static int schtasks_schedule_tasks(const char *cmd)\n \t\tschtasks_schedule_task(exec_path, SCHEDULE_WEEKLY, cmd);\n }\n \n-static int schtasks_update_schedule(int run_maintenance, int fd, const char *cmd)\n+static int schtasks_update_schedule(enum enable_or_disable run_maintenance,\n+\t\t\t\t    int fd, const char *cmd)\n {\n-\tif (run_maintenance)\n+\tswitch (run_maintenance) {\n+\tcase ENABLE:\n \t\treturn schtasks_schedule_tasks(cmd);\n-\telse\n+\tcase DISABLE:\n \t\treturn schtasks_remove_tasks(cmd);\n+\tdefault:\n+\t\tBUG(\"invalid enable_or_disable value\");\n+\t}\n }\n \n #define BEGIN_LINE \"# BEGIN GIT MAINTENANCE SCHEDULE\"\n #define END_LINE \"# END GIT MAINTENANCE SCHEDULE\"\n \n-static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n+static int crontab_update_schedule(enum enable_or_disable run_maintenance,\n+\t\t\t\t   int fd, const char *cmd)\n {\n \tint result = 0;\n \tint in_old_region = 0;\n@@ -1925,7 +1938,7 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n \t\t\tfprintf(cron_in, \"%s\\n\", line.buf);\n \t}\n \n-\tif (run_maintenance) {\n+\tif (run_maintenance == ENABLE) {\n \t\tstruct strbuf line_format = STRBUF_INIT;\n \t\tconst char *exec_path = git_exec_path();\n \n-- \n2.31.1\n\n"},{"id":"425162","messageId":"20210520221359.75615-5-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210520221359.75615-1-lenaic@lhuard.fr","subject":"[PATCH v3 4/4] maintenance: optionally use systemd timers on Linux","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-05-20T22:13:59Z","receivedAt":"2021-05-20T22:23:14Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"The existing mechanism for scheduling background maintenance is done\nthrough cron. On Linux systems managed by systemd, systemd provides an\nalternative to schedule recurring tasks: systemd timers.\n\nThe main motivations to implement systemd timers in addition to cron\nare:\n* cron is optional and Linux systems running systemd might not have it\n  installed.\n* The execution of `crontab -l` can tell us if cron is installed but not\n  if the daemon is actually running.\n* With systemd, each service is run in its own cgroup and its logs are\n  tagged by the service inside journald. With cron, all scheduled tasks\n  are running in the cron daemon cgroup and all the logs of the\n  user-scheduled tasks are pretended to belong to the system cron\n  service.\n  Concretely, a user that doesn’t have access to the system logs won’t\n  have access to the log of its own tasks scheduled by cron whereas he\n  will have access to the log of its own tasks scheduled by systemd\n  timer.\n  Although `cron` attempts to send email, that email may go unseen by\n  the user because these days, local mailboxes are not heavily used\n  anymore.\n\nIn order to schedule git maintenance, we need two unit template files:\n* ~/.config/systemd/user/git-maintenance@.service\n  to define the command to be started by systemd and\n* ~/.config/systemd/user/git-maintenance@.timer\n  to define the schedule at which the command should be run.\n\nThose units are templates that are parameterized by the frequency.\n\nBased on those templates, 3 timers are started:\n* git-maintenance@hourly.timer\n* git-maintenance@daily.timer\n* git-maintenance@weekly.timer\n\nThe command launched by those three timers are the same as with the\nother scheduling methods:\n\ngit for-each-repo --config=maintenance.repo maintenance run\n--schedule=%i\n\nwith the full path for git to ensure that the version of git launched\nfor the scheduled maintenance is the same as the one used to run\n`maintenance start`.\n\nThe timer unit contains `Persistent=true` so that, if the computer is\npowered down when a maintenance task should run, the task will be run\nwhen the computer is back powered on.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n Documentation/git-maintenance.txt |  57 +++++++++-\n builtin/gc.c                      | 181 ++++++++++++++++++++++++++++++\n t/t7900-maintenance.sh            |  66 ++++++++++-\n 3 files changed, 294 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\nindex 7c4bb38a2f..50179e010f 100644\n--- a/Documentation/git-maintenance.txt\n+++ b/Documentation/git-maintenance.txt\n@@ -181,16 +181,19 @@ OPTIONS\n \t`maintenance.<task>.enabled` configured as `true` are considered.\n \tSee the 'TASKS' section for the list of accepted `<task>` values.\n \n---scheduler=auto|crontab|launchctl|schtasks::\n+--scheduler=auto|crontab|systemd-timer|launchctl|schtasks::\n \tWhen combined with the `start` subcommand, specify the scheduler\n \tto use to run the hourly, daily and weekly executions of\n \t`git maintenance run`.\n \tThe possible values for `<scheduler>` depend on the system: `crontab`\n-\tis available on POSIX systems, `launchctl` is available on\n-\tMacOS and `schtasks` is available on Windows.\n+\tis available on POSIX systems, `systemd-timer` is available on Linux\n+\tsystems, `launchctl` is available on MacOS and `schtasks` is available\n+\ton Windows.\n \tBy default or when `auto` is specified, a suitable scheduler for\n \tthe system is used. On MacOS, `launchctl` is used. On Windows,\n-\t`schtasks` is used. On all other systems, `crontab` is used.\n+\t`schtasks` is used. On Linux, `systemd-timer` is used if user systemd\n+\ttimers are available, otherwise, `crontab` is used. On all other systems,\n+\t`crontab` is used.\n \n \n TROUBLESHOOTING\n@@ -290,6 +293,52 @@ schedule to ensure you are executing the correct binaries in your\n schedule.\n \n \n+BACKGROUND MAINTENANCE ON LINUX SYSTEMD SYSTEMS\n+-----------------------------------------------\n+\n+While Linux supports `cron`, depending on the distribution, `cron` may\n+be an optional package not necessarily installed. On modern Linux\n+distributions, systemd timers are superseding it.\n+\n+If user systemd timers are available, they will be used as a replacement\n+of `cron`.\n+\n+In this case, `git maintenance start` will create user systemd timer units\n+and start the timers. The current list of user-scheduled tasks can be found\n+by running `systemctl --user list-timers`. The timers written by `git\n+maintenance start` are similar to this:\n+\n+-----------------------------------------------------------------------\n+$ systemctl --user list-timers\n+NEXT                         LEFT          LAST                         PASSED     UNIT                         ACTIVATES\n+Thu 2021-04-29 19:00:00 CEST 42min left    Thu 2021-04-29 18:00:11 CEST 17min ago  git-maintenance@hourly.timer git-maintenance@hourly.service\n+Fri 2021-04-30 00:00:00 CEST 5h 42min left Thu 2021-04-29 00:00:11 CEST 18h ago    git-maintenance@daily.timer  git-maintenance@daily.service\n+Mon 2021-05-03 00:00:00 CEST 3 days left   Mon 2021-04-26 00:00:11 CEST 3 days ago git-maintenance@weekly.timer git-maintenance@weekly.service\n+-----------------------------------------------------------------------\n+\n+One timer is registered for each `--schedule=<frequency>` option.\n+\n+The definition of the systemd units can be inspected in the following files:\n+\n+-----------------------------------------------------------------------\n+~/.config/systemd/user/git-maintenance@.timer\n+~/.config/systemd/user/git-maintenance@.service\n+~/.config/systemd/user/timers.target.wants/git-maintenance@hourly.timer\n+~/.config/systemd/user/timers.target.wants/git-maintenance@daily.timer\n+~/.config/systemd/user/timers.target.wants/git-maintenance@weekly.timer\n+-----------------------------------------------------------------------\n+\n+`git maintenance start` will overwrite these files and start the timer\n+again with `systemctl --user`, so any customization should be done by\n+creating a drop-in file, i.e. a `.conf` suffixed file in the\n+`~/.config/systemd/user/git-maintenance@.service.d` directory.\n+\n+`git maintenance stop` will stop the user systemd timers and delete\n+the above mentioned files.\n+\n+For more details, see `systemd.timer(5)`.\n+\n+\n BACKGROUND MAINTENANCE ON MACOS SYSTEMS\n ---------------------------------------\n \ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex bf21cec059..d2432ee04f 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -2079,10 +2079,174 @@ static int crontab_update_schedule(enum enable_or_disable run_maintenance,\n \treturn result;\n }\n \n+static int is_systemd_timer_available(void)\n+{\n+\tconst char *cmd = \"systemctl\";\n+\tint is_available;\n+\tstatic int cached_result = -1;\n+#ifdef __linux__\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+#endif\n+\n+\tif (cached_result != -1)\n+\t\treturn cached_result;\n+\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+#ifdef __linux__\n+\tstrvec_split(&child.args, cmd);\n+\tstrvec_pushl(&child.args, \"--user\", \"list-timers\", NULL);\n+\tchild.no_stdin = 1;\n+\tchild.no_stdout = 1;\n+\tchild.no_stderr = 1;\n+\tchild.silent_exec_failure = 1;\n+\n+\tif (start_command(&child)) {\n+\t\tcached_result = 0;\n+\t\treturn cached_result;\n+\t}\n+\tif (finish_command(&child)) {\n+\t\tcached_result = 0;\n+\t\treturn cached_result;\n+\t}\n+\tcached_result = 1;\n+\treturn cached_result;\n+#else\n+\treturn 0;\n+#endif\n+}\n+\n+static char *xdg_config_home_systemd(const char *filename)\n+{\n+\tassert(filename);\n+\treturn xdg_config_home(\"systemd/user/%s\", filename);\n+}\n+\n+static int systemd_timer_enable_unit(int enable,\n+\t\t\t\t     enum schedule_priority schedule)\n+{\n+\tconst char *cmd = \"systemctl\";\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\tconst char *frequency = get_frequency(schedule);\n+\n+\tget_schedule_cmd(&cmd, NULL);\n+\tstrvec_split(&child.args, cmd);\n+\tstrvec_pushl(&child.args, \"--user\", enable ? \"enable\" : \"disable\",\n+\t\t     \"--now\", NULL);\n+\tstrvec_pushf(&child.args, \"git-maintenance@%s.timer\", frequency);\n+\n+\tif (start_command(&child))\n+\t\tdie(_(\"failed to run systemctl\"));\n+\treturn finish_command(&child);\n+}\n+\n+static int systemd_timer_delete_unit_templates(void)\n+{\n+\tchar *filename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n+\tunlink(filename);\n+\tfree(filename);\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n+\tunlink(filename);\n+\tfree(filename);\n+\n+\treturn 0;\n+}\n+\n+static int systemd_timer_delete_units(void)\n+{\n+\treturn systemd_timer_enable_unit(DISABLE, SCHEDULE_HOURLY) ||\n+\t       systemd_timer_enable_unit(DISABLE, SCHEDULE_DAILY) ||\n+\t       systemd_timer_enable_unit(DISABLE, SCHEDULE_WEEKLY) ||\n+\t       systemd_timer_delete_unit_templates();\n+}\n+\n+static int systemd_timer_write_unit_templates(const char *exec_path)\n+{\n+\tchar *filename;\n+\tFILE *file;\n+\tconst char *unit;\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n+\tif (safe_create_leading_directories(filename))\n+\t\tdie(_(\"failed to create directories for '%s'\"), filename);\n+\tfile = xfopen(filename, \"w\");\n+\tFREE_AND_NULL(filename);\n+\n+\tunit = \"# This file was created and is maintained by Git.\\n\"\n+\t       \"# Any edits made in this file might be replaced in the future\\n\"\n+\t       \"# by a Git command.\\n\"\n+\t       \"\\n\"\n+\t       \"[Unit]\\n\"\n+\t       \"Description=Optimize Git repositories data\\n\"\n+\t       \"\\n\"\n+\t       \"[Timer]\\n\"\n+\t       \"OnCalendar=%i\\n\"\n+\t       \"Persistent=true\\n\"\n+\t       \"\\n\"\n+\t       \"[Install]\\n\"\n+\t       \"WantedBy=timers.target\\n\";\n+\tfputs(unit, file);\n+\tfclose(file);\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n+\tfile = xfopen(filename, \"w\");\n+\tfree(filename);\n+\n+\tunit = \"# This file was created and is maintained by Git.\\n\"\n+\t       \"# Any edits made in this file might be replaced in the future\\n\"\n+\t       \"# by a Git command.\\n\"\n+\t       \"\\n\"\n+\t       \"[Unit]\\n\"\n+\t       \"Description=Optimize Git repositories data\\n\"\n+\t       \"\\n\"\n+\t       \"[Service]\\n\"\n+\t       \"Type=oneshot\\n\"\n+\t       \"ExecStart=\\\"%s/git\\\" --exec-path=\\\"%s\\\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\\n\"\n+\t       \"LockPersonality=yes\\n\"\n+\t       \"MemoryDenyWriteExecute=yes\\n\"\n+\t       \"NoNewPrivileges=yes\\n\"\n+\t       \"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\\n\"\n+\t       \"RestrictNamespaces=yes\\n\"\n+\t       \"RestrictRealtime=yes\\n\"\n+\t       \"RestrictSUIDSGID=yes\\n\"\n+\t       \"SystemCallArchitectures=native\\n\"\n+\t       \"SystemCallFilter=@system-service\\n\";\n+\tfprintf(file, unit, exec_path, exec_path);\n+\tfclose(file);\n+\n+\treturn 0;\n+}\n+\n+static int systemd_timer_setup_units(void)\n+{\n+\tconst char *exec_path = git_exec_path();\n+\n+\treturn systemd_timer_write_unit_templates(exec_path) ||\n+\t       systemd_timer_enable_unit(ENABLE, SCHEDULE_HOURLY) ||\n+\t       systemd_timer_enable_unit(ENABLE, SCHEDULE_DAILY) ||\n+\t       systemd_timer_enable_unit(ENABLE, SCHEDULE_WEEKLY);\n+}\n+\n+static int systemd_timer_update_schedule(enum enable_or_disable run_maintenance,\n+\t\t\t\t\t int fd)\n+{\n+\tswitch (run_maintenance) {\n+\tcase ENABLE:\n+\t\treturn systemd_timer_setup_units();\n+\tcase DISABLE:\n+\t\treturn systemd_timer_delete_units();\n+\tdefault:\n+\t\tBUG(\"invalid enable_or_disable value\");\n+\t}\n+}\n+\n enum scheduler {\n \tSCHEDULER_INVALID = -1,\n \tSCHEDULER_AUTO,\n \tSCHEDULER_CRON,\n+\tSCHEDULER_SYSTEMD,\n \tSCHEDULER_LAUNCHCTL,\n \tSCHEDULER_SCHTASKS,\n };\n@@ -2097,6 +2261,11 @@ static const struct {\n \t\t.is_available = is_crontab_available,\n \t\t.update_schedule = crontab_update_schedule,\n \t},\n+\t[SCHEDULER_SYSTEMD] = {\n+\t\t.name = \"systemctl\",\n+\t\t.is_available = is_systemd_timer_available,\n+\t\t.update_schedule = systemd_timer_update_schedule,\n+\t},\n \t[SCHEDULER_LAUNCHCTL] = {\n \t\t.name = \"launchctl\",\n \t\t.is_available = is_launchctl_available,\n@@ -2117,6 +2286,9 @@ static enum scheduler parse_scheduler(const char *value)\n \t\treturn SCHEDULER_AUTO;\n \telse if (!strcasecmp(value, \"cron\") || !strcasecmp(value, \"crontab\"))\n \t\treturn SCHEDULER_CRON;\n+\telse if (!strcasecmp(value, \"systemd\") ||\n+\t\t !strcasecmp(value, \"systemd-timer\"))\n+\t\treturn SCHEDULER_SYSTEMD;\n \telse if (!strcasecmp(value, \"launchctl\"))\n \t\treturn SCHEDULER_LAUNCHCTL;\n \telse if (!strcasecmp(value, \"schtasks\"))\n@@ -2155,6 +2327,15 @@ static void resolve_auto_scheduler(enum scheduler *scheduler)\n \t*scheduler = SCHEDULER_SCHTASKS;\n \treturn;\n \n+#elif defined(__linux__)\n+\tif (is_systemd_timer_available())\n+\t\t*scheduler = SCHEDULER_SYSTEMD;\n+\telse if (is_crontab_available())\n+\t\t*scheduler = SCHEDULER_CRON;\n+\telse\n+\t\tdie(_(\"neither systemd timers nor crontab are available\"));\n+\treturn;\n+\n #else\n \t*scheduler = SCHEDULER_CRON;\n \treturn;\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 9eac260307..c8a6f19ebc 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -20,6 +20,18 @@ test_xmllint () {\n \tfi\n }\n \n+test_lazy_prereq SYSTEMD_ANALYZE '\n+\tsystemd-analyze --help >out &&\n+\tgrep verify out\n+'\n+\n+test_systemd_analyze_verify () {\n+\tif test_have_prereq SYSTEMD_ANALYZE\n+\tthen\n+\t\tsystemd-analyze verify \"$@\"\n+\tfi\n+}\n+\n test_expect_success 'help text' '\n \ttest_expect_code 129 git maintenance -h 2>err &&\n \ttest_i18ngrep \"usage: git maintenance <subcommand>\" err &&\n@@ -628,14 +640,54 @@ test_expect_success 'start and stop Windows maintenance' '\n \ttest_cmp expect args\n '\n \n+test_expect_success 'start and stop Linux/systemd maintenance' '\n+\twrite_script print-args <<-\\EOF &&\n+\tprintf \"%s\\n\" \"$*\" >>args\n+\tEOF\n+\n+\tXDG_CONFIG_HOME=\"$PWD\" &&\n+\texport XDG_CONFIG_HOME &&\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args\" git maintenance start --scheduler=systemd-timer &&\n+\n+\t# start registers the repo\n+\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n+\n+\ttest_systemd_analyze_verify \"systemd/user/git-maintenance@.service\" &&\n+\n+\tprintf -- \"--user enable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args\" git maintenance stop &&\n+\n+\t# stop does not unregister the repo\n+\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n+\n+\ttest_path_is_missing \"systemd/user/git-maintenance@.timer\" &&\n+\ttest_path_is_missing \"systemd/user/git-maintenance@.service\" &&\n+\n+\tprintf -- \"--user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n+\ttest_cmp expect args\n+'\n+\n test_expect_success 'start and stop when several schedulers are available' '\n \twrite_script print-args <<-\\EOF &&\n \tprintf \"%s\\n\" \"$*\" | sed \"s:gui/[0-9][0-9]*:gui/[UID]:; s:\\(schtasks /create .* /xml\\).*:\\1:;\" >>args\n \tEOF\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n-\trm -f expect &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=systemd-timer &&\n+\tprintf -- \"systemctl --user enable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n+\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n+\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n \tfor frequency in hourly daily weekly\n \tdo\n \t\tPLIST=\"$pfx/Library/LaunchAgents/org.git-scm.git.$frequency.plist\" &&\n@@ -647,17 +699,19 @@ test_expect_success 'start and stop when several schedulers are available' '\n \ttest_cmp expect args &&\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n+\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n \tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n-\t\thourly daily weekly >expect &&\n+\t\thourly daily weekly >>expect &&\n \tprintf \"schtasks /create /tn Git Maintenance (%s) /f /xml\\n\" \\\n \t\thourly daily weekly >>expect &&\n \ttest_cmp expect args &&\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n+\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n \tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n-\t\thourly daily weekly >expect &&\n+\t\thourly daily weekly >>expect &&\n \tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n \t\thourly daily weekly >>expect &&\n \ttest_cmp expect args\n-- \n2.31.1\n\n"},{"id":"425163","messageId":"20210520221359.75615-1-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210509213217.449489-1-lenaic@lhuard.fr","subject":"[PATCH v3 0/4] maintenance: use systemd timers on Linux","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-05-20T22:13:55Z","receivedAt":"2021-05-20T22:23:30Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Hello,\n\nThanks again for your valuable feedback!\nI’ve reworked this submission to hopefully address all the raised\nconcerns.\n\nAs it’s becoming bigger, I’ve split it in several patches:\n\n* cache.h: rename \"xdg_config_home\" to \"xdg_config_home_git\"\n\nIn order to honor `$XDG_CONFIG_HOME` to built the path of systemd units,\na lot of code from the `xdg_config_home()` function was\nduplicated. This existing function couldn’t be used as-is because\nit was hard-coding the `git` sub-directory below `$XDG_CONFIG_HOME`.\n\nThis first preparatory patch re-purposes `xdg_config_home()` to make\nit more generic by removing the `git` path it used to append to\n`$XDG_CONFIG_HOME`.\nThanks to this refactoring, `xdg_config_home()` can be used to build\npaths like:\n* `$XDG_CONFIG_HOME/git/…` it’s the new `xdg_config_home_git()`\nfunction and\n* `$XDG_CONFIG_HOME/systemd/user/…` it’s the new\n`xdg_config_home_systemd()` function which will be introduced later,\nin the last patch of this series.\n\n* maintenance: introduce ENABLE/DISABLE for code clarity\n\nSome functions in git maintenance are doing one thing and the exact\nopposite, depending on a parameter that could be `0` or `1`.\nThis could sometimes be confusing.\nThis patch introduces an enum with `ENABLE` and `DISABLE` values to\nmake the code more explicit.\n\n* maintenance: `git maintenance run` learned `--scheduler=<scheduler>`\n\nThis patch contains all the code that is related to the addition of\nthe new `--scheduler` parameter of the `git maintenance start`\ncommand, independently of the systemd timers.\n\nThis part has significantly changed to review the testing mechanisms.\nThe `GIT_TEST_MAINT_SCHEDULER` environment variable can now take a\ncomma separated list of schedulers and their mock in order to be able\nto test cases where multiple schedulers are available.\n\nIt allows to test that `git maintenance start --scheduler=XXX` not\nonly enables the scheduler XXX, but it also disables all the other\navailable ones.\nWe can also test that `git maintenance stop`, stops all the available\nschedulers.\n\nIn order to remove the `cmd` parameter that was passed to a lot of\nfunctions, the parsing of `GIT_TEST_MAINT_SCHEDULER` is now factorized\nin a `get_schedule_cmd()` function that is invoked from the leaves of\nthe function call tree.\nIt means that this function is called many times to parse\n`GIT_TEST_MAINT_SCHEDULER` again and again.\nIt is inefficient but as it is used for test only, it shouldn’t be a\nconcern.\n\n`int get_schdule_cmd(const char **cmd, int *is_available)`\n\nThis function returns `true` if `GIT_TEST_MAINT_SCHEDULER` is\ndefined. `false` otherwise.\n\n`*is_available` is set to `true` if `*cmd` is present in\n`$GIT_TEST_MAINT_SCHEDULER`, and to `false` otherwise.\n\nIf `*cmd` is present in `$GIT_TEST_MAINT_SCHEDULER`, it’s value is\nupdated to its mock.\n\n* maintenance: optionally use systemd timers on Linux\n\nThis last patch adds the support for systemd timers.\n\nLénaïc Huard (4):\n  cache.h: rename \"xdg_config_home\" to \"xdg_config_home_git\"\n  maintenance: introduce ENABLE/DISABLE for code clarity\n  maintenance: `git maintenance run` learned `--scheduler=<scheduler>`\n  maintenance: optionally use systemd timers on Linux\n\n Documentation/git-maintenance.txt |  60 ++++\n attr.c                            |   2 +-\n builtin/config.c                  |   2 +-\n builtin/credential-cache.c        |   2 +-\n builtin/credential-store.c        |   2 +-\n builtin/gc.c                      | 549 ++++++++++++++++++++++++++----\n cache.h                           |  12 +-\n config.c                          |   2 +-\n dir.c                             |   2 +-\n path.c                            |  35 +-\n sequencer.c                       |   2 +-\n t/t7900-maintenance.sh            | 110 +++++-\n 12 files changed, 685 insertions(+), 95 deletions(-)\n\n-- \n2.31.1\n\n"},{"id":"425164","messageId":"20210520221359.75615-2-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210520221359.75615-1-lenaic@lhuard.fr","subject":"[PATCH v3 1/4] cache.h: rename \"xdg_config_home\" to \"xdg_config_home_git\"","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-05-20T22:13:56Z","receivedAt":"2021-05-20T22:23:31Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Current implementation of `xdg_config_home(str)` returns\n`$XDG_CONFIG_HOME/git/$str`, with the `git` subdirectory inserted\nbetween the `XDG_CONFIG_HOME` environment variable and the parameter.\n\nThis patch re-purposes `xdg_config_home(…)` to be more generic. It now\nonly concatenates \"$XDG_CONFIG_HOME\", or \"$HOME/.config\" if the former\nisn’t defined, with the parameter, without adding `git` in between.\nIts parameter is now a format string.\n\nThe previous functionality is now provided by a new\n`xdg_config_home_git(…)` function whose implementation leverages\n`xdg_config_home(…)`.\n\n`xdg_cache_home(…)` has been renamed `xdg_cache_home_git(…)` for\nconsistency.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n attr.c                     |  2 +-\n builtin/config.c           |  2 +-\n builtin/credential-cache.c |  2 +-\n builtin/credential-store.c |  2 +-\n cache.h                    | 12 ++++++++++--\n config.c                   |  2 +-\n dir.c                      |  2 +-\n path.c                     | 35 +++++++++++++++++++++++++++--------\n sequencer.c                |  2 +-\n 9 files changed, 44 insertions(+), 17 deletions(-)\n\ndiff --git a/attr.c b/attr.c\nindex ac8ec7ce51..0efafbd308 100644\n--- a/attr.c\n+++ b/attr.c\n@@ -831,7 +831,7 @@ static const char *git_etc_gitattributes(void)\n static const char *get_home_gitattributes(void)\n {\n \tif (!git_attributes_file)\n-\t\tgit_attributes_file = xdg_config_home(\"attributes\");\n+\t\tgit_attributes_file = xdg_config_home_git(\"attributes\");\n \n \treturn git_attributes_file;\n }\ndiff --git a/builtin/config.c b/builtin/config.c\nindex f71fa39b38..8cf2394eb8 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -672,7 +672,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \n \tif (use_global_config) {\n \t\tchar *user_config = expand_user_path(\"~/.gitconfig\", 0);\n-\t\tchar *xdg_config = xdg_config_home(\"config\");\n+\t\tchar *xdg_config = xdg_config_home_git(\"config\");\n \n \t\tif (!user_config)\n \t\t\t/*\ndiff --git a/builtin/credential-cache.c b/builtin/credential-cache.c\nindex 76a6ba3722..4c0b7c4d43 100644\n--- a/builtin/credential-cache.c\n+++ b/builtin/credential-cache.c\n@@ -94,7 +94,7 @@ static char *get_socket_path(void)\n \tif (old_dir && !stat(old_dir, &sb) && S_ISDIR(sb.st_mode))\n \t\tsocket = xstrfmt(\"%s/socket\", old_dir);\n \telse\n-\t\tsocket = xdg_cache_home(\"credential/socket\");\n+\t\tsocket = xdg_cache_home_git(\"credential/socket\");\n \tfree(old_dir);\n \treturn socket;\n }\ndiff --git a/builtin/credential-store.c b/builtin/credential-store.c\nindex ae3c1ba75f..34ca419bb6 100644\n--- a/builtin/credential-store.c\n+++ b/builtin/credential-store.c\n@@ -175,7 +175,7 @@ int cmd_credential_store(int argc, const char **argv, const char *prefix)\n \t} else {\n \t\tif ((file = expand_user_path(\"~/.git-credentials\", 0)))\n \t\t\tstring_list_append_nodup(&fns, file);\n-\t\tfile = xdg_config_home(\"credentials\");\n+\t\tfile = xdg_config_home_git(\"credentials\");\n \t\tif (file)\n \t\t\tstring_list_append_nodup(&fns, file);\n \t}\ndiff --git a/cache.h b/cache.h\nindex 148d9ab5f1..34fa48a438 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -1263,19 +1263,27 @@ int is_ntfs_dotgitattributes(const char *name);\n  */\n int looks_like_command_line_option(const char *str);\n \n+/**\n+ * Return a newly allocated string with the evaluation of\n+ * \"$XDG_CONFIG_HOME/$fmt...\" if $XDG_CONFIG_HOME is non-empty, otherwise\n+ * \"$HOME/.config/$fmt...\". Return NULL upon error.\n+ */\n+char *xdg_config_home(const char *fmt, ...)\n+\t__attribute__((format (printf, 1, 2)));\n+\n /**\n  * Return a newly allocated string with the evaluation of\n  * \"$XDG_CONFIG_HOME/git/$filename\" if $XDG_CONFIG_HOME is non-empty, otherwise\n  * \"$HOME/.config/git/$filename\". Return NULL upon error.\n  */\n-char *xdg_config_home(const char *filename);\n+char *xdg_config_home_git(const char *filename);\n \n /**\n  * Return a newly allocated string with the evaluation of\n  * \"$XDG_CACHE_HOME/git/$filename\" if $XDG_CACHE_HOME is non-empty, otherwise\n  * \"$HOME/.cache/git/$filename\". Return NULL upon error.\n  */\n-char *xdg_cache_home(const char *filename);\n+char *xdg_cache_home_git(const char *filename);\n \n int git_open_cloexec(const char *name, int flags);\n #define git_open(name) git_open_cloexec(name, O_RDONLY)\ndiff --git a/config.c b/config.c\nindex 6428393a41..648ae9b918 100644\n--- a/config.c\n+++ b/config.c\n@@ -1883,7 +1883,7 @@ static int do_git_config_sequence(const struct config_options *opts,\n \t\t\t\t  config_fn_t fn, void *data)\n {\n \tint ret = 0;\n-\tchar *xdg_config = xdg_config_home(\"config\");\n+\tchar *xdg_config = xdg_config_home_git(\"config\");\n \tchar *user_config = expand_user_path(\"~/.gitconfig\", 0);\n \tchar *repo_config;\n \tenum config_scope prev_parsing_scope = current_parsing_scope;\ndiff --git a/dir.c b/dir.c\nindex 3474e67e8f..3fdba7b6fe 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -2990,7 +2990,7 @@ void setup_standard_excludes(struct dir_struct *dir)\n \n \t/* core.excludesfile defaulting to $XDG_CONFIG_HOME/git/ignore */\n \tif (!excludes_file)\n-\t\texcludes_file = xdg_config_home(\"ignore\");\n+\t\texcludes_file = xdg_config_home_git(\"ignore\");\n \tif (excludes_file && !access_or_warn(excludes_file, R_OK, 0))\n \t\tadd_patterns_from_file_1(dir, excludes_file,\n \t\t\t\t\t dir->untracked ? &dir->ss_excludes_file : NULL);\ndiff --git a/path.c b/path.c\nindex 7b385e5eb2..15e2143e9f 100644\n--- a/path.c\n+++ b/path.c\n@@ -1498,22 +1498,41 @@ int looks_like_command_line_option(const char *str)\n \treturn str && str[0] == '-';\n }\n \n-char *xdg_config_home(const char *filename)\n+char *xdg_config_home(const char *fmt, ...)\n {\n \tconst char *home, *config_home;\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tchar *out = NULL;\n+\tva_list args;\n+\n+\tva_start(args, fmt);\n+\tstrbuf_vaddf(&buf, fmt, args);\n+\tva_end(args);\n \n-\tassert(filename);\n \tconfig_home = getenv(\"XDG_CONFIG_HOME\");\n-\tif (config_home && *config_home)\n-\t\treturn mkpathdup(\"%s/git/%s\", config_home, filename);\n+\tif (config_home && *config_home) {\n+\t\tout = mkpathdup(\"%s/%s\", config_home, buf.buf);\n+\t\tgoto done;\n+\t}\n \n \thome = getenv(\"HOME\");\n-\tif (home)\n-\t\treturn mkpathdup(\"%s/.config/git/%s\", home, filename);\n-\treturn NULL;\n+\tif (home) {\n+\t\tout = mkpathdup(\"%s/.config/%s\", home, buf.buf);\n+\t\tgoto done;\n+\t}\n+\n+done:\n+\tstrbuf_release(&buf);\n+\treturn out;\n+}\n+\n+char *xdg_config_home_git(const char *filename)\n+{\n+\tassert(filename);\n+\treturn xdg_config_home(\"git/%s\", filename);\n }\n \n-char *xdg_cache_home(const char *filename)\n+char *xdg_cache_home_git(const char *filename)\n {\n \tconst char *home, *cache_home;\n \ndiff --git a/sequencer.c b/sequencer.c\nindex fd183b5593..25f467e685 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1241,7 +1241,7 @@ N_(\"Your name and email address were configured automatically based\\n\"\n static const char *implicit_ident_advice(void)\n {\n \tchar *user_config = expand_user_path(\"~/.gitconfig\", 0);\n-\tchar *xdg_config = xdg_config_home(\"config\");\n+\tchar *xdg_config = xdg_config_home_git(\"config\");\n \tint config_exists = file_exists(user_config) || file_exists(xdg_config);\n \n \tfree(user_config);\n-- \n2.31.1\n\n"},{"id":"425165","messageId":"20210520221359.75615-4-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210520221359.75615-1-lenaic@lhuard.fr","subject":"[PATCH v3 3/4] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-05-20T22:13:58Z","receivedAt":"2021-05-20T22:23:37Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Depending on the system, different schedulers can be used to schedule\nthe hourly, daily and weekly executions of `git maintenance run`:\n* `launchctl` for MacOS,\n* `schtasks` for Windows and\n* `crontab` for everything else.\n\n`git maintenance run` now has an option to let the end-user explicitly\nchoose which scheduler he wants to use:\n`--scheduler=auto|crontab|launchctl|schtasks`.\n\nWhen `git maintenance start --scheduler=XXX` is run, it not only\nregisters `git maintenance run` tasks in the scheduler XXX, it also\nremoves the `git maintenance run` tasks from all the other schedulers to\nensure we cannot have two schedulers launching concurrent identical\ntasks.\n\nThe default value is `auto` which chooses a suitable scheduler for the\nsystem.\n\n`git maintenance stop` doesn't have any `--scheduler` parameter because\nthis command will try to remove the `git maintenance run` tasks from all\nthe available schedulers.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n Documentation/git-maintenance.txt |  11 +\n builtin/gc.c                      | 333 ++++++++++++++++++++++++------\n t/t7900-maintenance.sh            |  56 ++++-\n 3 files changed, 333 insertions(+), 67 deletions(-)\n\ndiff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\nindex 80ddd33ceb..7c4bb38a2f 100644\n--- a/Documentation/git-maintenance.txt\n+++ b/Documentation/git-maintenance.txt\n@@ -181,6 +181,17 @@ OPTIONS\n \t`maintenance.<task>.enabled` configured as `true` are considered.\n \tSee the 'TASKS' section for the list of accepted `<task>` values.\n \n+--scheduler=auto|crontab|launchctl|schtasks::\n+\tWhen combined with the `start` subcommand, specify the scheduler\n+\tto use to run the hourly, daily and weekly executions of\n+\t`git maintenance run`.\n+\tThe possible values for `<scheduler>` depend on the system: `crontab`\n+\tis available on POSIX systems, `launchctl` is available on\n+\tMacOS and `schtasks` is available on Windows.\n+\tBy default or when `auto` is specified, a suitable scheduler for\n+\tthe system is used. On MacOS, `launchctl` is used. On Windows,\n+\t`schtasks` is used. On all other systems, `crontab` is used.\n+\n \n TROUBLESHOOTING\n ---------------\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 0caf8d45c4..bf21cec059 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1544,6 +1544,60 @@ static const char *get_frequency(enum schedule_priority schedule)\n \t}\n }\n \n+static int get_schedule_cmd(const char **cmd, int *is_available)\n+{\n+\tchar *item;\n+\tstatic char test_cmd[32];\n+\tchar *testing = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n+\n+\tif (!testing)\n+\t\treturn 0;\n+\n+\tif (is_available)\n+\t\t*is_available = 0;\n+\n+\tfor(item = testing;;) {\n+\t\tchar *sep;\n+\t\tchar *end_item = strchr(item, ',');\n+\t\tif (end_item)\n+\t\t\t*end_item = '\\0';\n+\n+\t\tsep = strchr(item, ':');\n+\t\tif (!sep)\n+\t\t\tdie(\"GIT_TEST_MAINT_SCHEDULER unparseable: %s\", testing);\n+\t\t*sep = '\\0';\n+\n+\t\tif (!strcmp(*cmd, item)) {\n+\t\t\tstrlcpy(test_cmd, sep+1, ARRAY_SIZE(test_cmd));\n+\t\t\t*cmd = test_cmd;\n+\t\t\tif (is_available)\n+\t\t\t\t*is_available = 1;\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tif (!end_item)\n+\t\t\tbreak;\n+\t\titem = end_item + 1;\n+\t}\n+\n+\tfree(testing);\n+\treturn 1;\n+}\n+\n+static int is_launchctl_available(void)\n+{\n+\tconst char *cmd = \"launchctl\";\n+\tint is_available;\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+#ifdef __APPLE__\n+\treturn 1;\n+#else\n+\treturn 0;\n+#endif\n+}\n+\n static char *launchctl_service_name(const char *frequency)\n {\n \tstruct strbuf label = STRBUF_INIT;\n@@ -1576,12 +1630,14 @@ enum enable_or_disable {\n };\n \n static int launchctl_boot_plist(enum enable_or_disable enable,\n-\t\t\t\tconst char *filename, const char *cmd)\n+\t\t\t\tconst char *filename)\n {\n+\tconst char *cmd = \"launchctl\";\n \tint result;\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tchar *uid = launchctl_get_uid();\n \n+\tget_schedule_cmd(&cmd, NULL);\n \tstrvec_split(&child.args, cmd);\n \tstrvec_pushl(&child.args, enable == ENABLE ? \"bootstrap\" : \"bootout\",\n \t\t     uid, filename, NULL);\n@@ -1598,26 +1654,26 @@ static int launchctl_boot_plist(enum enable_or_disable enable,\n \treturn result;\n }\n \n-static int launchctl_remove_plist(enum schedule_priority schedule, const char *cmd)\n+static int launchctl_remove_plist(enum schedule_priority schedule)\n {\n \tconst char *frequency = get_frequency(schedule);\n \tchar *name = launchctl_service_name(frequency);\n \tchar *filename = launchctl_service_filename(name);\n-\tint result = launchctl_boot_plist(DISABLE, filename, cmd);\n+\tint result = launchctl_boot_plist(DISABLE, filename);\n \tunlink(filename);\n \tfree(filename);\n \tfree(name);\n \treturn result;\n }\n \n-static int launchctl_remove_plists(const char *cmd)\n+static int launchctl_remove_plists(void)\n {\n-\treturn launchctl_remove_plist(SCHEDULE_HOURLY, cmd) ||\n-\t\tlaunchctl_remove_plist(SCHEDULE_DAILY, cmd) ||\n-\t\tlaunchctl_remove_plist(SCHEDULE_WEEKLY, cmd);\n+\treturn launchctl_remove_plist(SCHEDULE_HOURLY) ||\n+\t\tlaunchctl_remove_plist(SCHEDULE_DAILY) ||\n+\t\tlaunchctl_remove_plist(SCHEDULE_WEEKLY);\n }\n \n-static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule, const char *cmd)\n+static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule)\n {\n \tFILE *plist;\n \tint i;\n@@ -1686,8 +1742,8 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n \tfclose(plist);\n \n \t/* bootout might fail if not already running, so ignore */\n-\tlaunchctl_boot_plist(DISABLE, filename, cmd);\n-\tif (launchctl_boot_plist(ENABLE, filename, cmd))\n+\tlaunchctl_boot_plist(DISABLE, filename);\n+\tif (launchctl_boot_plist(ENABLE, filename))\n \t\tdie(_(\"failed to bootstrap service %s\"), filename);\n \n \tfree(filename);\n@@ -1695,28 +1751,42 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n \treturn 0;\n }\n \n-static int launchctl_add_plists(const char *cmd)\n+static int launchctl_add_plists(void)\n {\n \tconst char *exec_path = git_exec_path();\n \n-\treturn launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY, cmd) ||\n-\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_DAILY, cmd) ||\n-\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY, cmd);\n+\treturn launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY) ||\n+\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_DAILY) ||\n+\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY);\n }\n \n static int launchctl_update_schedule(enum enable_or_disable run_maintenance,\n-\t\t\t\t     int fd, const char *cmd)\n+\t\t\t\t     int fd)\n {\n \tswitch (run_maintenance) {\n \tcase ENABLE:\n-\t\treturn launchctl_add_plists(cmd);\n+\t\treturn launchctl_add_plists();\n \tcase DISABLE:\n-\t\treturn launchctl_remove_plists(cmd);\n+\t\treturn launchctl_remove_plists();\n \tdefault:\n \t\tBUG(\"invalid enable_or_disable value\");\n \t}\n }\n \n+static int is_schtasks_available(void)\n+{\n+\tconst char *cmd = \"schtasks\";\n+\tint is_available;\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+#ifdef GIT_WINDOWS_NATIVE\n+\treturn 1;\n+#else\n+\treturn 0;\n+#endif\n+}\n+\n static char *schtasks_task_name(const char *frequency)\n {\n \tstruct strbuf label = STRBUF_INIT;\n@@ -1724,13 +1794,15 @@ static char *schtasks_task_name(const char *frequency)\n \treturn strbuf_detach(&label, NULL);\n }\n \n-static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd)\n+static int schtasks_remove_task(enum schedule_priority schedule)\n {\n+\tconst char *cmd = \"schtasks\";\n \tint result;\n \tstruct strvec args = STRVEC_INIT;\n \tconst char *frequency = get_frequency(schedule);\n \tchar *name = schtasks_task_name(frequency);\n \n+\tget_schedule_cmd(&cmd, NULL);\n \tstrvec_split(&args, cmd);\n \tstrvec_pushl(&args, \"/delete\", \"/tn\", name, \"/f\", NULL);\n \n@@ -1741,15 +1813,16 @@ static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd\n \treturn result;\n }\n \n-static int schtasks_remove_tasks(const char *cmd)\n+static int schtasks_remove_tasks(void)\n {\n-\treturn schtasks_remove_task(SCHEDULE_HOURLY, cmd) ||\n-\t\tschtasks_remove_task(SCHEDULE_DAILY, cmd) ||\n-\t\tschtasks_remove_task(SCHEDULE_WEEKLY, cmd);\n+\treturn schtasks_remove_task(SCHEDULE_HOURLY) ||\n+\t\tschtasks_remove_task(SCHEDULE_DAILY) ||\n+\t\tschtasks_remove_task(SCHEDULE_WEEKLY);\n }\n \n-static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule, const char *cmd)\n+static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule)\n {\n+\tconst char *cmd = \"schtasks\";\n \tint result;\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tconst char *xml;\n@@ -1758,6 +1831,8 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority\n \tchar *name = schtasks_task_name(frequency);\n \tstruct strbuf tfilename = STRBUF_INIT;\n \n+\tget_schedule_cmd(&cmd, NULL);\n+\n \tstrbuf_addf(&tfilename, \"%s/schedule_%s_XXXXXX\",\n \t\t    get_git_common_dir(), frequency);\n \ttfile = xmks_tempfile(tfilename.buf);\n@@ -1862,34 +1937,65 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority\n \treturn result;\n }\n \n-static int schtasks_schedule_tasks(const char *cmd)\n+static int schtasks_schedule_tasks(void)\n {\n \tconst char *exec_path = git_exec_path();\n \n-\treturn schtasks_schedule_task(exec_path, SCHEDULE_HOURLY, cmd) ||\n-\t\tschtasks_schedule_task(exec_path, SCHEDULE_DAILY, cmd) ||\n-\t\tschtasks_schedule_task(exec_path, SCHEDULE_WEEKLY, cmd);\n+\treturn schtasks_schedule_task(exec_path, SCHEDULE_HOURLY) ||\n+\t\tschtasks_schedule_task(exec_path, SCHEDULE_DAILY) ||\n+\t\tschtasks_schedule_task(exec_path, SCHEDULE_WEEKLY);\n }\n \n static int schtasks_update_schedule(enum enable_or_disable run_maintenance,\n-\t\t\t\t    int fd, const char *cmd)\n+\t\t\t\t    int fd)\n {\n \tswitch (run_maintenance) {\n \tcase ENABLE:\n-\t\treturn schtasks_schedule_tasks(cmd);\n+\t\treturn schtasks_schedule_tasks();\n \tcase DISABLE:\n-\t\treturn schtasks_remove_tasks(cmd);\n+\t\treturn schtasks_remove_tasks();\n \tdefault:\n \t\tBUG(\"invalid enable_or_disable value\");\n \t}\n }\n \n+static int is_crontab_available(void)\n+{\n+\tconst char *cmd = \"crontab\";\n+\tint is_available;\n+\tstatic int cached_result = -1;\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\n+\tif (cached_result != -1)\n+\t\treturn cached_result;\n+\n+\tif (get_schedule_cmd(&cmd, &is_available) && !is_available)\n+\t\treturn 0;\n+\n+\tstrvec_split(&child.args, cmd);\n+\tstrvec_push(&child.args, \"-l\");\n+\tchild.no_stdin = 1;\n+\tchild.no_stdout = 1;\n+\tchild.no_stderr = 1;\n+\tchild.silent_exec_failure = 1;\n+\n+\tif (start_command(&child)) {\n+\t\tcached_result = 0;\n+\t\treturn cached_result;\n+\t}\n+\t/* Ignore exit code, as an empty crontab will return error. */\n+\tfinish_command(&child);\n+\tcached_result = 1;\n+\treturn cached_result;\n+}\n+\n #define BEGIN_LINE \"# BEGIN GIT MAINTENANCE SCHEDULE\"\n #define END_LINE \"# END GIT MAINTENANCE SCHEDULE\"\n \n static int crontab_update_schedule(enum enable_or_disable run_maintenance,\n-\t\t\t\t   int fd, const char *cmd)\n+\t\t\t\t   int fd)\n {\n+\tconst char *cmd = \"crontab\";\n \tint result = 0;\n \tint in_old_region = 0;\n \tstruct child_process crontab_list = CHILD_PROCESS_INIT;\n@@ -1897,6 +2003,7 @@ static int crontab_update_schedule(enum enable_or_disable run_maintenance,\n \tFILE *cron_list, *cron_in;\n \tstruct strbuf line = STRBUF_INIT;\n \n+\tget_schedule_cmd(&cmd, NULL);\n \tstrvec_split(&crontab_list.args, cmd);\n \tstrvec_push(&crontab_list.args, \"-l\");\n \tcrontab_list.in = -1;\n@@ -1972,61 +2079,161 @@ static int crontab_update_schedule(enum enable_or_disable run_maintenance,\n \treturn result;\n }\n \n+enum scheduler {\n+\tSCHEDULER_INVALID = -1,\n+\tSCHEDULER_AUTO,\n+\tSCHEDULER_CRON,\n+\tSCHEDULER_LAUNCHCTL,\n+\tSCHEDULER_SCHTASKS,\n+};\n+\n+static const struct {\n+\tconst char *name;\n+\tint (*is_available)(void);\n+\tint (*update_schedule)(enum enable_or_disable run_maintenance, int fd);\n+} scheduler_fn[] = {\n+\t[SCHEDULER_CRON] = {\n+\t\t.name = \"crontab\",\n+\t\t.is_available = is_crontab_available,\n+\t\t.update_schedule = crontab_update_schedule,\n+\t},\n+\t[SCHEDULER_LAUNCHCTL] = {\n+\t\t.name = \"launchctl\",\n+\t\t.is_available = is_launchctl_available,\n+\t\t.update_schedule = launchctl_update_schedule,\n+\t},\n+\t[SCHEDULER_SCHTASKS] = {\n+\t\t.name = \"schtasks\",\n+\t\t.is_available = is_schtasks_available,\n+\t\t.update_schedule = schtasks_update_schedule,\n+\t},\n+};\n+\n+static enum scheduler parse_scheduler(const char *value)\n+{\n+\tif (!value)\n+\t\treturn SCHEDULER_INVALID;\n+\telse if (!strcasecmp(value, \"auto\"))\n+\t\treturn SCHEDULER_AUTO;\n+\telse if (!strcasecmp(value, \"cron\") || !strcasecmp(value, \"crontab\"))\n+\t\treturn SCHEDULER_CRON;\n+\telse if (!strcasecmp(value, \"launchctl\"))\n+\t\treturn SCHEDULER_LAUNCHCTL;\n+\telse if (!strcasecmp(value, \"schtasks\"))\n+\t\treturn SCHEDULER_SCHTASKS;\n+\telse\n+\t\treturn SCHEDULER_INVALID;\n+}\n+\n+static int maintenance_opt_scheduler(const struct option *opt, const char *arg,\n+\t\t\t\t     int unset)\n+{\n+\tenum scheduler *scheduler = opt->value;\n+\n+\tBUG_ON_OPT_NEG(unset);\n+\n+\t*scheduler = parse_scheduler(arg);\n+\tif (*scheduler == SCHEDULER_INVALID)\n+\t\treturn error(_(\"unrecognized --scheduler argument '%s'\"), arg);\n+\treturn 0;\n+}\n+\n+struct maintenance_start_opts {\n+\tenum scheduler scheduler;\n+};\n+\n+static void resolve_auto_scheduler(enum scheduler *scheduler)\n+{\n+\tif (*scheduler != SCHEDULER_AUTO)\n+\t\treturn;\n+\n #if defined(__APPLE__)\n-static const char platform_scheduler[] = \"launchctl\";\n+\t*scheduler = SCHEDULER_LAUNCHCTL;\n+\treturn;\n+\n #elif defined(GIT_WINDOWS_NATIVE)\n-static const char platform_scheduler[] = \"schtasks\";\n+\t*scheduler = SCHEDULER_SCHTASKS;\n+\treturn;\n+\n #else\n-static const char platform_scheduler[] = \"crontab\";\n+\t*scheduler = SCHEDULER_CRON;\n+\treturn;\n #endif\n+}\n \n-static int update_background_schedule(int enable)\n+static void validate_scheduler(enum scheduler scheduler)\n {\n-\tint result;\n-\tconst char *scheduler = platform_scheduler;\n-\tconst char *cmd = scheduler;\n-\tchar *testing;\n+\tif (scheduler == SCHEDULER_INVALID)\n+\t\tBUG(\"invalid scheduler\");\n+\tif (scheduler == SCHEDULER_AUTO)\n+\t\tBUG(\"resolve_auto_scheduler should have been called before\");\n+\n+\tif (!scheduler_fn[scheduler].is_available())\n+\t\tdie(_(\"%s scheduler is not available\"),\n+\t\t    scheduler_fn[scheduler].name);\n+}\n+\n+static int update_background_schedule(const struct maintenance_start_opts *opts,\n+\t\t\t\t      enum enable_or_disable enable)\n+{\n+\tunsigned int i;\n+\tint res, result = 0;\n \tstruct lock_file lk;\n \tchar *lock_path = xstrfmt(\"%s/schedule\", the_repository->objects->odb->path);\n \n-\ttesting = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n-\tif (testing) {\n-\t\tchar *sep = strchr(testing, ':');\n-\t\tif (!sep)\n-\t\t\tdie(\"GIT_TEST_MAINT_SCHEDULER unparseable: %s\", testing);\n-\t\t*sep = '\\0';\n-\t\tscheduler = testing;\n-\t\tcmd = sep + 1;\n-\t}\n-\n \tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0)\n \t\treturn error(_(\"another process is scheduling background maintenance\"));\n \n-\tif (!strcmp(scheduler, \"launchctl\"))\n-\t\tresult = launchctl_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse if (!strcmp(scheduler, \"schtasks\"))\n-\t\tresult = schtasks_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse if (!strcmp(scheduler, \"crontab\"))\n-\t\tresult = crontab_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse\n-\t\tdie(\"unknown background scheduler: %s\", scheduler);\n+\tfor (i = 1; i < ARRAY_SIZE(scheduler_fn); i++) {\n+\t\tenum enable_or_disable enable_scheduler =\n+\t\t\t(enable == ENABLE && (opts->scheduler == i)) ?\n+\t\t\tENABLE : DISABLE;\n+\t\tif (!scheduler_fn[i].is_available())\n+\t\t\tcontinue;\n+\t\tres = scheduler_fn[i].update_schedule(\n+\t\t\tenable_scheduler, get_lock_file_fd(&lk));\n+\t\tif (enable_scheduler)\n+\t\t\tresult = res;\n+\t}\n \n \trollback_lock_file(&lk);\n-\tfree(testing);\n \treturn result;\n }\n \n-static int maintenance_start(void)\n+static const char *const builtin_maintenance_start_usage[] = {\n+\tN_(\"git maintenance start [--scheduler=<scheduler>]\"), NULL\n+};\n+\n+static int maintenance_start(int argc, const char **argv, const char *prefix)\n {\n+\tstruct maintenance_start_opts opts;\n+\tstruct option builtin_maintenance_start_options[] = {\n+\t\tOPT_CALLBACK_F(\n+\t\t\t0, \"scheduler\", &opts.scheduler, N_(\"scheduler\"),\n+\t\t\tN_(\"scheduler to use to trigger git maintenance run\"),\n+\t\t\tPARSE_OPT_NONEG, maintenance_opt_scheduler),\n+\t\tOPT_END()\n+\t};\n+\tmemset(&opts, 0, sizeof(opts));\n+\n+\targc = parse_options(argc, argv, prefix,\n+\t\t\t     builtin_maintenance_start_options,\n+\t\t\t     builtin_maintenance_start_usage, 0);\n+\tif (argc)\n+\t\tusage_with_options(builtin_maintenance_start_usage,\n+\t\t\t\t   builtin_maintenance_start_options);\n+\n+\tresolve_auto_scheduler(&opts.scheduler);\n+\tvalidate_scheduler(opts.scheduler);\n+\n \tif (maintenance_register())\n \t\twarning(_(\"failed to add repo to global config\"));\n-\n-\treturn update_background_schedule(1);\n+\treturn update_background_schedule(&opts, 1);\n }\n \n static int maintenance_stop(void)\n {\n-\treturn update_background_schedule(0);\n+\treturn update_background_schedule(NULL, 0);\n }\n \n static const char builtin_maintenance_usage[] =\tN_(\"git maintenance <subcommand> [<options>]\");\n@@ -2040,7 +2247,7 @@ int cmd_maintenance(int argc, const char **argv, const char *prefix)\n \tif (!strcmp(argv[1], \"run\"))\n \t\treturn maintenance_run(argc - 1, argv + 1, prefix);\n \tif (!strcmp(argv[1], \"start\"))\n-\t\treturn maintenance_start();\n+\t\treturn maintenance_start(argc - 1, argv + 1, prefix);\n \tif (!strcmp(argv[1], \"stop\"))\n \t\treturn maintenance_stop();\n \tif (!strcmp(argv[1], \"register\"))\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 2412d8c5c0..9eac260307 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -488,8 +488,21 @@ test_expect_success !MINGW 'register and unregister with regex metacharacters' '\n \t\tmaintenance.repo \"$(pwd)/$META\"\n '\n \n+test_expect_success 'start --scheduler=<scheduler>' '\n+\ttest_expect_code 129 git maintenance start --scheduler=foo 2>err &&\n+\ttest_i18ngrep \"unrecognized --scheduler argument\" err &&\n+\n+\ttest_expect_code 129 git maintenance start --no-scheduler 2>err &&\n+\ttest_i18ngrep \"unknown option\" err &&\n+\n+\ttest_expect_code 128 \\\n+\t\tenv GIT_TEST_MAINT_SCHEDULER=\"launchctl:true,schtasks:true\" \\\n+\t\tgit maintenance start --scheduler=crontab 2>err &&\n+\ttest_i18ngrep \"fatal: crontab scheduler is not available\" err\n+'\n+\n test_expect_success 'start from empty cron table' '\n-\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start --scheduler=crontab &&\n \n \t# start registers the repo\n \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n@@ -512,7 +525,7 @@ test_expect_success 'stop from existing schedule' '\n \n test_expect_success 'start preserves existing schedule' '\n \techo \"Important information!\" >cron.txt &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start --scheduler=crontab &&\n \tgrep \"Important information!\" cron.txt\n '\n \n@@ -541,7 +554,7 @@ test_expect_success 'start and stop macOS maintenance' '\n \tEOF\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start --scheduler=launchctl &&\n \n \t# start registers the repo\n \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n@@ -592,7 +605,7 @@ test_expect_success 'start and stop Windows maintenance' '\n \tEOF\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"schtasks:./print-args\" git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"schtasks:./print-args\" git maintenance start --scheduler=schtasks &&\n \n \t# start registers the repo\n \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n@@ -615,6 +628,41 @@ test_expect_success 'start and stop Windows maintenance' '\n \ttest_cmp expect args\n '\n \n+test_expect_success 'start and stop when several schedulers are available' '\n+\twrite_script print-args <<-\\EOF &&\n+\tprintf \"%s\\n\" \"$*\" | sed \"s:gui/[0-9][0-9]*:gui/[UID]:; s:\\(schtasks /create .* /xml\\).*:\\1:;\" >>args\n+\tEOF\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n+\trm -f expect &&\n+\tfor frequency in hourly daily weekly\n+\tdo\n+\t\tPLIST=\"$pfx/Library/LaunchAgents/org.git-scm.git.$frequency.plist\" &&\n+\t\techo \"launchctl bootout gui/[UID] $PLIST\" >>expect &&\n+\t\techo \"launchctl bootstrap gui/[UID] $PLIST\" >>expect || return 1\n+\tdone &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n+\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n+\t\thourly daily weekly >expect &&\n+\tprintf \"schtasks /create /tn Git Maintenance (%s) /f /xml\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n+\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n+\t\thourly daily weekly >expect &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\ttest_cmp expect args\n+'\n+\n test_expect_success 'register preserves existing strategy' '\n \tgit config maintenance.strategy none &&\n \tgit maintenance register &&\n-- \n2.31.1\n\n"},{"id":"425170","messageId":"YKb0WImFLYpluHrQ@danh.dev","threadId":"55603","inReplyTo":"20210520221359.75615-2-lenaic@lhuard.fr","subject":"Re: [PATCH v3 1/4] cache.h: rename \"xdg_config_home\" to \"xdg_config_home_git\"","fromName":"Đoàn Trần Công Danh","fromEmail":"congdanhqx@gmail.com","sentAt":"2021-05-20T23:44:24Z","receivedAt":"2021-05-20T23:44:31Z","isPatch":true,"sender":{"key":"congdanhqx@gmail.com","avatar":"https://avatars.githubusercontent.com/u/42673067?v=4"},"body":"On 2021-05-21 00:13:56+0200, Lénaïc Huard <lenaic@lhuard.fr> wrote:\n> Current implementation of `xdg_config_home(str)` returns\n> `$XDG_CONFIG_HOME/git/$str`, with the `git` subdirectory inserted\n> between the `XDG_CONFIG_HOME` environment variable and the parameter.\n> \n> This patch re-purposes `xdg_config_home(…)` to be more generic. It now\n> only concatenates \"$XDG_CONFIG_HOME\", or \"$HOME/.config\" if the former\n> isn’t defined, with the parameter, without adding `git` in between.\n> Its parameter is now a format string.\n\nIntended or not, this change is going to make a logical conflict,\nshould other topics also call to xdg_config_home, i.e. no textual\nconflicts, programs compiled successfully but run into failure.\n\nI think we shouldn't re-purpose xdg_config_home, we should add a new\nfunction named something like\nxdg_config_home_{for,nongit,other,generic} instead.\n\n> -char *xdg_config_home(const char *filename)\n> +char *xdg_config_home(const char *fmt, ...)\n\nIn my opinion, we don't even need to over-engineer this function\nwith variadic arguments, I think below function should be enough for\nmost (all?) cases:\n\n\tchar *xdg_config_home_prog(const char *prog, const char *filename)\n\n>  {\n>  \tconst char *home, *config_home;\n> +\tstruct strbuf buf = STRBUF_INIT;\n> +\tchar *out = NULL;\n> +\tva_list args;\n> +\n> +\tva_start(args, fmt);\n> +\tstrbuf_vaddf(&buf, fmt, args);\n> +\tva_end(args);\n\nIf my imagination is sensible, it's not necessary to use a temporary\nstrbuf and strbuf_vaddf here ...\n\n>  \n> -\tassert(filename);\n>  \tconfig_home = getenv(\"XDG_CONFIG_HOME\");\n> -\tif (config_home && *config_home)\n> -\t\treturn mkpathdup(\"%s/git/%s\", config_home, filename);\n> +\tif (config_home && *config_home) {\n> +\t\tout = mkpathdup(\"%s/%s\", config_home, buf.buf);\n> +\t\tgoto done;\n> +\t}\n>  \n>  \thome = getenv(\"HOME\");\n> -\tif (home)\n> -\t\treturn mkpathdup(\"%s/.config/git/%s\", home, filename);\n> -\treturn NULL;\n> +\tif (home) {\n> +\t\tout = mkpathdup(\"%s/.config/%s\", home, buf.buf);\n> +\t\tgoto done;\n> +\t}\n> +\n> +done:\n> +\tstrbuf_release(&buf);\n\n... and go though the restructure of this function.\n\n\n-- \nDanh\n"},{"id":"425194","messageId":"31eda7fa-5935-91e8-0059-09188cb30511@gmail.com","threadId":"55603","inReplyTo":"20210520221359.75615-4-lenaic@lhuard.fr","subject":"Re: [PATCH v3 3/4] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Bagas Sanjaya","fromEmail":"bagasdotme@gmail.com","sentAt":"2021-05-21T09:52:21Z","receivedAt":"2021-05-21T09:53:52Z","isPatch":true,"sender":{"key":"bagasdotme@gmail.com","avatar":"https://avatars.githubusercontent.com/u/40219486?v=4"},"body":"On 21/05/21 05.13, Lénaïc Huard wrote:\n> Depending on the system, different schedulers can be used to schedule\n> the hourly, daily and weekly executions of `git maintenance run`:\n> * `launchctl` for MacOS,\n> * `schtasks` for Windows and\n> * `crontab` for everything else.\n> \n\n...and soon to be supported systemd timers.\n\n> `git maintenance run` now has an option to let the end-user explicitly\n> choose which scheduler he wants to use:\n> `--scheduler=auto|crontab|launchctl|schtasks`.\n> \n> When `git maintenance start --scheduler=XXX` is run, it not only\n> registers `git maintenance run` tasks in the scheduler XXX, it also\n> removes the `git maintenance run` tasks from all the other schedulers to\n> ensure we cannot have two schedulers launching concurrent identical\n> tasks.\n> \n> The default value is `auto` which chooses a suitable scheduler for the\n> system.\n>\n  \nUntil this point, we haven't supported systemd timers yet, but in the next\npatch we add support for it. So the patch description looks OK.\n\n-- \nAn old man doll... just what I always wanted! - Clara\n"},{"id":"425195","messageId":"715d8115-641b-5c06-d514-36911eb169ef@gmail.com","threadId":"55603","inReplyTo":"20210520221359.75615-5-lenaic@lhuard.fr","subject":"Re: [PATCH v3 4/4] maintenance: optionally use systemd timers on Linux","fromName":"Bagas Sanjaya","fromEmail":"bagasdotme@gmail.com","sentAt":"2021-05-21T09:59:26Z","receivedAt":"2021-05-21T10:04:20Z","isPatch":true,"sender":{"key":"bagasdotme@gmail.com","avatar":"https://avatars.githubusercontent.com/u/40219486?v=4"},"body":"On 21/05/21 05.13, Lénaïc Huard wrote:\n> The existing mechanism for scheduling background maintenance is done\n> through cron. On Linux systems managed by systemd, systemd provides an\n> alternative to schedule recurring tasks: systemd timers.\n> \n> The main motivations to implement systemd timers in addition to cron\n> are:\n> * cron is optional and Linux systems running systemd might not have it\n>    installed.\n> * The execution of `crontab -l` can tell us if cron is installed but not\n>    if the daemon is actually running.\n> * With systemd, each service is run in its own cgroup and its logs are\n>    tagged by the service inside journald. With cron, all scheduled tasks\n>    are running in the cron daemon cgroup and all the logs of the\n>    user-scheduled tasks are pretended to belong to the system cron\n>    service.\n>    Concretely, a user that doesn’t have access to the system logs won’t\n>    have access to the log of its own tasks scheduled by cron whereas he\n>    will have access to the log of its own tasks scheduled by systemd\n>    timer.\n\nFor gender neutrality, we can use he/she instead.\n\n>    Although `cron` attempts to send email, that email may go unseen by\n>    the user because these days, local mailboxes are not heavily used\n>    anymore.\n> \n> In order to schedule git maintenance, we need two unit template files:\n> * ~/.config/systemd/user/git-maintenance@.service\n>    to define the command to be started by systemd and\n> * ~/.config/systemd/user/git-maintenance@.timer\n>    to define the schedule at which the command should be run.\n> \n> Those units are templates that are parameterized by the frequency.\n> \n> Based on those templates, 3 timers are started:\n> * git-maintenance@hourly.timer\n> * git-maintenance@daily.timer\n> * git-maintenance@weekly.timer\n> \n> The command launched by those three timers are the same as with the\n> other scheduling methods:\n> \n> git for-each-repo --config=maintenance.repo maintenance run\n> --schedule=%i\n> \n> with the full path for git to ensure that the version of git launched\n> for the scheduled maintenance is the same as the one used to run\n> `maintenance start`.\n> \n\nWouldn't it be `/path/to/git for-each-repo <options>...`?\n\n> The timer unit contains `Persistent=true` so that, if the computer is\n> powered down when a maintenance task should run, the task will be run\n> when the computer is back powered on.\n> \n\nThe title for this patch implied that users running Linux can choose\nbetween classic crontab or systemd timers. However, the intent of this\npatch is we add support for systemd timers. So let's say the title\nshould be \"maintenance: add support for systemd timers on Linux\".\n\n-- \nAn old man doll... just what I always wanted! - Clara\n"},{"id":"425222","messageId":"44d937a0-e876-e185-f409-a4fd61eae580@gmail.com","threadId":"55603","inReplyTo":"715d8115-641b-5c06-d514-36911eb169ef@gmail.com","subject":"Re: [PATCH v3 4/4] maintenance: optionally use systemd timers on Linux","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-05-21T16:59:23Z","receivedAt":"2021-05-21T16:59:33Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 5/21/2021 5:59 AM, Bagas Sanjaya wrote:\n> On 21/05/21 05.13, Lénaïc Huard wrote:\n>> The existing mechanism for scheduling background maintenance is done\n>> through cron. On Linux systems managed by systemd, systemd provides an\n>> alternative to schedule recurring tasks: systemd timers.\n>>\n>> The main motivations to implement systemd timers in addition to cron\n>> are:\n>> * cron is optional and Linux systems running systemd might not have it\n>>    installed.\n>> * The execution of `crontab -l` can tell us if cron is installed but not\n>>    if the daemon is actually running.\n>> * With systemd, each service is run in its own cgroup and its logs are\n>>    tagged by the service inside journald. With cron, all scheduled tasks\n>>    are running in the cron daemon cgroup and all the logs of the\n>>    user-scheduled tasks are pretended to belong to the system cron\n>>    service.\n>>    Concretely, a user that doesn’t have access to the system logs won’t\n>>    have access to the log of its own tasks scheduled by cron whereas he\n>>    will have access to the log of its own tasks scheduled by systemd\n>>    timer.\n> \n> For gender neutrality, we can use he/she instead.\n\nSingular \"they\" is better. Fully accurate and less awkward.\n\nThanks,\n-Stolee\n"},{"id":"425382","messageId":"nycvar.QRO.7.76.6.2105220856320.57@tvgsbejvaqbjf.bet","threadId":"55603","inReplyTo":"44d937a0-e876-e185-f409-a4fd61eae580@gmail.com","subject":"Re: [PATCH v3 4/4] maintenance: optionally use systemd timers on Linux","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2021-05-22T06:57:53Z","receivedAt":"2021-05-23T16:39:43Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi,\n\nOn Fri, 21 May 2021, Derrick Stolee wrote:\n\n> On 5/21/2021 5:59 AM, Bagas Sanjaya wrote:\n> > On 21/05/21 05.13, Lénaïc Huard wrote:\n> >> The existing mechanism for scheduling background maintenance is done\n> >> through cron. On Linux systems managed by systemd, systemd provides an\n> >> alternative to schedule recurring tasks: systemd timers.\n> >>\n> >> The main motivations to implement systemd timers in addition to cron\n> >> are:\n> >> * cron is optional and Linux systems running systemd might not have it\n> >>    installed.\n> >> * The execution of `crontab -l` can tell us if cron is installed but not\n> >>    if the daemon is actually running.\n> >> * With systemd, each service is run in its own cgroup and its logs are\n> >>    tagged by the service inside journald. With cron, all scheduled tasks\n> >>    are running in the cron daemon cgroup and all the logs of the\n> >>    user-scheduled tasks are pretended to belong to the system cron\n> >>    service.\n> >>    Concretely, a user that doesn’t have access to the system logs won’t\n> >>    have access to the log of its own tasks scheduled by cron whereas he\n> >>    will have access to the log of its own tasks scheduled by systemd\n> >>    timer.\n> >\n> > For gender neutrality, we can use he/she instead.\n>\n> Singular \"they\" is better. Fully accurate and less awkward.\n\nI agree. If the singular they was good enough for Shakespeare, it is good\nenough for anyone. See for yourself:\nhttp://itre.cis.upenn.edu/~myl/languagelog/archives/002748.html\n\nCiao,\nDscho\n"},{"id":"425386","messageId":"60aaa09aebce4_454920811@natae.notmuch","threadId":"55603","inReplyTo":"nycvar.QRO.7.76.6.2105220856320.57@tvgsbejvaqbjf.bet","subject":"Re: [PATCH v3 4/4] maintenance: optionally use systemd timers on Linux","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2021-05-23T18:36:10Z","receivedAt":"2021-05-23T18:36:16Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Johannes Schindelin wrote:\n> On Fri, 21 May 2021, Derrick Stolee wrote:\n> \n> > On 5/21/2021 5:59 AM, Bagas Sanjaya wrote:\n> > > On 21/05/21 05.13, Lénaïc Huard wrote:\n> > >> The existing mechanism for scheduling background maintenance is done\n> > >> through cron. On Linux systems managed by systemd, systemd provides an\n> > >> alternative to schedule recurring tasks: systemd timers.\n> > >>\n> > >> The main motivations to implement systemd timers in addition to cron\n> > >> are:\n> > >> * cron is optional and Linux systems running systemd might not have it\n> > >>    installed.\n> > >> * The execution of `crontab -l` can tell us if cron is installed but not\n> > >>    if the daemon is actually running.\n> > >> * With systemd, each service is run in its own cgroup and its logs are\n> > >>    tagged by the service inside journald. With cron, all scheduled tasks\n> > >>    are running in the cron daemon cgroup and all the logs of the\n> > >>    user-scheduled tasks are pretended to belong to the system cron\n> > >>    service.\n> > >>    Concretely, a user that doesn’t have access to the system logs won’t\n> > >>    have access to the log of its own tasks scheduled by cron whereas he\n> > >>    will have access to the log of its own tasks scheduled by systemd\n> > >>    timer.\n> > >\n> > > For gender neutrality, we can use he/she instead.\n> >\n> > Singular \"they\" is better. Fully accurate and less awkward.\n> \n> I agree.\n\nI disagree.\n\n> If the singular they was good enough for Shakespeare,\n\nShakespeare:\n\n 1. Did not know gammar\n 2. Invented words as we went along\n 3. Was no writing prose\n\nThis is not the kind of English we wish to replicate:\n\n  \"This was the most unkindest cut of all.\"\n\n> See for yourself:\n> http://itre.cis.upenn.edu/~myl/languagelog/archives/002748.html\n\nI do not see a single instance of a singular antecedent there.\n\nNot that it matters, because unlike Shakespeare we are wriing classic\nprose style. The styles could not be more different.\n\n\nThe singular they is a controversial topic[1][2], even among linguists.\nThis is a software project, we must not make decrees about proper use of\nEnglish language, especially when linguists themselves have not yet\nfully decided.\n\nIf you want to use \"they\", go ahead, other people want to use \"he/she\".\nThe git project should steer cleer of value judgements that one is\n_better_ than the other.\n\nNot to mention that these kinds of promulgations invite the culture war.\n\nCheers.\n\n[1] https://time.com/5748649/word-of-year-they-merriam-webster/\n[2] https://www.theatlantic.com/culture/archive/2013/01/singular-their-affront-good-writing/319329/\n\n-- \nFelipe Contreras"},{"id":"425389","messageId":"YKrk4dEjEm6+48ji@camp.crustytoothpaste.net","threadId":"55603","inReplyTo":"60aaa09aebce4_454920811@natae.notmuch","subject":"Re: [PATCH v3 4/4] maintenance: optionally use systemd timers on Linux","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2021-05-23T23:27:29Z","receivedAt":"2021-05-23T23:28:09Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2021-05-23 at 18:36:10, Felipe Contreras wrote:\n> Johannes Schindelin wrote:\n> > On Fri, 21 May 2021, Derrick Stolee wrote:\n> > \n> > > On 5/21/2021 5:59 AM, Bagas Sanjaya wrote:\n> > > > On 21/05/21 05.13, Lénaïc Huard wrote:\n> > > >> The existing mechanism for scheduling background maintenance is done\n> > > >> through cron. On Linux systems managed by systemd, systemd provides an\n> > > >> alternative to schedule recurring tasks: systemd timers.\n> > > >>\n> > > >> The main motivations to implement systemd timers in addition to cron\n> > > >> are:\n> > > >> * cron is optional and Linux systems running systemd might not have it\n> > > >>    installed.\n> > > >> * The execution of `crontab -l` can tell us if cron is installed but not\n> > > >>    if the daemon is actually running.\n> > > >> * With systemd, each service is run in its own cgroup and its logs are\n> > > >>    tagged by the service inside journald. With cron, all scheduled tasks\n> > > >>    are running in the cron daemon cgroup and all the logs of the\n> > > >>    user-scheduled tasks are pretended to belong to the system cron\n> > > >>    service.\n> > > >>    Concretely, a user that doesn’t have access to the system logs won’t\n> > > >>    have access to the log of its own tasks scheduled by cron whereas he\n> > > >>    will have access to the log of its own tasks scheduled by systemd\n> > > >>    timer.\n> > > >\n> > > > For gender neutrality, we can use he/she instead.\n> > >\n> > > Singular \"they\" is better. Fully accurate and less awkward.\n> > \n> > I agree.\n> \n> I disagree.\n\nI'm fully in support of singular \"they\".  It provides a useful pronoun\nto use in this context, is widely understood and used, is less awkward\nthan \"he/she,\" and is less sexist than the indefinite \"he.\"\n\n> > If the singular they was good enough for Shakespeare,\n> \n> Shakespeare:\n> \n>  1. Did not know gammar\n>  2. Invented words as we went along\n>  3. Was no writing prose\n\nThe point is that it has been used by native English speakers as part of\nthe language for over half a millennium and is widely used and\nunderstood.  This usage is specified in Merriam Webster[0]:\n\n  The use of they, their, them, and themselves as pronouns of indefinite\n  gender and indefinite number is well established in speech and\n  writing, even in literary and formal contexts.\n\nWiktionary notes[1] (references omitted):\n\n  Usage of they as a singular pronoun began in the 1300s and has been\n  common ever since, despite attempts by some grammarians, beginning in\n  1795, to condemn it as a violation of traditional (Latinate)\n  agreement rules.  Some other grammarians have countered that criticism\n  since at least 1896.  Fowler's Modern English Usage (third edition)\n  notes that it \"is being left unaltered by copy editors\" and is \"not\n  widely felt to lie in a prohibited zone.\"  Some authors compare use of\n  singular they to widespread use of singular you instead of thou.\n\n> The singular they is a controversial topic[1][2], even among linguists.\n> This is a software project, we must not make decrees about proper use of\n> English language, especially when linguists themselves have not yet\n> fully decided.\n\nLinguists fit roughly into two camps: prescriptive and descriptive.  The\nformer specify rules for people to use, and the latter document language\nas it is actually used without forming a judgment.  While I am not a\nlinguist, I have a B.A. in English, and my views fit firmly into the\ndescriptivist camp.\n\nSome prescriptivists think it is acceptable, and some do not.  But\ndescriptivists will rightly note that it is and has been commonly used\nin English across countries, cultures, and contexts for an extended\nperiod of time and is therefore generally accepted by most English\nspeakers as a normal part of the language.  Since we are writing text\nfor an English language audience who are mostly not linguists, we should\nprobably consider using the language that most people will use in this\ncontext.\n\n> If you want to use \"they\", go ahead, other people want to use \"he/she\".\n> The git project should steer cleer of value judgements that one is\n> _better_ than the other.\n\nAny time we provide a suggestion in a code review, we are proposing that\nwe have an idea that may be better than the existing one.  It may or may\nnot actually be so, but we are proposing it in an effort to make the\ncode and documentation better.  No good-faith contributor would propose\na suggestion to make the project _worse_.\n\nIt's completely fine for a contributor to propose that they think an\nidea is better provided that they do so in a respectful and considerate\nway, which I think happened here.  As with all matters of opinion,\nwhether a thing is truly better or not is unknowable, and the best we\ncan do is to adopt an approach that seems to be the most widely accepted\nand most provident.\n\nIn this case, given the fact that singular they is accepted in a wide\nvariety of contexts, including many literary and formal contexts, and\neven the relatively stalwart Chicago \"recognizes that such usage is\ngaining [further] acceptance,\" I think it should be fine to use singular\nthey here.  You are, of course, free to feel differently.\n\n[0] https://www.merriam-webster.com/dictionary/they\n[1] https://en.wiktionary.org/wiki/they\n-- \nbrian m. carlson (he/him or they/them)\nHouston, Texas, US\n"},{"id":"425393","messageId":"60aafecba4279_8eb820875@natae.notmuch","threadId":"55603","inReplyTo":"YKrk4dEjEm6+48ji@camp.crustytoothpaste.net","subject":"Re: [PATCH v3 4/4] maintenance: optionally use systemd timers on Linux","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2021-05-24T01:18:03Z","receivedAt":"2021-05-24T01:18:09Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"brian m. carlson wrote:\n> On 2021-05-23 at 18:36:10, Felipe Contreras wrote:\n> > Johannes Schindelin wrote:\n> > > On Fri, 21 May 2021, Derrick Stolee wrote:\n> > > > On 5/21/2021 5:59 AM, Bagas Sanjaya wrote:\n> > > > > On 21/05/21 05.13, Lénaïc Huard wrote:\n> > > > >> The existing mechanism for scheduling background maintenance is done\n> > > > >> through cron. On Linux systems managed by systemd, systemd provides an\n> > > > >> alternative to schedule recurring tasks: systemd timers.\n> > > > >>\n> > > > >> The main motivations to implement systemd timers in addition to cron\n> > > > >> are:\n> > > > >> * cron is optional and Linux systems running systemd might not have it\n> > > > >>    installed.\n> > > > >> * The execution of `crontab -l` can tell us if cron is installed but not\n> > > > >>    if the daemon is actually running.\n> > > > >> * With systemd, each service is run in its own cgroup and its logs are\n> > > > >>    tagged by the service inside journald. With cron, all scheduled tasks\n> > > > >>    are running in the cron daemon cgroup and all the logs of the\n> > > > >>    user-scheduled tasks are pretended to belong to the system cron\n> > > > >>    service.\n> > > > >>    Concretely, a user that doesn’t have access to the system logs won’t\n> > > > >>    have access to the log of its own tasks scheduled by cron whereas he\n> > > > >>    will have access to the log of its own tasks scheduled by systemd\n> > > > >>    timer.\n> > > > >\n> > > > > For gender neutrality, we can use he/she instead.\n> > > >\n> > > > Singular \"they\" is better. Fully accurate and less awkward.\n> > > \n> > > I agree.\n> > \n> > I disagree.\n> \n> I'm fully in support of singular \"they\".\n\nTo each her/his own.\n\n> It provides a useful pronoun to use in this context, is widely\n> understood and used, is less awkward than \"he/she,\" and is less sexist\n> than the indefinite \"he.\"\n\nI disagree. But I wouldn't presume to dictate how other people speak.\n\nIf you like it, use it.\n\n> > > If the singular they was good enough for Shakespeare,\n> > \n> > Shakespeare:\n> > \n> >  1. Did not know gammar\n> >  2. Invented words as we went along\n> >  3. Was no writing prose\n> \n> The point is that it has been used by native English speakers as part of\n> the language for over half a millennium and is widely used and\n> understood.\n\nA similar thing happens with the word \"nucular\" [1]; it is used by\nnative English speakers as part of the language for many decades and is\nwidely used and understood.\n\nDoes that mean it's a valid word? Maybe.\n\nBut does that make \"nuclear\" invalid? No.\n\nYou can use \"nucular\" if you want (many people do). I will use\n\"nuclear\".\n\nBoth can be valid.\n\n> This usage is specified in Merriam Webster[0]:\n\nMerriam-Webster is not infallible.\n\nBut fine: let's say they are correct.\n\nWhere does it say \"they\" is *better*? Or worse: where does it say\n\"his/her\" is discouraged?\n\n> > The singular they is a controversial topic[1][2], even among linguists.\n> > This is a software project, we must not make decrees about proper use of\n> > English language, especially when linguists themselves have not yet\n> > fully decided.\n> \n> Linguists fit roughly into two camps: prescriptive and descriptive.\n\nI am perfectly aware of the two camps, and I have written about it.\n\n> While I am not a linguist, I have a B.A. in English, and my views fit\n> firmly into the descriptivist camp.\n\nIf you are a descriptivist (as am I), then you must acknowledge that\npeople use \"his/her\" *today*.\n\nAs a descriptivist you shouldn't dare to _prescribe_ how people use words.\n\nTherefore, if I use \"his/her\" (as many people do), you should not\nprescribe otherwise.\n\n> Since we are writing text for an English language audience who are\n> mostly not linguists, we should probably consider using the language\n> that most people will use in this context.\n\nConsider, yes. Dictate? No.\n\nAnd you don't know how *most* most people speak. Your guess is as good\nas mine.\n\n> It's completely fine for a contributor to propose that they think an\n> idea is better provided that they do so in a respectful and considerate\n> way, which I think happened here.\n\nIf you say you think **in your personal opinion** that A is better than\nB, that's fine.\n\nWhat was stated here was \"A is better\". So that's not what happened.\n\n> I think it should be fine to use singular they here.  You are, of\n> course, free to feel differently.\n\nI do feel differently, but I would not presume to dictate that B is\nbetter; I argue both A and B are fine.\n\n A. he/she\n B. they\n\n\nTo be perfectly clear; I'm not saying you shouldn't use B; I'm saying A\nis fine.\n\nCheers.\n\n[1] https://en.wikipedia.org/wiki/Nucular\n\n-- \nFelipe Contreras"},{"id":"425398","messageId":"20210524071538.46862-1-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210520221359.75615-1-lenaic@lhuard.fr","subject":"[PATCH v4 0/4] add support for systemd timers on Linux","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-05-24T07:15:34Z","receivedAt":"2021-05-24T07:16:45Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Hello,\n\nThe main difference between this v4 patchset and the previous v3\nconcerns the first patch, the one introducing a new function to use\n$XDG_CONFIG_HOME.\nInstead of re-purposing the existing `xdg_config_home(filename)`\nfunction, I’ve created a new generic one named\n`xdg_config_home_for(prog, filename)` to address the potential\nconflict issue raised by Danh.\n\nI’ve also reworded the commit message of the last patch to address the\nreview comments.\nI replaced the “he” by a “they”.\nTo be honest, I’m not an English native speaker, so I don’t feel\ncomfortable debating which usage is common in informal speech or in\nformal writtings.\nSo, I would be happy to change it to “he or she” if it is more\nconsensual and looks less “artificial” than “he/she”.\n\nLénaïc Huard (4):\n  cache.h: Introduce a generic \"xdg_config_home_for(…)\" function\n  maintenance: introduce ENABLE/DISABLE for code clarity\n  maintenance: `git maintenance run` learned `--scheduler=<scheduler>`\n  maintenance: add support for systemd timers on Linux\n\n Documentation/git-maintenance.txt |  60 ++++\n builtin/gc.c                      | 548 ++++++++++++++++++++++++++----\n cache.h                           |   7 +\n path.c                            |  13 +-\n t/t7900-maintenance.sh            | 110 +++++-\n 5 files changed, 657 insertions(+), 81 deletions(-)\n\n-- \n2.31.1\n\n"},{"id":"425399","messageId":"20210524071538.46862-5-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210524071538.46862-1-lenaic@lhuard.fr","subject":"[PATCH v4 4/4] maintenance: add support for systemd timers on Linux","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-05-24T07:15:38Z","receivedAt":"2021-05-24T07:16:46Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"The existing mechanism for scheduling background maintenance is done\nthrough cron. On Linux systems managed by systemd, systemd provides an\nalternative to schedule recurring tasks: systemd timers.\n\nThe main motivations to implement systemd timers in addition to cron\nare:\n* cron is optional and Linux systems running systemd might not have it\n  installed.\n* The execution of `crontab -l` can tell us if cron is installed but not\n  if the daemon is actually running.\n* With systemd, each service is run in its own cgroup and its logs are\n  tagged by the service inside journald. With cron, all scheduled tasks\n  are running in the cron daemon cgroup and all the logs of the\n  user-scheduled tasks are pretended to belong to the system cron\n  service.\n  Concretely, a user that doesn’t have access to the system logs won’t\n  have access to the log of their own tasks scheduled by cron whereas\n  they will have access to the log of their own tasks scheduled by\n  systemd timer.\n  Although `cron` attempts to send email, that email may go unseen by\n  the user because these days, local mailboxes are not heavily used\n  anymore.\n\nIn order to schedule git maintenance, we need two unit template files:\n* ~/.config/systemd/user/git-maintenance@.service\n  to define the command to be started by systemd and\n* ~/.config/systemd/user/git-maintenance@.timer\n  to define the schedule at which the command should be run.\n\nThose units are templates that are parameterized by the frequency.\n\nBased on those templates, 3 timers are started:\n* git-maintenance@hourly.timer\n* git-maintenance@daily.timer\n* git-maintenance@weekly.timer\n\nThe command launched by those three timers are the same as with the\nother scheduling methods:\n\n/path/to/git for-each-repo --exec-path=/path/to\n--config=maintenance.repo maintenance run --schedule=%i\n\nwith the full path for git to ensure that the version of git launched\nfor the scheduled maintenance is the same as the one used to run\n`maintenance start`.\n\nThe timer unit contains `Persistent=true` so that, if the computer is\npowered down when a maintenance task should run, the task will be run\nwhen the computer is back powered on.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n Documentation/git-maintenance.txt |  57 +++++++++-\n builtin/gc.c                      | 180 ++++++++++++++++++++++++++++++\n t/t7900-maintenance.sh            |  66 ++++++++++-\n 3 files changed, 293 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\nindex 7c4bb38a2f..50179e010f 100644\n--- a/Documentation/git-maintenance.txt\n+++ b/Documentation/git-maintenance.txt\n@@ -181,16 +181,19 @@ OPTIONS\n \t`maintenance.<task>.enabled` configured as `true` are considered.\n \tSee the 'TASKS' section for the list of accepted `<task>` values.\n \n---scheduler=auto|crontab|launchctl|schtasks::\n+--scheduler=auto|crontab|systemd-timer|launchctl|schtasks::\n \tWhen combined with the `start` subcommand, specify the scheduler\n \tto use to run the hourly, daily and weekly executions of\n \t`git maintenance run`.\n \tThe possible values for `<scheduler>` depend on the system: `crontab`\n-\tis available on POSIX systems, `launchctl` is available on\n-\tMacOS and `schtasks` is available on Windows.\n+\tis available on POSIX systems, `systemd-timer` is available on Linux\n+\tsystems, `launchctl` is available on MacOS and `schtasks` is available\n+\ton Windows.\n \tBy default or when `auto` is specified, a suitable scheduler for\n \tthe system is used. On MacOS, `launchctl` is used. On Windows,\n-\t`schtasks` is used. On all other systems, `crontab` is used.\n+\t`schtasks` is used. On Linux, `systemd-timer` is used if user systemd\n+\ttimers are available, otherwise, `crontab` is used. On all other systems,\n+\t`crontab` is used.\n \n \n TROUBLESHOOTING\n@@ -290,6 +293,52 @@ schedule to ensure you are executing the correct binaries in your\n schedule.\n \n \n+BACKGROUND MAINTENANCE ON LINUX SYSTEMD SYSTEMS\n+-----------------------------------------------\n+\n+While Linux supports `cron`, depending on the distribution, `cron` may\n+be an optional package not necessarily installed. On modern Linux\n+distributions, systemd timers are superseding it.\n+\n+If user systemd timers are available, they will be used as a replacement\n+of `cron`.\n+\n+In this case, `git maintenance start` will create user systemd timer units\n+and start the timers. The current list of user-scheduled tasks can be found\n+by running `systemctl --user list-timers`. The timers written by `git\n+maintenance start` are similar to this:\n+\n+-----------------------------------------------------------------------\n+$ systemctl --user list-timers\n+NEXT                         LEFT          LAST                         PASSED     UNIT                         ACTIVATES\n+Thu 2021-04-29 19:00:00 CEST 42min left    Thu 2021-04-29 18:00:11 CEST 17min ago  git-maintenance@hourly.timer git-maintenance@hourly.service\n+Fri 2021-04-30 00:00:00 CEST 5h 42min left Thu 2021-04-29 00:00:11 CEST 18h ago    git-maintenance@daily.timer  git-maintenance@daily.service\n+Mon 2021-05-03 00:00:00 CEST 3 days left   Mon 2021-04-26 00:00:11 CEST 3 days ago git-maintenance@weekly.timer git-maintenance@weekly.service\n+-----------------------------------------------------------------------\n+\n+One timer is registered for each `--schedule=<frequency>` option.\n+\n+The definition of the systemd units can be inspected in the following files:\n+\n+-----------------------------------------------------------------------\n+~/.config/systemd/user/git-maintenance@.timer\n+~/.config/systemd/user/git-maintenance@.service\n+~/.config/systemd/user/timers.target.wants/git-maintenance@hourly.timer\n+~/.config/systemd/user/timers.target.wants/git-maintenance@daily.timer\n+~/.config/systemd/user/timers.target.wants/git-maintenance@weekly.timer\n+-----------------------------------------------------------------------\n+\n+`git maintenance start` will overwrite these files and start the timer\n+again with `systemctl --user`, so any customization should be done by\n+creating a drop-in file, i.e. a `.conf` suffixed file in the\n+`~/.config/systemd/user/git-maintenance@.service.d` directory.\n+\n+`git maintenance stop` will stop the user systemd timers and delete\n+the above mentioned files.\n+\n+For more details, see `systemd.timer(5)`.\n+\n+\n BACKGROUND MAINTENANCE ON MACOS SYSTEMS\n ---------------------------------------\n \ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex bf21cec059..3eca1e5e6a 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -2079,10 +2079,173 @@ static int crontab_update_schedule(enum enable_or_disable run_maintenance,\n \treturn result;\n }\n \n+static int is_systemd_timer_available(void)\n+{\n+\tconst char *cmd = \"systemctl\";\n+\tint is_available;\n+\tstatic int cached_result = -1;\n+#ifdef __linux__\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+#endif\n+\n+\tif (cached_result != -1)\n+\t\treturn cached_result;\n+\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+#ifdef __linux__\n+\tstrvec_split(&child.args, cmd);\n+\tstrvec_pushl(&child.args, \"--user\", \"list-timers\", NULL);\n+\tchild.no_stdin = 1;\n+\tchild.no_stdout = 1;\n+\tchild.no_stderr = 1;\n+\tchild.silent_exec_failure = 1;\n+\n+\tif (start_command(&child)) {\n+\t\tcached_result = 0;\n+\t\treturn cached_result;\n+\t}\n+\tif (finish_command(&child)) {\n+\t\tcached_result = 0;\n+\t\treturn cached_result;\n+\t}\n+\tcached_result = 1;\n+\treturn cached_result;\n+#else\n+\treturn 0;\n+#endif\n+}\n+\n+static char *xdg_config_home_systemd(const char *filename)\n+{\n+\treturn xdg_config_home_for(\"systemd/user\", filename);\n+}\n+\n+static int systemd_timer_enable_unit(int enable,\n+\t\t\t\t     enum schedule_priority schedule)\n+{\n+\tconst char *cmd = \"systemctl\";\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\tconst char *frequency = get_frequency(schedule);\n+\n+\tget_schedule_cmd(&cmd, NULL);\n+\tstrvec_split(&child.args, cmd);\n+\tstrvec_pushl(&child.args, \"--user\", enable ? \"enable\" : \"disable\",\n+\t\t     \"--now\", NULL);\n+\tstrvec_pushf(&child.args, \"git-maintenance@%s.timer\", frequency);\n+\n+\tif (start_command(&child))\n+\t\tdie(_(\"failed to run systemctl\"));\n+\treturn finish_command(&child);\n+}\n+\n+static int systemd_timer_delete_unit_templates(void)\n+{\n+\tchar *filename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n+\tunlink(filename);\n+\tfree(filename);\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n+\tunlink(filename);\n+\tfree(filename);\n+\n+\treturn 0;\n+}\n+\n+static int systemd_timer_delete_units(void)\n+{\n+\treturn systemd_timer_enable_unit(DISABLE, SCHEDULE_HOURLY) ||\n+\t       systemd_timer_enable_unit(DISABLE, SCHEDULE_DAILY) ||\n+\t       systemd_timer_enable_unit(DISABLE, SCHEDULE_WEEKLY) ||\n+\t       systemd_timer_delete_unit_templates();\n+}\n+\n+static int systemd_timer_write_unit_templates(const char *exec_path)\n+{\n+\tchar *filename;\n+\tFILE *file;\n+\tconst char *unit;\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n+\tif (safe_create_leading_directories(filename))\n+\t\tdie(_(\"failed to create directories for '%s'\"), filename);\n+\tfile = xfopen(filename, \"w\");\n+\tFREE_AND_NULL(filename);\n+\n+\tunit = \"# This file was created and is maintained by Git.\\n\"\n+\t       \"# Any edits made in this file might be replaced in the future\\n\"\n+\t       \"# by a Git command.\\n\"\n+\t       \"\\n\"\n+\t       \"[Unit]\\n\"\n+\t       \"Description=Optimize Git repositories data\\n\"\n+\t       \"\\n\"\n+\t       \"[Timer]\\n\"\n+\t       \"OnCalendar=%i\\n\"\n+\t       \"Persistent=true\\n\"\n+\t       \"\\n\"\n+\t       \"[Install]\\n\"\n+\t       \"WantedBy=timers.target\\n\";\n+\tfputs(unit, file);\n+\tfclose(file);\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n+\tfile = xfopen(filename, \"w\");\n+\tfree(filename);\n+\n+\tunit = \"# This file was created and is maintained by Git.\\n\"\n+\t       \"# Any edits made in this file might be replaced in the future\\n\"\n+\t       \"# by a Git command.\\n\"\n+\t       \"\\n\"\n+\t       \"[Unit]\\n\"\n+\t       \"Description=Optimize Git repositories data\\n\"\n+\t       \"\\n\"\n+\t       \"[Service]\\n\"\n+\t       \"Type=oneshot\\n\"\n+\t       \"ExecStart=\\\"%s/git\\\" --exec-path=\\\"%s\\\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\\n\"\n+\t       \"LockPersonality=yes\\n\"\n+\t       \"MemoryDenyWriteExecute=yes\\n\"\n+\t       \"NoNewPrivileges=yes\\n\"\n+\t       \"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\\n\"\n+\t       \"RestrictNamespaces=yes\\n\"\n+\t       \"RestrictRealtime=yes\\n\"\n+\t       \"RestrictSUIDSGID=yes\\n\"\n+\t       \"SystemCallArchitectures=native\\n\"\n+\t       \"SystemCallFilter=@system-service\\n\";\n+\tfprintf(file, unit, exec_path, exec_path);\n+\tfclose(file);\n+\n+\treturn 0;\n+}\n+\n+static int systemd_timer_setup_units(void)\n+{\n+\tconst char *exec_path = git_exec_path();\n+\n+\treturn systemd_timer_write_unit_templates(exec_path) ||\n+\t       systemd_timer_enable_unit(ENABLE, SCHEDULE_HOURLY) ||\n+\t       systemd_timer_enable_unit(ENABLE, SCHEDULE_DAILY) ||\n+\t       systemd_timer_enable_unit(ENABLE, SCHEDULE_WEEKLY);\n+}\n+\n+static int systemd_timer_update_schedule(enum enable_or_disable run_maintenance,\n+\t\t\t\t\t int fd)\n+{\n+\tswitch (run_maintenance) {\n+\tcase ENABLE:\n+\t\treturn systemd_timer_setup_units();\n+\tcase DISABLE:\n+\t\treturn systemd_timer_delete_units();\n+\tdefault:\n+\t\tBUG(\"invalid enable_or_disable value\");\n+\t}\n+}\n+\n enum scheduler {\n \tSCHEDULER_INVALID = -1,\n \tSCHEDULER_AUTO,\n \tSCHEDULER_CRON,\n+\tSCHEDULER_SYSTEMD,\n \tSCHEDULER_LAUNCHCTL,\n \tSCHEDULER_SCHTASKS,\n };\n@@ -2097,6 +2260,11 @@ static const struct {\n \t\t.is_available = is_crontab_available,\n \t\t.update_schedule = crontab_update_schedule,\n \t},\n+\t[SCHEDULER_SYSTEMD] = {\n+\t\t.name = \"systemctl\",\n+\t\t.is_available = is_systemd_timer_available,\n+\t\t.update_schedule = systemd_timer_update_schedule,\n+\t},\n \t[SCHEDULER_LAUNCHCTL] = {\n \t\t.name = \"launchctl\",\n \t\t.is_available = is_launchctl_available,\n@@ -2117,6 +2285,9 @@ static enum scheduler parse_scheduler(const char *value)\n \t\treturn SCHEDULER_AUTO;\n \telse if (!strcasecmp(value, \"cron\") || !strcasecmp(value, \"crontab\"))\n \t\treturn SCHEDULER_CRON;\n+\telse if (!strcasecmp(value, \"systemd\") ||\n+\t\t !strcasecmp(value, \"systemd-timer\"))\n+\t\treturn SCHEDULER_SYSTEMD;\n \telse if (!strcasecmp(value, \"launchctl\"))\n \t\treturn SCHEDULER_LAUNCHCTL;\n \telse if (!strcasecmp(value, \"schtasks\"))\n@@ -2155,6 +2326,15 @@ static void resolve_auto_scheduler(enum scheduler *scheduler)\n \t*scheduler = SCHEDULER_SCHTASKS;\n \treturn;\n \n+#elif defined(__linux__)\n+\tif (is_systemd_timer_available())\n+\t\t*scheduler = SCHEDULER_SYSTEMD;\n+\telse if (is_crontab_available())\n+\t\t*scheduler = SCHEDULER_CRON;\n+\telse\n+\t\tdie(_(\"neither systemd timers nor crontab are available\"));\n+\treturn;\n+\n #else\n \t*scheduler = SCHEDULER_CRON;\n \treturn;\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 9eac260307..c8a6f19ebc 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -20,6 +20,18 @@ test_xmllint () {\n \tfi\n }\n \n+test_lazy_prereq SYSTEMD_ANALYZE '\n+\tsystemd-analyze --help >out &&\n+\tgrep verify out\n+'\n+\n+test_systemd_analyze_verify () {\n+\tif test_have_prereq SYSTEMD_ANALYZE\n+\tthen\n+\t\tsystemd-analyze verify \"$@\"\n+\tfi\n+}\n+\n test_expect_success 'help text' '\n \ttest_expect_code 129 git maintenance -h 2>err &&\n \ttest_i18ngrep \"usage: git maintenance <subcommand>\" err &&\n@@ -628,14 +640,54 @@ test_expect_success 'start and stop Windows maintenance' '\n \ttest_cmp expect args\n '\n \n+test_expect_success 'start and stop Linux/systemd maintenance' '\n+\twrite_script print-args <<-\\EOF &&\n+\tprintf \"%s\\n\" \"$*\" >>args\n+\tEOF\n+\n+\tXDG_CONFIG_HOME=\"$PWD\" &&\n+\texport XDG_CONFIG_HOME &&\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args\" git maintenance start --scheduler=systemd-timer &&\n+\n+\t# start registers the repo\n+\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n+\n+\ttest_systemd_analyze_verify \"systemd/user/git-maintenance@.service\" &&\n+\n+\tprintf -- \"--user enable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args\" git maintenance stop &&\n+\n+\t# stop does not unregister the repo\n+\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n+\n+\ttest_path_is_missing \"systemd/user/git-maintenance@.timer\" &&\n+\ttest_path_is_missing \"systemd/user/git-maintenance@.service\" &&\n+\n+\tprintf -- \"--user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n+\ttest_cmp expect args\n+'\n+\n test_expect_success 'start and stop when several schedulers are available' '\n \twrite_script print-args <<-\\EOF &&\n \tprintf \"%s\\n\" \"$*\" | sed \"s:gui/[0-9][0-9]*:gui/[UID]:; s:\\(schtasks /create .* /xml\\).*:\\1:;\" >>args\n \tEOF\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n-\trm -f expect &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=systemd-timer &&\n+\tprintf -- \"systemctl --user enable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n+\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n+\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n \tfor frequency in hourly daily weekly\n \tdo\n \t\tPLIST=\"$pfx/Library/LaunchAgents/org.git-scm.git.$frequency.plist\" &&\n@@ -647,17 +699,19 @@ test_expect_success 'start and stop when several schedulers are available' '\n \ttest_cmp expect args &&\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n+\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n \tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n-\t\thourly daily weekly >expect &&\n+\t\thourly daily weekly >>expect &&\n \tprintf \"schtasks /create /tn Git Maintenance (%s) /f /xml\\n\" \\\n \t\thourly daily weekly >>expect &&\n \ttest_cmp expect args &&\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n+\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n \tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n-\t\thourly daily weekly >expect &&\n+\t\thourly daily weekly >>expect &&\n \tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n \t\thourly daily weekly >>expect &&\n \ttest_cmp expect args\n-- \n2.31.1\n\n"},{"id":"425400","messageId":"20210524071538.46862-4-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210524071538.46862-1-lenaic@lhuard.fr","subject":"[PATCH v4 3/4] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-05-24T07:15:37Z","receivedAt":"2021-05-24T07:17:10Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Depending on the system, different schedulers can be used to schedule\nthe hourly, daily and weekly executions of `git maintenance run`:\n* `launchctl` for MacOS,\n* `schtasks` for Windows and\n* `crontab` for everything else.\n\n`git maintenance run` now has an option to let the end-user explicitly\nchoose which scheduler he wants to use:\n`--scheduler=auto|crontab|launchctl|schtasks`.\n\nWhen `git maintenance start --scheduler=XXX` is run, it not only\nregisters `git maintenance run` tasks in the scheduler XXX, it also\nremoves the `git maintenance run` tasks from all the other schedulers to\nensure we cannot have two schedulers launching concurrent identical\ntasks.\n\nThe default value is `auto` which chooses a suitable scheduler for the\nsystem.\n\n`git maintenance stop` doesn't have any `--scheduler` parameter because\nthis command will try to remove the `git maintenance run` tasks from all\nthe available schedulers.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n Documentation/git-maintenance.txt |  11 +\n builtin/gc.c                      | 333 ++++++++++++++++++++++++------\n t/t7900-maintenance.sh            |  56 ++++-\n 3 files changed, 333 insertions(+), 67 deletions(-)\n\ndiff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\nindex 80ddd33ceb..7c4bb38a2f 100644\n--- a/Documentation/git-maintenance.txt\n+++ b/Documentation/git-maintenance.txt\n@@ -181,6 +181,17 @@ OPTIONS\n \t`maintenance.<task>.enabled` configured as `true` are considered.\n \tSee the 'TASKS' section for the list of accepted `<task>` values.\n \n+--scheduler=auto|crontab|launchctl|schtasks::\n+\tWhen combined with the `start` subcommand, specify the scheduler\n+\tto use to run the hourly, daily and weekly executions of\n+\t`git maintenance run`.\n+\tThe possible values for `<scheduler>` depend on the system: `crontab`\n+\tis available on POSIX systems, `launchctl` is available on\n+\tMacOS and `schtasks` is available on Windows.\n+\tBy default or when `auto` is specified, a suitable scheduler for\n+\tthe system is used. On MacOS, `launchctl` is used. On Windows,\n+\t`schtasks` is used. On all other systems, `crontab` is used.\n+\n \n TROUBLESHOOTING\n ---------------\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 0caf8d45c4..bf21cec059 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1544,6 +1544,60 @@ static const char *get_frequency(enum schedule_priority schedule)\n \t}\n }\n \n+static int get_schedule_cmd(const char **cmd, int *is_available)\n+{\n+\tchar *item;\n+\tstatic char test_cmd[32];\n+\tchar *testing = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n+\n+\tif (!testing)\n+\t\treturn 0;\n+\n+\tif (is_available)\n+\t\t*is_available = 0;\n+\n+\tfor(item = testing;;) {\n+\t\tchar *sep;\n+\t\tchar *end_item = strchr(item, ',');\n+\t\tif (end_item)\n+\t\t\t*end_item = '\\0';\n+\n+\t\tsep = strchr(item, ':');\n+\t\tif (!sep)\n+\t\t\tdie(\"GIT_TEST_MAINT_SCHEDULER unparseable: %s\", testing);\n+\t\t*sep = '\\0';\n+\n+\t\tif (!strcmp(*cmd, item)) {\n+\t\t\tstrlcpy(test_cmd, sep+1, ARRAY_SIZE(test_cmd));\n+\t\t\t*cmd = test_cmd;\n+\t\t\tif (is_available)\n+\t\t\t\t*is_available = 1;\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tif (!end_item)\n+\t\t\tbreak;\n+\t\titem = end_item + 1;\n+\t}\n+\n+\tfree(testing);\n+\treturn 1;\n+}\n+\n+static int is_launchctl_available(void)\n+{\n+\tconst char *cmd = \"launchctl\";\n+\tint is_available;\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+#ifdef __APPLE__\n+\treturn 1;\n+#else\n+\treturn 0;\n+#endif\n+}\n+\n static char *launchctl_service_name(const char *frequency)\n {\n \tstruct strbuf label = STRBUF_INIT;\n@@ -1576,12 +1630,14 @@ enum enable_or_disable {\n };\n \n static int launchctl_boot_plist(enum enable_or_disable enable,\n-\t\t\t\tconst char *filename, const char *cmd)\n+\t\t\t\tconst char *filename)\n {\n+\tconst char *cmd = \"launchctl\";\n \tint result;\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tchar *uid = launchctl_get_uid();\n \n+\tget_schedule_cmd(&cmd, NULL);\n \tstrvec_split(&child.args, cmd);\n \tstrvec_pushl(&child.args, enable == ENABLE ? \"bootstrap\" : \"bootout\",\n \t\t     uid, filename, NULL);\n@@ -1598,26 +1654,26 @@ static int launchctl_boot_plist(enum enable_or_disable enable,\n \treturn result;\n }\n \n-static int launchctl_remove_plist(enum schedule_priority schedule, const char *cmd)\n+static int launchctl_remove_plist(enum schedule_priority schedule)\n {\n \tconst char *frequency = get_frequency(schedule);\n \tchar *name = launchctl_service_name(frequency);\n \tchar *filename = launchctl_service_filename(name);\n-\tint result = launchctl_boot_plist(DISABLE, filename, cmd);\n+\tint result = launchctl_boot_plist(DISABLE, filename);\n \tunlink(filename);\n \tfree(filename);\n \tfree(name);\n \treturn result;\n }\n \n-static int launchctl_remove_plists(const char *cmd)\n+static int launchctl_remove_plists(void)\n {\n-\treturn launchctl_remove_plist(SCHEDULE_HOURLY, cmd) ||\n-\t\tlaunchctl_remove_plist(SCHEDULE_DAILY, cmd) ||\n-\t\tlaunchctl_remove_plist(SCHEDULE_WEEKLY, cmd);\n+\treturn launchctl_remove_plist(SCHEDULE_HOURLY) ||\n+\t\tlaunchctl_remove_plist(SCHEDULE_DAILY) ||\n+\t\tlaunchctl_remove_plist(SCHEDULE_WEEKLY);\n }\n \n-static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule, const char *cmd)\n+static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule)\n {\n \tFILE *plist;\n \tint i;\n@@ -1686,8 +1742,8 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n \tfclose(plist);\n \n \t/* bootout might fail if not already running, so ignore */\n-\tlaunchctl_boot_plist(DISABLE, filename, cmd);\n-\tif (launchctl_boot_plist(ENABLE, filename, cmd))\n+\tlaunchctl_boot_plist(DISABLE, filename);\n+\tif (launchctl_boot_plist(ENABLE, filename))\n \t\tdie(_(\"failed to bootstrap service %s\"), filename);\n \n \tfree(filename);\n@@ -1695,28 +1751,42 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n \treturn 0;\n }\n \n-static int launchctl_add_plists(const char *cmd)\n+static int launchctl_add_plists(void)\n {\n \tconst char *exec_path = git_exec_path();\n \n-\treturn launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY, cmd) ||\n-\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_DAILY, cmd) ||\n-\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY, cmd);\n+\treturn launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY) ||\n+\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_DAILY) ||\n+\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY);\n }\n \n static int launchctl_update_schedule(enum enable_or_disable run_maintenance,\n-\t\t\t\t     int fd, const char *cmd)\n+\t\t\t\t     int fd)\n {\n \tswitch (run_maintenance) {\n \tcase ENABLE:\n-\t\treturn launchctl_add_plists(cmd);\n+\t\treturn launchctl_add_plists();\n \tcase DISABLE:\n-\t\treturn launchctl_remove_plists(cmd);\n+\t\treturn launchctl_remove_plists();\n \tdefault:\n \t\tBUG(\"invalid enable_or_disable value\");\n \t}\n }\n \n+static int is_schtasks_available(void)\n+{\n+\tconst char *cmd = \"schtasks\";\n+\tint is_available;\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+#ifdef GIT_WINDOWS_NATIVE\n+\treturn 1;\n+#else\n+\treturn 0;\n+#endif\n+}\n+\n static char *schtasks_task_name(const char *frequency)\n {\n \tstruct strbuf label = STRBUF_INIT;\n@@ -1724,13 +1794,15 @@ static char *schtasks_task_name(const char *frequency)\n \treturn strbuf_detach(&label, NULL);\n }\n \n-static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd)\n+static int schtasks_remove_task(enum schedule_priority schedule)\n {\n+\tconst char *cmd = \"schtasks\";\n \tint result;\n \tstruct strvec args = STRVEC_INIT;\n \tconst char *frequency = get_frequency(schedule);\n \tchar *name = schtasks_task_name(frequency);\n \n+\tget_schedule_cmd(&cmd, NULL);\n \tstrvec_split(&args, cmd);\n \tstrvec_pushl(&args, \"/delete\", \"/tn\", name, \"/f\", NULL);\n \n@@ -1741,15 +1813,16 @@ static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd\n \treturn result;\n }\n \n-static int schtasks_remove_tasks(const char *cmd)\n+static int schtasks_remove_tasks(void)\n {\n-\treturn schtasks_remove_task(SCHEDULE_HOURLY, cmd) ||\n-\t\tschtasks_remove_task(SCHEDULE_DAILY, cmd) ||\n-\t\tschtasks_remove_task(SCHEDULE_WEEKLY, cmd);\n+\treturn schtasks_remove_task(SCHEDULE_HOURLY) ||\n+\t\tschtasks_remove_task(SCHEDULE_DAILY) ||\n+\t\tschtasks_remove_task(SCHEDULE_WEEKLY);\n }\n \n-static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule, const char *cmd)\n+static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule)\n {\n+\tconst char *cmd = \"schtasks\";\n \tint result;\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tconst char *xml;\n@@ -1758,6 +1831,8 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority\n \tchar *name = schtasks_task_name(frequency);\n \tstruct strbuf tfilename = STRBUF_INIT;\n \n+\tget_schedule_cmd(&cmd, NULL);\n+\n \tstrbuf_addf(&tfilename, \"%s/schedule_%s_XXXXXX\",\n \t\t    get_git_common_dir(), frequency);\n \ttfile = xmks_tempfile(tfilename.buf);\n@@ -1862,34 +1937,65 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority\n \treturn result;\n }\n \n-static int schtasks_schedule_tasks(const char *cmd)\n+static int schtasks_schedule_tasks(void)\n {\n \tconst char *exec_path = git_exec_path();\n \n-\treturn schtasks_schedule_task(exec_path, SCHEDULE_HOURLY, cmd) ||\n-\t\tschtasks_schedule_task(exec_path, SCHEDULE_DAILY, cmd) ||\n-\t\tschtasks_schedule_task(exec_path, SCHEDULE_WEEKLY, cmd);\n+\treturn schtasks_schedule_task(exec_path, SCHEDULE_HOURLY) ||\n+\t\tschtasks_schedule_task(exec_path, SCHEDULE_DAILY) ||\n+\t\tschtasks_schedule_task(exec_path, SCHEDULE_WEEKLY);\n }\n \n static int schtasks_update_schedule(enum enable_or_disable run_maintenance,\n-\t\t\t\t    int fd, const char *cmd)\n+\t\t\t\t    int fd)\n {\n \tswitch (run_maintenance) {\n \tcase ENABLE:\n-\t\treturn schtasks_schedule_tasks(cmd);\n+\t\treturn schtasks_schedule_tasks();\n \tcase DISABLE:\n-\t\treturn schtasks_remove_tasks(cmd);\n+\t\treturn schtasks_remove_tasks();\n \tdefault:\n \t\tBUG(\"invalid enable_or_disable value\");\n \t}\n }\n \n+static int is_crontab_available(void)\n+{\n+\tconst char *cmd = \"crontab\";\n+\tint is_available;\n+\tstatic int cached_result = -1;\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\n+\tif (cached_result != -1)\n+\t\treturn cached_result;\n+\n+\tif (get_schedule_cmd(&cmd, &is_available) && !is_available)\n+\t\treturn 0;\n+\n+\tstrvec_split(&child.args, cmd);\n+\tstrvec_push(&child.args, \"-l\");\n+\tchild.no_stdin = 1;\n+\tchild.no_stdout = 1;\n+\tchild.no_stderr = 1;\n+\tchild.silent_exec_failure = 1;\n+\n+\tif (start_command(&child)) {\n+\t\tcached_result = 0;\n+\t\treturn cached_result;\n+\t}\n+\t/* Ignore exit code, as an empty crontab will return error. */\n+\tfinish_command(&child);\n+\tcached_result = 1;\n+\treturn cached_result;\n+}\n+\n #define BEGIN_LINE \"# BEGIN GIT MAINTENANCE SCHEDULE\"\n #define END_LINE \"# END GIT MAINTENANCE SCHEDULE\"\n \n static int crontab_update_schedule(enum enable_or_disable run_maintenance,\n-\t\t\t\t   int fd, const char *cmd)\n+\t\t\t\t   int fd)\n {\n+\tconst char *cmd = \"crontab\";\n \tint result = 0;\n \tint in_old_region = 0;\n \tstruct child_process crontab_list = CHILD_PROCESS_INIT;\n@@ -1897,6 +2003,7 @@ static int crontab_update_schedule(enum enable_or_disable run_maintenance,\n \tFILE *cron_list, *cron_in;\n \tstruct strbuf line = STRBUF_INIT;\n \n+\tget_schedule_cmd(&cmd, NULL);\n \tstrvec_split(&crontab_list.args, cmd);\n \tstrvec_push(&crontab_list.args, \"-l\");\n \tcrontab_list.in = -1;\n@@ -1972,61 +2079,161 @@ static int crontab_update_schedule(enum enable_or_disable run_maintenance,\n \treturn result;\n }\n \n+enum scheduler {\n+\tSCHEDULER_INVALID = -1,\n+\tSCHEDULER_AUTO,\n+\tSCHEDULER_CRON,\n+\tSCHEDULER_LAUNCHCTL,\n+\tSCHEDULER_SCHTASKS,\n+};\n+\n+static const struct {\n+\tconst char *name;\n+\tint (*is_available)(void);\n+\tint (*update_schedule)(enum enable_or_disable run_maintenance, int fd);\n+} scheduler_fn[] = {\n+\t[SCHEDULER_CRON] = {\n+\t\t.name = \"crontab\",\n+\t\t.is_available = is_crontab_available,\n+\t\t.update_schedule = crontab_update_schedule,\n+\t},\n+\t[SCHEDULER_LAUNCHCTL] = {\n+\t\t.name = \"launchctl\",\n+\t\t.is_available = is_launchctl_available,\n+\t\t.update_schedule = launchctl_update_schedule,\n+\t},\n+\t[SCHEDULER_SCHTASKS] = {\n+\t\t.name = \"schtasks\",\n+\t\t.is_available = is_schtasks_available,\n+\t\t.update_schedule = schtasks_update_schedule,\n+\t},\n+};\n+\n+static enum scheduler parse_scheduler(const char *value)\n+{\n+\tif (!value)\n+\t\treturn SCHEDULER_INVALID;\n+\telse if (!strcasecmp(value, \"auto\"))\n+\t\treturn SCHEDULER_AUTO;\n+\telse if (!strcasecmp(value, \"cron\") || !strcasecmp(value, \"crontab\"))\n+\t\treturn SCHEDULER_CRON;\n+\telse if (!strcasecmp(value, \"launchctl\"))\n+\t\treturn SCHEDULER_LAUNCHCTL;\n+\telse if (!strcasecmp(value, \"schtasks\"))\n+\t\treturn SCHEDULER_SCHTASKS;\n+\telse\n+\t\treturn SCHEDULER_INVALID;\n+}\n+\n+static int maintenance_opt_scheduler(const struct option *opt, const char *arg,\n+\t\t\t\t     int unset)\n+{\n+\tenum scheduler *scheduler = opt->value;\n+\n+\tBUG_ON_OPT_NEG(unset);\n+\n+\t*scheduler = parse_scheduler(arg);\n+\tif (*scheduler == SCHEDULER_INVALID)\n+\t\treturn error(_(\"unrecognized --scheduler argument '%s'\"), arg);\n+\treturn 0;\n+}\n+\n+struct maintenance_start_opts {\n+\tenum scheduler scheduler;\n+};\n+\n+static void resolve_auto_scheduler(enum scheduler *scheduler)\n+{\n+\tif (*scheduler != SCHEDULER_AUTO)\n+\t\treturn;\n+\n #if defined(__APPLE__)\n-static const char platform_scheduler[] = \"launchctl\";\n+\t*scheduler = SCHEDULER_LAUNCHCTL;\n+\treturn;\n+\n #elif defined(GIT_WINDOWS_NATIVE)\n-static const char platform_scheduler[] = \"schtasks\";\n+\t*scheduler = SCHEDULER_SCHTASKS;\n+\treturn;\n+\n #else\n-static const char platform_scheduler[] = \"crontab\";\n+\t*scheduler = SCHEDULER_CRON;\n+\treturn;\n #endif\n+}\n \n-static int update_background_schedule(int enable)\n+static void validate_scheduler(enum scheduler scheduler)\n {\n-\tint result;\n-\tconst char *scheduler = platform_scheduler;\n-\tconst char *cmd = scheduler;\n-\tchar *testing;\n+\tif (scheduler == SCHEDULER_INVALID)\n+\t\tBUG(\"invalid scheduler\");\n+\tif (scheduler == SCHEDULER_AUTO)\n+\t\tBUG(\"resolve_auto_scheduler should have been called before\");\n+\n+\tif (!scheduler_fn[scheduler].is_available())\n+\t\tdie(_(\"%s scheduler is not available\"),\n+\t\t    scheduler_fn[scheduler].name);\n+}\n+\n+static int update_background_schedule(const struct maintenance_start_opts *opts,\n+\t\t\t\t      enum enable_or_disable enable)\n+{\n+\tunsigned int i;\n+\tint res, result = 0;\n \tstruct lock_file lk;\n \tchar *lock_path = xstrfmt(\"%s/schedule\", the_repository->objects->odb->path);\n \n-\ttesting = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n-\tif (testing) {\n-\t\tchar *sep = strchr(testing, ':');\n-\t\tif (!sep)\n-\t\t\tdie(\"GIT_TEST_MAINT_SCHEDULER unparseable: %s\", testing);\n-\t\t*sep = '\\0';\n-\t\tscheduler = testing;\n-\t\tcmd = sep + 1;\n-\t}\n-\n \tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0)\n \t\treturn error(_(\"another process is scheduling background maintenance\"));\n \n-\tif (!strcmp(scheduler, \"launchctl\"))\n-\t\tresult = launchctl_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse if (!strcmp(scheduler, \"schtasks\"))\n-\t\tresult = schtasks_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse if (!strcmp(scheduler, \"crontab\"))\n-\t\tresult = crontab_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse\n-\t\tdie(\"unknown background scheduler: %s\", scheduler);\n+\tfor (i = 1; i < ARRAY_SIZE(scheduler_fn); i++) {\n+\t\tenum enable_or_disable enable_scheduler =\n+\t\t\t(enable == ENABLE && (opts->scheduler == i)) ?\n+\t\t\tENABLE : DISABLE;\n+\t\tif (!scheduler_fn[i].is_available())\n+\t\t\tcontinue;\n+\t\tres = scheduler_fn[i].update_schedule(\n+\t\t\tenable_scheduler, get_lock_file_fd(&lk));\n+\t\tif (enable_scheduler)\n+\t\t\tresult = res;\n+\t}\n \n \trollback_lock_file(&lk);\n-\tfree(testing);\n \treturn result;\n }\n \n-static int maintenance_start(void)\n+static const char *const builtin_maintenance_start_usage[] = {\n+\tN_(\"git maintenance start [--scheduler=<scheduler>]\"), NULL\n+};\n+\n+static int maintenance_start(int argc, const char **argv, const char *prefix)\n {\n+\tstruct maintenance_start_opts opts;\n+\tstruct option builtin_maintenance_start_options[] = {\n+\t\tOPT_CALLBACK_F(\n+\t\t\t0, \"scheduler\", &opts.scheduler, N_(\"scheduler\"),\n+\t\t\tN_(\"scheduler to use to trigger git maintenance run\"),\n+\t\t\tPARSE_OPT_NONEG, maintenance_opt_scheduler),\n+\t\tOPT_END()\n+\t};\n+\tmemset(&opts, 0, sizeof(opts));\n+\n+\targc = parse_options(argc, argv, prefix,\n+\t\t\t     builtin_maintenance_start_options,\n+\t\t\t     builtin_maintenance_start_usage, 0);\n+\tif (argc)\n+\t\tusage_with_options(builtin_maintenance_start_usage,\n+\t\t\t\t   builtin_maintenance_start_options);\n+\n+\tresolve_auto_scheduler(&opts.scheduler);\n+\tvalidate_scheduler(opts.scheduler);\n+\n \tif (maintenance_register())\n \t\twarning(_(\"failed to add repo to global config\"));\n-\n-\treturn update_background_schedule(1);\n+\treturn update_background_schedule(&opts, 1);\n }\n \n static int maintenance_stop(void)\n {\n-\treturn update_background_schedule(0);\n+\treturn update_background_schedule(NULL, 0);\n }\n \n static const char builtin_maintenance_usage[] =\tN_(\"git maintenance <subcommand> [<options>]\");\n@@ -2040,7 +2247,7 @@ int cmd_maintenance(int argc, const char **argv, const char *prefix)\n \tif (!strcmp(argv[1], \"run\"))\n \t\treturn maintenance_run(argc - 1, argv + 1, prefix);\n \tif (!strcmp(argv[1], \"start\"))\n-\t\treturn maintenance_start();\n+\t\treturn maintenance_start(argc - 1, argv + 1, prefix);\n \tif (!strcmp(argv[1], \"stop\"))\n \t\treturn maintenance_stop();\n \tif (!strcmp(argv[1], \"register\"))\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 2412d8c5c0..9eac260307 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -488,8 +488,21 @@ test_expect_success !MINGW 'register and unregister with regex metacharacters' '\n \t\tmaintenance.repo \"$(pwd)/$META\"\n '\n \n+test_expect_success 'start --scheduler=<scheduler>' '\n+\ttest_expect_code 129 git maintenance start --scheduler=foo 2>err &&\n+\ttest_i18ngrep \"unrecognized --scheduler argument\" err &&\n+\n+\ttest_expect_code 129 git maintenance start --no-scheduler 2>err &&\n+\ttest_i18ngrep \"unknown option\" err &&\n+\n+\ttest_expect_code 128 \\\n+\t\tenv GIT_TEST_MAINT_SCHEDULER=\"launchctl:true,schtasks:true\" \\\n+\t\tgit maintenance start --scheduler=crontab 2>err &&\n+\ttest_i18ngrep \"fatal: crontab scheduler is not available\" err\n+'\n+\n test_expect_success 'start from empty cron table' '\n-\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start --scheduler=crontab &&\n \n \t# start registers the repo\n \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n@@ -512,7 +525,7 @@ test_expect_success 'stop from existing schedule' '\n \n test_expect_success 'start preserves existing schedule' '\n \techo \"Important information!\" >cron.txt &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start --scheduler=crontab &&\n \tgrep \"Important information!\" cron.txt\n '\n \n@@ -541,7 +554,7 @@ test_expect_success 'start and stop macOS maintenance' '\n \tEOF\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start --scheduler=launchctl &&\n \n \t# start registers the repo\n \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n@@ -592,7 +605,7 @@ test_expect_success 'start and stop Windows maintenance' '\n \tEOF\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"schtasks:./print-args\" git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"schtasks:./print-args\" git maintenance start --scheduler=schtasks &&\n \n \t# start registers the repo\n \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n@@ -615,6 +628,41 @@ test_expect_success 'start and stop Windows maintenance' '\n \ttest_cmp expect args\n '\n \n+test_expect_success 'start and stop when several schedulers are available' '\n+\twrite_script print-args <<-\\EOF &&\n+\tprintf \"%s\\n\" \"$*\" | sed \"s:gui/[0-9][0-9]*:gui/[UID]:; s:\\(schtasks /create .* /xml\\).*:\\1:;\" >>args\n+\tEOF\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n+\trm -f expect &&\n+\tfor frequency in hourly daily weekly\n+\tdo\n+\t\tPLIST=\"$pfx/Library/LaunchAgents/org.git-scm.git.$frequency.plist\" &&\n+\t\techo \"launchctl bootout gui/[UID] $PLIST\" >>expect &&\n+\t\techo \"launchctl bootstrap gui/[UID] $PLIST\" >>expect || return 1\n+\tdone &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n+\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n+\t\thourly daily weekly >expect &&\n+\tprintf \"schtasks /create /tn Git Maintenance (%s) /f /xml\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n+\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n+\t\thourly daily weekly >expect &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\ttest_cmp expect args\n+'\n+\n test_expect_success 'register preserves existing strategy' '\n \tgit config maintenance.strategy none &&\n \tgit maintenance register &&\n-- \n2.31.1\n\n"},{"id":"425401","messageId":"20210524071538.46862-3-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210524071538.46862-1-lenaic@lhuard.fr","subject":"[PATCH v4 2/4] maintenance: introduce ENABLE/DISABLE for code clarity","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-05-24T07:15:36Z","receivedAt":"2021-05-24T07:17:13Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"The first parameter of `XXX_update_schedule` and alike functions is a\nboolean specifying if the tasks should be scheduled or unscheduled.\n\nUsing an `enum` with `ENABLE` and `DISABLE` values can make the code\nclearer.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n builtin/gc.c | 49 +++++++++++++++++++++++++++++++------------------\n 1 file changed, 31 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex ef7226d7bc..0caf8d45c4 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1570,19 +1570,21 @@ static char *launchctl_get_uid(void)\n \treturn xstrfmt(\"gui/%d\", getuid());\n }\n \n-static int launchctl_boot_plist(int enable, const char *filename, const char *cmd)\n+enum enable_or_disable {\n+\tDISABLE,\n+\tENABLE\n+};\n+\n+static int launchctl_boot_plist(enum enable_or_disable enable,\n+\t\t\t\tconst char *filename, const char *cmd)\n {\n \tint result;\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tchar *uid = launchctl_get_uid();\n \n \tstrvec_split(&child.args, cmd);\n-\tif (enable)\n-\t\tstrvec_push(&child.args, \"bootstrap\");\n-\telse\n-\t\tstrvec_push(&child.args, \"bootout\");\n-\tstrvec_push(&child.args, uid);\n-\tstrvec_push(&child.args, filename);\n+\tstrvec_pushl(&child.args, enable == ENABLE ? \"bootstrap\" : \"bootout\",\n+\t\t     uid, filename, NULL);\n \n \tchild.no_stderr = 1;\n \tchild.no_stdout = 1;\n@@ -1601,7 +1603,7 @@ static int launchctl_remove_plist(enum schedule_priority schedule, const char *c\n \tconst char *frequency = get_frequency(schedule);\n \tchar *name = launchctl_service_name(frequency);\n \tchar *filename = launchctl_service_filename(name);\n-\tint result = launchctl_boot_plist(0, filename, cmd);\n+\tint result = launchctl_boot_plist(DISABLE, filename, cmd);\n \tunlink(filename);\n \tfree(filename);\n \tfree(name);\n@@ -1684,8 +1686,8 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n \tfclose(plist);\n \n \t/* bootout might fail if not already running, so ignore */\n-\tlaunchctl_boot_plist(0, filename, cmd);\n-\tif (launchctl_boot_plist(1, filename, cmd))\n+\tlaunchctl_boot_plist(DISABLE, filename, cmd);\n+\tif (launchctl_boot_plist(ENABLE, filename, cmd))\n \t\tdie(_(\"failed to bootstrap service %s\"), filename);\n \n \tfree(filename);\n@@ -1702,12 +1704,17 @@ static int launchctl_add_plists(const char *cmd)\n \t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY, cmd);\n }\n \n-static int launchctl_update_schedule(int run_maintenance, int fd, const char *cmd)\n+static int launchctl_update_schedule(enum enable_or_disable run_maintenance,\n+\t\t\t\t     int fd, const char *cmd)\n {\n-\tif (run_maintenance)\n+\tswitch (run_maintenance) {\n+\tcase ENABLE:\n \t\treturn launchctl_add_plists(cmd);\n-\telse\n+\tcase DISABLE:\n \t\treturn launchctl_remove_plists(cmd);\n+\tdefault:\n+\t\tBUG(\"invalid enable_or_disable value\");\n+\t}\n }\n \n static char *schtasks_task_name(const char *frequency)\n@@ -1864,18 +1871,24 @@ static int schtasks_schedule_tasks(const char *cmd)\n \t\tschtasks_schedule_task(exec_path, SCHEDULE_WEEKLY, cmd);\n }\n \n-static int schtasks_update_schedule(int run_maintenance, int fd, const char *cmd)\n+static int schtasks_update_schedule(enum enable_or_disable run_maintenance,\n+\t\t\t\t    int fd, const char *cmd)\n {\n-\tif (run_maintenance)\n+\tswitch (run_maintenance) {\n+\tcase ENABLE:\n \t\treturn schtasks_schedule_tasks(cmd);\n-\telse\n+\tcase DISABLE:\n \t\treturn schtasks_remove_tasks(cmd);\n+\tdefault:\n+\t\tBUG(\"invalid enable_or_disable value\");\n+\t}\n }\n \n #define BEGIN_LINE \"# BEGIN GIT MAINTENANCE SCHEDULE\"\n #define END_LINE \"# END GIT MAINTENANCE SCHEDULE\"\n \n-static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n+static int crontab_update_schedule(enum enable_or_disable run_maintenance,\n+\t\t\t\t   int fd, const char *cmd)\n {\n \tint result = 0;\n \tint in_old_region = 0;\n@@ -1925,7 +1938,7 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n \t\t\tfprintf(cron_in, \"%s\\n\", line.buf);\n \t}\n \n-\tif (run_maintenance) {\n+\tif (run_maintenance == ENABLE) {\n \t\tstruct strbuf line_format = STRBUF_INIT;\n \t\tconst char *exec_path = git_exec_path();\n \n-- \n2.31.1\n\n"},{"id":"425402","messageId":"20210524071538.46862-2-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210524071538.46862-1-lenaic@lhuard.fr","subject":"[PATCH v4 1/4] cache.h: Introduce a generic \"xdg_config_home_for(…)\" function","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-05-24T07:15:35Z","receivedAt":"2021-05-24T07:23:39Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Current implementation of `xdg_config_home(filename)` returns\n`$XDG_CONFIG_HOME/git/$filename`, with the `git` subdirectory inserted\nbetween the `XDG_CONFIG_HOME` environment variable and the parameter.\n\nThis patch introduces a `xdg_config_home_for(prog, filename)` function\nwhich is more generic. It only concatenates \"$XDG_CONFIG_HOME\", or\n\"$HOME/.config\" if the former isn’t defined, with the parameters,\nwithout adding `git` in between.\n\n`xdg_config_home(filename)` is now implemented by calling\n`xdg_config_home_for(\"git\", filename)` but this new generic function can\nbe used to compute the configuration directory of other programs.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n cache.h |  7 +++++++\n path.c  | 13 ++++++++++---\n 2 files changed, 17 insertions(+), 3 deletions(-)\n\ndiff --git a/cache.h b/cache.h\nindex 148d9ab5f1..8a2969414a 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -1263,6 +1263,13 @@ int is_ntfs_dotgitattributes(const char *name);\n  */\n int looks_like_command_line_option(const char *str);\n \n+/**\n+ * Return a newly allocated string with the evaluation of\n+ * \"$XDG_CONFIG_HOME/$prog/$filename\" if $XDG_CONFIG_HOME is non-empty, otherwise\n+ * \"$HOME/.config/$prog/$filename\". Return NULL upon error.\n+ */\n+char *xdg_config_home_for(const char *prog, const char *filename);\n+\n /**\n  * Return a newly allocated string with the evaluation of\n  * \"$XDG_CONFIG_HOME/git/$filename\" if $XDG_CONFIG_HOME is non-empty, otherwise\ndiff --git a/path.c b/path.c\nindex 7b385e5eb2..3641d4c456 100644\n--- a/path.c\n+++ b/path.c\n@@ -1498,21 +1498,28 @@ int looks_like_command_line_option(const char *str)\n \treturn str && str[0] == '-';\n }\n \n-char *xdg_config_home(const char *filename)\n+char *xdg_config_home_for(const char *prog, const char *filename)\n {\n \tconst char *home, *config_home;\n \n+\tassert(prog);\n \tassert(filename);\n \tconfig_home = getenv(\"XDG_CONFIG_HOME\");\n \tif (config_home && *config_home)\n-\t\treturn mkpathdup(\"%s/git/%s\", config_home, filename);\n+\t\treturn mkpathdup(\"%s/%s/%s\", config_home, prog, filename);\n \n \thome = getenv(\"HOME\");\n \tif (home)\n-\t\treturn mkpathdup(\"%s/.config/git/%s\", home, filename);\n+\t\treturn mkpathdup(\"%s/.config/%s/%s\", home, prog, filename);\n+\n \treturn NULL;\n }\n \n+char *xdg_config_home(const char *filename)\n+{\n+\treturn xdg_config_home_for(\"git\", filename);\n+}\n+\n char *xdg_cache_home(const char *filename)\n {\n \tconst char *home, *cache_home;\n-- \n2.31.1\n\n"},{"id":"425403","messageId":"87wnrooa17.fsf@evledraar.gmail.com","threadId":"55603","inReplyTo":"YKrk4dEjEm6+48ji@camp.crustytoothpaste.net","subject":"Re: [PATCH v3 4/4] maintenance: optionally use systemd timers on Linux","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-24T07:03:59Z","receivedAt":"2021-05-24T07:39:37Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Sun, May 23 2021, brian m. carlson wrote:\n\n> [[PGP Signed Part:Undecided]]\n> On 2021-05-23 at 18:36:10, Felipe Contreras wrote:\n>> Johannes Schindelin wrote:\n>> > On Fri, 21 May 2021, Derrick Stolee wrote:\n>> > \n>> > > On 5/21/2021 5:59 AM, Bagas Sanjaya wrote:\n>> > > > On 21/05/21 05.13, Lénaïc Huard wrote:\n>> > > >> The existing mechanism for scheduling background maintenance is done\n>> > > >> through cron. On Linux systems managed by systemd, systemd provides an\n>> > > >> alternative to schedule recurring tasks: systemd timers.\n>> > > >>\n>> > > >> The main motivations to implement systemd timers in addition to cron\n>> > > >> are:\n>> > > >> * cron is optional and Linux systems running systemd might not have it\n>> > > >>    installed.\n>> > > >> * The execution of `crontab -l` can tell us if cron is installed but not\n>> > > >>    if the daemon is actually running.\n>> > > >> * With systemd, each service is run in its own cgroup and its logs are\n>> > > >>    tagged by the service inside journald. With cron, all scheduled tasks\n>> > > >>    are running in the cron daemon cgroup and all the logs of the\n>> > > >>    user-scheduled tasks are pretended to belong to the system cron\n>> > > >>    service.\n>> > > >>    Concretely, a user that doesn’t have access to the system logs won’t\n>> > > >>    have access to the log of its own tasks scheduled by cron whereas he\n>> > > >>    will have access to the log of its own tasks scheduled by systemd\n>> > > >>    timer.\n>> > > >\n>> > > > For gender neutrality, we can use he/she instead.\n>> > >\n>> > > Singular \"they\" is better. Fully accurate and less awkward.\n>> > \n>> > I agree.\n>> \n>> I disagree.\n>\n> I'm fully in support of singular \"they\".  It provides a useful pronoun\n> to use in this context, is widely understood and used, is less awkward\n> than \"he/she,\" and is less sexist than the indefinite \"he.\"\n\nI think we should be the most concerned about the lack of inclusivity\nand chilling effect in us being overly picky about the minute details of\ncommit message wording, long past the point of practical utility.\n\nIn this particular case the context is a discussion about \"a user\" on a\n*nix system and what they do and don't have access to.\n\nI think it's a particularly misguided distraction to argue about\nhe/she/they here, since the most accurate thing would really be \"it\".\n\nAt least on my *nix systems most users are system users, and don't map\nto any particular human being, but I digress.\n\nI would like to encourage people in this thread who are calling for a\nchange in wording here to consider whether this sort of discussion is a\ngood use of the ML's time, and the chilling effect of being overly picky\nwhen many contributors are working in their second, third etc. language.\n\nPersonally I don't care whether someone submits a patch where their\ncommit message discusses an example of \"he\", \"she\", \"they\", \"it\" or\nwhatever. It's just meant as an example, and not some statement about\nwhat the gender (or lack thereof) of such a user *should* be.\n\nIt's immediately obvious what the author meant in this case, and that\nthe particular wording is arbitrary. For the purposes of discussing the\ncontribution it matters whether it's unclear or ambiguous, which it's\nnot.\n"},{"id":"425418","messageId":"xmqqh7ismrir.fsf@gitster.g","threadId":"55603","inReplyTo":"20210524071538.46862-1-lenaic@lhuard.fr","subject":"Re: [PATCH v4 0/4] add support for systemd timers on Linux","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-05-24T09:04:44Z","receivedAt":"2021-05-24T09:04:47Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Lénaïc Huard <lenaic@lhuard.fr> writes:\n\n> Hello,\n>\n> The main difference between this v4 patchset and the previous v3\n> concerns ...\n\nEhh, before that, please be helpful to those who weren't involved in\nthe discussion of earlier rounds and haven't seen the paches v3 and\nbelow.  What is this topic about in the bigger picture, why readers\nshould be interested in reading them, on top of what commit is the\nseries designed to apply, etc. etc.\n\nThanks.\n\n> the first patch, the one introducing a new function to use\n> $XDG_CONFIG_HOME.\n> Instead of re-purposing the existing `xdg_config_home(filename)`\n> function, I’ve created a new generic one named\n> `xdg_config_home_for(prog, filename)` to address the potential\n> conflict issue raised by Danh.\n>\n> I’ve also reworded the commit message of the last patch to address the\n> review comments.\n> I replaced the “he” by a “they”.\n> To be honest, I’m not an English native speaker, so I don’t feel\n> comfortable debating which usage is common in informal speech or in\n> formal writtings.\n> So, I would be happy to change it to “he or she” if it is more\n> consensual and looks less “artificial” than “he/she”.\n>\n> Lénaïc Huard (4):\n>   cache.h: Introduce a generic \"xdg_config_home_for(…)\" function\n>   maintenance: introduce ENABLE/DISABLE for code clarity\n>   maintenance: `git maintenance run` learned `--scheduler=<scheduler>`\n>   maintenance: add support for systemd timers on Linux\n>\n>  Documentation/git-maintenance.txt |  60 ++++\n>  builtin/gc.c                      | 548 ++++++++++++++++++++++++++----\n>  cache.h                           |   7 +\n>  path.c                            |  13 +-\n>  t/t7900-maintenance.sh            | 110 +++++-\n>  5 files changed, 657 insertions(+), 81 deletions(-)\n"},{"id":"425419","messageId":"120b6b5c-ab30-6985-d5af-a0b0290b864f@gmail.com","threadId":"55603","inReplyTo":"20210524071538.46862-2-lenaic@lhuard.fr","subject":"Re: [PATCH v4 1/4] cache.h: Introduce a generic \"xdg_config_home_for(…)\" function","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2021-05-24T09:33:30Z","receivedAt":"2021-05-24T09:33:41Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Lénaïc\n\nThis looks fine to me. I'm not 100% sold on calling the parameter prog \nas our program name later in the series ends up being \"systemd/user\" so \nsomething like \"subdir\" might have been better but that is not worth \nrerolling for.\n\nBest Wishes\n\nPhillip\n\nOn 24/05/2021 08:15, Lénaïc Huard wrote:\n> Current implementation of `xdg_config_home(filename)` returns\n> `$XDG_CONFIG_HOME/git/$filename`, with the `git` subdirectory inserted\n> between the `XDG_CONFIG_HOME` environment variable and the parameter.\n> \n> This patch introduces a `xdg_config_home_for(prog, filename)` function\n> which is more generic. It only concatenates \"$XDG_CONFIG_HOME\", or\n> \"$HOME/.config\" if the former isn’t defined, with the parameters,\n> without adding `git` in between.\n> \n> `xdg_config_home(filename)` is now implemented by calling\n> `xdg_config_home_for(\"git\", filename)` but this new generic function can\n> be used to compute the configuration directory of other programs.\n> \n> Signed-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n> ---\n>   cache.h |  7 +++++++\n>   path.c  | 13 ++++++++++---\n>   2 files changed, 17 insertions(+), 3 deletions(-)\n> \n> diff --git a/cache.h b/cache.h\n> index 148d9ab5f1..8a2969414a 100644\n> --- a/cache.h\n> +++ b/cache.h\n> @@ -1263,6 +1263,13 @@ int is_ntfs_dotgitattributes(const char *name);\n>    */\n>   int looks_like_command_line_option(const char *str);\n>   \n> +/**\n> + * Return a newly allocated string with the evaluation of\n> + * \"$XDG_CONFIG_HOME/$prog/$filename\" if $XDG_CONFIG_HOME is non-empty, otherwise\n> + * \"$HOME/.config/$prog/$filename\". Return NULL upon error.\n> + */\n> +char *xdg_config_home_for(const char *prog, const char *filename);\n> +\n>   /**\n>    * Return a newly allocated string with the evaluation of\n>    * \"$XDG_CONFIG_HOME/git/$filename\" if $XDG_CONFIG_HOME is non-empty, otherwise\n> diff --git a/path.c b/path.c\n> index 7b385e5eb2..3641d4c456 100644\n> --- a/path.c\n> +++ b/path.c\n> @@ -1498,21 +1498,28 @@ int looks_like_command_line_option(const char *str)\n>   \treturn str && str[0] == '-';\n>   }\n>   \n> -char *xdg_config_home(const char *filename)\n> +char *xdg_config_home_for(const char *prog, const char *filename)\n>   {\n>   \tconst char *home, *config_home;\n>   \n> +\tassert(prog);\n>   \tassert(filename);\n>   \tconfig_home = getenv(\"XDG_CONFIG_HOME\");\n>   \tif (config_home && *config_home)\n> -\t\treturn mkpathdup(\"%s/git/%s\", config_home, filename);\n> +\t\treturn mkpathdup(\"%s/%s/%s\", config_home, prog, filename);\n>   \n>   \thome = getenv(\"HOME\");\n>   \tif (home)\n> -\t\treturn mkpathdup(\"%s/.config/git/%s\", home, filename);\n> +\t\treturn mkpathdup(\"%s/.config/%s/%s\", home, prog, filename);\n> +\n>   \treturn NULL;\n>   }\n>   \n> +char *xdg_config_home(const char *filename)\n> +{\n> +\treturn xdg_config_home_for(\"git\", filename);\n> +}\n> +\n>   char *xdg_cache_home(const char *filename)\n>   {\n>   \tconst char *home, *cache_home;\n> \n\n"},{"id":"425420","messageId":"8e013441-08dc-fbb5-f9b9-649b2ffd78db@gmail.com","threadId":"55603","inReplyTo":"20210524071538.46862-3-lenaic@lhuard.fr","subject":"Re: [PATCH v4 2/4] maintenance: introduce ENABLE/DISABLE for code clarity","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2021-05-24T09:41:18Z","receivedAt":"2021-05-24T09:41:28Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Lénaïc\n\nOn 24/05/2021 08:15, Lénaïc Huard wrote:\n> The first parameter of `XXX_update_schedule` and alike functions is a\n> boolean specifying if the tasks should be scheduled or unscheduled.\n> \n> Using an `enum` with `ENABLE` and `DISABLE` values can make the code\n> clearer.\n\nI'm sorry to say that I'm not sure this does make the code clearer \noverall - I wish I'd spoken up when Danh suggested it.\nWhile\n\tlaunchctl_boot_plist(DISABLE, filename, cmd)\nis arguably clearer than\n\tlaunchctl_boot_plist(0, filename, cmd)\nwe end up with bizarre tests like\n  \tif (enabled == ENABLED)\nrather than\n\tif (enabled)\nand in the next patch we have\n\t(enable == ENABLE && (opts->scheduler == i)) ?\n\t\t\tENABLE : DISABLE;\nrather than\n\tenable && opts->scheduler == i\n\nAlso looking at the next patch it seems as this one is missing some \nconversions in maintenance_start() as it is still calling \nupdate_background_schedule() with an integer rather than the new enum.\n\nI'd be happy to see this being dropped I'm afraid\n\nBest Wishes\n\nPhillip\n\n> Signed-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n> ---\n>   builtin/gc.c | 49 +++++++++++++++++++++++++++++++------------------\n>   1 file changed, 31 insertions(+), 18 deletions(-)\n> \n> diff --git a/builtin/gc.c b/builtin/gc.c\n> index ef7226d7bc..0caf8d45c4 100644\n> --- a/builtin/gc.c\n> +++ b/builtin/gc.c\n> @@ -1570,19 +1570,21 @@ static char *launchctl_get_uid(void)\n>   \treturn xstrfmt(\"gui/%d\", getuid());\n>   }\n>   \n> -static int launchctl_boot_plist(int enable, const char *filename, const char *cmd)\n> +enum enable_or_disable {\n> +\tDISABLE,\n> +\tENABLE\n> +};\n> +\n> +static int launchctl_boot_plist(enum enable_or_disable enable,\n> +\t\t\t\tconst char *filename, const char *cmd)\n>   {\n>   \tint result;\n>   \tstruct child_process child = CHILD_PROCESS_INIT;\n>   \tchar *uid = launchctl_get_uid();\n>   \n>   \tstrvec_split(&child.args, cmd);\n> -\tif (enable)\n> -\t\tstrvec_push(&child.args, \"bootstrap\");\n> -\telse\n> -\t\tstrvec_push(&child.args, \"bootout\");\n> -\tstrvec_push(&child.args, uid);\n> -\tstrvec_push(&child.args, filename);\n> +\tstrvec_pushl(&child.args, enable == ENABLE ? \"bootstrap\" : \"bootout\",\n> +\t\t     uid, filename, NULL);\n>   \n>   \tchild.no_stderr = 1;\n>   \tchild.no_stdout = 1;\n> @@ -1601,7 +1603,7 @@ static int launchctl_remove_plist(enum schedule_priority schedule, const char *c\n>   \tconst char *frequency = get_frequency(schedule);\n>   \tchar *name = launchctl_service_name(frequency);\n>   \tchar *filename = launchctl_service_filename(name);\n> -\tint result = launchctl_boot_plist(0, filename, cmd);\n> +\tint result = launchctl_boot_plist(DISABLE, filename, cmd);\n>   \tunlink(filename);\n>   \tfree(filename);\n>   \tfree(name);\n> @@ -1684,8 +1686,8 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n>   \tfclose(plist);\n>   \n>   \t/* bootout might fail if not already running, so ignore */\n> -\tlaunchctl_boot_plist(0, filename, cmd);\n> -\tif (launchctl_boot_plist(1, filename, cmd))\n> +\tlaunchctl_boot_plist(DISABLE, filename, cmd);\n> +\tif (launchctl_boot_plist(ENABLE, filename, cmd))\n>   \t\tdie(_(\"failed to bootstrap service %s\"), filename);\n>   \n>   \tfree(filename);\n> @@ -1702,12 +1704,17 @@ static int launchctl_add_plists(const char *cmd)\n>   \t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY, cmd);\n>   }\n>   \n> -static int launchctl_update_schedule(int run_maintenance, int fd, const char *cmd)\n> +static int launchctl_update_schedule(enum enable_or_disable run_maintenance,\n> +\t\t\t\t     int fd, const char *cmd)\n>   {\n> -\tif (run_maintenance)\n> +\tswitch (run_maintenance) {\n> +\tcase ENABLE:\n>   \t\treturn launchctl_add_plists(cmd);\n> -\telse\n> +\tcase DISABLE:\n>   \t\treturn launchctl_remove_plists(cmd);\n> +\tdefault:\n> +\t\tBUG(\"invalid enable_or_disable value\");\n> +\t}\n>   }\n>   \n>   static char *schtasks_task_name(const char *frequency)\n> @@ -1864,18 +1871,24 @@ static int schtasks_schedule_tasks(const char *cmd)\n>   \t\tschtasks_schedule_task(exec_path, SCHEDULE_WEEKLY, cmd);\n>   }\n>   \n> -static int schtasks_update_schedule(int run_maintenance, int fd, const char *cmd)\n> +static int schtasks_update_schedule(enum enable_or_disable run_maintenance,\n> +\t\t\t\t    int fd, const char *cmd)\n>   {\n> -\tif (run_maintenance)\n> +\tswitch (run_maintenance) {\n> +\tcase ENABLE:\n>   \t\treturn schtasks_schedule_tasks(cmd);\n> -\telse\n> +\tcase DISABLE:\n>   \t\treturn schtasks_remove_tasks(cmd);\n> +\tdefault:\n> +\t\tBUG(\"invalid enable_or_disable value\");\n> +\t}\n>   }\n>   \n>   #define BEGIN_LINE \"# BEGIN GIT MAINTENANCE SCHEDULE\"\n>   #define END_LINE \"# END GIT MAINTENANCE SCHEDULE\"\n>   \n> -static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n> +static int crontab_update_schedule(enum enable_or_disable run_maintenance,\n> +\t\t\t\t   int fd, const char *cmd)\n>   {\n>   \tint result = 0;\n>   \tint in_old_region = 0;\n> @@ -1925,7 +1938,7 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n>   \t\t\tfprintf(cron_in, \"%s\\n\", line.buf);\n>   \t}\n>   \n> -\tif (run_maintenance) {\n> +\tif (run_maintenance == ENABLE) {\n>   \t\tstruct strbuf line_format = STRBUF_INIT;\n>   \t\tconst char *exec_path = git_exec_path();\n>   \n> \n\n"},{"id":"425422","messageId":"87tumso3xn.fsf@evledraar.gmail.com","threadId":"55603","inReplyTo":"20210524071538.46862-3-lenaic@lhuard.fr","subject":"Re: [PATCH v4 2/4] maintenance: introduce ENABLE/DISABLE for code clarity","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-24T09:47:16Z","receivedAt":"2021-05-24T09:51:24Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Mon, May 24 2021, Lénaïc Huard wrote:\n\n> The first parameter of `XXX_update_schedule` and alike functions is a\n> boolean specifying if the tasks should be scheduled or unscheduled.\n>\n> Using an `enum` with `ENABLE` and `DISABLE` values can make the code\n> clearer.\n\nI'm a fan of enums in general for N values, but I think for this sort of\nboolean case it's stepping into the territory of just making things less\nreadable. There's nothing unreadable about 0/1 as an on/off.\n\n> -static int launchctl_boot_plist(int enable, const char *filename, const char *cmd)\n> +enum enable_or_disable {\n> +\tDISABLE,\n> +\tENABLE\n> +};\n\nSo here we have ENABLE/DISABLE...\n\n> +static int launchctl_boot_plist(enum enable_or_disable enable,\n> +\t\t\t\tconst char *filename, const char *cmd)\n>  {\n>  \tint result;\n>  \tstruct child_process child = CHILD_PROCESS_INIT;\n>  \tchar *uid = launchctl_get_uid();\n>  \n>  \tstrvec_split(&child.args, cmd);\n> -\tif (enable)\n> -\t\tstrvec_push(&child.args, \"bootstrap\");\n> -\telse\n> -\t\tstrvec_push(&child.args, \"bootout\");\n> -\tstrvec_push(&child.args, uid);\n> -\tstrvec_push(&child.args, filename);\n> +\tstrvec_pushl(&child.args, enable == ENABLE ? \"bootstrap\" : \"bootout\",\n> +\t\t     uid, filename, NULL);\n\n..And here we just check ENABLE, and assume !ENABLE == DISABLE...\n[...]\n\n>  {\n> -\tif (run_maintenance)\n> +\tswitch (run_maintenance) {\n> +\tcase ENABLE:\n>  \t\treturn launchctl_add_plists(cmd);\n> -\telse\n> +\tcase DISABLE:\n>  \t\treturn launchctl_remove_plists(cmd);\n> +\tdefault:\n> +\t\tBUG(\"invalid enable_or_disable value\");\n> +\t}\n>  }\n\nAnd here we use a switch, but also a \"default\". It's actually better if\nyou're going to use an enum like this to leave out the \"default\", the\ncompiler will catch non-enumerated values for us.\n\n>  \n>  static char *schtasks_task_name(const char *frequency)\n> @@ -1864,18 +1871,24 @@ static int schtasks_schedule_tasks(const char *cmd)\n>  \t\tschtasks_schedule_task(exec_path, SCHEDULE_WEEKLY, cmd);\n>  }\n>  \n> -static int schtasks_update_schedule(int run_maintenance, int fd, const char *cmd)\n> +static int schtasks_update_schedule(enum enable_or_disable run_maintenance,\n> +\t\t\t\t    int fd, const char *cmd)\n>  {\n> -\tif (run_maintenance)\n> +\tswitch (run_maintenance) {\n> +\tcase ENABLE:\n>  \t\treturn schtasks_schedule_tasks(cmd);\n> -\telse\n> +\tcase DISABLE:\n>  \t\treturn schtasks_remove_tasks(cmd);\n> +\tdefault:\n> +\t\tBUG(\"invalid enable_or_disable value\");\n> +\t}\n>  }\n\nAs an aside (I haven't read much/all the context) I wonder why we have\nthese wrapper functions, can't the caller just pass an \"enable\" flag?\n\n>  #define BEGIN_LINE \"# BEGIN GIT MAINTENANCE SCHEDULE\"\n>  #define END_LINE \"# END GIT MAINTENANCE SCHEDULE\"\n>  \n> -static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n> +static int crontab_update_schedule(enum enable_or_disable run_maintenance,\n> +\t\t\t\t   int fd, const char *cmd)\n>  {\n>  \tint result = 0;\n>  \tint in_old_region = 0;\n> @@ -1925,7 +1938,7 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n>  \t\t\tfprintf(cron_in, \"%s\\n\", line.buf);\n>  \t}\n>  \n> -\tif (run_maintenance) {\n> +\tif (run_maintenance == ENABLE) {\n>  \t\tstruct strbuf line_format = STRBUF_INIT;\n>  \t\tconst char *exec_path = git_exec_path();\n\nSame !ENABLE == DISABLE assumption?\n\n"},{"id":"425423","messageId":"87r1hwo3e3.fsf@evledraar.gmail.com","threadId":"55603","inReplyTo":"20210524071538.46862-5-lenaic@lhuard.fr","subject":"Re: [PATCH v4 4/4] maintenance: add support for systemd timers on Linux","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-24T09:55:09Z","receivedAt":"2021-05-24T10:03:07Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Mon, May 24 2021, Lénaïc Huard wrote:\n\nIf we're going to use ifdefs then... \n\n> +static int is_systemd_timer_available(void)\n> +{\n> +\tconst char *cmd = \"systemctl\";\n> +\tint is_available;\n> +\tstatic int cached_result = -1;\n> +#ifdef __linux__\n> +\tstruct child_process child = CHILD_PROCESS_INIT;\n> +#endif\n> +\n> +\tif (cached_result != -1)\n> +\t\treturn cached_result;\n> +\n> +\tif (get_schedule_cmd(&cmd, &is_available))\n> +\t\treturn is_available;\n> +\n> +#ifdef __linux__\n> +\tstrvec_split(&child.args, cmd);\n> +\tstrvec_pushl(&child.args, \"--user\", \"list-timers\", NULL);\n> +\tchild.no_stdin = 1;\n> +\tchild.no_stdout = 1;\n> +\tchild.no_stderr = 1;\n> +\tchild.silent_exec_failure = 1;\n> +\n> +\tif (start_command(&child)) {\n> +\t\tcached_result = 0;\n> +\t\treturn cached_result;\n> +\t}\n> +\tif (finish_command(&child)) {\n> +\t\tcached_result = 0;\n> +\t\treturn cached_result;\n> +\t}\n> +\tcached_result = 1;\n> +\treturn cached_result;\n> +#else\n> +\treturn 0;\n> +#endif\n> +}\n\nThis sort of function would, I think, be clearer if we just had two\ndifferent functions in an ifdef/else, e.g. here cached_result\" is\nchecked under !__linux__, but is never set in that case.\n\nBut aside from that, why we do we need all this is_available caching,\nisn't this all called as a one-off? If it's not, it seems much better to\npush that cache one level up the callstack. Have whatevere's keeping\ntrack of the struct cache is_available itself, or cache it in another\n(or the same, then tristate) field in the same struct.\n\n> [...]\n> +test_expect_success 'start and stop Linux/systemd maintenance' '\n> +\twrite_script print-args <<-\\EOF &&\n> +\tprintf \"%s\\n\" \"$*\" >>args\n> +\tEOF\n> +\n> +\tXDG_CONFIG_HOME=\"$PWD\" &&\n> +\texport XDG_CONFIG_HOME &&\n> +\trm -f args &&\n\nIf you're going to care about cleanup here, and personally I wouldn't,\njust call that \"args\" by the name \"expect\" instead (as is convention)\nand clobber it every time...\n\nAnyway, a better way to do the cleanup is:\n\n    test_when_finished \"rm args\" &&\n    echo this is the first time you write the file >args\n    [the rest of the test code]\n\nThen you don't need to re-rm it.\n"},{"id":"425424","messageId":"ce0e096d-cd82-800a-9ef5-5bcc4b25046d@gmail.com","threadId":"55603","inReplyTo":"20210524071538.46862-4-lenaic@lhuard.fr","subject":"Re: [PATCH v4 3/4] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2021-05-24T10:12:10Z","receivedAt":"2021-05-24T10:12:27Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Lénaïc\n\nOn 24/05/2021 08:15, Lénaïc Huard wrote:\n> Depending on the system, different schedulers can be used to schedule\n> the hourly, daily and weekly executions of `git maintenance run`:\n> * `launchctl` for MacOS,\n> * `schtasks` for Windows and\n> * `crontab` for everything else.\n> \n> `git maintenance run` now has an option to let the end-user explicitly\n> choose which scheduler he wants to use:\n> `--scheduler=auto|crontab|launchctl|schtasks`.\n> \n> When `git maintenance start --scheduler=XXX` is run, it not only\n> registers `git maintenance run` tasks in the scheduler XXX, it also\n> removes the `git maintenance run` tasks from all the other schedulers to\n> ensure we cannot have two schedulers launching concurrent identical\n> tasks.\n> \n> The default value is `auto` which chooses a suitable scheduler for the\n> system.\n> \n> `git maintenance stop` doesn't have any `--scheduler` parameter because\n> this command will try to remove the `git maintenance run` tasks from all\n> the available schedulers.\n\nI like this change, it makes the test infrastructure less intrusive.\n\n> Signed-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n> ---\n>   Documentation/git-maintenance.txt |  11 +\n>   builtin/gc.c                      | 333 ++++++++++++++++++++++++------\n>   t/t7900-maintenance.sh            |  56 ++++-\n>   3 files changed, 333 insertions(+), 67 deletions(-)\n> \n> diff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\n> index 80ddd33ceb..7c4bb38a2f 100644\n> --- a/Documentation/git-maintenance.txt\n> +++ b/Documentation/git-maintenance.txt\n> @@ -181,6 +181,17 @@ OPTIONS\n>   \t`maintenance.<task>.enabled` configured as `true` are considered.\n>   \tSee the 'TASKS' section for the list of accepted `<task>` values.\n>   \n> +--scheduler=auto|crontab|launchctl|schtasks::\n> +\tWhen combined with the `start` subcommand, specify the scheduler\n> +\tto use to run the hourly, daily and weekly executions of\n> +\t`git maintenance run`.\n> +\tThe possible values for `<scheduler>` depend on the system: `crontab`\n> +\tis available on POSIX systems, `launchctl` is available on\n> +\tMacOS and `schtasks` is available on Windows.\n> +\tBy default or when `auto` is specified, a suitable scheduler for\n> +\tthe system is used. On MacOS, `launchctl` is used. On Windows,\n> +\t`schtasks` is used. On all other systems, `crontab` is used.\n> +\n>   \n>   TROUBLESHOOTING\n>   ---------------\n> diff --git a/builtin/gc.c b/builtin/gc.c\n> index 0caf8d45c4..bf21cec059 100644\n> --- a/builtin/gc.c\n> +++ b/builtin/gc.c\n> @@ -1544,6 +1544,60 @@ static const char *get_frequency(enum schedule_priority schedule)\n>   \t}\n>   }\n>   \n> +static int get_schedule_cmd(const char **cmd, int *is_available)\n> +{\n> +\tchar *item;\n> +\tstatic char test_cmd[32];\n> +\tchar *testing = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n> +\n> +\tif (!testing)\n> +\t\treturn 0;\n> +\n> +\tif (is_available)\n> +\t\t*is_available = 0;\n> +\n> +\tfor(item = testing;;) {\n> +\t\tchar *sep;\n> +\t\tchar *end_item = strchr(item, ',');\n> +\t\tif (end_item)\n> +\t\t\t*end_item = '\\0';\n> +\n> +\t\tsep = strchr(item, ':');\n> +\t\tif (!sep)\n> +\t\t\tdie(\"GIT_TEST_MAINT_SCHEDULER unparseable: %s\", testing);\n> +\t\t*sep = '\\0';\n> +\n> +\t\tif (!strcmp(*cmd, item)) {\n> +\t\t\tstrlcpy(test_cmd, sep+1, ARRAY_SIZE(test_cmd));\n\nThis falls into the trap of assuming strlcpy() is safe and does not \ncheck the return value. It will silently truncate the command if it is \ntoo long. As this is for testing I'd be happy just to return 'sep + 1' \nin *cmd and leak it. We could mark 'testing' with UNLEAK() to keep asan \nhappy.\n\n> +\t\t\t*cmd = test_cmd;\n> +\t\t\tif (is_available)\n> +\t\t\t\t*is_available = 1;\n> +\t\t\tbreak;\n> +\t\t}\n> +\n> +\t\tif (!end_item)\n> +\t\t\tbreak;\n> +\t\titem = end_item + 1;\n> +\t}\n> +\n> +\tfree(testing);\n> +\treturn 1;\n> +}\n> +\n> +static int is_launchctl_available(void)\n> +{\n> +\tconst char *cmd = \"launchctl\";\n> +\tint is_available;\n> +\tif (get_schedule_cmd(&cmd, &is_available))\n> +\t\treturn is_available;\n> +\n> +#ifdef __APPLE__\n> +\treturn 1;\n> +#else\n> +\treturn 0;\n> +#endif\n> +}\n> +\n>   static char *launchctl_service_name(const char *frequency)\n>   {\n>   \tstruct strbuf label = STRBUF_INIT;\n> @@ -1576,12 +1630,14 @@ enum enable_or_disable {\n>   };\n>   \n>   static int launchctl_boot_plist(enum enable_or_disable enable,\n> -\t\t\t\tconst char *filename, const char *cmd)\n> +\t\t\t\tconst char *filename)\n\nI'm so pleased to see all these 'cmd' arguments disappearing!\n\n>   {\n> +\tconst char *cmd = \"launchctl\";\n>   \tint result;\n>   \tstruct child_process child = CHILD_PROCESS_INIT;\n>   \tchar *uid = launchctl_get_uid();\n>   \n> +\tget_schedule_cmd(&cmd, NULL);\n>   \tstrvec_split(&child.args, cmd);\n\nIt's a shame we still have to have this strvec_split() call just to \nhandle testing but your changes are an improvement.\n\n>   \tstrvec_pushl(&child.args, enable == ENABLE ? \"bootstrap\" : \"bootout\",\n>   \t\t     uid, filename, NULL);\n> @@ -1598,26 +1654,26 @@ static int launchctl_boot_plist(enum enable_or_disable enable,\n>   \treturn result;\n>   }\n>   \n> -static int launchctl_remove_plist(enum schedule_priority schedule, const char *cmd)\n> +static int launchctl_remove_plist(enum schedule_priority schedule)\n>   {\n>   \tconst char *frequency = get_frequency(schedule);\n>   \tchar *name = launchctl_service_name(frequency);\n>   \tchar *filename = launchctl_service_filename(name);\n> -\tint result = launchctl_boot_plist(DISABLE, filename, cmd);\n> +\tint result = launchctl_boot_plist(DISABLE, filename);\n>   \tunlink(filename);\n>   \tfree(filename);\n>   \tfree(name);\n>   \treturn result;\n>   }\n>   \n> -static int launchctl_remove_plists(const char *cmd)\n> +static int launchctl_remove_plists(void)\n>   {\n> -\treturn launchctl_remove_plist(SCHEDULE_HOURLY, cmd) ||\n> -\t\tlaunchctl_remove_plist(SCHEDULE_DAILY, cmd) ||\n> -\t\tlaunchctl_remove_plist(SCHEDULE_WEEKLY, cmd);\n> +\treturn launchctl_remove_plist(SCHEDULE_HOURLY) ||\n> +\t\tlaunchctl_remove_plist(SCHEDULE_DAILY) ||\n> +\t\tlaunchctl_remove_plist(SCHEDULE_WEEKLY);\n>   }\n>   \n> -static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule, const char *cmd)\n> +static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule)\n>   {\n>   \tFILE *plist;\n>   \tint i;\n> @@ -1686,8 +1742,8 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n>   \tfclose(plist);\n>   \n>   \t/* bootout might fail if not already running, so ignore */\n> -\tlaunchctl_boot_plist(DISABLE, filename, cmd);\n> -\tif (launchctl_boot_plist(ENABLE, filename, cmd))\n> +\tlaunchctl_boot_plist(DISABLE, filename);\n> +\tif (launchctl_boot_plist(ENABLE, filename))\n>   \t\tdie(_(\"failed to bootstrap service %s\"), filename);\n>   \n>   \tfree(filename);\n> @@ -1695,28 +1751,42 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n>   \treturn 0;\n>   }\n>   \n> -static int launchctl_add_plists(const char *cmd)\n> +static int launchctl_add_plists(void)\n>   {\n>   \tconst char *exec_path = git_exec_path();\n>   \n> -\treturn launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY, cmd) ||\n> -\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_DAILY, cmd) ||\n> -\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY, cmd);\n> +\treturn launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY) ||\n> +\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_DAILY) ||\n> +\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY);\n>   }\n>   \n>   static int launchctl_update_schedule(enum enable_or_disable run_maintenance,\n> -\t\t\t\t     int fd, const char *cmd)\n> +\t\t\t\t     int fd)\n>   {\n>   \tswitch (run_maintenance) {\n>   \tcase ENABLE:\n> -\t\treturn launchctl_add_plists(cmd);\n> +\t\treturn launchctl_add_plists();\n>   \tcase DISABLE:\n> -\t\treturn launchctl_remove_plists(cmd);\n> +\t\treturn launchctl_remove_plists();\n>   \tdefault:\n>   \t\tBUG(\"invalid enable_or_disable value\");\n>   \t}\n>   }\n>   \n> +static int is_schtasks_available(void)\n> +{\n> +\tconst char *cmd = \"schtasks\";\n> +\tint is_available;\n> +\tif (get_schedule_cmd(&cmd, &is_available))\n> +\t\treturn is_available;\n> +\n> +#ifdef GIT_WINDOWS_NATIVE\n> +\treturn 1;\n> +#else\n> +\treturn 0;\n> +#endif\n> +}\n> +\n>   static char *schtasks_task_name(const char *frequency)\n>   {\n>   \tstruct strbuf label = STRBUF_INIT;\n> @@ -1724,13 +1794,15 @@ static char *schtasks_task_name(const char *frequency)\n>   \treturn strbuf_detach(&label, NULL);\n>   }\n>   \n> -static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd)\n> +static int schtasks_remove_task(enum schedule_priority schedule)\n>   {\n> +\tconst char *cmd = \"schtasks\";\n>   \tint result;\n>   \tstruct strvec args = STRVEC_INIT;\n>   \tconst char *frequency = get_frequency(schedule);\n>   \tchar *name = schtasks_task_name(frequency);\n>   \n> +\tget_schedule_cmd(&cmd, NULL);\n>   \tstrvec_split(&args, cmd);\n>   \tstrvec_pushl(&args, \"/delete\", \"/tn\", name, \"/f\", NULL);\n>   \n> @@ -1741,15 +1813,16 @@ static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd\n>   \treturn result;\n>   }\n>   \n> -static int schtasks_remove_tasks(const char *cmd)\n> +static int schtasks_remove_tasks(void)\n>   {\n> -\treturn schtasks_remove_task(SCHEDULE_HOURLY, cmd) ||\n> -\t\tschtasks_remove_task(SCHEDULE_DAILY, cmd) ||\n> -\t\tschtasks_remove_task(SCHEDULE_WEEKLY, cmd);\n> +\treturn schtasks_remove_task(SCHEDULE_HOURLY) ||\n> +\t\tschtasks_remove_task(SCHEDULE_DAILY) ||\n> +\t\tschtasks_remove_task(SCHEDULE_WEEKLY);\n>   }\n>   \n> -static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule, const char *cmd)\n> +static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule)\n>   {\n> +\tconst char *cmd = \"schtasks\";\n>   \tint result;\n>   \tstruct child_process child = CHILD_PROCESS_INIT;\n>   \tconst char *xml;\n> @@ -1758,6 +1831,8 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority\n>   \tchar *name = schtasks_task_name(frequency);\n>   \tstruct strbuf tfilename = STRBUF_INIT;\n>   \n> +\tget_schedule_cmd(&cmd, NULL);\n> +\n>   \tstrbuf_addf(&tfilename, \"%s/schedule_%s_XXXXXX\",\n>   \t\t    get_git_common_dir(), frequency);\n>   \ttfile = xmks_tempfile(tfilename.buf);\n> @@ -1862,34 +1937,65 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority\n>   \treturn result;\n>   }\n>   \n> -static int schtasks_schedule_tasks(const char *cmd)\n> +static int schtasks_schedule_tasks(void)\n>   {\n>   \tconst char *exec_path = git_exec_path();\n>   \n> -\treturn schtasks_schedule_task(exec_path, SCHEDULE_HOURLY, cmd) ||\n> -\t\tschtasks_schedule_task(exec_path, SCHEDULE_DAILY, cmd) ||\n> -\t\tschtasks_schedule_task(exec_path, SCHEDULE_WEEKLY, cmd);\n> +\treturn schtasks_schedule_task(exec_path, SCHEDULE_HOURLY) ||\n> +\t\tschtasks_schedule_task(exec_path, SCHEDULE_DAILY) ||\n> +\t\tschtasks_schedule_task(exec_path, SCHEDULE_WEEKLY);\n>   }\n>   \n>   static int schtasks_update_schedule(enum enable_or_disable run_maintenance,\n> -\t\t\t\t    int fd, const char *cmd)\n> +\t\t\t\t    int fd)\n>   {\n>   \tswitch (run_maintenance) {\n>   \tcase ENABLE:\n> -\t\treturn schtasks_schedule_tasks(cmd);\n> +\t\treturn schtasks_schedule_tasks();\n>   \tcase DISABLE:\n> -\t\treturn schtasks_remove_tasks(cmd);\n> +\t\treturn schtasks_remove_tasks();\n>   \tdefault:\n>   \t\tBUG(\"invalid enable_or_disable value\");\n>   \t}\n>   }\n>   \n> +static int is_crontab_available(void)\n> +{\n> +\tconst char *cmd = \"crontab\";\n> +\tint is_available;\n> +\tstatic int cached_result = -1;\n> +\tstruct child_process child = CHILD_PROCESS_INIT;\n> +\n> +\tif (cached_result != -1)\n> +\t\treturn cached_result;\n> +\n> +\tif (get_schedule_cmd(&cmd, &is_available) && !is_available)\n> +\t\treturn 0;\n> +\n> +\tstrvec_split(&child.args, cmd);\n> +\tstrvec_push(&child.args, \"-l\");\n> +\tchild.no_stdin = 1;\n> +\tchild.no_stdout = 1;\n> +\tchild.no_stderr = 1;\n> +\tchild.silent_exec_failure = 1;\n> +\n> +\tif (start_command(&child)) {\n> +\t\tcached_result = 0;\n> +\t\treturn cached_result;\n> +\t}\n> +\t/* Ignore exit code, as an empty crontab will return error. */\n> +\tfinish_command(&child);\n> +\tcached_result = 1;\n> +\treturn cached_result;\n> +}\n> +\n>   #define BEGIN_LINE \"# BEGIN GIT MAINTENANCE SCHEDULE\"\n>   #define END_LINE \"# END GIT MAINTENANCE SCHEDULE\"\n>   \n>   static int crontab_update_schedule(enum enable_or_disable run_maintenance,\n> -\t\t\t\t   int fd, const char *cmd)\n> +\t\t\t\t   int fd)\n>   {\n> +\tconst char *cmd = \"crontab\";\n>   \tint result = 0;\n>   \tint in_old_region = 0;\n>   \tstruct child_process crontab_list = CHILD_PROCESS_INIT;\n> @@ -1897,6 +2003,7 @@ static int crontab_update_schedule(enum enable_or_disable run_maintenance,\n>   \tFILE *cron_list, *cron_in;\n>   \tstruct strbuf line = STRBUF_INIT;\n>   \n> +\tget_schedule_cmd(&cmd, NULL);\n>   \tstrvec_split(&crontab_list.args, cmd);\n>   \tstrvec_push(&crontab_list.args, \"-l\");\n>   \tcrontab_list.in = -1;\n> @@ -1972,61 +2079,161 @@ static int crontab_update_schedule(enum enable_or_disable run_maintenance,\n>   \treturn result;\n>   }\n>   \n> +enum scheduler {\n> +\tSCHEDULER_INVALID = -1,\n> +\tSCHEDULER_AUTO,\n> +\tSCHEDULER_CRON,\n> +\tSCHEDULER_LAUNCHCTL,\n> +\tSCHEDULER_SCHTASKS,\n> +};\n> +\n> +static const struct {\n> +\tconst char *name;\n> +\tint (*is_available)(void);\n> +\tint (*update_schedule)(enum enable_or_disable run_maintenance, int fd);\n> +} scheduler_fn[] = {\n> +\t[SCHEDULER_CRON] = {\n> +\t\t.name = \"crontab\",\n> +\t\t.is_available = is_crontab_available,\n> +\t\t.update_schedule = crontab_update_schedule,\n> +\t},\n> +\t[SCHEDULER_LAUNCHCTL] = {\n> +\t\t.name = \"launchctl\",\n> +\t\t.is_available = is_launchctl_available,\n> +\t\t.update_schedule = launchctl_update_schedule,\n> +\t},\n> +\t[SCHEDULER_SCHTASKS] = {\n> +\t\t.name = \"schtasks\",\n> +\t\t.is_available = is_schtasks_available,\n> +\t\t.update_schedule = schtasks_update_schedule,\n> +\t},\n> +};\n> +\n> +static enum scheduler parse_scheduler(const char *value)\n> +{\n> +\tif (!value)\n> +\t\treturn SCHEDULER_INVALID;\n> +\telse if (!strcasecmp(value, \"auto\"))\n> +\t\treturn SCHEDULER_AUTO;\n> +\telse if (!strcasecmp(value, \"cron\") || !strcasecmp(value, \"crontab\"))\n> +\t\treturn SCHEDULER_CRON;\n> +\telse if (!strcasecmp(value, \"launchctl\"))\n> +\t\treturn SCHEDULER_LAUNCHCTL;\n> +\telse if (!strcasecmp(value, \"schtasks\"))\n> +\t\treturn SCHEDULER_SCHTASKS;\n> +\telse\n> +\t\treturn SCHEDULER_INVALID;\n> +}\n> +\n> +static int maintenance_opt_scheduler(const struct option *opt, const char *arg,\n> +\t\t\t\t     int unset)\n> +{\n> +\tenum scheduler *scheduler = opt->value;\n> +\n> +\tBUG_ON_OPT_NEG(unset);\n> +\n> +\t*scheduler = parse_scheduler(arg);\n> +\tif (*scheduler == SCHEDULER_INVALID)\n> +\t\treturn error(_(\"unrecognized --scheduler argument '%s'\"), arg);\n> +\treturn 0;\n> +}\n> +\n> +struct maintenance_start_opts {\n> +\tenum scheduler scheduler;\n> +};\n> +\n> +static void resolve_auto_scheduler(enum scheduler *scheduler)\n> +{\n> +\tif (*scheduler != SCHEDULER_AUTO)\n> +\t\treturn;\n> +\n>   #if defined(__APPLE__)\n> -static const char platform_scheduler[] = \"launchctl\";\n> +\t*scheduler = SCHEDULER_LAUNCHCTL;\n> +\treturn;\n> +\n>   #elif defined(GIT_WINDOWS_NATIVE)\n> -static const char platform_scheduler[] = \"schtasks\";\n> +\t*scheduler = SCHEDULER_SCHTASKS;\n> +\treturn;\n> +\n>   #else\n> -static const char platform_scheduler[] = \"crontab\";\n> +\t*scheduler = SCHEDULER_CRON;\n> +\treturn;\n>   #endif\n> +}\n>   \n> -static int update_background_schedule(int enable)\n> +static void validate_scheduler(enum scheduler scheduler)\n>   {\n> -\tint result;\n> -\tconst char *scheduler = platform_scheduler;\n> -\tconst char *cmd = scheduler;\n> -\tchar *testing;\n> +\tif (scheduler == SCHEDULER_INVALID)\n> +\t\tBUG(\"invalid scheduler\");\n> +\tif (scheduler == SCHEDULER_AUTO)\n> +\t\tBUG(\"resolve_auto_scheduler should have been called before\");\n> +\n> +\tif (!scheduler_fn[scheduler].is_available())\n> +\t\tdie(_(\"%s scheduler is not available\"),\n> +\t\t    scheduler_fn[scheduler].name);\n> +}\n> +\n> +static int update_background_schedule(const struct maintenance_start_opts *opts,\n> +\t\t\t\t      enum enable_or_disable enable)\n> +{\n> +\tunsigned int i;\n> +\tint res, result = 0;\n>   \tstruct lock_file lk;\n>   \tchar *lock_path = xstrfmt(\"%s/schedule\", the_repository->objects->odb->path);\n>   \n> -\ttesting = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n> -\tif (testing) {\n> -\t\tchar *sep = strchr(testing, ':');\n> -\t\tif (!sep)\n> -\t\t\tdie(\"GIT_TEST_MAINT_SCHEDULER unparseable: %s\", testing);\n> -\t\t*sep = '\\0';\n> -\t\tscheduler = testing;\n> -\t\tcmd = sep + 1;\n> -\t}\n> -\n>   \tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0)\n>   \t\treturn error(_(\"another process is scheduling background maintenance\"));\n>   \n> -\tif (!strcmp(scheduler, \"launchctl\"))\n> -\t\tresult = launchctl_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n> -\telse if (!strcmp(scheduler, \"schtasks\"))\n> -\t\tresult = schtasks_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n> -\telse if (!strcmp(scheduler, \"crontab\"))\n> -\t\tresult = crontab_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n> -\telse\n> -\t\tdie(\"unknown background scheduler: %s\", scheduler);\n> +\tfor (i = 1; i < ARRAY_SIZE(scheduler_fn); i++) {\n> +\t\tenum enable_or_disable enable_scheduler =\n> +\t\t\t(enable == ENABLE && (opts->scheduler == i)) ?\n\nStrictly speaking none of the parenthesis are required, I'd certainly \ndrop the inner ones.\n\n> +\t\t\tENABLE : DISABLE;\n> +\t\tif (!scheduler_fn[i].is_available())\n> +\t\t\tcontinue;\n> +\t\tres = scheduler_fn[i].update_schedule(\n> +\t\t\tenable_scheduler, get_lock_file_fd(&lk));\n\nIt does not matter for this patch as we're not really disabling anything \nwith the existing range of schedulers but in the next patch we can end \nup enabling the new scheduler before disabling the old one leading to a \nrace where they can both start 'git maintenance' on the same repo. As I \nsaid in my previous review it would be clearer to disable all the \nschedulers first before enabling the new one.\n\n> +\t\tif (enable_scheduler)\n> +\t\t\tresult = res;\n> +\t}\n>   \n>   \trollback_lock_file(&lk);\n> -\tfree(testing);\n>   \treturn result;\n>   }\n>   \n> -static int maintenance_start(void)\n> +static const char *const builtin_maintenance_start_usage[] = {\n> +\tN_(\"git maintenance start [--scheduler=<scheduler>]\"), NULL\n> +};\n\nI'm not sure what the { } and NULL are doing here, it should just be \nassigning a string. You could put it inside maintenance_start() and just \ncall the variable \"usage\"\n\n> +static int maintenance_start(int argc, const char **argv, const char *prefix)\n>   {\n> +\tstruct maintenance_start_opts opts;\n> +\tstruct option builtin_maintenance_start_options[] = {\n\nAs this variable is local to the function you could call it something \nshorter like options.\n\n> +\t\tOPT_CALLBACK_F(\n> +\t\t\t0, \"scheduler\", &opts.scheduler, N_(\"scheduler\"),\n> +\t\t\tN_(\"scheduler to use to trigger git maintenance run\"),\n> +\t\t\tPARSE_OPT_NONEG, maintenance_opt_scheduler),\n> +\t\tOPT_END()\n> +\t};\n> +\tmemset(&opts, 0, sizeof(opts));\n> +\n> +\targc = parse_options(argc, argv, prefix,\n> +\t\t\t     builtin_maintenance_start_options,\n> +\t\t\t     builtin_maintenance_start_usage, 0);\n> +\tif (argc)\n> +\t\tusage_with_options(builtin_maintenance_start_usage,\n> +\t\t\t\t   builtin_maintenance_start_options);\n> +\n> +\tresolve_auto_scheduler(&opts.scheduler);\n> +\tvalidate_scheduler(opts.scheduler);\n> +\n>   \tif (maintenance_register())\n>   \t\twarning(_(\"failed to add repo to global config\"));\n> -\n> -\treturn update_background_schedule(1);\n> +\treturn update_background_schedule(&opts, 1);\n\nThis conversion got missed in the last patch (that is if we end up \nwanting to use an enum).\n\n>   }\n>   \n>   static int maintenance_stop(void)\n>   {\n> -\treturn update_background_schedule(0);\n> +\treturn update_background_schedule(NULL, 0);\n>   }\n>   \n>   static const char builtin_maintenance_usage[] =\tN_(\"git maintenance <subcommand> [<options>]\");\n> @@ -2040,7 +2247,7 @@ int cmd_maintenance(int argc, const char **argv, const char *prefix)\n>   \tif (!strcmp(argv[1], \"run\"))\n>   \t\treturn maintenance_run(argc - 1, argv + 1, prefix);\n>   \tif (!strcmp(argv[1], \"start\"))\n> -\t\treturn maintenance_start();\n> +\t\treturn maintenance_start(argc - 1, argv + 1, prefix);\n>   \tif (!strcmp(argv[1], \"stop\"))\n>   \t\treturn maintenance_stop();\n>   \tif (!strcmp(argv[1], \"register\"))\n> diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\n> index 2412d8c5c0..9eac260307 100755\n> --- a/t/t7900-maintenance.sh\n> +++ b/t/t7900-maintenance.sh\n> @@ -488,8 +488,21 @@ test_expect_success !MINGW 'register and unregister with regex metacharacters' '\n>   \t\tmaintenance.repo \"$(pwd)/$META\"\n>   '\n>   \n> +test_expect_success 'start --scheduler=<scheduler>' '\n> +\ttest_expect_code 129 git maintenance start --scheduler=foo 2>err &&\n> +\ttest_i18ngrep \"unrecognized --scheduler argument\" err &&\n >\n> +\ttest_expect_code 129 git maintenance start --no-scheduler 2>err &&\n> +\ttest_i18ngrep \"unknown option\" err &&\n> +\n> +\ttest_expect_code 128 \\\n> +\t\tenv GIT_TEST_MAINT_SCHEDULER=\"launchctl:true,schtasks:true\" \\\n> +\t\tgit maintenance start --scheduler=crontab 2>err &&\n> +\ttest_i18ngrep \"fatal: crontab scheduler is not available\" err\n> +'\n> +\n>   test_expect_success 'start from empty cron table' '\n> -\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start --scheduler=crontab &&\n>   \n>   \t# start registers the repo\n>   \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n> @@ -512,7 +525,7 @@ test_expect_success 'stop from existing schedule' '\n>   \n>   test_expect_success 'start preserves existing schedule' '\n>   \techo \"Important information!\" >cron.txt &&\n> -\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start --scheduler=crontab &&\n>   \tgrep \"Important information!\" cron.txt\n>   '\n>   \n> @@ -541,7 +554,7 @@ test_expect_success 'start and stop macOS maintenance' '\n>   \tEOF\n>   \n>   \trm -f args &&\n> -\tGIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start &&\n> +\tGIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start --scheduler=launchctl &&\n>   \n>   \t# start registers the repo\n>   \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n> @@ -592,7 +605,7 @@ test_expect_success 'start and stop Windows maintenance' '\n>   \tEOF\n>   \n>   \trm -f args &&\n> -\tGIT_TEST_MAINT_SCHEDULER=\"schtasks:./print-args\" git maintenance start &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"schtasks:./print-args\" git maintenance start --scheduler=schtasks &&\n>   \n>   \t# start registers the repo\n>   \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n> @@ -615,6 +628,41 @@ test_expect_success 'start and stop Windows maintenance' '\n>   \ttest_cmp expect args\n>   '\n>   \n> +test_expect_success 'start and stop when several schedulers are available' '\n> +\twrite_script print-args <<-\\EOF &&\n> +\tprintf \"%s\\n\" \"$*\" | sed \"s:gui/[0-9][0-9]*:gui/[UID]:; s:\\(schtasks /create .* /xml\\).*:\\1:;\" >>args\n> +\tEOF\n> +\n> +\trm -f args &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n> +\trm -f expect &&\n> +\tfor frequency in hourly daily weekly\n> +\tdo\n> +\t\tPLIST=\"$pfx/Library/LaunchAgents/org.git-scm.git.$frequency.plist\" &&\n> +\t\techo \"launchctl bootout gui/[UID] $PLIST\" >>expect &&\n> +\t\techo \"launchctl bootstrap gui/[UID] $PLIST\" >>expect || return 1\n> +\tdone &&\n> +\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n> +\t\thourly daily weekly >>expect &&\n> +\ttest_cmp expect args &&\n> +\n> +\trm -f args &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n> +\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n> +\t\thourly daily weekly >expect &&\n> +\tprintf \"schtasks /create /tn Git Maintenance (%s) /f /xml\\n\" \\\n> +\t\thourly daily weekly >>expect &&\n> +\ttest_cmp expect args &&\n> +\n> +\trm -f args &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n> +\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n> +\t\thourly daily weekly >expect &&\n> +\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n> +\t\thourly daily weekly >>expect &&\n> +\ttest_cmp expect args\n> +'\n> +\n\nThanks for adding tests for the new option and converting the old ones \nto use it. I think that with a couple of small changes this patch will \nbe ready. I've run out of time for now but I'll try and look at the \nfourth patch in the next couple of days.\nBest Wishes\n\nPhillip\n\n>   test_expect_success 'register preserves existing strategy' '\n>   \tgit config maintenance.strategy none &&\n>   \tgit maintenance register &&\n> \n\n"},{"id":"425428","messageId":"YKuayK+lmNfBoYWM@danh.dev","threadId":"55603","inReplyTo":"120b6b5c-ab30-6985-d5af-a0b0290b864f@gmail.com","subject":"Re: [PATCH v4 1/4] cache.h: Introduce a generic \"xdg_config_home_for(…)\" function","fromName":"Đoàn Trần Công Danh","fromEmail":"congdanhqx@gmail.com","sentAt":"2021-05-24T12:23:36Z","receivedAt":"2021-05-24T12:23:39Z","isPatch":true,"sender":{"key":"congdanhqx@gmail.com","avatar":"https://avatars.githubusercontent.com/u/42673067?v=4"},"body":"On 2021-05-24 10:33:30+0100, Phillip Wood <phillip.wood123@gmail.com> wrote:\n> Hi Lénaïc\n> \n> This looks fine to me. I'm not 100% sold on calling the parameter prog as\n> our program name later in the series ends up being \"systemd/user\" so\n> something like \"subdir\" might have been better but that is not worth\n> rerolling for.\n\nI'll take the blame for that \"prog\".\nI didn't think very hard at the time of writing :(\n\nYes, \"subdir\" is definitely better.\nAnd it's aligned with the XDG Base Directory specifications:\n\n\tA user-specific version of the configuration file may be\n\tcreated in $XDG_CONFIG_HOME/subdir/filename\n\n> On 24/05/2021 08:15, Lénaïc Huard wrote:\n> > Current implementation of `xdg_config_home(filename)` returns\n> > `$XDG_CONFIG_HOME/git/$filename`, with the `git` subdirectory inserted\n> > between the `XDG_CONFIG_HOME` environment variable and the parameter.\n> > \n> > This patch introduces a `xdg_config_home_for(prog, filename)` function\n> > which is more generic. It only concatenates \"$XDG_CONFIG_HOME\", or\n> > \"$HOME/.config\" if the former isn’t defined, with the parameters,\n> > without adding `git` in between.\n> > \n> > `xdg_config_home(filename)` is now implemented by calling\n> > `xdg_config_home_for(\"git\", filename)` but this new generic function can\n> > be used to compute the configuration directory of other programs.\n\n-- \nDanh\n"},{"id":"425429","messageId":"YKudvp0m3m+Yi1+m@danh.dev","threadId":"55603","inReplyTo":"8e013441-08dc-fbb5-f9b9-649b2ffd78db@gmail.com","subject":"Re: [PATCH v4 2/4] maintenance: introduce ENABLE/DISABLE for code clarity","fromName":"Đoàn Trần Công Danh","fromEmail":"congdanhqx@gmail.com","sentAt":"2021-05-24T12:36:14Z","receivedAt":"2021-05-24T12:36:25Z","isPatch":true,"sender":{"key":"congdanhqx@gmail.com","avatar":"https://avatars.githubusercontent.com/u/42673067?v=4"},"body":"On 2021-05-24 10:41:18+0100, Phillip Wood <phillip.wood123@gmail.com> wrote:\n> Hi Lénaïc\n> \n> On 24/05/2021 08:15, Lénaïc Huard wrote:\n> > The first parameter of `XXX_update_schedule` and alike functions is a\n> > boolean specifying if the tasks should be scheduled or unscheduled.\n> > \n> > Using an `enum` with `ENABLE` and `DISABLE` values can make the code\n> > clearer.\n> \n> I'm sorry to say that I'm not sure this does make the code clearer overall -\n> I wish I'd spoken up when Danh suggested it.\n> While\n> \tlaunchctl_boot_plist(DISABLE, filename, cmd)\n> is arguably clearer than\n> \tlaunchctl_boot_plist(0, filename, cmd)\n> we end up with bizarre tests like\n>  \tif (enabled == ENABLED)\n> rather than\n> \tif (enabled)\n> and in the next patch we have\n> \t(enable == ENABLE && (opts->scheduler == i)) ?\n> \t\t\tENABLE : DISABLE;\n> rather than\n> \tenable && opts->scheduler == i\n> \n> Also looking at the next patch it seems as this one is missing some\n> conversions in maintenance_start() as it is still calling\n> update_background_schedule() with an integer rather than the new enum.\n\nYes, in this form, I also think the change looks bizarre.\nAnd, it's entirely my fault.\n\nI also agree with Ævar that 0 and 1 is meant well for off/on.\n\nHowever, I still think\n\n \tlaunchctl_boot_plist(0, filename, cmd)\n\nwould require some degree on code navigation to figure out what would\nthat LoC does.\n\nI'm thinking about rename the function. But, it would trigger a forever\nbikeshedding, which shouldn't be a blocker for this series.\n\n> I'd be happy to see this being dropped I'm afraid\n\nSo, let's drop this patch and start a new conversation when the dust\nsettled.\n\n-- \nDanh\n"},{"id":"425435","messageId":"xmqqim38jfja.fsf@gitster.g","threadId":"55603","inReplyTo":"87wnrooa17.fsf@evledraar.gmail.com","subject":"Re: [PATCH v3 4/4] maintenance: optionally use systemd timers on Linux","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-05-24T15:51:53Z","receivedAt":"2021-05-24T15:53:58Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n> Personally I don't care whether someone submits a patch where their\n> commit message discusses an example of \"he\", \"she\", \"they\", \"it\" or\n> whatever. It's just meant as an example, and not some statement about\n> what the gender (or lack thereof) of such a user *should* be.\n>\n> It's immediately obvious what the author meant in this case, and that\n> the particular wording is arbitrary. For the purposes of discussing the\n> contribution it matters whether it's unclear or ambiguous, which it's\n> not.\n\nNicely put.  Thanks.\n"},{"id":"425437","messageId":"CAPig+cQSbHM1ph+hg2dkR3fBoxF2pxAh+xYe8to4yGfXOpMLMA@mail.gmail.com","threadId":"55603","inReplyTo":"87r1hwo3e3.fsf@evledraar.gmail.com","subject":"Re: [PATCH v4 4/4] maintenance: add support for systemd timers on Linux","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2021-05-24T16:39:48Z","receivedAt":"2021-05-24T16:40:33Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Mon, May 24, 2021 at 6:03 AM Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n> On Mon, May 24 2021, Lénaïc Huard wrote:\n> > +test_expect_success 'start and stop Linux/systemd maintenance' '\n> > +     write_script print-args <<-\\EOF &&\n> > +     printf \"%s\\n\" \"$*\" >>args\n> > +     EOF\n> > +\n> > +     XDG_CONFIG_HOME=\"$PWD\" &&\n> > +     export XDG_CONFIG_HOME &&\n> > +     rm -f args &&\n>\n> If you're going to care about cleanup here, and personally I wouldn't,\n> just call that \"args\" by the name \"expect\" instead (as is convention)\n> and clobber it every time...\n>\n> Anyway, a better way to do the cleanup is:\n>\n>     test_when_finished \"rm args\" &&\n>     echo this is the first time you write the file >args\n>     [the rest of the test code]\n>\n> Then you don't need to re-rm it.\n\nA few comments:\n\nThis is following an already-established pattern in this test script,\nso it would be unfortunate and out of place to change only these\nnewly-added tests, thus it's probably better to follow the existing\nidiom as is done here. If someone wants to rework all this, it can be\ndone later script-wide and need not be part of this series nor done by\nLénaïc.\n\nThe name `args` is reflective of what is being captured here;\nspecifically, it captures the arguments passed to the system-specific\nscheduler command. It's also the name used by all the other tests, so\nit's probably fine as-is.\n\nThe git-maintenance command is invoked multiple times in a single test\nand `args` needs to be removed before each invocation since its\ncontent is accumulated via `>>` within the `print-args` script, which\nis necessary since that script may be run multiple times by a single\ngit-maintenance command. So, `rm -f args` is not mere cleanup here;\nit's an integral part of the test, thus test_when_finished() would be\nincorrect.\n"},{"id":"425444","messageId":"60abe7fbde461_1b20920897@natae.notmuch","threadId":"55603","inReplyTo":"87wnrooa17.fsf@evledraar.gmail.com","subject":"Re: [PATCH v3 4/4] maintenance: optionally use systemd timers on Linux","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2021-05-24T17:52:59Z","receivedAt":"2021-05-24T17:53:06Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Ævar Arnfjörð Bjarmason wrote:\n> I would like to encourage people in this thread who are calling for a\n> change in wording here to consider whether this sort of discussion is a\n> good use of the ML's time, and the chilling effect of being overly picky\n> when many contributors are working in their second, third etc. language.\n\nI completely agree. However, there's a difference between calling for a\nchange, and expressing one's personal taste. The later can be useful\nfeedback, the former tramples the progress of a patch series.\n\n> Personally I don't care whether someone submits a patch where their\n> commit message discusses an example of \"he\", \"she\", \"they\", \"it\" or\n> whatever. It's just meant as an example, and not some statement about\n> what the gender (or lack thereof) of such a user *should* be.\n\nAgreed. My only concern is readability, nonetheless a poorly readable\ncommit message is not a road-blocker.\n\n\nWhat should not be debatable is that this is a software project, it\nshould not concern itself with linguistic debates. The proper use of\nlanguage is a matter for others to decide.\n\nIf a member of the community expresses her/his personal taste that Y is a\nbetter word than X, that's fine. But to *demand* a word change is\nsomething else.\n\nCheers.\n\n-- \nFelipe Contreras"},{"id":"425446","messageId":"60abebae78f95_1b20920898@natae.notmuch","threadId":"55603","inReplyTo":"20210524071538.46862-5-lenaic@lhuard.fr","subject":"RE: [PATCH v4 4/4] maintenance: add support for systemd timers on Linux","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2021-05-24T18:08:46Z","receivedAt":"2021-05-24T18:08:49Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Lénaïc Huard wrote:\n>   Concretely, a user that doesn’t have access to the system logs won’t\n>   have access to the log of their own tasks scheduled by cron whereas\n>   they will have access to the log of their own tasks scheduled by\n>   systemd timer.\n\nA user is not a person:\n\n  a user that doesn't have access to the system logs won't have access\n  to the log of its own tasks scheduled by cron, whereas it will have\n  access to the log of its own tasks scheduled by systemd timer.\n\nCheers.\n\n-- \nFelipe Contreras"},{"id":"425497","messageId":"9164987.SKD4A0viWJ@coruscant.lhuard.fr","threadId":"55603","inReplyTo":"YKudvp0m3m+Yi1+m@danh.dev","subject":"Re: [PATCH v4 2/4] maintenance: introduce ENABLE/DISABLE for code clarity","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-05-25T07:18:09Z","receivedAt":"2021-05-25T07:18:37Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Le lundi 24 mai 2021, 14:36:14 CEST Đoàn Trần Công Danh a écrit :\n> On 2021-05-24 10:41:18+0100, Phillip Wood <phillip.wood123@gmail.com> wrote:\n> > Hi Lénaïc\n> > \n> > On 24/05/2021 08:15, Lénaïc Huard wrote:\n> > > The first parameter of `XXX_update_schedule` and alike functions is a\n> > > boolean specifying if the tasks should be scheduled or unscheduled.\n> > > \n> > > Using an `enum` with `ENABLE` and `DISABLE` values can make the code\n> > > clearer.\n> > \n> > I'm sorry to say that I'm not sure this does make the code clearer overall\n> > - I wish I'd spoken up when Danh suggested it.\n> > While\n> > \n> > \tlaunchctl_boot_plist(DISABLE, filename, cmd)\n> > \n> > is arguably clearer than\n> > \n> > \tlaunchctl_boot_plist(0, filename, cmd)\n> > \n> > we end up with bizarre tests like\n> > \n> >  \tif (enabled == ENABLED)\n> > \n> > rather than\n> > \n> > \tif (enabled)\n> > \n> > and in the next patch we have\n> > \n> > \t(enable == ENABLE && (opts->scheduler == i)) ?\n> > \t\n> > \t\t\tENABLE : DISABLE;\n> > \n> > rather than\n> > \n> > \tenable && opts->scheduler == i\n> > \n> > Also looking at the next patch it seems as this one is missing some\n> > conversions in maintenance_start() as it is still calling\n> > update_background_schedule() with an integer rather than the new enum.\n> \n> Yes, in this form, I also think the change looks bizarre.\n> And, it's entirely my fault.\n> \n> I also agree with Ævar that 0 and 1 is meant well for off/on.\n> \n> However, I still think\n> \n>  \tlaunchctl_boot_plist(0, filename, cmd)\n> \n> would require some degree on code navigation to figure out what would\n> that LoC does.\n> \n> I'm thinking about rename the function. But, it would trigger a forever\n> bikeshedding, which shouldn't be a blocker for this series.\n> \n> > I'd be happy to see this being dropped I'm afraid\n> \n> So, let's drop this patch and start a new conversation when the dust\n> settled.\n\nHi,\n\nI think the reason why the code looks worse is because I used an enum and I \ndidn’t want to make any assumption about how the enum members would be \nevaluated in a boolean context.\n\nDo you think it would make sense to drop the enum type, to revert all logic \nchanges (Use `if (enabled)` back instead of `switch`, etc.), and to define the \nfollowing constants :\n\nstatic const int DISABLE = 0;\nstatic const int ENABLE = 1;\n\nso that we can keep function invocation in the form of \n`launchctl_boot_plist(DISABLE, filename, cmd)` ?\n\n\n"},{"id":"425501","messageId":"xmqqa6ojfdhd.fsf@gitster.g","threadId":"55603","inReplyTo":"9164987.SKD4A0viWJ@coruscant.lhuard.fr","subject":"Re: [PATCH v4 2/4] maintenance: introduce ENABLE/DISABLE for code clarity","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-05-25T08:02:06Z","receivedAt":"2021-05-25T08:08:14Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Lénaïc Huard <lenaic@lhuard.fr> writes:\n\n> I think the reason why the code looks worse is because I used an enum and I \n> didn’t want to make any assumption about how the enum members would be \n> evaluated in a boolean context.\n>\n> Do you think it would make sense to drop the enum type, to revert all logic \n> changes (Use `if (enabled)` back instead of `switch`, etc.), and to define the \n> following constants :\n>\n> static const int DISABLE = 0;\n> static const int ENABLE = 1;\n>\n> so that we can keep function invocation in the form of \n> `launchctl_boot_plist(DISABLE, filename, cmd)` ?\n\nI think the code is much better off without DISABLE/ENABLE at all.\n\nAs has already been pointed out, you cannot read and write _without_\nbeing aware of the fact that DISABLE is 0 if you want to write\nreadable code, i.e. instead of \"if (able == ENABLE) do this;\", you\nwould want to say \"if (able) do this;\".\n\n"},{"id":"425503","messageId":"nycvar.QRO.7.76.6.2105250327550.57@tvgsbejvaqbjf.bet","threadId":"55603","inReplyTo":"xmqqim38jfja.fsf@gitster.g","subject":"Re: [PATCH v3 4/4] maintenance: optionally use systemd timers on Linux","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2021-05-25T01:50:08Z","receivedAt":"2021-05-25T09:18:00Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi,\n\nOn Tue, 25 May 2021, Junio C Hamano wrote:\n\n> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n>\n> > Personally I don't care whether someone submits a patch where their\n> > commit message discusses an example of \"he\", \"she\", \"they\", \"it\" or\n> > whatever. It's just meant as an example, and not some statement about\n> > what the gender (or lack thereof) of such a user *should* be.\n> >\n> > It's immediately obvious what the author meant in this case, and that\n> > the particular wording is arbitrary. For the purposes of discussing the\n> > contribution it matters whether it's unclear or ambiguous, which it's\n> > not.\n>\n> Nicely put.  Thanks.\n\n_Personally_ I don't care either. Because I am exactly in that group of\nyoung, Caucasian male developers that are so highly overrepresented in our\ncommunity already. I will never have a problem thinking that I don't\nbelong here. I never had that problem.\n\nAnd I believe that you, Ævar, are very much in the same boat. You will\nnever feel as if you don't belong in tech. You're Caucasian, male, and\nlike me, come with an abundance of confidence.\n\nNow, let's go for a little thought experiment. Let's pretend for a moment\nthat we lacked that confidence. That we were trying to enter a community\nwith almost no male members at all. Where an email thread was going on\nabout inviting us to join, using no male pronouns, or putting them last.\nAnd remember: no confidence, no representation. Now, would you dare\nchiming in, offering myself as a target? I know I wouldn't. Yet that's\nexactly the atmosphere we're fostering here.\n\nWhat you say, matters. _How_ you say it, matters.\n\nIn other words, I think that the _personal_ opinions of everybody who\nspoke up in this mail thread (you might have noticed that all of us are\nmale, you could even call it a \"male thread\") are not the problem we are\ndiscussing. Personal opinions are kind of missing the point here. By a\nmile. And then some.\n\nThe actual point is that we want to avoid giving the impression that\nonly people who feel included by the pronoun \"he\" are invited. That we\nonly really care about male users and developers, and pay only scant\ntribute to the rest.\n\nAnd yes, even \"he/she\", or \"(s)he\" would give that impression, by\nemphasizing a priority order. And \"they\" simply would not do that. And if\nit makes a few male readers slightly uncomfortable, it might present a\nfine opportunity to exercise some empathy with those who feel\nuncomfortable and excluded all the time.\n\nThank you,\nJohannes\n"},{"id":"425521","messageId":"60acdbe31e9e8_2257a20838@natae.notmuch","threadId":"55603","inReplyTo":"nycvar.QRO.7.76.6.2105250327550.57@tvgsbejvaqbjf.bet","subject":"Re: [PATCH v3 4/4] maintenance: optionally use systemd timers on Linux","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2021-05-25T11:13:39Z","receivedAt":"2021-05-25T11:13:54Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Johannes Schindelin wrote:\n\n> Where an email thread was going on about inviting us to join, using no\n> male pronouns, or putting them last.\n\nThis is totally and completely missing the point. Nobody objected to\nusing a female pronoun, nor using she/he.\n\nI for one would have no problem using a female pronoun, and in fact I\noften do [1].\n\nThis has absolutely nothing to do the issue at hand.\n\n> I know I wouldn't. Yet that's exactly the atmosphere we're fostering\n> here.\n\nExcept this is disconnected from reality.\n\n 1. I know female colleagues who don't want to be treated more\n    delicately than their male counterparts. And they certainly don't\n    want unsolicited help, especially regarding hypothetical feelings\n    they might have towards the use of a pronoun.\n\n 2. On the other hand I do know people that lurk the mailing list and\n    are afraid of expressing their opinions because of fear of reprisals\n    in this PC-culture climate.\n\nIf anybody needs consideration I would side with the real, rather than\nhypothetical, people.\n\n[1] https://lore.kernel.org/git/60a66b11d6ffd_2448320885@natae.notmuch/\n\n-- \nFelipe Contreras\n"},{"id":"425559","messageId":"78148258-8b9c-7b88-54ee-ae899ce6af69@gmail.com","threadId":"55603","inReplyTo":"20210524071538.46862-5-lenaic@lhuard.fr","subject":"Re: [PATCH v4 4/4] maintenance: add support for systemd timers on Linux","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2021-05-26T10:26:01Z","receivedAt":"2021-05-26T10:26:08Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Lénaïc\n\nOn 24/05/2021 08:15, Lénaïc Huard wrote:\n> The existing mechanism for scheduling background maintenance is done\n> through cron. On Linux systems managed by systemd, systemd provides an\n> alternative to schedule recurring tasks: systemd timers.\n> \n> The main motivations to implement systemd timers in addition to cron\n> are:\n> * cron is optional and Linux systems running systemd might not have it\n>    installed.\n> * The execution of `crontab -l` can tell us if cron is installed but not\n>    if the daemon is actually running.\n> * With systemd, each service is run in its own cgroup and its logs are\n>    tagged by the service inside journald. With cron, all scheduled tasks\n>    are running in the cron daemon cgroup and all the logs of the\n>    user-scheduled tasks are pretended to belong to the system cron\n>    service.\n>    Concretely, a user that doesn’t have access to the system logs won’t\n>    have access to the log of their own tasks scheduled by cron whereas\n>    they will have access to the log of their own tasks scheduled by\n>    systemd timer.\n>    Although `cron` attempts to send email, that email may go unseen by\n>    the user because these days, local mailboxes are not heavily used\n>    anymore.\n> \n> In order to schedule git maintenance, we need two unit template files:\n> * ~/.config/systemd/user/git-maintenance@.service\n>    to define the command to be started by systemd and\n> * ~/.config/systemd/user/git-maintenance@.timer\n>    to define the schedule at which the command should be run.\n> \n> Those units are templates that are parameterized by the frequency.\n> \n> Based on those templates, 3 timers are started:\n> * git-maintenance@hourly.timer\n> * git-maintenance@daily.timer\n> * git-maintenance@weekly.timer\n> \n> The command launched by those three timers are the same as with the\n> other scheduling methods:\n> \n> /path/to/git for-each-repo --exec-path=/path/to\n> --config=maintenance.repo maintenance run --schedule=%i\n> \n> with the full path for git to ensure that the version of git launched\n> for the scheduled maintenance is the same as the one used to run\n> `maintenance start`.\n> \n> The timer unit contains `Persistent=true` so that, if the computer is\n> powered down when a maintenance task should run, the task will be run\n> when the computer is back powered on.\n\nThe commit message is comprehensive and does a good job of explaining \nthe patch I do not think there is any need to reword it - I find using \n'they' rather than 'it' to refer to a user is more readable.\n\n> Signed-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n> ---\n>   Documentation/git-maintenance.txt |  57 +++++++++-\n>   builtin/gc.c                      | 180 ++++++++++++++++++++++++++++++\n>   t/t7900-maintenance.sh            |  66 ++++++++++-\n>   3 files changed, 293 insertions(+), 10 deletions(-)\n> \n> diff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\n> index 7c4bb38a2f..50179e010f 100644\n> --- a/Documentation/git-maintenance.txt\n> +++ b/Documentation/git-maintenance.txt\n> @@ -181,16 +181,19 @@ OPTIONS\n>   \t`maintenance.<task>.enabled` configured as `true` are considered.\n>   \tSee the 'TASKS' section for the list of accepted `<task>` values.\n>   \n> ---scheduler=auto|crontab|launchctl|schtasks::\n> +--scheduler=auto|crontab|systemd-timer|launchctl|schtasks::\n>   \tWhen combined with the `start` subcommand, specify the scheduler\n>   \tto use to run the hourly, daily and weekly executions of\n>   \t`git maintenance run`.\n>   \tThe possible values for `<scheduler>` depend on the system: `crontab`\n> -\tis available on POSIX systems, `launchctl` is available on\n> -\tMacOS and `schtasks` is available on Windows.\n> +\tis available on POSIX systems, `systemd-timer` is available on Linux\n> +\tsystems, `launchctl` is available on MacOS and `schtasks` is available\n> +\ton Windows.\n>   \tBy default or when `auto` is specified, a suitable scheduler for\n>   \tthe system is used. On MacOS, `launchctl` is used. On Windows,\n> -\t`schtasks` is used. On all other systems, `crontab` is used.\n> +\t`schtasks` is used. On Linux, `systemd-timer` is used if user systemd\n> +\ttimers are available, otherwise, `crontab` is used. On all other systems,\n> +\t`crontab` is used.\n>   \n>   \n>   TROUBLESHOOTING\n> @@ -290,6 +293,52 @@ schedule to ensure you are executing the correct binaries in your\n>   schedule.\n>   \n>   \n> +BACKGROUND MAINTENANCE ON LINUX SYSTEMD SYSTEMS\n> +-----------------------------------------------\n> +\n> +While Linux supports `cron`, depending on the distribution, `cron` may\n> +be an optional package not necessarily installed. On modern Linux\n> +distributions, systemd timers are superseding it.\n> +\n> +If user systemd timers are available, they will be used as a replacement\n> +of `cron`.\n> +\n> +In this case, `git maintenance start` will create user systemd timer units\n> +and start the timers. The current list of user-scheduled tasks can be found\n> +by running `systemctl --user list-timers`. The timers written by `git\n> +maintenance start` are similar to this:\n> +\n> +-----------------------------------------------------------------------\n> +$ systemctl --user list-timers\n> +NEXT                         LEFT          LAST                         PASSED     UNIT                         ACTIVATES\n> +Thu 2021-04-29 19:00:00 CEST 42min left    Thu 2021-04-29 18:00:11 CEST 17min ago  git-maintenance@hourly.timer git-maintenance@hourly.service\n> +Fri 2021-04-30 00:00:00 CEST 5h 42min left Thu 2021-04-29 00:00:11 CEST 18h ago    git-maintenance@daily.timer  git-maintenance@daily.service\n> +Mon 2021-05-03 00:00:00 CEST 3 days left   Mon 2021-04-26 00:00:11 CEST 3 days ago git-maintenance@weekly.timer git-maintenance@weekly.service\n> +-----------------------------------------------------------------------\n> +\n> +One timer is registered for each `--schedule=<frequency>` option.\n> +\n> +The definition of the systemd units can be inspected in the following files:\n> +\n> +-----------------------------------------------------------------------\n> +~/.config/systemd/user/git-maintenance@.timer\n> +~/.config/systemd/user/git-maintenance@.service\n> +~/.config/systemd/user/timers.target.wants/git-maintenance@hourly.timer\n> +~/.config/systemd/user/timers.target.wants/git-maintenance@daily.timer\n> +~/.config/systemd/user/timers.target.wants/git-maintenance@weekly.timer\n> +-----------------------------------------------------------------------\n> +\n> +`git maintenance start` will overwrite these files and start the timer\n> +again with `systemctl --user`, so any customization should be done by\n> +creating a drop-in file, i.e. a `.conf` suffixed file in the\n> +`~/.config/systemd/user/git-maintenance@.service.d` directory.\n> +\n> +`git maintenance stop` will stop the user systemd timers and delete\n> +the above mentioned files.\n> +\n> +For more details, see `systemd.timer(5)`.\n> +\n> +\n>   BACKGROUND MAINTENANCE ON MACOS SYSTEMS\n>   ---------------------------------------\n>   \n> diff --git a/builtin/gc.c b/builtin/gc.c\n> index bf21cec059..3eca1e5e6a 100644\n> --- a/builtin/gc.c\n> +++ b/builtin/gc.c\n> @@ -2079,10 +2079,173 @@ static int crontab_update_schedule(enum enable_or_disable run_maintenance,\n>   \treturn result;\n>   }\n>   \n> +static int is_systemd_timer_available(void)\n> +{\n> +\tconst char *cmd = \"systemctl\";\n> +\tint is_available;\n> +\tstatic int cached_result = -1;\n> +#ifdef __linux__\n> +\tstruct child_process child = CHILD_PROCESS_INIT;\n> +#endif\n> +\n> +\tif (cached_result != -1)\n> +\t\treturn cached_result;\n> +\n> +\tif (get_schedule_cmd(&cmd, &is_available))\n> +\t\treturn is_available;\n> +\n> +#ifdef __linux__\n> +\tstrvec_split(&child.args, cmd);\n> +\tstrvec_pushl(&child.args, \"--user\", \"list-timers\", NULL);\n> +\tchild.no_stdin = 1;\n> +\tchild.no_stdout = 1;\n> +\tchild.no_stderr = 1;\n> +\tchild.silent_exec_failure = 1;\n> +\n> +\tif (start_command(&child)) {\n> +\t\tcached_result = 0;\n> +\t\treturn cached_result;\n> +\t}\n> +\tif (finish_command(&child)) {\n> +\t\tcached_result = 0;\n> +\t\treturn cached_result;\n> +\t}\n> +\tcached_result = 1;\n> +\treturn cached_result;\n> +#else\n> +\treturn 0;\n> +#endif\n> +}\n\nI agree with Ævar that this would be more readable with two separate \ndefinitions rather chosen by a single #ifdef. I'm dubious that we need \nthe caching as well.\n\n> +static char *xdg_config_home_systemd(const char *filename)\n> +{\n> +\treturn xdg_config_home_for(\"systemd/user\", filename);\n> +}\n> +\n> +static int systemd_timer_enable_unit(int enable,\n> +\t\t\t\t     enum schedule_priority schedule)\n> +{\n> +\tconst char *cmd = \"systemctl\";\n> +\tstruct child_process child = CHILD_PROCESS_INIT;\n> +\tconst char *frequency = get_frequency(schedule);\n> +\n> +\tget_schedule_cmd(&cmd, NULL);\n> +\tstrvec_split(&child.args, cmd);\n> +\tstrvec_pushl(&child.args, \"--user\", enable ? \"enable\" : \"disable\",\n> +\t\t     \"--now\", NULL);\n> +\tstrvec_pushf(&child.args, \"git-maintenance@%s.timer\", frequency);\n> +\n> +\tif (start_command(&child))\n> +\t\tdie(_(\"failed to run systemctl\"));\n> +\treturn finish_command(&child);\n\nI left some comments about the combination of dying and returning an \nerror when reading v2[1]. If you disagree with a reviewers comments \nthat's fine but please try to comment on the reviewers suggestions so \nthat they know their suggestions weren't just forgotten by accident. \nLooking through the patch this applies to a number of comments I left \nabout the error handling (or lack of it) and cleanup. I don't think I \nhave anything new to add.\n\nBest Wishes\n\nPhillip\n\n[1] \nhttps://lore.kernel.org/git/3fd17223-8667-24be-2e65-f1970d411bdf@gmail.com/\n\n> +}\n> +\n> +static int systemd_timer_delete_unit_templates(void)\n> +{\n> +\tchar *filename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n> +\tunlink(filename);\n> +\tfree(filename);\n> +\n> +\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n> +\tunlink(filename);\n> +\tfree(filename);\n> +\n> +\treturn 0;\n> +}\n> +\n> +static int systemd_timer_delete_units(void)\n> +{\n> +\treturn systemd_timer_enable_unit(DISABLE, SCHEDULE_HOURLY) ||\n> +\t       systemd_timer_enable_unit(DISABLE, SCHEDULE_DAILY) ||\n> +\t       systemd_timer_enable_unit(DISABLE, SCHEDULE_WEEKLY) ||\n> +\t       systemd_timer_delete_unit_templates();\n> +}\n> +\n> +static int systemd_timer_write_unit_templates(const char *exec_path)\n> +{\n> +\tchar *filename;\n> +\tFILE *file;\n> +\tconst char *unit;\n> +\n> +\tfilename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n> +\tif (safe_create_leading_directories(filename))\n> +\t\tdie(_(\"failed to create directories for '%s'\"), filename);\n> +\tfile = xfopen(filename, \"w\");\n> +\tFREE_AND_NULL(filename);\n> +\n> +\tunit = \"# This file was created and is maintained by Git.\\n\"\n> +\t       \"# Any edits made in this file might be replaced in the future\\n\"\n> +\t       \"# by a Git command.\\n\"\n> +\t       \"\\n\"\n> +\t       \"[Unit]\\n\"\n> +\t       \"Description=Optimize Git repositories data\\n\"\n> +\t       \"\\n\"\n> +\t       \"[Timer]\\n\"\n> +\t       \"OnCalendar=%i\\n\"\n> +\t       \"Persistent=true\\n\"\n> +\t       \"\\n\"\n> +\t       \"[Install]\\n\"\n> +\t       \"WantedBy=timers.target\\n\";\n> +\tfputs(unit, file);\n> +\tfclose(file);\n> +\n> +\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n> +\tfile = xfopen(filename, \"w\");\n> +\tfree(filename);\n> +\n> +\tunit = \"# This file was created and is maintained by Git.\\n\"\n> +\t       \"# Any edits made in this file might be replaced in the future\\n\"\n> +\t       \"# by a Git command.\\n\"\n> +\t       \"\\n\"\n> +\t       \"[Unit]\\n\"\n> +\t       \"Description=Optimize Git repositories data\\n\"\n> +\t       \"\\n\"\n> +\t       \"[Service]\\n\"\n> +\t       \"Type=oneshot\\n\"\n> +\t       \"ExecStart=\\\"%s/git\\\" --exec-path=\\\"%s\\\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\\n\"\n> +\t       \"LockPersonality=yes\\n\"\n> +\t       \"MemoryDenyWriteExecute=yes\\n\"\n> +\t       \"NoNewPrivileges=yes\\n\"\n> +\t       \"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\\n\"\n> +\t       \"RestrictNamespaces=yes\\n\"\n> +\t       \"RestrictRealtime=yes\\n\"\n> +\t       \"RestrictSUIDSGID=yes\\n\"\n> +\t       \"SystemCallArchitectures=native\\n\"\n> +\t       \"SystemCallFilter=@system-service\\n\";\n> +\tfprintf(file, unit, exec_path, exec_path);\n> +\tfclose(file);\n> +\n> +\treturn 0;\n> +}\n> +\n> +static int systemd_timer_setup_units(void)\n> +{\n> +\tconst char *exec_path = git_exec_path();\n> +\n> +\treturn systemd_timer_write_unit_templates(exec_path) ||\n> +\t       systemd_timer_enable_unit(ENABLE, SCHEDULE_HOURLY) ||\n> +\t       systemd_timer_enable_unit(ENABLE, SCHEDULE_DAILY) ||\n> +\t       systemd_timer_enable_unit(ENABLE, SCHEDULE_WEEKLY);\n> +}\n> +\n> +static int systemd_timer_update_schedule(enum enable_or_disable run_maintenance,\n> +\t\t\t\t\t int fd)\n> +{\n> +\tswitch (run_maintenance) {\n> +\tcase ENABLE:\n> +\t\treturn systemd_timer_setup_units();\n> +\tcase DISABLE:\n> +\t\treturn systemd_timer_delete_units();\n> +\tdefault:\n> +\t\tBUG(\"invalid enable_or_disable value\");\n> +\t}\n> +}\n> +\n>   enum scheduler {\n>   \tSCHEDULER_INVALID = -1,\n>   \tSCHEDULER_AUTO,\n>   \tSCHEDULER_CRON,\n> +\tSCHEDULER_SYSTEMD,\n>   \tSCHEDULER_LAUNCHCTL,\n>   \tSCHEDULER_SCHTASKS,\n>   };\n> @@ -2097,6 +2260,11 @@ static const struct {\n>   \t\t.is_available = is_crontab_available,\n>   \t\t.update_schedule = crontab_update_schedule,\n>   \t},\n> +\t[SCHEDULER_SYSTEMD] = {\n> +\t\t.name = \"systemctl\",\n> +\t\t.is_available = is_systemd_timer_available,\n> +\t\t.update_schedule = systemd_timer_update_schedule,\n> +\t},\n>   \t[SCHEDULER_LAUNCHCTL] = {\n>   \t\t.name = \"launchctl\",\n>   \t\t.is_available = is_launchctl_available,\n> @@ -2117,6 +2285,9 @@ static enum scheduler parse_scheduler(const char *value)\n>   \t\treturn SCHEDULER_AUTO;\n>   \telse if (!strcasecmp(value, \"cron\") || !strcasecmp(value, \"crontab\"))\n>   \t\treturn SCHEDULER_CRON;\n> +\telse if (!strcasecmp(value, \"systemd\") ||\n> +\t\t !strcasecmp(value, \"systemd-timer\"))\n> +\t\treturn SCHEDULER_SYSTEMD;\n>   \telse if (!strcasecmp(value, \"launchctl\"))\n>   \t\treturn SCHEDULER_LAUNCHCTL;\n>   \telse if (!strcasecmp(value, \"schtasks\"))\n> @@ -2155,6 +2326,15 @@ static void resolve_auto_scheduler(enum scheduler *scheduler)\n>   \t*scheduler = SCHEDULER_SCHTASKS;\n>   \treturn;\n>   \n> +#elif defined(__linux__)\n> +\tif (is_systemd_timer_available())\n> +\t\t*scheduler = SCHEDULER_SYSTEMD;\n> +\telse if (is_crontab_available())\n> +\t\t*scheduler = SCHEDULER_CRON;\n> +\telse\n> +\t\tdie(_(\"neither systemd timers nor crontab are available\"));\n> +\treturn;\n> +\n>   #else\n>   \t*scheduler = SCHEDULER_CRON;\n>   \treturn;\n> diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\n> index 9eac260307..c8a6f19ebc 100755\n> --- a/t/t7900-maintenance.sh\n> +++ b/t/t7900-maintenance.sh\n> @@ -20,6 +20,18 @@ test_xmllint () {\n>   \tfi\n>   }\n>   \n> +test_lazy_prereq SYSTEMD_ANALYZE '\n> +\tsystemd-analyze --help >out &&\n> +\tgrep verify out\n> +'\n> +\n> +test_systemd_analyze_verify () {\n> +\tif test_have_prereq SYSTEMD_ANALYZE\n> +\tthen\n> +\t\tsystemd-analyze verify \"$@\"\n> +\tfi\n> +}\n> +\n>   test_expect_success 'help text' '\n>   \ttest_expect_code 129 git maintenance -h 2>err &&\n>   \ttest_i18ngrep \"usage: git maintenance <subcommand>\" err &&\n> @@ -628,14 +640,54 @@ test_expect_success 'start and stop Windows maintenance' '\n>   \ttest_cmp expect args\n>   '\n>   \n> +test_expect_success 'start and stop Linux/systemd maintenance' '\n> +\twrite_script print-args <<-\\EOF &&\n> +\tprintf \"%s\\n\" \"$*\" >>args\n> +\tEOF\n> +\n> +\tXDG_CONFIG_HOME=\"$PWD\" &&\n> +\texport XDG_CONFIG_HOME &&\n> +\trm -f args &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args\" git maintenance start --scheduler=systemd-timer &&\n> +\n> +\t# start registers the repo\n> +\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n> +\n> +\ttest_systemd_analyze_verify \"systemd/user/git-maintenance@.service\" &&\n> +\n> +\tprintf -- \"--user enable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n> +\ttest_cmp expect args &&\n> +\n> +\trm -f args &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args\" git maintenance stop &&\n> +\n> +\t# stop does not unregister the repo\n> +\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n> +\n> +\ttest_path_is_missing \"systemd/user/git-maintenance@.timer\" &&\n> +\ttest_path_is_missing \"systemd/user/git-maintenance@.service\" &&\n> +\n> +\tprintf -- \"--user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n> +\ttest_cmp expect args\n> +'\n> +\n>   test_expect_success 'start and stop when several schedulers are available' '\n>   \twrite_script print-args <<-\\EOF &&\n>   \tprintf \"%s\\n\" \"$*\" | sed \"s:gui/[0-9][0-9]*:gui/[UID]:; s:\\(schtasks /create .* /xml\\).*:\\1:;\" >>args\n>   \tEOF\n>   \n>   \trm -f args &&\n> -\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n> -\trm -f expect &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=systemd-timer &&\n> +\tprintf -- \"systemctl --user enable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n> +\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n> +\t\thourly daily weekly >>expect &&\n> +\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n> +\t\thourly daily weekly >>expect &&\n> +\ttest_cmp expect args &&\n> +\n> +\trm -f args &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n> +\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n>   \tfor frequency in hourly daily weekly\n>   \tdo\n>   \t\tPLIST=\"$pfx/Library/LaunchAgents/org.git-scm.git.$frequency.plist\" &&\n> @@ -647,17 +699,19 @@ test_expect_success 'start and stop when several schedulers are available' '\n>   \ttest_cmp expect args &&\n>   \n>   \trm -f args &&\n> -\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n> +\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n>   \tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n> -\t\thourly daily weekly >expect &&\n> +\t\thourly daily weekly >>expect &&\n>   \tprintf \"schtasks /create /tn Git Maintenance (%s) /f /xml\\n\" \\\n>   \t\thourly daily weekly >>expect &&\n>   \ttest_cmp expect args &&\n>   \n>   \trm -f args &&\n> -\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n> +\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n>   \tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n> -\t\thourly daily weekly >expect &&\n> +\t\thourly daily weekly >>expect &&\n>   \tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n>   \t\thourly daily weekly >>expect &&\n>   \ttest_cmp expect args\n> \n\n"},{"id":"425563","messageId":"87mtshn3vj.fsf@evledraar.gmail.com","threadId":"55603","inReplyTo":"nycvar.QRO.7.76.6.2105250327550.57@tvgsbejvaqbjf.bet","subject":"CoC, inclusivity etc. (was \"Re: [...] systemd timers on Linux\")","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-26T10:29:01Z","receivedAt":"2021-05-26T11:14:46Z","isPatch":false,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Tue, May 25 2021, Johannes Schindelin wrote:\n\n> Hi,\n>\n> On Tue, 25 May 2021, Junio C Hamano wrote:\n>\n>> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n>>\n>> > Personally I don't care whether someone submits a patch where their\n>> > commit message discusses an example of \"he\", \"she\", \"they\", \"it\" or\n>> > whatever. It's just meant as an example, and not some statement about\n>> > what the gender (or lack thereof) of such a user *should* be.\n>> >\n>> > It's immediately obvious what the author meant in this case, and that\n>> > the particular wording is arbitrary. For the purposes of discussing the\n>> > contribution it matters whether it's unclear or ambiguous, which it's\n>> > not.\n>>\n>> Nicely put.  Thanks.\n>\n> _Personally_ I don't care either. Because I am exactly in that group of\n> young, Caucasian male developers that are so highly overrepresented in our\n> community already. I will never have a problem thinking that I don't\n> belong here. I never had that problem.\n>\n> And I believe that you, Ævar, are very much in the same boat. You will\n> never feel as if you don't belong in tech. You're Caucasian, male, and\n> like me, come with an abundance of confidence.\n>\n> Now, let's go for a little thought experiment. Let's pretend for a moment\n> that we lacked that confidence. That we were trying to enter a community\n> with almost no male members at all. Where an email thread was going on\n> about inviting us to join, using no male pronouns, or putting them last.\n> And remember: no confidence, no representation. Now, would you dare\n> chiming in, offering myself as a target? I know I wouldn't. Yet that's\n> exactly the atmosphere we're fostering here.\n>\n> What you say, matters. _How_ you say it, matters.\n>\n> In other words, I think that the _personal_ opinions of everybody who\n> spoke up in this mail thread (you might have noticed that all of us are\n> male, you could even call it a \"male thread\") are not the problem we are\n> discussing. Personal opinions are kind of missing the point here. By a\n> mile. And then some.\n>\n> The actual point is that we want to avoid giving the impression that\n> only people who feel included by the pronoun \"he\" are invited. That we\n> only really care about male users and developers, and pay only scant\n> tribute to the rest.\n>\n> And yes, even \"he/she\", or \"(s)he\" would give that impression, by\n> emphasizing a priority order. And \"they\" simply would not do that. And if\n> it makes a few male readers slightly uncomfortable, it might present a\n> fine opportunity to exercise some empathy with those who feel\n> uncomfortable and excluded all the time.\n>\n> Thank you,\n\nI don't think it's helpful or in the spirit of our CoC to characterize\ndiscussion in terms of the genders or nationalities of its participants.\n\nI'm not accusing you of some CoC violation, just suggesting that it\nmakes more better conversation to take participants at their word and\nassume good faith.\n\nYou can't say based on some superficial piece of information what a\nproject participant might be dealing with, e.g. perhaps they're anxious\nabout screwing up some trivial thing on a public ML, feel that they're\ninexperienced and/or have some \"impostor syndrome\" etc.\n\nThe reason I chimed in on this thread was that I thought concern over\none such topic had started to negatively impact another. We've got a lot\nof people trying to contribute who aren't comfortable contributing in\nEnglish, or whose proficiency doesn't extend to the latest linguistic\ntrends.\n\nI'm suggesting that it's more helpful to leave certain things be than to\npounce on contributors about things that are ultimately not integral to\ntheir work, and which can be readily understood.\n\nE.g. if someone's clearly a speaker of a Slavic language and it shows in\ntheir grammar I don't think we should be aiming to have them massage\ntheir patch until it's indistinguishable from that of a native speaker\nof English.\n\nWe should make sure we understand what's being said[1], but I think\nanything past that point is starting to bring us negative value. At some\npoint it's good enough that we can understand the intent, and by\nassuming good faith we're better off letting things like \"he\" v.s. \"she\"\nv.s. \"they\" slide.\n\nTo not do so is to needlessly create a barrier to participation in this\nproject. I think the side discussion about Merriam Webster[2] somewhat\nmisses the mark here.\n\nThe question isn't whether what's being suggested would be a valid\nsentence according to a dictionary, but whether e.g. someone who's\nlearned English from a commonly available textbook (I daresay most\n\"Beginner English\" textbooks have more \"he\" and \"she\" examples than\n\"they\") is going to inadvertently run afoul of other people's\npreferences.\n\nIf they do it's more work for them (partially because they're a\nnon-native speaker), and ultimately more things we need to cover in\nDocumentation/SubmittingPatches etc. All of that's more barriers of\nentry to project participation.\n\n1. https://lore.kernel.org/git/87wns6u8pc.fsf@evledraar.gmail.com/\n2. https://lore.kernel.org/git/YKrk4dEjEm6+48ji@camp.crustytoothpaste.net/\n"},{"id":"425574","messageId":"60ae71c829ca2_31f20812@natae.notmuch","threadId":"55603","inReplyTo":"87mtshn3vj.fsf@evledraar.gmail.com","subject":"RE: CoC, inclusivity etc. (was \"Re: [...] systemd timers on Linux\")","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2021-05-26T16:05:28Z","receivedAt":"2021-05-26T16:05:38Z","isPatch":false,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Ævar Arnfjörð Bjarmason wrote:\n> On Tue, May 25 2021, Johannes Schindelin wrote:\n\n> > _Personally_ I don't care either. Because I am exactly in that group of\n> > young, Caucasian male developers that are so highly overrepresented in our\n> > community already. I will never have a problem thinking that I don't\n> > belong here. I never had that problem.\n> >\n> > And I believe that you, Ævar, are very much in the same boat. You will\n> > never feel as if you don't belong in tech. You're Caucasian, male, and\n> > like me, come with an abundance of confidence.\n> \n> I don't think it's helpful or in the spirit of our CoC to characterize\n> discussion in terms of the genders or nationalities of its participants.\n\nNot to mention that it's worryingly close to implying \"if you are a straight\nwhite cis male, your opinion doesn't matter\".\n\nI don't think the Git project should be assigning weight to the opinions\nof community members based on gender, or race.\n\n-- \nFelipe Contreras"},{"id":"425678","messageId":"YK+rmNWh+jPais9P@coredump.intra.peff.net","threadId":"55603","inReplyTo":"87mtshn3vj.fsf@evledraar.gmail.com","subject":"Re: CoC, inclusivity etc. (was \"Re: [...] systemd timers on Linux\")","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2021-05-27T14:24:24Z","receivedAt":"2021-05-27T14:24:27Z","isPatch":false,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Wed, May 26, 2021 at 12:29:01PM +0200, Ævar Arnfjörð Bjarmason wrote:\n\n> The reason I chimed in on this thread was that I thought concern over\n> one such topic had started to negatively impact another. We've got a lot\n> of people trying to contribute who aren't comfortable contributing in\n> English, or whose proficiency doesn't extend to the latest linguistic\n> trends.\n> \n> I'm suggesting that it's more helpful to leave certain things be than to\n> pounce on contributors about things that are ultimately not integral to\n> their work, and which can be readily understood.\n\nYes, I want to express my support of this point, and not just for this\nparticular issue.\n\nIf you're a new contributor (or even an old one), it can be frustrating\nto spend a lot of time working on and polishing up an improvement to the\nsoftware, to be met with feedback that mostly consists of \"there's a\ntypo in your commit message\". Whether that's true or not, or whether it\nimproves the commit message or not, it can feel like reviewers are\nmissing the point of the patch, which will discourage contributors.\n\nAs reviewers, I think we can do a few things to encourage people,\nespecially new contributors:\n\n  - let little errors slide if they're not important. I think sometimes\n    we get into a mentality that the commit message is baked into\n    history, and thus needs to be revised and perfected. But commit\n    messages are also just a conversation that's happening and being\n    recorded. There will be hiccups, and polishing them forever has\n    diminishing returns.\n\n    (Of course this requires judgement; some commit messages really are\n    just hard to follow, and I think you made that distinction with the\n    phrase \"make sure understand what's being said\").\n\n  - temper small corrections with positive feedback. Especially for new\n    contributors, being told explicitly \"yes, what you're trying to do\n    here overall is welcome, and it all looks good except for this...\"\n    is much more encouraging than \"this part is wrong\". In the latter,\n    they're left to guess if anybody even values the rest of the work at\n    all.\n\n  - likewise, I think it helps to give feedback on expectations for the\n    process. Saying explicitly \"this looks good; I think with this style\n    change, it would be ready to get picked up\" helps them understand\n    that the fix will get them across the finish line (as opposed to\n    just getting another round of fix requests).\n\nI would even extend some of those into the code itself. Obviously we\ndon't want to lower the bar and take incorrect code, or even typos in\nerror messages. But I think we could stand to relax sometimes on issues\nof style or \"I would do it like this\" (and at the very least, the\n\"temper small corrections\" advice may apply).\n\nI'm not really targeting anybody in particular in this thread (and\nLénaïc seems to have taken it all in stride in this case). It's more\njust a reminder that it's easy to forget to do these kinds of things,\nand keep this kind of perspective. I know I have not always done it\nperfectly at times.\n\n-Peff\n"},{"id":"425697","messageId":"60afd72defc6b_2653020886@natae.notmuch","threadId":"55603","inReplyTo":"YK+rmNWh+jPais9P@coredump.intra.peff.net","subject":"Re: CoC, inclusivity etc. (was \"Re: [...] systemd timers on Linux\")","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2021-05-27T17:30:21Z","receivedAt":"2021-05-27T17:30:29Z","isPatch":false,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Jeff King wrote:\n> On Wed, May 26, 2021 at 12:29:01PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> > The reason I chimed in on this thread was that I thought concern over\n> > one such topic had started to negatively impact another. We've got a lot\n> > of people trying to contribute who aren't comfortable contributing in\n> > English, or whose proficiency doesn't extend to the latest linguistic\n> > trends.\n> > \n> > I'm suggesting that it's more helpful to leave certain things be than to\n> > pounce on contributors about things that are ultimately not integral to\n> > their work, and which can be readily understood.\n> \n> Yes, I want to express my support of this point, and not just for this\n> particular issue.\n> \n> If you're a new contributor (or even an old one), it can be frustrating\n> to spend a lot of time working on and polishing up an improvement to the\n> software, to be met with feedback that mostly consists of \"there's a\n> typo in your commit message\". Whether that's true or not, or whether it\n> improves the commit message or not, it can feel like reviewers are\n> missing the point of the patch, which will discourage contributors.\n> \n> As reviewers, I think we can do a few things to encourage people,\n> especially new contributors:\n> \n>   - let little errors slide if they're not important. I think sometimes\n>     we get into a mentality that the commit message is baked into\n>     history, and thus needs to be revised and perfected. But commit\n>     messages are also just a conversation that's happening and being\n>     recorded. There will be hiccups, and polishing them forever has\n>     diminishing returns.\n\nYes, polishing forever has diminishing returns, but that's not really\nthe hard problem here; the problem is when two people (or more) can't\nagree on what \"polishing\" means. That's a more eternal forever.\n\nFortunately we do have a dictator, and he can make determinations to end\nthe eternal debate.\n\nHe can simply fix the commit message--or any other minor\ndetails--himself.\n\nSure, that doesn't scale, and ideally we would want the patch\ncontributors to do these tasks themselves, and not burden the\nmaintainer. But if the task of herding the cats is taken forever, why\nkeep insisting? Just fix the commit message and move on.\n\n> I would even extend some of those into the code itself. Obviously we\n> don't want to lower the bar and take incorrect code, or even typos in\n> error messages. But I think we could stand to relax sometimes on issues\n> of style or \"I would do it like this\" (and at the very least, the\n> \"temper small corrections\" advice may apply).\n\nPlus code can be fixed later. If as a reviewer you really dislike one\naspect of a patch, you don't necessarily need to convince the author to\nchange it. You can propose a separate patch later on.\n\n\nSometimes good is good enough. Don't let perfect be the enemy of good.\n\nCheers.\n\n-- \nFelipe Contreras"},{"id":"425719","messageId":"xmqqsg2791b0.fsf@gitster.g","threadId":"55603","inReplyTo":"YK+rmNWh+jPais9P@coredump.intra.peff.net","subject":"Re: CoC, inclusivity etc. (was \"Re: [...] systemd timers on Linux\")","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-05-27T23:58:27Z","receivedAt":"2021-05-27T23:58:43Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> I'm not really targeting anybody in particular in this thread (and\n> Lénaïc seems to have taken it all in stride in this case). It's more\n> just a reminder that it's easy to forget to do these kinds of things,\n> and keep this kind of perspective. I know I have not always done it\n> perfectly at times.\n\nThanks.\n"},{"id":"425804","messageId":"87h7imaowt.fsf@vps.thesusis.net","threadId":"55603","inReplyTo":"YK+rmNWh+jPais9P@coredump.intra.peff.net","subject":"Re: CoC, inclusivity etc. (was \"Re: [...] systemd timers on Linux\")","fromName":"Phillip Susi","fromEmail":"phill@thesusis.net","sentAt":"2021-05-28T14:44:25Z","receivedAt":"2021-05-28T14:55:33Z","isPatch":false,"sender":{"key":"phill@thesusis.net","avatar":null},"body":"\nJeff King writes:\n\n>   - temper small corrections with positive feedback. Especially for new\n>     contributors, being told explicitly \"yes, what you're trying to do\n>     here overall is welcome, and it all looks good except for this...\"\n>     is much more encouraging than \"this part is wrong\". In the latter,\n>     they're left to guess if anybody even values the rest of the work at\n>     all.\n\nWhen I see only a minor nit like that I assume that by default, that\nmeans there are no more serious issues, fix the typo, and resubmit.  If\na new contributor thinks that means they aren't welcome then I think\nthey have an expectation mismatch.\n\n>   - likewise, I think it helps to give feedback on expectations for the\n>     process. Saying explicitly \"this looks good; I think with this style\n>     change, it would be ready to get picked up\" helps them understand\n>     that the fix will get them across the finish line (as opposed to\n>     just getting another round of fix requests).\n\nThat would be nice, but such comments can really only come from a\nmaintainer that plans on pushing the patch.  Most comments come from\nbystanders and so nessesarily only consist of pointing out flaws, and\ndon't really need to be bloated with a bunch of fluff.  I prefer short,\nand to the point communication.\n\n> I would even extend some of those into the code itself. Obviously we\n> don't want to lower the bar and take incorrect code, or even typos in\n> error messages. But I think we could stand to relax sometimes on issues\n> of style or \"I would do it like this\" (and at the very least, the\n> \"temper small corrections\" advice may apply).\n\nIsn't saying \"I would do it like this\" already a tempering statement?  I\ntake that as meaning there isn't anything neccesarily wrong with what\nyou did, but you might consider this advice.\n\n"},{"id":"425905","messageId":"2953592.SyOBHIlnlS@coruscant.lhuard.fr","threadId":"55603","inReplyTo":"ce0e096d-cd82-800a-9ef5-5bcc4b25046d@gmail.com","subject":"Re: [PATCH v4 3/4] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-05-30T06:39:48Z","receivedAt":"2021-05-30T06:40:06Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Hello Phillip,\n\nI’m working on the next iteration of this patch, but I would have a question \nabout one comment of the review.\n\nLe lundi 24 mai 2021, 12:12:10 CEST Phillip Wood a écrit :\n> > -static int maintenance_start(void)\n> > +static const char *const builtin_maintenance_start_usage[] = {\n> > +\tN_(\"git maintenance start [--scheduler=<scheduler>]\"), NULL\n> > +};\n> \n> I'm not sure what the { } and NULL are doing here, it should just be\n> assigning a string. You could put it inside maintenance_start() and just\n> call the variable \"usage\"\n\nI did this because `parse_options(…, usage, …)` expects a NULL-terminated \narray of strings and not a single string.\n\nWhile I agree that this variable doesn’t need to be global as it is used only \nfrom inside a single function, I followed the pattern that I saw elsewhere.\n\nFor ex. `builting_gc_usage` or `builtin_maintenance_run_usage` in `builtin/\ngc.c`.\n\nIn fact, we’re even told to do so in `Documentation/technical/api-parse-\noptions.txt` which says:\n\n    . define a NULL-terminated\n      `static const char * const builtin_foo_usage[]` array\n      containing alternative usage strings\n\nIn the files that I looked at, the command usage was always defined as a global \nlong-named variable even if it was used in a single function.\n\n> > +static int maintenance_start(int argc, const char **argv, const char\n> > *prefix)> \n> >   {\n> > \n> > +\tstruct maintenance_start_opts opts;\n> > +\tstruct option builtin_maintenance_start_options[] = {\n> \n> As this variable is local to the function you could call it something\n> shorter like options.\n\nI agree.\nI also followed the pattern that I saw elsewhere.\nFor ex., still in `builtin/gc.c`, there are `builtin_gc_options` and \n`builtin_maintenance_run_options` which are local variables, but still defined \nwith an explicit and unique long name.\n\nSo, I’m wondering if I should follow the existing pattern or if I should \nshorten the name of the local variable.\nI thought the existing convention could be useful when grepping for option or \nusage of a command in the code ?\n\n\n"},{"id":"425910","messageId":"295e8a2c-913f-ec53-0865-d3153c54b9cc@gmail.com","threadId":"55603","inReplyTo":"2953592.SyOBHIlnlS@coruscant.lhuard.fr","subject":"Re: [PATCH v4 3/4] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2021-05-30T10:16:36Z","receivedAt":"2021-05-30T10:18:55Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Lénaïc\n\nOn 30/05/2021 07:39, Lénaïc Huard wrote:\n> Hello Phillip,\n> \n> I’m working on the next iteration of this patch, but I would have a question\n> about one comment of the review.\n> \n> Le lundi 24 mai 2021, 12:12:10 CEST Phillip Wood a écrit :\n>>> -static int maintenance_start(void)\n>>> +static const char *const builtin_maintenance_start_usage[] = {\n>>> +\tN_(\"git maintenance start [--scheduler=<scheduler>]\"), NULL\n>>> +};\n>>\n>> I'm not sure what the { } and NULL are doing here, it should just be\n>> assigning a string. You could put it inside maintenance_start() and just\n>> call the variable \"usage\"\n> \n> I did this because `parse_options(…, usage, …)` expects a NULL-terminated\n> array of strings and not a single string.\n\nOh sorry I got confused by the definition of builtin_maintenance_usage \nwhich is just a string but cmd_maintenance() uses usage() rather than \nparse_options() which has a different api.\n\n> While I agree that this variable doesn’t need to be global as it is used only\n> from inside a single function, I followed the pattern that I saw elsewhere.\n> \n> For ex. `builting_gc_usage` or `builtin_maintenance_run_usage` in `builtin/\n> gc.c`.\n> \n> In fact, we’re even told to do so in `Documentation/technical/api-parse-\n> options.txt` which says:\n> \n>      . define a NULL-terminated\n>        `static const char * const builtin_foo_usage[]` array\n>        containing alternative usage strings\n> \n> In the files that I looked at, the command usage was always defined as a global\n> long-named variable even if it was used in a single function.\n\nYeah, I'm not sure why that convention has built up. If there is a \nsingle command in a file then defining the usage string at the top of \nthe file arguably acts as some form of documentation but when there is \nmore than one command in a file I'm not so sure. Anyway feel free to \nleave it as it is.\n\n>>> +static int maintenance_start(int argc, const char **argv, const char\n>>> *prefix)>\n>>>    {\n>>>\n>>> +\tstruct maintenance_start_opts opts;\n>>> +\tstruct option builtin_maintenance_start_options[] = {\n>>\n>> As this variable is local to the function you could call it something\n>> shorter like options.\n> \n> I agree.\n> I also followed the pattern that I saw elsewhere.\n> For ex., still in `builtin/gc.c`, there are `builtin_gc_options` and\n> `builtin_maintenance_run_options` which are local variables, but still defined\n> with an explicit and unique long name.\n> \n> So, I’m wondering if I should follow the existing pattern or if I should\n> shorten the name of the local variable.\n> I thought the existing convention could be useful when grepping for option or\n> usage of a command in the code ?\n\nIt's up to you, personally I'd lean towards a shorter name defined in \nthe function where it is used but if you're following a existing pattern \nthen that should be fine too and would mean that you don't need to spend \ntime changing what you've got already.\n\nBest Wishes\n\nPhillip\n"},{"id":"425944","messageId":"YLQKd5wzN5iESxvQ@coredump.intra.peff.net","threadId":"55603","inReplyTo":"87h7imaowt.fsf@vps.thesusis.net","subject":"Re: CoC, inclusivity etc. (was \"Re: [...] systemd timers on Linux\")","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2021-05-30T21:58:15Z","receivedAt":"2021-05-30T21:58:20Z","isPatch":false,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Fri, May 28, 2021 at 10:44:25AM -0400, Phillip Susi wrote:\n\n> >   - temper small corrections with positive feedback. Especially for new\n> >     contributors, being told explicitly \"yes, what you're trying to do\n> >     here overall is welcome, and it all looks good except for this...\"\n> >     is much more encouraging than \"this part is wrong\". In the latter,\n> >     they're left to guess if anybody even values the rest of the work at\n> >     all.\n> \n> When I see only a minor nit like that I assume that by default, that\n> means there are no more serious issues, fix the typo, and resubmit.  If\n> a new contributor thinks that means they aren't welcome then I think\n> they have an expectation mismatch.\n\nSure, that may be your intent as a reviewer. My point was that the\nrecipient of the review does not always know that. Helping them\nunderstand those expectations is part of welcoming them into the\ncommunity.\n\nAnd even as somebody who has been part of the community for a long time\nand understands that, it is still comforting to get actual positive\nfeedback, rather than an assumed \"if I did not complain, it is OK\".\n\n> >   - likewise, I think it helps to give feedback on expectations for the\n> >     process. Saying explicitly \"this looks good; I think with this style\n> >     change, it would be ready to get picked up\" helps them understand\n> >     that the fix will get them across the finish line (as opposed to\n> >     just getting another round of fix requests).\n> \n> That would be nice, but such comments can really only come from a\n> maintainer that plans on pushing the patch.  Most comments come from\n> bystanders and so nessesarily only consist of pointing out flaws, and\n> don't really need to be bloated with a bunch of fluff.  I prefer short,\n> and to the point communication.\n\nYes, I hesitated a little bit on this advice for that reason: it may be\neven worse to mislead people about the state of a patch series, if you\nthe reviewer and the maintainer do not agree. IMHO it is still good for\nreviewers to try to help manage newcomers through the process, but it\ndoes make sense for them to be careful not to over-promise.\n\n> > I would even extend some of those into the code itself. Obviously we\n> > don't want to lower the bar and take incorrect code, or even typos in\n> > error messages. But I think we could stand to relax sometimes on issues\n> > of style or \"I would do it like this\" (and at the very least, the\n> > \"temper small corrections\" advice may apply).\n> \n> Isn't saying \"I would do it like this\" already a tempering statement?  I\n> take that as meaning there isn't anything neccesarily wrong with what\n> you did, but you might consider this advice.\n\nHere I more meant cases where the two approaches have about the same\nvalue. If your \"I would do it like this\" can be backed up with reasons\nwhy it might be better (more efficient, more maintainable, and so on),\nthen that's probably helpful review to give. If it can't, then I'd\nquestion whether it is worth the time to even bring up.\n\nThere's a big gray area, of course. Saying \"it would be more readable\nlike this...\" is sometimes a nuisance, and sometimes great advice. One\nextra complication is that new contributors are often unsure how strong\nthe request is (e.g., if they disagree, do they _need_ to change it for\nthe patch to be accepted, or is it OK). I'll often qualify comments with\nan explicit \"I'm OK with doing it this way, but in case you really like this\nother direction, I thought I'd mention it...\".\n\nAnother complication with all of this advice is that sometimes new\ncontributors are in a mentoring relationship with one or more reviewers\n(e.g., GSoC, Outreachy, or just people who have asked for help). And\nthere the cost/benefit tradeoff is different between frustrating a new\ncontributor and teaching them our style, norms, etc.\n\n-Peff\n"},{"id":"426771","messageId":"20210608134000.663398-4-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210608134000.663398-1-lenaic@lhuard.fr","subject":"[PATCH v5 3/3] maintenance: add support for systemd timers on Linux","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-06-08T13:40:00Z","receivedAt":"2021-06-08T13:41:22Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"The existing mechanism for scheduling background maintenance is done\nthrough cron. On Linux systems managed by systemd, systemd provides an\nalternative to schedule recurring tasks: systemd timers.\n\nThe main motivations to implement systemd timers in addition to cron\nare:\n* cron is optional and Linux systems running systemd might not have it\n  installed.\n* The execution of `crontab -l` can tell us if cron is installed but not\n  if the daemon is actually running.\n* With systemd, each service is run in its own cgroup and its logs are\n  tagged by the service inside journald. With cron, all scheduled tasks\n  are running in the cron daemon cgroup and all the logs of the\n  user-scheduled tasks are pretended to belong to the system cron\n  service.\n  Concretely, a user that doesn’t have access to the system logs won’t\n  have access to the log of their own tasks scheduled by cron whereas\n  they will have access to the log of their own tasks scheduled by\n  systemd timer.\n  Although `cron` attempts to send email, that email may go unseen by\n  the user because these days, local mailboxes are not heavily used\n  anymore.\n\nIn order to schedule git maintenance, we need two unit template files:\n* ~/.config/systemd/user/git-maintenance@.service\n  to define the command to be started by systemd and\n* ~/.config/systemd/user/git-maintenance@.timer\n  to define the schedule at which the command should be run.\n\nThose units are templates that are parameterized by the frequency.\n\nBased on those templates, 3 timers are started:\n* git-maintenance@hourly.timer\n* git-maintenance@daily.timer\n* git-maintenance@weekly.timer\n\nThe command launched by those three timers are the same as with the\nother scheduling methods:\n\n/path/to/git for-each-repo --exec-path=/path/to\n--config=maintenance.repo maintenance run --schedule=%i\n\nwith the full path for git to ensure that the version of git launched\nfor the scheduled maintenance is the same as the one used to run\n`maintenance start`.\n\nThe timer unit contains `Persistent=true` so that, if the computer is\npowered down when a maintenance task should run, the task will be run\nwhen the computer is back powered on.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n Documentation/git-maintenance.txt |  57 +++++++-\n builtin/gc.c                      | 228 ++++++++++++++++++++++++++++++\n t/t7900-maintenance.sh            |  67 ++++++++-\n 3 files changed, 342 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\nindex 07065ed4f3..7b7dbbbca9 100644\n--- a/Documentation/git-maintenance.txt\n+++ b/Documentation/git-maintenance.txt\n@@ -179,16 +179,19 @@ OPTIONS\n \t`maintenance.<task>.enabled` configured as `true` are considered.\n \tSee the 'TASKS' section for the list of accepted `<task>` values.\n \n---scheduler=auto|crontab|launchctl|schtasks::\n+--scheduler=auto|crontab|systemd-timer|launchctl|schtasks::\n \tWhen combined with the `start` subcommand, specify the scheduler\n \tto use to run the hourly, daily and weekly executions of\n \t`git maintenance run`.\n \tThe possible values for `<scheduler>` depend on the system: `crontab`\n-\tis available on POSIX systems, `launchctl` is available on\n-\tMacOS and `schtasks` is available on Windows.\n+\tis available on POSIX systems, `systemd-timer` is available on Linux\n+\tsystems, `launchctl` is available on MacOS and `schtasks` is available\n+\ton Windows.\n \tBy default or when `auto` is specified, a suitable scheduler for\n \tthe system is used. On MacOS, `launchctl` is used. On Windows,\n-\t`schtasks` is used. On all other systems, `crontab` is used.\n+\t`schtasks` is used. On Linux, `systemd-timer` is used if user systemd\n+\ttimers are available, otherwise, `crontab` is used. On all other systems,\n+\t`crontab` is used.\n \n \n TROUBLESHOOTING\n@@ -288,6 +291,52 @@ schedule to ensure you are executing the correct binaries in your\n schedule.\n \n \n+BACKGROUND MAINTENANCE ON LINUX SYSTEMD SYSTEMS\n+-----------------------------------------------\n+\n+While Linux supports `cron`, depending on the distribution, `cron` may\n+be an optional package not necessarily installed. On modern Linux\n+distributions, systemd timers are superseding it.\n+\n+If user systemd timers are available, they will be used as a replacement\n+of `cron`.\n+\n+In this case, `git maintenance start` will create user systemd timer units\n+and start the timers. The current list of user-scheduled tasks can be found\n+by running `systemctl --user list-timers`. The timers written by `git\n+maintenance start` are similar to this:\n+\n+-----------------------------------------------------------------------\n+$ systemctl --user list-timers\n+NEXT                         LEFT          LAST                         PASSED     UNIT                         ACTIVATES\n+Thu 2021-04-29 19:00:00 CEST 42min left    Thu 2021-04-29 18:00:11 CEST 17min ago  git-maintenance@hourly.timer git-maintenance@hourly.service\n+Fri 2021-04-30 00:00:00 CEST 5h 42min left Thu 2021-04-29 00:00:11 CEST 18h ago    git-maintenance@daily.timer  git-maintenance@daily.service\n+Mon 2021-05-03 00:00:00 CEST 3 days left   Mon 2021-04-26 00:00:11 CEST 3 days ago git-maintenance@weekly.timer git-maintenance@weekly.service\n+-----------------------------------------------------------------------\n+\n+One timer is registered for each `--schedule=<frequency>` option.\n+\n+The definition of the systemd units can be inspected in the following files:\n+\n+-----------------------------------------------------------------------\n+~/.config/systemd/user/git-maintenance@.timer\n+~/.config/systemd/user/git-maintenance@.service\n+~/.config/systemd/user/timers.target.wants/git-maintenance@hourly.timer\n+~/.config/systemd/user/timers.target.wants/git-maintenance@daily.timer\n+~/.config/systemd/user/timers.target.wants/git-maintenance@weekly.timer\n+-----------------------------------------------------------------------\n+\n+`git maintenance start` will overwrite these files and start the timer\n+again with `systemctl --user`, so any customization should be done by\n+creating a drop-in file, i.e. a `.conf` suffixed file in the\n+`~/.config/systemd/user/git-maintenance@.service.d` directory.\n+\n+`git maintenance stop` will stop the user systemd timers and delete\n+the above mentioned files.\n+\n+For more details, see `systemd.timer(5)`.\n+\n+\n BACKGROUND MAINTENANCE ON MACOS SYSTEMS\n ---------------------------------------\n \ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex f2a81ecb44..5fe416c903 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -2040,10 +2040,221 @@ static int crontab_update_schedule(int run_maintenance, int fd)\n \treturn result;\n }\n \n+#ifdef __linux__\n+\n+static int real_is_systemd_timer_available(void)\n+{\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\n+\tstrvec_pushl(&child.args, \"systemctl\", \"--user\", \"list-timers\", NULL);\n+\tchild.no_stdin = 1;\n+\tchild.no_stdout = 1;\n+\tchild.no_stderr = 1;\n+\tchild.silent_exec_failure = 1;\n+\n+\tif (start_command(&child))\n+\t\treturn 0;\n+\tif (finish_command(&child))\n+\t\treturn 0;\n+\treturn 1;\n+}\n+\n+#else\n+\n+static int real_is_systemd_timer_available(void)\n+{\n+\treturn 0;\n+}\n+\n+#endif\n+\n+static int is_systemd_timer_available(void)\n+{\n+\tconst char *cmd = \"systemctl\";\n+\tint is_available;\n+\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+\treturn real_is_systemd_timer_available();\n+}\n+\n+static char *xdg_config_home_systemd(const char *filename)\n+{\n+\treturn xdg_config_home_for(\"systemd/user\", filename);\n+}\n+\n+static int systemd_timer_enable_unit(int enable,\n+\t\t\t\t     enum schedule_priority schedule)\n+{\n+\tconst char *cmd = \"systemctl\";\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\tconst char *frequency = get_frequency(schedule);\n+\n+\t/*\n+\t * Disabling the systemd unit while it is already disabled makes\n+\t * systemctl print an error.\n+\t * Let's ignore it since it means we already are in the expected state:\n+\t * the unit is disabled.\n+\t *\n+\t * On the other hand, enabling a systemd unit which is already enabled\n+\t * produces no error.\n+\t */\n+\tif (!enable)\n+\t\tchild.no_stderr = 1;\n+\n+\tget_schedule_cmd(&cmd, NULL);\n+\tstrvec_split(&child.args, cmd);\n+\tstrvec_pushl(&child.args, \"--user\", enable ? \"enable\" : \"disable\",\n+\t\t     \"--now\", NULL);\n+\tstrvec_pushf(&child.args, \"git-maintenance@%s.timer\", frequency);\n+\n+\tif (start_command(&child))\n+\t\treturn error(_(\"failed to start systemctl\"));\n+\tif (finish_command(&child))\n+\t\t/*\n+\t\t * Disabling an already disabled systemd unit makes\n+\t\t * systemctl fail.\n+\t\t * Let's ignore this failure.\n+\t\t *\n+\t\t * Enabling an enabled systemd unit doesn't fail.\n+\t\t */\n+\t\tif (enable)\n+\t\t\treturn error(_(\"failed to run systemctl\"));\n+\treturn 0;\n+}\n+\n+static int systemd_timer_delete_unit_templates(void)\n+{\n+\tint ret = 0;\n+\tchar *filename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n+\tif(unlink(filename) && !is_missing_file_error(errno))\n+\t\tret = error_errno(_(\"failed to delete '%s'\"), filename);\n+\tFREE_AND_NULL(filename);\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n+\tif(unlink(filename) && !is_missing_file_error(errno))\n+\t\tret = error_errno(_(\"failed to delete '%s'\"), filename);\n+\n+\tfree(filename);\n+\treturn ret;\n+}\n+\n+static int systemd_timer_delete_units(void)\n+{\n+\treturn systemd_timer_enable_unit(0, SCHEDULE_HOURLY) ||\n+\t       systemd_timer_enable_unit(0, SCHEDULE_DAILY) ||\n+\t       systemd_timer_enable_unit(0, SCHEDULE_WEEKLY) ||\n+\t       systemd_timer_delete_unit_templates();\n+}\n+\n+static int systemd_timer_write_unit_templates(const char *exec_path)\n+{\n+\tchar *filename;\n+\tFILE *file;\n+\tconst char *unit;\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n+\tif (safe_create_leading_directories(filename)) {\n+\t\terror(_(\"failed to create directories for '%s'\"), filename);\n+\t\tgoto error;\n+\t}\n+\tfile = fopen_or_warn(filename, \"w\");\n+\tif (file == NULL)\n+\t\tgoto error;\n+\tFREE_AND_NULL(filename);\n+\n+\tunit = \"# This file was created and is maintained by Git.\\n\"\n+\t       \"# Any edits made in this file might be replaced in the future\\n\"\n+\t       \"# by a Git command.\\n\"\n+\t       \"\\n\"\n+\t       \"[Unit]\\n\"\n+\t       \"Description=Optimize Git repositories data\\n\"\n+\t       \"\\n\"\n+\t       \"[Timer]\\n\"\n+\t       \"OnCalendar=%i\\n\"\n+\t       \"Persistent=true\\n\"\n+\t       \"\\n\"\n+\t       \"[Install]\\n\"\n+\t       \"WantedBy=timers.target\\n\";\n+\tif (fputs(unit, file) == EOF) {\n+\t\terror(_(\"failed to write to '%s'\"), filename);\n+\t\tfclose(file);\n+\t\tgoto error;\n+\t}\n+\tif (fclose(file) == EOF) {\n+\t\terror_errno(_(\"failed to flush '%s'\"), filename);\n+\t\tgoto error;\n+\t}\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n+\tfile = fopen_or_warn(filename, \"w\");\n+\tif (file == NULL)\n+\t\tgoto error;\n+\tFREE_AND_NULL(filename);\n+\n+\tunit = \"# This file was created and is maintained by Git.\\n\"\n+\t       \"# Any edits made in this file might be replaced in the future\\n\"\n+\t       \"# by a Git command.\\n\"\n+\t       \"\\n\"\n+\t       \"[Unit]\\n\"\n+\t       \"Description=Optimize Git repositories data\\n\"\n+\t       \"\\n\"\n+\t       \"[Service]\\n\"\n+\t       \"Type=oneshot\\n\"\n+\t       \"ExecStart=\\\"%s/git\\\" --exec-path=\\\"%s\\\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\\n\"\n+\t       \"LockPersonality=yes\\n\"\n+\t       \"MemoryDenyWriteExecute=yes\\n\"\n+\t       \"NoNewPrivileges=yes\\n\"\n+\t       \"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\\n\"\n+\t       \"RestrictNamespaces=yes\\n\"\n+\t       \"RestrictRealtime=yes\\n\"\n+\t       \"RestrictSUIDSGID=yes\\n\"\n+\t       \"SystemCallArchitectures=native\\n\"\n+\t       \"SystemCallFilter=@system-service\\n\";\n+\tif (fprintf(file, unit, exec_path, exec_path) < 0) {\n+\t\terror(_(\"failed to write to '%s'\"), filename);\n+\t\tfclose(file);\n+\t\tgoto error;\n+\t}\n+\tif (fclose(file) == EOF) {\n+\t\terror_errno(_(\"failed to flush '%s'\"), filename);\n+\t\tgoto error;\n+\t}\n+\treturn 0;\n+\n+error:\n+\tfree(filename);\n+\tsystemd_timer_delete_unit_templates();\n+\treturn -1;\n+}\n+\n+static int systemd_timer_setup_units(void)\n+{\n+\tconst char *exec_path = git_exec_path();\n+\n+\tint ret = systemd_timer_write_unit_templates(exec_path) ||\n+\t          systemd_timer_enable_unit(1, SCHEDULE_HOURLY) ||\n+\t          systemd_timer_enable_unit(1, SCHEDULE_DAILY) ||\n+\t          systemd_timer_enable_unit(1, SCHEDULE_WEEKLY);\n+\tif (ret)\n+\t\tsystemd_timer_delete_units();\n+\treturn ret;\n+}\n+\n+static int systemd_timer_update_schedule(int run_maintenance, int fd)\n+{\n+\tif (run_maintenance)\n+\t\treturn systemd_timer_setup_units();\n+\telse\n+\t\treturn systemd_timer_delete_units();\n+}\n+\n enum scheduler {\n \tSCHEDULER_INVALID = -1,\n \tSCHEDULER_AUTO,\n \tSCHEDULER_CRON,\n+\tSCHEDULER_SYSTEMD,\n \tSCHEDULER_LAUNCHCTL,\n \tSCHEDULER_SCHTASKS,\n };\n@@ -2058,6 +2269,11 @@ static const struct {\n \t\t.is_available = is_crontab_available,\n \t\t.update_schedule = crontab_update_schedule,\n \t},\n+\t[SCHEDULER_SYSTEMD] = {\n+\t\t.name = \"systemctl\",\n+\t\t.is_available = is_systemd_timer_available,\n+\t\t.update_schedule = systemd_timer_update_schedule,\n+\t},\n \t[SCHEDULER_LAUNCHCTL] = {\n \t\t.name = \"launchctl\",\n \t\t.is_available = is_launchctl_available,\n@@ -2078,6 +2294,9 @@ static enum scheduler parse_scheduler(const char *value)\n \t\treturn SCHEDULER_AUTO;\n \telse if (!strcasecmp(value, \"cron\") || !strcasecmp(value, \"crontab\"))\n \t\treturn SCHEDULER_CRON;\n+\telse if (!strcasecmp(value, \"systemd\") ||\n+\t\t !strcasecmp(value, \"systemd-timer\"))\n+\t\treturn SCHEDULER_SYSTEMD;\n \telse if (!strcasecmp(value, \"launchctl\"))\n \t\treturn SCHEDULER_LAUNCHCTL;\n \telse if (!strcasecmp(value, \"schtasks\"))\n@@ -2116,6 +2335,15 @@ static void resolve_auto_scheduler(enum scheduler *scheduler)\n \t*scheduler = SCHEDULER_SCHTASKS;\n \treturn;\n \n+#elif defined(__linux__)\n+\tif (is_systemd_timer_available())\n+\t\t*scheduler = SCHEDULER_SYSTEMD;\n+\telse if (is_crontab_available())\n+\t\t*scheduler = SCHEDULER_CRON;\n+\telse\n+\t\tdie(_(\"neither systemd timers nor crontab are available\"));\n+\treturn;\n+\n #else\n \t*scheduler = SCHEDULER_CRON;\n \treturn;\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex b36b7f5fb0..b289cae6b9 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -20,6 +20,18 @@ test_xmllint () {\n \tfi\n }\n \n+test_lazy_prereq SYSTEMD_ANALYZE '\n+\tsystemd-analyze --help >out &&\n+\tgrep verify out\n+'\n+\n+test_systemd_analyze_verify () {\n+\tif test_have_prereq SYSTEMD_ANALYZE\n+\tthen\n+\t\tsystemd-analyze verify \"$@\"\n+\tfi\n+}\n+\n test_expect_success 'help text' '\n \ttest_expect_code 129 git maintenance -h 2>err &&\n \ttest_i18ngrep \"usage: git maintenance <subcommand>\" err &&\n@@ -634,15 +646,56 @@ test_expect_success 'start and stop Windows maintenance' '\n \ttest_cmp expect args\n '\n \n+test_expect_success 'start and stop Linux/systemd maintenance' '\n+\twrite_script print-args <<-\\EOF &&\n+\tprintf \"%s\\n\" \"$*\" >>args\n+\tEOF\n+\n+\tXDG_CONFIG_HOME=\"$PWD\" &&\n+\texport XDG_CONFIG_HOME &&\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args\" git maintenance start --scheduler=systemd-timer &&\n+\n+\t# start registers the repo\n+\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n+\n+\ttest_systemd_analyze_verify \"systemd/user/git-maintenance@.service\" &&\n+\n+\tprintf -- \"--user enable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args\" git maintenance stop &&\n+\n+\t# stop does not unregister the repo\n+\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n+\n+\ttest_path_is_missing \"systemd/user/git-maintenance@.timer\" &&\n+\ttest_path_is_missing \"systemd/user/git-maintenance@.service\" &&\n+\n+\tprintf -- \"--user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n+\ttest_cmp expect args\n+'\n+\n test_expect_success 'start and stop when several schedulers are available' '\n \twrite_script print-args <<-\\EOF &&\n \tprintf \"%s\\n\" \"$*\" | sed \"s:gui/[0-9][0-9]*:gui/[UID]:; s:\\(schtasks /create .* /xml\\).*:\\1:;\" >>args\n \tEOF\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n-\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=systemd-timer &&\n+\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n \t\thourly daily weekly >expect &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\tprintf -- \"systemctl --user enable --now git-maintenance@%s.timer\\n\" hourly daily weekly >>expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n+\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >>expect &&\n \tfor frequency in hourly daily weekly\n \tdo\n \t\tPLIST=\"$pfx/Library/LaunchAgents/org.git-scm.git.$frequency.plist\" &&\n@@ -652,17 +705,19 @@ test_expect_success 'start and stop when several schedulers are available' '\n \ttest_cmp expect args &&\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n+\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n \tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n-\t\thourly daily weekly >expect &&\n+\t\thourly daily weekly >>expect &&\n \tprintf \"schtasks /create /tn Git Maintenance (%s) /f /xml\\n\" \\\n \t\thourly daily weekly >>expect &&\n \ttest_cmp expect args &&\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n+\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n \tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n-\t\thourly daily weekly >expect &&\n+\t\thourly daily weekly >>expect &&\n \tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n \t\thourly daily weekly >>expect &&\n \ttest_cmp expect args\n-- \n2.32.0\n\n"},{"id":"426772","messageId":"20210608134000.663398-1-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210524071538.46862-1-lenaic@lhuard.fr","subject":"[PATCH v5 0/3] add support for systemd timers on Linux","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-06-08T13:39:57Z","receivedAt":"2021-06-08T13:41:44Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Hello,\n\nI’ve reworked this submission based on the valuable feedback I’ve received.\nThanks again for it!\n\nThe patchset contains now the following patches:\n\n* cache.h: Introduce a generic \"xdg_config_home_for(…)\" function\n\n  This patch introduces a function to compute configuration files\n  paths inside $XDG_CONFIG_HOME or ~/.config for other programs than\n  git itself.\n  It is used in the latest patch of this series to compute systemd\n  unit files location.\n\n  The only change in this patch compared to its previous version is\n  the renaming of the first parameter of the `xdg_config_home_for(…)`\n  function from `prog` to `subdir`.\n\n* maintenance: introduce ENABLE/DISABLE for code clarity\n\n  I just completely dropped this patch as it turned out that replacing\n  some 0/1 values by `ENABLE`/`DISABLE` enum values wasn’t making the\n  code look nicer as initially expected.\n\n* maintenance: `git maintenance run` learned `--scheduler=<scheduler>`\n\n  This patch contains all the code that is related to the addition of\n  the new `--scheduler` parameter of the `git maintenance start`\n  command, independently of the systemd timers.\n\n  The main changes in this patch compared to its previous version are:\n\n    * Revert all the changes that were previously introduced by the\n      `ENABLE`/`DISABLE` enum values.\n\n    * Remove the `strlcpy` in the testing framework inside the\n      `get_schedule_cmd` function.\n\n    * `update_background_schedule` loops over all the available\n      schedulers, disables all of them except the one which is\n      enabled.\n      In this new version of the patch, it is now ensured that all the\n      schedulers deactivation are done before the activation.\n      The goal of this change is avoid a potential race condition\n      where two schedulers could be enabled at the same time.\n      This behaviour change has been reflected in the tests.\n\n    * The local variable `builtin_maintenance_start_options` has been\n      shortened.\n\n* maintenance: add support for systemd timers on Linux\n\n  This patch implements the support of systemd timers on top of\n  crontab scheduler on Linux systems.\n\n  The main changes in this patch compared to its previous version are:\n\n    * The caching logic of `is_systemd_timer_available` has been\n      dropped.\n      I initially wanted to cache the outcome of forking and executing\n      an external command to avoid doing it several times as\n      `is_systemd_timer_available` is invoked from several places\n      (`resolve_auto_scheduler`, `validate_scheduler` and\n      `update_background_scheduler`).\n      But it’s true they’re not always all called.\n      In the case of `maintenance stop`, `resolve_auto_scheduler` and\n      `validate_scheduler` are not called.\n      In the case of `maintenance start`, the `if (enable &&\n      opts->scheduler == i)` statement inside\n      `update_background_schedule` skips the execution of\n      `is_systemd_timer_available`.\n\n    * The `is_systemd_timer_available` has been split in two parts:\n      * `is_systemd_timer_available` is the entry point and holds the\n        platform agnostic testing framework logic.\n      * `real_is_systemd_timer_available` contains the platform\n        specific logic.\n\n    * The error management of `systemd_timer_write_unit_templates` has\n      been reviewed.\n      The return code of `fopen`, `fputs`, `fclose`, etc. are now\n      checked.\n      If this function manages to write one file, but fails at writing\n      the second one, it will attempt to delete the first one to not\n      leave the system in an inconsistent state.\n\n    * The error management of `systemd_timer_delete_unit_templates`\n      has also been reviewed. The error code of `unlink` is now\n      checked.\n\nI hope I’ve addressed all your valuable feedback. Do not hesitate to\nlet me know if I’ve forgotten anything.\n\nLénaïc Huard (3):\n  cache.h: Introduce a generic \"xdg_config_home_for(…)\" function\n  maintenance: `git maintenance run` learned `--scheduler=<scheduler>`\n  maintenance: add support for systemd timers on Linux\n\n Documentation/git-maintenance.txt |  60 ++++\n builtin/gc.c                      | 564 ++++++++++++++++++++++++++----\n cache.h                           |   7 +\n path.c                            |  13 +-\n t/t7900-maintenance.sh            | 110 +++++-\n 5 files changed, 676 insertions(+), 78 deletions(-)\n\n-- \n2.32.0\n\n"},{"id":"426773","messageId":"20210608134000.663398-2-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210608134000.663398-1-lenaic@lhuard.fr","subject":"[PATCH v5 1/3] cache.h: Introduce a generic \"xdg_config_home_for(…)\" function","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-06-08T13:39:58Z","receivedAt":"2021-06-08T13:41:45Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Current implementation of `xdg_config_home(filename)` returns\n`$XDG_CONFIG_HOME/git/$filename`, with the `git` subdirectory inserted\nbetween the `XDG_CONFIG_HOME` environment variable and the parameter.\n\nThis patch introduces a `xdg_config_home_for(subdir, filename)` function\nwhich is more generic. It only concatenates \"$XDG_CONFIG_HOME\", or\n\"$HOME/.config\" if the former isn’t defined, with the parameters,\nwithout adding `git` in between.\n\n`xdg_config_home(filename)` is now implemented by calling\n`xdg_config_home_for(\"git\", filename)` but this new generic function can\nbe used to compute the configuration directory of other programs.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n cache.h |  7 +++++++\n path.c  | 13 ++++++++++---\n 2 files changed, 17 insertions(+), 3 deletions(-)\n\ndiff --git a/cache.h b/cache.h\nindex ba04ff8bd3..2a0fb3e4ba 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -1286,6 +1286,13 @@ int is_ntfs_dotmailmap(const char *name);\n  */\n int looks_like_command_line_option(const char *str);\n \n+/**\n+ * Return a newly allocated string with the evaluation of\n+ * \"$XDG_CONFIG_HOME/$subdir/$filename\" if $XDG_CONFIG_HOME is non-empty, otherwise\n+ * \"$HOME/.config/$subdir/$filename\". Return NULL upon error.\n+ */\n+char *xdg_config_home_for(const char *subdir, const char *filename);\n+\n /**\n  * Return a newly allocated string with the evaluation of\n  * \"$XDG_CONFIG_HOME/git/$filename\" if $XDG_CONFIG_HOME is non-empty, otherwise\ndiff --git a/path.c b/path.c\nindex 7bccd830e9..1b1de3be09 100644\n--- a/path.c\n+++ b/path.c\n@@ -1503,21 +1503,28 @@ int looks_like_command_line_option(const char *str)\n \treturn str && str[0] == '-';\n }\n \n-char *xdg_config_home(const char *filename)\n+char *xdg_config_home_for(const char *subdir, const char *filename)\n {\n \tconst char *home, *config_home;\n \n+\tassert(subdir);\n \tassert(filename);\n \tconfig_home = getenv(\"XDG_CONFIG_HOME\");\n \tif (config_home && *config_home)\n-\t\treturn mkpathdup(\"%s/git/%s\", config_home, filename);\n+\t\treturn mkpathdup(\"%s/%s/%s\", config_home, subdir, filename);\n \n \thome = getenv(\"HOME\");\n \tif (home)\n-\t\treturn mkpathdup(\"%s/.config/git/%s\", home, filename);\n+\t\treturn mkpathdup(\"%s/.config/%s/%s\", home, subdir, filename);\n+\n \treturn NULL;\n }\n \n+char *xdg_config_home(const char *filename)\n+{\n+\treturn xdg_config_home_for(\"git\", filename);\n+}\n+\n char *xdg_cache_home(const char *filename)\n {\n \tconst char *home, *cache_home;\n-- \n2.32.0\n\n"},{"id":"426774","messageId":"20210608134000.663398-3-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210608134000.663398-1-lenaic@lhuard.fr","subject":"[PATCH v5 2/3] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-06-08T13:39:59Z","receivedAt":"2021-06-08T13:41:47Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Depending on the system, different schedulers can be used to schedule\nthe hourly, daily and weekly executions of `git maintenance run`:\n* `launchctl` for MacOS,\n* `schtasks` for Windows and\n* `crontab` for everything else.\n\n`git maintenance run` now has an option to let the end-user explicitly\nchoose which scheduler he wants to use:\n`--scheduler=auto|crontab|launchctl|schtasks`.\n\nWhen `git maintenance start --scheduler=XXX` is run, it not only\nregisters `git maintenance run` tasks in the scheduler XXX, it also\nremoves the `git maintenance run` tasks from all the other schedulers to\nensure we cannot have two schedulers launching concurrent identical\ntasks.\n\nThe default value is `auto` which chooses a suitable scheduler for the\nsystem.\n\n`git maintenance stop` doesn't have any `--scheduler` parameter because\nthis command will try to remove the `git maintenance run` tasks from all\nthe available schedulers.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n Documentation/git-maintenance.txt |  11 +\n builtin/gc.c                      | 336 +++++++++++++++++++++++-------\n t/t7900-maintenance.sh            |  55 ++++-\n 3 files changed, 327 insertions(+), 75 deletions(-)\n\ndiff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\nindex 1e738ad398..07065ed4f3 100644\n--- a/Documentation/git-maintenance.txt\n+++ b/Documentation/git-maintenance.txt\n@@ -179,6 +179,17 @@ OPTIONS\n \t`maintenance.<task>.enabled` configured as `true` are considered.\n \tSee the 'TASKS' section for the list of accepted `<task>` values.\n \n+--scheduler=auto|crontab|launchctl|schtasks::\n+\tWhen combined with the `start` subcommand, specify the scheduler\n+\tto use to run the hourly, daily and weekly executions of\n+\t`git maintenance run`.\n+\tThe possible values for `<scheduler>` depend on the system: `crontab`\n+\tis available on POSIX systems, `launchctl` is available on\n+\tMacOS and `schtasks` is available on Windows.\n+\tBy default or when `auto` is specified, a suitable scheduler for\n+\tthe system is used. On MacOS, `launchctl` is used. On Windows,\n+\t`schtasks` is used. On all other systems, `crontab` is used.\n+\n \n TROUBLESHOOTING\n ---------------\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex f05d2f0a1a..f2a81ecb44 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1529,6 +1529,59 @@ static const char *get_frequency(enum schedule_priority schedule)\n \t}\n }\n \n+static int get_schedule_cmd(const char **cmd, int *is_available)\n+{\n+\tchar *item;\n+\tchar *testing = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n+\n+\tif (!testing)\n+\t\treturn 0;\n+\n+\tif (is_available)\n+\t\t*is_available = 0;\n+\n+\tfor(item = testing;;) {\n+\t\tchar *sep;\n+\t\tchar *end_item = strchr(item, ',');\n+\t\tif (end_item)\n+\t\t\t*end_item = '\\0';\n+\n+\t\tsep = strchr(item, ':');\n+\t\tif (!sep)\n+\t\t\tdie(\"GIT_TEST_MAINT_SCHEDULER unparseable: %s\", testing);\n+\t\t*sep = '\\0';\n+\n+\t\tif (!strcmp(*cmd, item)) {\n+\t\t\t*cmd = sep+1;\n+\t\t\tif (is_available)\n+\t\t\t\t*is_available = 1;\n+\t\t\tUNLEAK(testing);\n+\t\t\treturn 1;\n+\t\t}\n+\n+\t\tif (!end_item)\n+\t\t\tbreak;\n+\t\titem = end_item + 1;\n+\t}\n+\n+\tfree(testing);\n+\treturn 1;\n+}\n+\n+static int is_launchctl_available(void)\n+{\n+\tconst char *cmd = \"launchctl\";\n+\tint is_available;\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+#ifdef __APPLE__\n+\treturn 1;\n+#else\n+\treturn 0;\n+#endif\n+}\n+\n static char *launchctl_service_name(const char *frequency)\n {\n \tstruct strbuf label = STRBUF_INIT;\n@@ -1555,19 +1608,17 @@ static char *launchctl_get_uid(void)\n \treturn xstrfmt(\"gui/%d\", getuid());\n }\n \n-static int launchctl_boot_plist(int enable, const char *filename, const char *cmd)\n+static int launchctl_boot_plist(int enable, const char *filename)\n {\n+\tconst char *cmd = \"launchctl\";\n \tint result;\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tchar *uid = launchctl_get_uid();\n \n+\tget_schedule_cmd(&cmd, NULL);\n \tstrvec_split(&child.args, cmd);\n-\tif (enable)\n-\t\tstrvec_push(&child.args, \"bootstrap\");\n-\telse\n-\t\tstrvec_push(&child.args, \"bootout\");\n-\tstrvec_push(&child.args, uid);\n-\tstrvec_push(&child.args, filename);\n+\tstrvec_pushl(&child.args, enable ? \"bootstrap\" : \"bootout\",\n+\t\t     uid, filename, NULL);\n \n \tchild.no_stderr = 1;\n \tchild.no_stdout = 1;\n@@ -1581,26 +1632,26 @@ static int launchctl_boot_plist(int enable, const char *filename, const char *cm\n \treturn result;\n }\n \n-static int launchctl_remove_plist(enum schedule_priority schedule, const char *cmd)\n+static int launchctl_remove_plist(enum schedule_priority schedule)\n {\n \tconst char *frequency = get_frequency(schedule);\n \tchar *name = launchctl_service_name(frequency);\n \tchar *filename = launchctl_service_filename(name);\n-\tint result = launchctl_boot_plist(0, filename, cmd);\n+\tint result = launchctl_boot_plist(0, filename);\n \tunlink(filename);\n \tfree(filename);\n \tfree(name);\n \treturn result;\n }\n \n-static int launchctl_remove_plists(const char *cmd)\n+static int launchctl_remove_plists(void)\n {\n-\treturn launchctl_remove_plist(SCHEDULE_HOURLY, cmd) ||\n-\t\tlaunchctl_remove_plist(SCHEDULE_DAILY, cmd) ||\n-\t\tlaunchctl_remove_plist(SCHEDULE_WEEKLY, cmd);\n+\treturn launchctl_remove_plist(SCHEDULE_HOURLY) ||\n+\t\tlaunchctl_remove_plist(SCHEDULE_DAILY) ||\n+\t\tlaunchctl_remove_plist(SCHEDULE_WEEKLY);\n }\n \n-static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule, const char *cmd)\n+static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule)\n {\n \tFILE *plist;\n \tint i;\n@@ -1669,8 +1720,8 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n \tfclose(plist);\n \n \t/* bootout might fail if not already running, so ignore */\n-\tlaunchctl_boot_plist(0, filename, cmd);\n-\tif (launchctl_boot_plist(1, filename, cmd))\n+\tlaunchctl_boot_plist(0, filename);\n+\tif (launchctl_boot_plist(1, filename))\n \t\tdie(_(\"failed to bootstrap service %s\"), filename);\n \n \tfree(filename);\n@@ -1678,21 +1729,35 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n \treturn 0;\n }\n \n-static int launchctl_add_plists(const char *cmd)\n+static int launchctl_add_plists(void)\n {\n \tconst char *exec_path = git_exec_path();\n \n-\treturn launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY, cmd) ||\n-\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_DAILY, cmd) ||\n-\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY, cmd);\n+\treturn launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY) ||\n+\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_DAILY) ||\n+\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY);\n }\n \n-static int launchctl_update_schedule(int run_maintenance, int fd, const char *cmd)\n+static int launchctl_update_schedule(int run_maintenance, int fd)\n {\n \tif (run_maintenance)\n-\t\treturn launchctl_add_plists(cmd);\n+\t\treturn launchctl_add_plists();\n \telse\n-\t\treturn launchctl_remove_plists(cmd);\n+\t\treturn launchctl_remove_plists();\n+}\n+\n+static int is_schtasks_available(void)\n+{\n+\tconst char *cmd = \"schtasks\";\n+\tint is_available;\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+#ifdef GIT_WINDOWS_NATIVE\n+\treturn 1;\n+#else\n+\treturn 0;\n+#endif\n }\n \n static char *schtasks_task_name(const char *frequency)\n@@ -1702,13 +1767,15 @@ static char *schtasks_task_name(const char *frequency)\n \treturn strbuf_detach(&label, NULL);\n }\n \n-static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd)\n+static int schtasks_remove_task(enum schedule_priority schedule)\n {\n+\tconst char *cmd = \"schtasks\";\n \tint result;\n \tstruct strvec args = STRVEC_INIT;\n \tconst char *frequency = get_frequency(schedule);\n \tchar *name = schtasks_task_name(frequency);\n \n+\tget_schedule_cmd(&cmd, NULL);\n \tstrvec_split(&args, cmd);\n \tstrvec_pushl(&args, \"/delete\", \"/tn\", name, \"/f\", NULL);\n \n@@ -1719,15 +1786,16 @@ static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd\n \treturn result;\n }\n \n-static int schtasks_remove_tasks(const char *cmd)\n+static int schtasks_remove_tasks(void)\n {\n-\treturn schtasks_remove_task(SCHEDULE_HOURLY, cmd) ||\n-\t\tschtasks_remove_task(SCHEDULE_DAILY, cmd) ||\n-\t\tschtasks_remove_task(SCHEDULE_WEEKLY, cmd);\n+\treturn schtasks_remove_task(SCHEDULE_HOURLY) ||\n+\t\tschtasks_remove_task(SCHEDULE_DAILY) ||\n+\t\tschtasks_remove_task(SCHEDULE_WEEKLY);\n }\n \n-static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule, const char *cmd)\n+static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule)\n {\n+\tconst char *cmd = \"schtasks\";\n \tint result;\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tconst char *xml;\n@@ -1736,6 +1804,8 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority\n \tchar *name = schtasks_task_name(frequency);\n \tstruct strbuf tfilename = STRBUF_INIT;\n \n+\tget_schedule_cmd(&cmd, NULL);\n+\n \tstrbuf_addf(&tfilename, \"%s/schedule_%s_XXXXXX\",\n \t\t    get_git_common_dir(), frequency);\n \ttfile = xmks_tempfile(tfilename.buf);\n@@ -1840,28 +1910,52 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority\n \treturn result;\n }\n \n-static int schtasks_schedule_tasks(const char *cmd)\n+static int schtasks_schedule_tasks(void)\n {\n \tconst char *exec_path = git_exec_path();\n \n-\treturn schtasks_schedule_task(exec_path, SCHEDULE_HOURLY, cmd) ||\n-\t\tschtasks_schedule_task(exec_path, SCHEDULE_DAILY, cmd) ||\n-\t\tschtasks_schedule_task(exec_path, SCHEDULE_WEEKLY, cmd);\n+\treturn schtasks_schedule_task(exec_path, SCHEDULE_HOURLY) ||\n+\t\tschtasks_schedule_task(exec_path, SCHEDULE_DAILY) ||\n+\t\tschtasks_schedule_task(exec_path, SCHEDULE_WEEKLY);\n }\n \n-static int schtasks_update_schedule(int run_maintenance, int fd, const char *cmd)\n+static int schtasks_update_schedule(int run_maintenance, int fd)\n {\n \tif (run_maintenance)\n-\t\treturn schtasks_schedule_tasks(cmd);\n+\t\treturn schtasks_schedule_tasks();\n \telse\n-\t\treturn schtasks_remove_tasks(cmd);\n+\t\treturn schtasks_remove_tasks();\n+}\n+\n+static int is_crontab_available(void)\n+{\n+\tconst char *cmd = \"crontab\";\n+\tint is_available;\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\n+\tif (get_schedule_cmd(&cmd, &is_available) && !is_available)\n+\t\treturn 0;\n+\n+\tstrvec_split(&child.args, cmd);\n+\tstrvec_push(&child.args, \"-l\");\n+\tchild.no_stdin = 1;\n+\tchild.no_stdout = 1;\n+\tchild.no_stderr = 1;\n+\tchild.silent_exec_failure = 1;\n+\n+\tif (start_command(&child))\n+\t\treturn 0;\n+\t/* Ignore exit code, as an empty crontab will return error. */\n+\tfinish_command(&child);\n+\treturn 1;\n }\n \n #define BEGIN_LINE \"# BEGIN GIT MAINTENANCE SCHEDULE\"\n #define END_LINE \"# END GIT MAINTENANCE SCHEDULE\"\n \n-static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n+static int crontab_update_schedule(int run_maintenance, int fd)\n {\n+\tconst char *cmd = \"crontab\";\n \tint result = 0;\n \tint in_old_region = 0;\n \tstruct child_process crontab_list = CHILD_PROCESS_INIT;\n@@ -1869,6 +1963,7 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n \tFILE *cron_list, *cron_in;\n \tstruct strbuf line = STRBUF_INIT;\n \n+\tget_schedule_cmd(&cmd, NULL);\n \tstrvec_split(&crontab_list.args, cmd);\n \tstrvec_push(&crontab_list.args, \"-l\");\n \tcrontab_list.in = -1;\n@@ -1945,66 +2040,165 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n \treturn result;\n }\n \n+enum scheduler {\n+\tSCHEDULER_INVALID = -1,\n+\tSCHEDULER_AUTO,\n+\tSCHEDULER_CRON,\n+\tSCHEDULER_LAUNCHCTL,\n+\tSCHEDULER_SCHTASKS,\n+};\n+\n+static const struct {\n+\tconst char *name;\n+\tint (*is_available)(void);\n+\tint (*update_schedule)(int run_maintenance, int fd);\n+} scheduler_fn[] = {\n+\t[SCHEDULER_CRON] = {\n+\t\t.name = \"crontab\",\n+\t\t.is_available = is_crontab_available,\n+\t\t.update_schedule = crontab_update_schedule,\n+\t},\n+\t[SCHEDULER_LAUNCHCTL] = {\n+\t\t.name = \"launchctl\",\n+\t\t.is_available = is_launchctl_available,\n+\t\t.update_schedule = launchctl_update_schedule,\n+\t},\n+\t[SCHEDULER_SCHTASKS] = {\n+\t\t.name = \"schtasks\",\n+\t\t.is_available = is_schtasks_available,\n+\t\t.update_schedule = schtasks_update_schedule,\n+\t},\n+};\n+\n+static enum scheduler parse_scheduler(const char *value)\n+{\n+\tif (!value)\n+\t\treturn SCHEDULER_INVALID;\n+\telse if (!strcasecmp(value, \"auto\"))\n+\t\treturn SCHEDULER_AUTO;\n+\telse if (!strcasecmp(value, \"cron\") || !strcasecmp(value, \"crontab\"))\n+\t\treturn SCHEDULER_CRON;\n+\telse if (!strcasecmp(value, \"launchctl\"))\n+\t\treturn SCHEDULER_LAUNCHCTL;\n+\telse if (!strcasecmp(value, \"schtasks\"))\n+\t\treturn SCHEDULER_SCHTASKS;\n+\telse\n+\t\treturn SCHEDULER_INVALID;\n+}\n+\n+static int maintenance_opt_scheduler(const struct option *opt, const char *arg,\n+\t\t\t\t     int unset)\n+{\n+\tenum scheduler *scheduler = opt->value;\n+\n+\tBUG_ON_OPT_NEG(unset);\n+\n+\t*scheduler = parse_scheduler(arg);\n+\tif (*scheduler == SCHEDULER_INVALID)\n+\t\treturn error(_(\"unrecognized --scheduler argument '%s'\"), arg);\n+\treturn 0;\n+}\n+\n+struct maintenance_start_opts {\n+\tenum scheduler scheduler;\n+};\n+\n+static void resolve_auto_scheduler(enum scheduler *scheduler)\n+{\n+\tif (*scheduler != SCHEDULER_AUTO)\n+\t\treturn;\n+\n #if defined(__APPLE__)\n-static const char platform_scheduler[] = \"launchctl\";\n+\t*scheduler = SCHEDULER_LAUNCHCTL;\n+\treturn;\n+\n #elif defined(GIT_WINDOWS_NATIVE)\n-static const char platform_scheduler[] = \"schtasks\";\n+\t*scheduler = SCHEDULER_SCHTASKS;\n+\treturn;\n+\n #else\n-static const char platform_scheduler[] = \"crontab\";\n+\t*scheduler = SCHEDULER_CRON;\n+\treturn;\n #endif\n+}\n \n-static int update_background_schedule(int enable)\n+static void validate_scheduler(enum scheduler scheduler)\n {\n-\tint result;\n-\tconst char *scheduler = platform_scheduler;\n-\tconst char *cmd = scheduler;\n-\tchar *testing;\n+\tif (scheduler == SCHEDULER_INVALID)\n+\t\tBUG(\"invalid scheduler\");\n+\tif (scheduler == SCHEDULER_AUTO)\n+\t\tBUG(\"resolve_auto_scheduler should have been called before\");\n+\n+\tif (!scheduler_fn[scheduler].is_available())\n+\t\tdie(_(\"%s scheduler is not available\"),\n+\t\t    scheduler_fn[scheduler].name);\n+}\n+\n+static int update_background_schedule(const struct maintenance_start_opts *opts,\n+\t\t\t\t      int enable)\n+{\n+\tunsigned int i;\n+\tint result = 0;\n \tstruct lock_file lk;\n \tchar *lock_path = xstrfmt(\"%s/schedule\", the_repository->objects->odb->path);\n \n-\ttesting = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n-\tif (testing) {\n-\t\tchar *sep = strchr(testing, ':');\n-\t\tif (!sep)\n-\t\t\tdie(\"GIT_TEST_MAINT_SCHEDULER unparseable: %s\", testing);\n-\t\t*sep = '\\0';\n-\t\tscheduler = testing;\n-\t\tcmd = sep + 1;\n+\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {\n+\t\tfree(lock_path);\n+\t\treturn error(_(\"another process is scheduling background maintenance\"));\n \t}\n \n-\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {\n-\t\tresult = error(_(\"another process is scheduling background maintenance\"));\n-\t\tgoto cleanup;\n+\tfor (i = 1; i < ARRAY_SIZE(scheduler_fn); i++) {\n+\t\tif (enable && opts->scheduler == i)\n+\t\t\tcontinue;\n+\t\tif (!scheduler_fn[i].is_available())\n+\t\t\tcontinue;\n+\t\tscheduler_fn[i].update_schedule(\n+\t\t\t0, get_lock_file_fd(&lk));\n \t}\n \n-\tif (!strcmp(scheduler, \"launchctl\"))\n-\t\tresult = launchctl_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse if (!strcmp(scheduler, \"schtasks\"))\n-\t\tresult = schtasks_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse if (!strcmp(scheduler, \"crontab\"))\n-\t\tresult = crontab_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse\n-\t\tdie(\"unknown background scheduler: %s\", scheduler);\n+\tif (enable)\n+\t\tresult = scheduler_fn[opts->scheduler].update_schedule(\n+\t\t\t1, get_lock_file_fd(&lk));\n \n \trollback_lock_file(&lk);\n \n-cleanup:\n \tfree(lock_path);\n-\tfree(testing);\n \treturn result;\n }\n \n-static int maintenance_start(void)\n+static const char *const builtin_maintenance_start_usage[] = {\n+\tN_(\"git maintenance start [--scheduler=<scheduler>]\"),\n+\tNULL\n+};\n+\n+static int maintenance_start(int argc, const char **argv, const char *prefix)\n {\n+\tstruct maintenance_start_opts opts = { 0 };\n+\tstruct option options[] = {\n+\t\tOPT_CALLBACK_F(\n+\t\t\t0, \"scheduler\", &opts.scheduler, N_(\"scheduler\"),\n+\t\t\tN_(\"scheduler to use to trigger git maintenance run\"),\n+\t\t\tPARSE_OPT_NONEG, maintenance_opt_scheduler),\n+\t\tOPT_END()\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     builtin_maintenance_start_usage, 0);\n+\tif (argc)\n+\t\tusage_with_options(builtin_maintenance_start_usage,\n+\t\t\t\t   options);\n+\n+\tresolve_auto_scheduler(&opts.scheduler);\n+\tvalidate_scheduler(opts.scheduler);\n+\n \tif (maintenance_register())\n \t\twarning(_(\"failed to add repo to global config\"));\n-\n-\treturn update_background_schedule(1);\n+\treturn update_background_schedule(&opts, 1);\n }\n \n static int maintenance_stop(void)\n {\n-\treturn update_background_schedule(0);\n+\treturn update_background_schedule(NULL, 0);\n }\n \n static const char builtin_maintenance_usage[] =\tN_(\"git maintenance <subcommand> [<options>]\");\n@@ -2018,7 +2212,7 @@ int cmd_maintenance(int argc, const char **argv, const char *prefix)\n \tif (!strcmp(argv[1], \"run\"))\n \t\treturn maintenance_run(argc - 1, argv + 1, prefix);\n \tif (!strcmp(argv[1], \"start\"))\n-\t\treturn maintenance_start();\n+\t\treturn maintenance_start(argc - 1, argv + 1, prefix);\n \tif (!strcmp(argv[1], \"stop\"))\n \t\treturn maintenance_stop();\n \tif (!strcmp(argv[1], \"register\"))\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex b93ae014ee..b36b7f5fb0 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -494,8 +494,21 @@ test_expect_success !MINGW 'register and unregister with regex metacharacters' '\n \t\tmaintenance.repo \"$(pwd)/$META\"\n '\n \n+test_expect_success 'start --scheduler=<scheduler>' '\n+\ttest_expect_code 129 git maintenance start --scheduler=foo 2>err &&\n+\ttest_i18ngrep \"unrecognized --scheduler argument\" err &&\n+\n+\ttest_expect_code 129 git maintenance start --no-scheduler 2>err &&\n+\ttest_i18ngrep \"unknown option\" err &&\n+\n+\ttest_expect_code 128 \\\n+\t\tenv GIT_TEST_MAINT_SCHEDULER=\"launchctl:true,schtasks:true\" \\\n+\t\tgit maintenance start --scheduler=crontab 2>err &&\n+\ttest_i18ngrep \"fatal: crontab scheduler is not available\" err\n+'\n+\n test_expect_success 'start from empty cron table' '\n-\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start --scheduler=crontab &&\n \n \t# start registers the repo\n \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n@@ -518,7 +531,7 @@ test_expect_success 'stop from existing schedule' '\n \n test_expect_success 'start preserves existing schedule' '\n \techo \"Important information!\" >cron.txt &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start --scheduler=crontab &&\n \tgrep \"Important information!\" cron.txt\n '\n \n@@ -547,7 +560,7 @@ test_expect_success 'start and stop macOS maintenance' '\n \tEOF\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start --scheduler=launchctl &&\n \n \t# start registers the repo\n \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n@@ -598,7 +611,7 @@ test_expect_success 'start and stop Windows maintenance' '\n \tEOF\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"schtasks:./print-args\" git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"schtasks:./print-args\" git maintenance start --scheduler=schtasks &&\n \n \t# start registers the repo\n \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n@@ -621,6 +634,40 @@ test_expect_success 'start and stop Windows maintenance' '\n \ttest_cmp expect args\n '\n \n+test_expect_success 'start and stop when several schedulers are available' '\n+\twrite_script print-args <<-\\EOF &&\n+\tprintf \"%s\\n\" \"$*\" | sed \"s:gui/[0-9][0-9]*:gui/[UID]:; s:\\(schtasks /create .* /xml\\).*:\\1:;\" >>args\n+\tEOF\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >expect &&\n+\tfor frequency in hourly daily weekly\n+\tdo\n+\t\tPLIST=\"$pfx/Library/LaunchAgents/org.git-scm.git.$frequency.plist\" &&\n+\t\techo \"launchctl bootout gui/[UID] $PLIST\" >>expect &&\n+\t\techo \"launchctl bootstrap gui/[UID] $PLIST\" >>expect || return 1\n+\tdone &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n+\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n+\t\thourly daily weekly >expect &&\n+\tprintf \"schtasks /create /tn Git Maintenance (%s) /f /xml\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n+\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n+\t\thourly daily weekly >expect &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\ttest_cmp expect args\n+'\n+\n test_expect_success 'register preserves existing strategy' '\n \tgit config maintenance.strategy none &&\n \tgit maintenance register &&\n-- \n2.32.0\n\n"},{"id":"426830","messageId":"xmqqh7i7nb0q.fsf@gitster.g","threadId":"55603","inReplyTo":"20210608134000.663398-1-lenaic@lhuard.fr","subject":"Re: [PATCH v5 0/3] add support for systemd timers on Linux","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-06-09T00:21:09Z","receivedAt":"2021-06-09T00:21:12Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Lénaïc Huard <lenaic@lhuard.fr> writes:\n\n> Hello,\n>\n> I’ve reworked this submission based on the valuable feedback I’ve received.\n> Thanks again for it!\n>\n> The patchset contains now the following patches:\n> ...\n\nA summary very well written.  I wish all the cover letters were\nwritten like this one.\n\n> I hope I’ve addressed all your valuable feedback. Do not hesitate to\n> let me know if I’ve forgotten anything.\n>\n> Lénaïc Huard (3):\n>   cache.h: Introduce a generic \"xdg_config_home_for(…)\" function\n>   maintenance: `git maintenance run` learned `--scheduler=<scheduler>`\n>   maintenance: add support for systemd timers on Linux\n\nThanks.\n"},{"id":"426859","messageId":"13530009.9cuodmXfNX@coruscant.lhuard.fr","threadId":"55603","inReplyTo":"3fd17223-8667-24be-2e65-f1970d411bdf@gmail.com","subject":"Re: [PATCH v2 1/1] maintenance: use systemd timers on Linux","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-06-08T14:55:29Z","receivedAt":"2021-06-09T06:39:27Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Hello Phillip,\n\nLe lundi 10 mai 2021, 20:03:58 CEST Phillip Wood a écrit :\n> > +\tunit = \"# This file was created and is maintained by Git.\\n\"\n> > +\t       \"# Any edits made in this file might be replaced in the \nfuture\\n\"\n> > +\t       \"# by a Git command.\\n\"\n> > +\t       \"\\n\"\n> > +\t       \"[Unit]\\n\"\n> > +\t       \"Description=Optimize Git repositories data\\n\"\n> > +\t       \"\\n\"\n> > +\t       \"[Service]\\n\"\n> > +\t       \"Type=oneshot\\n\"\n> > +\t       \"ExecStart=\\\"%1$s/git\\\" --exec-path=\\\"%1$s\\\" for-each-repo\n> > --config=maintenance.repo maintenance run --schedule=%%i\\n\" +\t      \n> > \"LockPersonality=yes\\n\"\n> > +\t       \"MemoryDenyWriteExecute=yes\\n\"\n> > +\t       \"NoNewPrivileges=yes\\n\"\n> > +\t       \"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\\n\"\n> > +\t       \"RestrictNamespaces=yes\\n\"\n> > +\t       \"RestrictRealtime=yes\\n\"\n> > +\t       \"RestrictSUIDSGID=yes\\n\"\n> \n> After a quick read of the systemd.exec man page it is unclear to me if\n> these Restrict... lines are needed as we already have\n> NoNewPrivileges=yes - maybe they have some effect if `git maintence` is\n> run as root?\n\nI think that the only thing that `NoNewPrivileges=yes` do is to set the no new \nprivileges flag described in [1] on the process.\n\nThe `Restrict…` options are enabling some other sandboxing features by \nblocking some syscalls through a seccomp profile.\n\nMy understanding of the systemd.exec man page is the other way round, i.e.: as \nsoon as there’s a `Restrict…` option, the `NoNewPrivileges=yes` is implied.\n\nSo, I would say that strictly speaking `NoNewPrivileges=yes` isn’t needed.\nBut `NoNewPrivileges=yes` doesn’t imply the `Restrict…` options.\n\nBut I thought that, from a security point of view, it’s better to set as many \nsandboxing options as possible and be as explicit as possible.\n\n[1] https://www.kernel.org/doc/html/latest/userspace-api/no_new_privs.html\n\n\n\n"},{"id":"426869","messageId":"YMCLQ5pOyg+SSKhD@coredump.intra.peff.net","threadId":"55603","inReplyTo":"20210608134000.663398-4-lenaic@lhuard.fr","subject":"Re: [PATCH v5 3/3] maintenance: add support for systemd timers on Linux","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2021-06-09T09:34:59Z","receivedAt":"2021-06-09T09:41:48Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Tue, Jun 08, 2021 at 03:40:00PM +0200, Lénaïc Huard wrote:\n\n> +static int systemd_timer_write_unit_templates(const char *exec_path)\n> +{\n> +\tchar *filename;\n> +\tFILE *file;\n> +\tconst char *unit;\n> +\n> +\tfilename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n> +\tif (safe_create_leading_directories(filename)) {\n> +\t\terror(_(\"failed to create directories for '%s'\"), filename);\n> +\t\tgoto error;\n> +\t}\n> +\tfile = fopen_or_warn(filename, \"w\");\n> +\tif (file == NULL)\n> +\t\tgoto error;\n> +\tFREE_AND_NULL(filename);\n\nHere we free the filename variable. But later...\n\n> +\tunit = \"# This file was created and is maintained by Git.\\n\"\n> +\t       \"# Any edits made in this file might be replaced in the future\\n\"\n> +\t       \"# by a Git command.\\n\"\n> +\t       \"\\n\"\n> +\t       \"[Unit]\\n\"\n> +\t       \"Description=Optimize Git repositories data\\n\"\n> +\t       \"\\n\"\n> +\t       \"[Timer]\\n\"\n> +\t       \"OnCalendar=%i\\n\"\n> +\t       \"Persistent=true\\n\"\n> +\t       \"\\n\"\n> +\t       \"[Install]\\n\"\n> +\t       \"WantedBy=timers.target\\n\";\n> +\tif (fputs(unit, file) == EOF) {\n> +\t\terror(_(\"failed to write to '%s'\"), filename);\n> +\t\tfclose(file);\n> +\t\tgoto error;\n> +\t}\n> +\tif (fclose(file) == EOF) {\n> +\t\terror_errno(_(\"failed to flush '%s'\"), filename);\n> +\t\tgoto error;\n> +\t}\n\nIf we see an error we'll try to use it as part of the message. I think\nthe FREE_AND_NULL() can just be moved down here. And really just be\nfree(), since we then immediately reassign it:\n\n> +\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n> +\tfile = fopen_or_warn(filename, \"w\");\n> +\tif (file == NULL)\n> +\t\tgoto error;\n> +\tFREE_AND_NULL(filename);\n\nAnd then this one has the same problem. We free here, but...\n\n> +\tunit = \"# This file was created and is maintained by Git.\\n\"\n> +\t       \"# Any edits made in this file might be replaced in the future\\n\"\n> +\t       \"# by a Git command.\\n\"\n> +\t       \"\\n\"\n> +\t       \"[Unit]\\n\"\n> +\t       \"Description=Optimize Git repositories data\\n\"\n> +\t       \"\\n\"\n> +\t       \"[Service]\\n\"\n> +\t       \"Type=oneshot\\n\"\n> +\t       \"ExecStart=\\\"%s/git\\\" --exec-path=\\\"%s\\\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\\n\"\n> +\t       \"LockPersonality=yes\\n\"\n> +\t       \"MemoryDenyWriteExecute=yes\\n\"\n> +\t       \"NoNewPrivileges=yes\\n\"\n> +\t       \"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\\n\"\n> +\t       \"RestrictNamespaces=yes\\n\"\n> +\t       \"RestrictRealtime=yes\\n\"\n> +\t       \"RestrictSUIDSGID=yes\\n\"\n> +\t       \"SystemCallArchitectures=native\\n\"\n> +\t       \"SystemCallFilter=@system-service\\n\";\n> +\tif (fprintf(file, unit, exec_path, exec_path) < 0) {\n> +\t\terror(_(\"failed to write to '%s'\"), filename);\n> +\t\tfclose(file);\n> +\t\tgoto error;\n> +\t}\n> +\tif (fclose(file) == EOF) {\n> +\t\terror_errno(_(\"failed to flush '%s'\"), filename);\n> +\t\tgoto error;\n> +\t}\n\n...use it in the error messages. This one could also just be free()\nbefore we return:\n\n> +\treturn 0;\n> +\n> +error:\n> +\tfree(filename);\n> +\tsystemd_timer_delete_unit_templates();\n> +\treturn -1;\n> +}\n\nAnd all of the jumps to the error label are fine, since it frees\nthe filename (and we don't have to worry about FREE_AND_NULL, since it\nwould always be valid during those jumps).\n\n-Peff\n"},{"id":"426890","messageId":"01774318-93c3-d3e9-6303-7f375008a9b8@gmail.com","threadId":"55603","inReplyTo":"20210608134000.663398-1-lenaic@lhuard.fr","subject":"Re: [PATCH v5 0/3] add support for systemd timers on Linux","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2021-06-09T14:54:29Z","receivedAt":"2021-06-09T14:55:01Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Lénaïc\n\nThanks for the excellent cover letter, I found it very useful while \nreviewing these patches. I think the changes address all of my previous \nconcerns, the error handling in the last patch looks good. Having read \nthrough the patches I don't have anything to add to Peff's comments - \nwith those small memory management fixed I think this will be a good shape.\n\nThanks for your work on this\n\nPhillip\n\nOn 08/06/2021 14:39, Lénaïc Huard wrote:\n>[...] > The patchset contains now the following patches:\n> \n> * cache.h: Introduce a generic \"xdg_config_home_for(…)\" function\n> \n>    This patch introduces a function to compute configuration files\n>    paths inside $XDG_CONFIG_HOME or ~/.config for other programs than\n>    git itself.\n>    It is used in the latest patch of this series to compute systemd\n>    unit files location.\n> \n>    The only change in this patch compared to its previous version is\n>    the renaming of the first parameter of the `xdg_config_home_for(…)`\n>    function from `prog` to `subdir`.\n> \n> * maintenance: introduce ENABLE/DISABLE for code clarity\n> \n>    I just completely dropped this patch as it turned out that replacing\n>    some 0/1 values by `ENABLE`/`DISABLE` enum values wasn’t making the\n>    code look nicer as initially expected.\n> \n> * maintenance: `git maintenance run` learned `--scheduler=<scheduler>`\n> \n>    This patch contains all the code that is related to the addition of\n>    the new `--scheduler` parameter of the `git maintenance start`\n>    command, independently of the systemd timers.\n> \n>    The main changes in this patch compared to its previous version are:\n> \n>      * Revert all the changes that were previously introduced by the\n>        `ENABLE`/`DISABLE` enum values.\n> \n>      * Remove the `strlcpy` in the testing framework inside the\n>        `get_schedule_cmd` function.\n> \n>      * `update_background_schedule` loops over all the available\n>        schedulers, disables all of them except the one which is\n>        enabled.\n>        In this new version of the patch, it is now ensured that all the\n>        schedulers deactivation are done before the activation.\n>        The goal of this change is avoid a potential race condition\n>        where two schedulers could be enabled at the same time.\n>        This behaviour change has been reflected in the tests.\n> \n>      * The local variable `builtin_maintenance_start_options` has been\n>        shortened.\n> \n> * maintenance: add support for systemd timers on Linux\n> \n>    This patch implements the support of systemd timers on top of\n>    crontab scheduler on Linux systems.\n> \n>    The main changes in this patch compared to its previous version are:\n> \n>      * The caching logic of `is_systemd_timer_available` has been\n>        dropped.\n>        I initially wanted to cache the outcome of forking and executing\n>        an external command to avoid doing it several times as\n>        `is_systemd_timer_available` is invoked from several places\n>        (`resolve_auto_scheduler`, `validate_scheduler` and\n>        `update_background_scheduler`).\n>        But it’s true they’re not always all called.\n>        In the case of `maintenance stop`, `resolve_auto_scheduler` and\n>        `validate_scheduler` are not called.\n>        In the case of `maintenance start`, the `if (enable &&\n>        opts->scheduler == i)` statement inside\n>        `update_background_schedule` skips the execution of\n>        `is_systemd_timer_available`.\n> \n>      * The `is_systemd_timer_available` has been split in two parts:\n>        * `is_systemd_timer_available` is the entry point and holds the\n>          platform agnostic testing framework logic.\n>        * `real_is_systemd_timer_available` contains the platform\n>          specific logic.\n> \n>      * The error management of `systemd_timer_write_unit_templates` has\n>        been reviewed.\n>        The return code of `fopen`, `fputs`, `fclose`, etc. are now\n>        checked.\n>        If this function manages to write one file, but fails at writing\n>        the second one, it will attempt to delete the first one to not\n>        leave the system in an inconsistent state.\n> \n>      * The error management of `systemd_timer_delete_unit_templates`\n>        has also been reviewed. The error code of `unlink` is now\n>        checked.\n> \n> I hope I’ve addressed all your valuable feedback. Do not hesitate to\n> let me know if I’ve forgotten anything.\n> \n> Lénaïc Huard (3):\n>    cache.h: Introduce a generic \"xdg_config_home_for(…)\" function\n>    maintenance: `git maintenance run` learned `--scheduler=<scheduler>`\n>    maintenance: add support for systemd timers on Linux\n> \n>   Documentation/git-maintenance.txt |  60 ++++\n>   builtin/gc.c                      | 564 ++++++++++++++++++++++++++----\n>   cache.h                           |   7 +\n>   path.c                            |  13 +-\n>   t/t7900-maintenance.sh            | 110 +++++-\n>   5 files changed, 676 insertions(+), 78 deletions(-)\n> \n"},{"id":"426892","messageId":"959c56a3-3290-b277-5d30-e16244ae44bf@gmail.com","threadId":"55603","inReplyTo":"20210608134000.663398-4-lenaic@lhuard.fr","subject":"Re: [PATCH v5 3/3] maintenance: add support for systemd timers on Linux","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2021-06-09T15:01:09Z","receivedAt":"2021-06-09T15:01:39Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"\n\nOn 08/06/2021 14:40, Lénaïc Huard wrote:\n> The existing mechanism for scheduling background maintenance is done\n> through cron. On Linux systems managed by systemd, systemd provides an\n> alternative to schedule recurring tasks: systemd timers.\n> \n> The main motivations to implement systemd timers in addition to cron\n> are:\n> * cron is optional and Linux systems running systemd might not have it\n>    installed.\n> * The execution of `crontab -l` can tell us if cron is installed but not\n>    if the daemon is actually running.\n> * With systemd, each service is run in its own cgroup and its logs are\n>    tagged by the service inside journald. With cron, all scheduled tasks\n>    are running in the cron daemon cgroup and all the logs of the\n>    user-scheduled tasks are pretended to belong to the system cron\n>    service.\n>    Concretely, a user that doesn’t have access to the system logs won’t\n>    have access to the log of their own tasks scheduled by cron whereas\n>    they will have access to the log of their own tasks scheduled by\n>    systemd timer.\n>    Although `cron` attempts to send email, that email may go unseen by\n>    the user because these days, local mailboxes are not heavily used\n>    anymore.\n> \n> In order to schedule git maintenance, we need two unit template files:\n> * ~/.config/systemd/user/git-maintenance@.service\n>    to define the command to be started by systemd and\n> * ~/.config/systemd/user/git-maintenance@.timer\n>    to define the schedule at which the command should be run.\n> \n> Those units are templates that are parameterized by the frequency.\n> \n> Based on those templates, 3 timers are started:\n> * git-maintenance@hourly.timer\n> * git-maintenance@daily.timer\n> * git-maintenance@weekly.timer\n> \n> The command launched by those three timers are the same as with the\n> other scheduling methods:\n> \n> /path/to/git for-each-repo --exec-path=/path/to\n> --config=maintenance.repo maintenance run --schedule=%i\n> \n> with the full path for git to ensure that the version of git launched\n> for the scheduled maintenance is the same as the one used to run\n> `maintenance start`.\n> \n> The timer unit contains `Persistent=true` so that, if the computer is\n> powered down when a maintenance task should run, the task will be run\n> when the computer is back powered on.\n> \n> Signed-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n> ---\n>   Documentation/git-maintenance.txt |  57 +++++++-\n>   builtin/gc.c                      | 228 ++++++++++++++++++++++++++++++\n>   t/t7900-maintenance.sh            |  67 ++++++++-\n>   3 files changed, 342 insertions(+), 10 deletions(-)\n> \n> diff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\n> index 07065ed4f3..7b7dbbbca9 100644\n> --- a/Documentation/git-maintenance.txt\n> +++ b/Documentation/git-maintenance.txt\n> @@ -179,16 +179,19 @@ OPTIONS\n>   \t`maintenance.<task>.enabled` configured as `true` are considered.\n>   \tSee the 'TASKS' section for the list of accepted `<task>` values.\n>   \n> ---scheduler=auto|crontab|launchctl|schtasks::\n> +--scheduler=auto|crontab|systemd-timer|launchctl|schtasks::\n>   \tWhen combined with the `start` subcommand, specify the scheduler\n>   \tto use to run the hourly, daily and weekly executions of\n>   \t`git maintenance run`.\n>   \tThe possible values for `<scheduler>` depend on the system: `crontab`\n> -\tis available on POSIX systems, `launchctl` is available on\n> -\tMacOS and `schtasks` is available on Windows.\n> +\tis available on POSIX systems, `systemd-timer` is available on Linux\n> +\tsystems, `launchctl` is available on MacOS and `schtasks` is available\n> +\ton Windows.\n>   \tBy default or when `auto` is specified, a suitable scheduler for\n>   \tthe system is used. On MacOS, `launchctl` is used. On Windows,\n> -\t`schtasks` is used. On all other systems, `crontab` is used.\n> +\t`schtasks` is used. On Linux, `systemd-timer` is used if user systemd\n> +\ttimers are available, otherwise, `crontab` is used. On all other systems,\n> +\t`crontab` is used.\n>   \n>   \n>   TROUBLESHOOTING\n> @@ -288,6 +291,52 @@ schedule to ensure you are executing the correct binaries in your\n>   schedule.\n>   \n>   \n> +BACKGROUND MAINTENANCE ON LINUX SYSTEMD SYSTEMS\n> +-----------------------------------------------\n> +\n> +While Linux supports `cron`, depending on the distribution, `cron` may\n> +be an optional package not necessarily installed. On modern Linux\n> +distributions, systemd timers are superseding it.\n> +\n> +If user systemd timers are available, they will be used as a replacement\n> +of `cron`.\n> +\n> +In this case, `git maintenance start` will create user systemd timer units\n> +and start the timers. The current list of user-scheduled tasks can be found\n> +by running `systemctl --user list-timers`. The timers written by `git\n> +maintenance start` are similar to this:\n> +\n> +-----------------------------------------------------------------------\n> +$ systemctl --user list-timers\n> +NEXT                         LEFT          LAST                         PASSED     UNIT                         ACTIVATES\n> +Thu 2021-04-29 19:00:00 CEST 42min left    Thu 2021-04-29 18:00:11 CEST 17min ago  git-maintenance@hourly.timer git-maintenance@hourly.service\n> +Fri 2021-04-30 00:00:00 CEST 5h 42min left Thu 2021-04-29 00:00:11 CEST 18h ago    git-maintenance@daily.timer  git-maintenance@daily.service\n> +Mon 2021-05-03 00:00:00 CEST 3 days left   Mon 2021-04-26 00:00:11 CEST 3 days ago git-maintenance@weekly.timer git-maintenance@weekly.service\n> +-----------------------------------------------------------------------\n> +\n> +One timer is registered for each `--schedule=<frequency>` option.\n> +\n> +The definition of the systemd units can be inspected in the following files:\n> +\n> +-----------------------------------------------------------------------\n> +~/.config/systemd/user/git-maintenance@.timer\n> +~/.config/systemd/user/git-maintenance@.service\n> +~/.config/systemd/user/timers.target.wants/git-maintenance@hourly.timer\n> +~/.config/systemd/user/timers.target.wants/git-maintenance@daily.timer\n> +~/.config/systemd/user/timers.target.wants/git-maintenance@weekly.timer\n> +-----------------------------------------------------------------------\n> +\n> +`git maintenance start` will overwrite these files and start the timer\n> +again with `systemctl --user`, so any customization should be done by\n> +creating a drop-in file, i.e. a `.conf` suffixed file in the\n> +`~/.config/systemd/user/git-maintenance@.service.d` directory.\n> +\n> +`git maintenance stop` will stop the user systemd timers and delete\n> +the above mentioned files.\n> +\n> +For more details, see `systemd.timer(5)`.\n> +\n> +\n>   BACKGROUND MAINTENANCE ON MACOS SYSTEMS\n>   ---------------------------------------\n>   \n> diff --git a/builtin/gc.c b/builtin/gc.c\n> index f2a81ecb44..5fe416c903 100644\n> --- a/builtin/gc.c\n> +++ b/builtin/gc.c\n> @@ -2040,10 +2040,221 @@ static int crontab_update_schedule(int run_maintenance, int fd)\n>   \treturn result;\n>   }\n>   \n> +#ifdef __linux__\n> +\n> +static int real_is_systemd_timer_available(void)\n> +{\n> +\tstruct child_process child = CHILD_PROCESS_INIT;\n> +\n> +\tstrvec_pushl(&child.args, \"systemctl\", \"--user\", \"list-timers\", NULL);\n> +\tchild.no_stdin = 1;\n> +\tchild.no_stdout = 1;\n> +\tchild.no_stderr = 1;\n> +\tchild.silent_exec_failure = 1;\n> +\n> +\tif (start_command(&child))\n> +\t\treturn 0;\n> +\tif (finish_command(&child))\n> +\t\treturn 0;\n> +\treturn 1;\n> +}\n> +\n> +#else\n> +\n> +static int real_is_systemd_timer_available(void)\n> +{\n> +\treturn 0;\n> +}\n> +\n> +#endif\n> +\n> +static int is_systemd_timer_available(void)\n> +{\n> +\tconst char *cmd = \"systemctl\";\n> +\tint is_available;\n> +\n> +\tif (get_schedule_cmd(&cmd, &is_available))\n> +\t\treturn is_available;\n> +\n> +\treturn real_is_systemd_timer_available();\n> +}\n> +\n> +static char *xdg_config_home_systemd(const char *filename)\n> +{\n> +\treturn xdg_config_home_for(\"systemd/user\", filename);\n> +}\n> +\n> +static int systemd_timer_enable_unit(int enable,\n> +\t\t\t\t     enum schedule_priority schedule)\n> +{\n> +\tconst char *cmd = \"systemctl\";\n> +\tstruct child_process child = CHILD_PROCESS_INIT;\n> +\tconst char *frequency = get_frequency(schedule);\n> +\n> +\t/*\n> +\t * Disabling the systemd unit while it is already disabled makes\n> +\t * systemctl print an error.\n> +\t * Let's ignore it since it means we already are in the expected state:\n> +\t * the unit is disabled.\n> +\t *\n> +\t * On the other hand, enabling a systemd unit which is already enabled\n> +\t * produces no error.\n> +\t */\n> +\tif (!enable)\n> +\t\tchild.no_stderr = 1;\n> +\n> +\tget_schedule_cmd(&cmd, NULL);\n> +\tstrvec_split(&child.args, cmd);\n> +\tstrvec_pushl(&child.args, \"--user\", enable ? \"enable\" : \"disable\",\n> +\t\t     \"--now\", NULL);\n> +\tstrvec_pushf(&child.args, \"git-maintenance@%s.timer\", frequency);\n> +\n> +\tif (start_command(&child))\n> +\t\treturn error(_(\"failed to start systemctl\"));\n> +\tif (finish_command(&child))\n> +\t\t/*\n> +\t\t * Disabling an already disabled systemd unit makes\n> +\t\t * systemctl fail.\n> +\t\t * Let's ignore this failure.\n> +\t\t *\n> +\t\t * Enabling an enabled systemd unit doesn't fail.\n> +\t\t */\n> +\t\tif (enable)\n> +\t\t\treturn error(_(\"failed to run systemctl\"));\n> +\treturn 0;\n> +}\n> +\n> +static int systemd_timer_delete_unit_templates(void)\n> +{\n> +\tint ret = 0;\n> +\tchar *filename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n> +\tif(unlink(filename) && !is_missing_file_error(errno))\n> +\t\tret = error_errno(_(\"failed to delete '%s'\"), filename);\n> +\tFREE_AND_NULL(filename);\n> +\n> +\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n> +\tif(unlink(filename) && !is_missing_file_error(errno))\n> +\t\tret = error_errno(_(\"failed to delete '%s'\"), filename);\n> +\n> +\tfree(filename);\n> +\treturn ret;\n> +}\n> +\n> +static int systemd_timer_delete_units(void)\n> +{\n> +\treturn systemd_timer_enable_unit(0, SCHEDULE_HOURLY) ||\n> +\t       systemd_timer_enable_unit(0, SCHEDULE_DAILY) ||\n> +\t       systemd_timer_enable_unit(0, SCHEDULE_WEEKLY) ||\n> +\t       systemd_timer_delete_unit_templates();\n> +}\n> +\n> +static int systemd_timer_write_unit_templates(const char *exec_path)\n> +{\n> +\tchar *filename;\n> +\tFILE *file;\n> +\tconst char *unit;\n> +\n> +\tfilename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n> +\tif (safe_create_leading_directories(filename)) {\n> +\t\terror(_(\"failed to create directories for '%s'\"), filename);\n> +\t\tgoto error;\n> +\t}\n> +\tfile = fopen_or_warn(filename, \"w\");\n> +\tif (file == NULL)\n> +\t\tgoto error;\n> +\tFREE_AND_NULL(filename);\n> +\n> +\tunit = \"# This file was created and is maintained by Git.\\n\"\n> +\t       \"# Any edits made in this file might be replaced in the future\\n\"\n> +\t       \"# by a Git command.\\n\"\n> +\t       \"\\n\"\n> +\t       \"[Unit]\\n\"\n> +\t       \"Description=Optimize Git repositories data\\n\"\n> +\t       \"\\n\"\n> +\t       \"[Timer]\\n\"\n> +\t       \"OnCalendar=%i\\n\"\n> +\t       \"Persistent=true\\n\"\n> +\t       \"\\n\"\n> +\t       \"[Install]\\n\"\n> +\t       \"WantedBy=timers.target\\n\";\n> +\tif (fputs(unit, file) == EOF) {\n> +\t\terror(_(\"failed to write to '%s'\"), filename);\n> +\t\tfclose(file);\n> +\t\tgoto error;\n> +\t}\n> +\tif (fclose(file) == EOF) {\n> +\t\terror_errno(_(\"failed to flush '%s'\"), filename);\n> +\t\tgoto error;\n> +\t}\n> +\n> +\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n> +\tfile = fopen_or_warn(filename, \"w\");\n> +\tif (file == NULL)\n> +\t\tgoto error;\n> +\tFREE_AND_NULL(filename);\n> +\n> +\tunit = \"# This file was created and is maintained by Git.\\n\"\n> +\t       \"# Any edits made in this file might be replaced in the future\\n\"\n> +\t       \"# by a Git command.\\n\"\n> +\t       \"\\n\"\n> +\t       \"[Unit]\\n\"\n> +\t       \"Description=Optimize Git repositories data\\n\"\n> +\t       \"\\n\"\n> +\t       \"[Service]\\n\"\n> +\t       \"Type=oneshot\\n\"\n> +\t       \"ExecStart=\\\"%s/git\\\" --exec-path=\\\"%s\\\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\\n\"\n> +\t       \"LockPersonality=yes\\n\"\n> +\t       \"MemoryDenyWriteExecute=yes\\n\"\n> +\t       \"NoNewPrivileges=yes\\n\"\n> +\t       \"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\\n\"\n> +\t       \"RestrictNamespaces=yes\\n\"\n> +\t       \"RestrictRealtime=yes\\n\"\n> +\t       \"RestrictSUIDSGID=yes\\n\"\n> +\t       \"SystemCallArchitectures=native\\n\"\n> +\t       \"SystemCallFilter=@system-service\\n\";\n> +\tif (fprintf(file, unit, exec_path, exec_path) < 0) {\n> +\t\terror(_(\"failed to write to '%s'\"), filename);\n> +\t\tfclose(file);\n> +\t\tgoto error;\n> +\t}\n> +\tif (fclose(file) == EOF) {\n> +\t\terror_errno(_(\"failed to flush '%s'\"), filename);\n> +\t\tgoto error;\n> +\t}\n> +\treturn 0;\n> +\n> +error:\n> +\tfree(filename);\n> +\tsystemd_timer_delete_unit_templates();\n\nOne small comment just to show I have read the patches - I think \nsystemd_timer_delete_unit_templates() ends up being called twice if we \nfail to write one of the template files as systemd_timer_setup_units() \ncalls it if this function fails. I don't think this matters and there is \nno need to change it - from a future maintenance perspective it is \nprobably safer to leave it as is.\n\nBest Wishes\n\nPhillip\n\n> +\treturn -1;\n> +}\n> +\n> +static int systemd_timer_setup_units(void)\n> +{\n> +\tconst char *exec_path = git_exec_path();\n> +\n> +\tint ret = systemd_timer_write_unit_templates(exec_path) ||\n> +\t          systemd_timer_enable_unit(1, SCHEDULE_HOURLY) ||\n> +\t          systemd_timer_enable_unit(1, SCHEDULE_DAILY) ||\n> +\t          systemd_timer_enable_unit(1, SCHEDULE_WEEKLY);\n> +\tif (ret)\n> +\t\tsystemd_timer_delete_units();\n> +\treturn ret;\n> +}\n> +\n> +static int systemd_timer_update_schedule(int run_maintenance, int fd)\n> +{\n> +\tif (run_maintenance)\n> +\t\treturn systemd_timer_setup_units();\n> +\telse\n> +\t\treturn systemd_timer_delete_units();\n> +}\n> +\n>   enum scheduler {\n>   \tSCHEDULER_INVALID = -1,\n>   \tSCHEDULER_AUTO,\n>   \tSCHEDULER_CRON,\n> +\tSCHEDULER_SYSTEMD,\n>   \tSCHEDULER_LAUNCHCTL,\n>   \tSCHEDULER_SCHTASKS,\n>   };\n> @@ -2058,6 +2269,11 @@ static const struct {\n>   \t\t.is_available = is_crontab_available,\n>   \t\t.update_schedule = crontab_update_schedule,\n>   \t},\n> +\t[SCHEDULER_SYSTEMD] = {\n> +\t\t.name = \"systemctl\",\n> +\t\t.is_available = is_systemd_timer_available,\n> +\t\t.update_schedule = systemd_timer_update_schedule,\n> +\t},\n>   \t[SCHEDULER_LAUNCHCTL] = {\n>   \t\t.name = \"launchctl\",\n>   \t\t.is_available = is_launchctl_available,\n> @@ -2078,6 +2294,9 @@ static enum scheduler parse_scheduler(const char *value)\n>   \t\treturn SCHEDULER_AUTO;\n>   \telse if (!strcasecmp(value, \"cron\") || !strcasecmp(value, \"crontab\"))\n>   \t\treturn SCHEDULER_CRON;\n> +\telse if (!strcasecmp(value, \"systemd\") ||\n> +\t\t !strcasecmp(value, \"systemd-timer\"))\n> +\t\treturn SCHEDULER_SYSTEMD;\n>   \telse if (!strcasecmp(value, \"launchctl\"))\n>   \t\treturn SCHEDULER_LAUNCHCTL;\n>   \telse if (!strcasecmp(value, \"schtasks\"))\n> @@ -2116,6 +2335,15 @@ static void resolve_auto_scheduler(enum scheduler *scheduler)\n>   \t*scheduler = SCHEDULER_SCHTASKS;\n>   \treturn;\n>   \n> +#elif defined(__linux__)\n> +\tif (is_systemd_timer_available())\n> +\t\t*scheduler = SCHEDULER_SYSTEMD;\n> +\telse if (is_crontab_available())\n> +\t\t*scheduler = SCHEDULER_CRON;\n> +\telse\n> +\t\tdie(_(\"neither systemd timers nor crontab are available\"));\n> +\treturn;\n> +\n>   #else\n>   \t*scheduler = SCHEDULER_CRON;\n>   \treturn;\n> diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\n> index b36b7f5fb0..b289cae6b9 100755\n> --- a/t/t7900-maintenance.sh\n> +++ b/t/t7900-maintenance.sh\n> @@ -20,6 +20,18 @@ test_xmllint () {\n>   \tfi\n>   }\n>   \n> +test_lazy_prereq SYSTEMD_ANALYZE '\n> +\tsystemd-analyze --help >out &&\n> +\tgrep verify out\n> +'\n> +\n> +test_systemd_analyze_verify () {\n> +\tif test_have_prereq SYSTEMD_ANALYZE\n> +\tthen\n> +\t\tsystemd-analyze verify \"$@\"\n> +\tfi\n> +}\n> +\n>   test_expect_success 'help text' '\n>   \ttest_expect_code 129 git maintenance -h 2>err &&\n>   \ttest_i18ngrep \"usage: git maintenance <subcommand>\" err &&\n> @@ -634,15 +646,56 @@ test_expect_success 'start and stop Windows maintenance' '\n>   \ttest_cmp expect args\n>   '\n>   \n> +test_expect_success 'start and stop Linux/systemd maintenance' '\n> +\twrite_script print-args <<-\\EOF &&\n> +\tprintf \"%s\\n\" \"$*\" >>args\n> +\tEOF\n> +\n> +\tXDG_CONFIG_HOME=\"$PWD\" &&\n> +\texport XDG_CONFIG_HOME &&\n> +\trm -f args &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args\" git maintenance start --scheduler=systemd-timer &&\n> +\n> +\t# start registers the repo\n> +\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n> +\n> +\ttest_systemd_analyze_verify \"systemd/user/git-maintenance@.service\" &&\n> +\n> +\tprintf -- \"--user enable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n> +\ttest_cmp expect args &&\n> +\n> +\trm -f args &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args\" git maintenance stop &&\n> +\n> +\t# stop does not unregister the repo\n> +\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n> +\n> +\ttest_path_is_missing \"systemd/user/git-maintenance@.timer\" &&\n> +\ttest_path_is_missing \"systemd/user/git-maintenance@.service\" &&\n> +\n> +\tprintf -- \"--user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n> +\ttest_cmp expect args\n> +'\n> +\n>   test_expect_success 'start and stop when several schedulers are available' '\n>   \twrite_script print-args <<-\\EOF &&\n>   \tprintf \"%s\\n\" \"$*\" | sed \"s:gui/[0-9][0-9]*:gui/[UID]:; s:\\(schtasks /create .* /xml\\).*:\\1:;\" >>args\n>   \tEOF\n>   \n>   \trm -f args &&\n> -\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n> -\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n> +\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=systemd-timer &&\n> +\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n>   \t\thourly daily weekly >expect &&\n> +\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n> +\t\thourly daily weekly >>expect &&\n> +\tprintf -- \"systemctl --user enable --now git-maintenance@%s.timer\\n\" hourly daily weekly >>expect &&\n> +\ttest_cmp expect args &&\n> +\n> +\trm -f args &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n> +\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n> +\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n> +\t\thourly daily weekly >>expect &&\n>   \tfor frequency in hourly daily weekly\n>   \tdo\n>   \t\tPLIST=\"$pfx/Library/LaunchAgents/org.git-scm.git.$frequency.plist\" &&\n> @@ -652,17 +705,19 @@ test_expect_success 'start and stop when several schedulers are available' '\n>   \ttest_cmp expect args &&\n>   \n>   \trm -f args &&\n> -\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n> +\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n>   \tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n> -\t\thourly daily weekly >expect &&\n> +\t\thourly daily weekly >>expect &&\n>   \tprintf \"schtasks /create /tn Git Maintenance (%s) /f /xml\\n\" \\\n>   \t\thourly daily weekly >>expect &&\n>   \ttest_cmp expect args &&\n>   \n>   \trm -f args &&\n> -\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n> +\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n>   \tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n> -\t\thourly daily weekly >expect &&\n> +\t\thourly daily weekly >>expect &&\n>   \tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n>   \t\thourly daily weekly >>expect &&\n>   \ttest_cmp expect args\n> \n"},{"id":"427188","messageId":"20210612165043.165579-1-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210608134000.663398-1-lenaic@lhuard.fr","subject":"[PATCH v6 0/3] maintenance: add support for systemd timers on Linux","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-06-12T16:50:40Z","receivedAt":"2021-06-12T16:51:10Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Hello,\n\nPlease find hereafter my fixed patchset to add support for systemd\ntimers on Linux for the `git maintenance start` command.\n\nThere are very few changes compared to the previous version.\nThe main change is the fix for the use-after-free bug that Jeff\npointed out.\nExcept from that, I’ve done some minor style fixes based on the `git\nclang-format --diff …` recommendations.\n\nThe patches are:\n\n* cache.h: Introduce a generic \"xdg_config_home_for(…)\" function\n\n  This patch introduces a function to compute configuration files\n  paths inside $XDG_CONFIG_HOME.\n\n  This patch is unchanged compared to its previous version.\n\n* maintenance: `git maintenance run` learned `--scheduler=<scheduler>`\n\n  This patch adds a new parameter to the `git maintenance run` to let\n  the user choose a scheduler.\n\n  The only changes in this patch compared to its previous version are\n  some code alignment changes that were suggested by `git clang-format\n  --diff …`\n\n* maintenance: add support for systemd timers on Linux\n\n  This patch implements the support of systemd timers on top of\n  crontab scheduler on Linux systems.\n\n  The only change in this patch compared to its previous version is\n  the fix of the use-after-free bug that Jeff pointed out.\n  I’ve moved the `free(filename)` after the last usage of `filename`.\n  I left the `free(filename)` below the `error` label as the\n  `filename` will always be allocated at each `goto error`.\n\nBest wishes,\nLénaïc.\n\nLénaïc Huard (3):\n  cache.h: Introduce a generic \"xdg_config_home_for(…)\" function\n  maintenance: `git maintenance run` learned `--scheduler=<scheduler>`\n  maintenance: add support for systemd timers on Linux\n\n Documentation/git-maintenance.txt |  60 ++++\n builtin/gc.c                      | 562 ++++++++++++++++++++++++++----\n cache.h                           |   7 +\n path.c                            |  13 +-\n t/t7900-maintenance.sh            | 110 +++++-\n 5 files changed, 674 insertions(+), 78 deletions(-)\n\n-- \n2.32.0\n\n"},{"id":"427189","messageId":"20210612165043.165579-2-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210612165043.165579-1-lenaic@lhuard.fr","subject":"[PATCH v6 1/3] cache.h: Introduce a generic \"xdg_config_home_for(…)\" function","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-06-12T16:50:41Z","receivedAt":"2021-06-12T16:51:10Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Current implementation of `xdg_config_home(filename)` returns\n`$XDG_CONFIG_HOME/git/$filename`, with the `git` subdirectory inserted\nbetween the `XDG_CONFIG_HOME` environment variable and the parameter.\n\nThis patch introduces a `xdg_config_home_for(subdir, filename)` function\nwhich is more generic. It only concatenates \"$XDG_CONFIG_HOME\", or\n\"$HOME/.config\" if the former isn’t defined, with the parameters,\nwithout adding `git` in between.\n\n`xdg_config_home(filename)` is now implemented by calling\n`xdg_config_home_for(\"git\", filename)` but this new generic function can\nbe used to compute the configuration directory of other programs.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n cache.h |  7 +++++++\n path.c  | 13 ++++++++++---\n 2 files changed, 17 insertions(+), 3 deletions(-)\n\ndiff --git a/cache.h b/cache.h\nindex ba04ff8bd3..2a0fb3e4ba 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -1286,6 +1286,13 @@ int is_ntfs_dotmailmap(const char *name);\n  */\n int looks_like_command_line_option(const char *str);\n \n+/**\n+ * Return a newly allocated string with the evaluation of\n+ * \"$XDG_CONFIG_HOME/$subdir/$filename\" if $XDG_CONFIG_HOME is non-empty, otherwise\n+ * \"$HOME/.config/$subdir/$filename\". Return NULL upon error.\n+ */\n+char *xdg_config_home_for(const char *subdir, const char *filename);\n+\n /**\n  * Return a newly allocated string with the evaluation of\n  * \"$XDG_CONFIG_HOME/git/$filename\" if $XDG_CONFIG_HOME is non-empty, otherwise\ndiff --git a/path.c b/path.c\nindex 7bccd830e9..1b1de3be09 100644\n--- a/path.c\n+++ b/path.c\n@@ -1503,21 +1503,28 @@ int looks_like_command_line_option(const char *str)\n \treturn str && str[0] == '-';\n }\n \n-char *xdg_config_home(const char *filename)\n+char *xdg_config_home_for(const char *subdir, const char *filename)\n {\n \tconst char *home, *config_home;\n \n+\tassert(subdir);\n \tassert(filename);\n \tconfig_home = getenv(\"XDG_CONFIG_HOME\");\n \tif (config_home && *config_home)\n-\t\treturn mkpathdup(\"%s/git/%s\", config_home, filename);\n+\t\treturn mkpathdup(\"%s/%s/%s\", config_home, subdir, filename);\n \n \thome = getenv(\"HOME\");\n \tif (home)\n-\t\treturn mkpathdup(\"%s/.config/git/%s\", home, filename);\n+\t\treturn mkpathdup(\"%s/.config/%s/%s\", home, subdir, filename);\n+\n \treturn NULL;\n }\n \n+char *xdg_config_home(const char *filename)\n+{\n+\treturn xdg_config_home_for(\"git\", filename);\n+}\n+\n char *xdg_cache_home(const char *filename)\n {\n \tconst char *home, *cache_home;\n-- \n2.32.0\n\n"},{"id":"427190","messageId":"20210612165043.165579-4-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210612165043.165579-1-lenaic@lhuard.fr","subject":"[PATCH v6 3/3] maintenance: add support for systemd timers on Linux","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-06-12T16:50:43Z","receivedAt":"2021-06-12T16:51:10Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"The existing mechanism for scheduling background maintenance is done\nthrough cron. On Linux systems managed by systemd, systemd provides an\nalternative to schedule recurring tasks: systemd timers.\n\nThe main motivations to implement systemd timers in addition to cron\nare:\n* cron is optional and Linux systems running systemd might not have it\n  installed.\n* The execution of `crontab -l` can tell us if cron is installed but not\n  if the daemon is actually running.\n* With systemd, each service is run in its own cgroup and its logs are\n  tagged by the service inside journald. With cron, all scheduled tasks\n  are running in the cron daemon cgroup and all the logs of the\n  user-scheduled tasks are pretended to belong to the system cron\n  service.\n  Concretely, a user that doesn’t have access to the system logs won’t\n  have access to the log of their own tasks scheduled by cron whereas\n  they will have access to the log of their own tasks scheduled by\n  systemd timer.\n  Although `cron` attempts to send email, that email may go unseen by\n  the user because these days, local mailboxes are not heavily used\n  anymore.\n\nIn order to schedule git maintenance, we need two unit template files:\n* ~/.config/systemd/user/git-maintenance@.service\n  to define the command to be started by systemd and\n* ~/.config/systemd/user/git-maintenance@.timer\n  to define the schedule at which the command should be run.\n\nThose units are templates that are parameterized by the frequency.\n\nBased on those templates, 3 timers are started:\n* git-maintenance@hourly.timer\n* git-maintenance@daily.timer\n* git-maintenance@weekly.timer\n\nThe command launched by those three timers are the same as with the\nother scheduling methods:\n\n/path/to/git for-each-repo --exec-path=/path/to\n--config=maintenance.repo maintenance run --schedule=%i\n\nwith the full path for git to ensure that the version of git launched\nfor the scheduled maintenance is the same as the one used to run\n`maintenance start`.\n\nThe timer unit contains `Persistent=true` so that, if the computer is\npowered down when a maintenance task should run, the task will be run\nwhen the computer is back powered on.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n Documentation/git-maintenance.txt |  57 +++++++-\n builtin/gc.c                      | 228 ++++++++++++++++++++++++++++++\n t/t7900-maintenance.sh            |  67 ++++++++-\n 3 files changed, 342 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\nindex 07065ed4f3..7b7dbbbca9 100644\n--- a/Documentation/git-maintenance.txt\n+++ b/Documentation/git-maintenance.txt\n@@ -179,16 +179,19 @@ OPTIONS\n \t`maintenance.<task>.enabled` configured as `true` are considered.\n \tSee the 'TASKS' section for the list of accepted `<task>` values.\n \n---scheduler=auto|crontab|launchctl|schtasks::\n+--scheduler=auto|crontab|systemd-timer|launchctl|schtasks::\n \tWhen combined with the `start` subcommand, specify the scheduler\n \tto use to run the hourly, daily and weekly executions of\n \t`git maintenance run`.\n \tThe possible values for `<scheduler>` depend on the system: `crontab`\n-\tis available on POSIX systems, `launchctl` is available on\n-\tMacOS and `schtasks` is available on Windows.\n+\tis available on POSIX systems, `systemd-timer` is available on Linux\n+\tsystems, `launchctl` is available on MacOS and `schtasks` is available\n+\ton Windows.\n \tBy default or when `auto` is specified, a suitable scheduler for\n \tthe system is used. On MacOS, `launchctl` is used. On Windows,\n-\t`schtasks` is used. On all other systems, `crontab` is used.\n+\t`schtasks` is used. On Linux, `systemd-timer` is used if user systemd\n+\ttimers are available, otherwise, `crontab` is used. On all other systems,\n+\t`crontab` is used.\n \n \n TROUBLESHOOTING\n@@ -288,6 +291,52 @@ schedule to ensure you are executing the correct binaries in your\n schedule.\n \n \n+BACKGROUND MAINTENANCE ON LINUX SYSTEMD SYSTEMS\n+-----------------------------------------------\n+\n+While Linux supports `cron`, depending on the distribution, `cron` may\n+be an optional package not necessarily installed. On modern Linux\n+distributions, systemd timers are superseding it.\n+\n+If user systemd timers are available, they will be used as a replacement\n+of `cron`.\n+\n+In this case, `git maintenance start` will create user systemd timer units\n+and start the timers. The current list of user-scheduled tasks can be found\n+by running `systemctl --user list-timers`. The timers written by `git\n+maintenance start` are similar to this:\n+\n+-----------------------------------------------------------------------\n+$ systemctl --user list-timers\n+NEXT                         LEFT          LAST                         PASSED     UNIT                         ACTIVATES\n+Thu 2021-04-29 19:00:00 CEST 42min left    Thu 2021-04-29 18:00:11 CEST 17min ago  git-maintenance@hourly.timer git-maintenance@hourly.service\n+Fri 2021-04-30 00:00:00 CEST 5h 42min left Thu 2021-04-29 00:00:11 CEST 18h ago    git-maintenance@daily.timer  git-maintenance@daily.service\n+Mon 2021-05-03 00:00:00 CEST 3 days left   Mon 2021-04-26 00:00:11 CEST 3 days ago git-maintenance@weekly.timer git-maintenance@weekly.service\n+-----------------------------------------------------------------------\n+\n+One timer is registered for each `--schedule=<frequency>` option.\n+\n+The definition of the systemd units can be inspected in the following files:\n+\n+-----------------------------------------------------------------------\n+~/.config/systemd/user/git-maintenance@.timer\n+~/.config/systemd/user/git-maintenance@.service\n+~/.config/systemd/user/timers.target.wants/git-maintenance@hourly.timer\n+~/.config/systemd/user/timers.target.wants/git-maintenance@daily.timer\n+~/.config/systemd/user/timers.target.wants/git-maintenance@weekly.timer\n+-----------------------------------------------------------------------\n+\n+`git maintenance start` will overwrite these files and start the timer\n+again with `systemctl --user`, so any customization should be done by\n+creating a drop-in file, i.e. a `.conf` suffixed file in the\n+`~/.config/systemd/user/git-maintenance@.service.d` directory.\n+\n+`git maintenance stop` will stop the user systemd timers and delete\n+the above mentioned files.\n+\n+For more details, see `systemd.timer(5)`.\n+\n+\n BACKGROUND MAINTENANCE ON MACOS SYSTEMS\n ---------------------------------------\n \ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 4a5ed7cb6f..7fe0e03b0e 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -2040,10 +2040,221 @@ static int crontab_update_schedule(int run_maintenance, int fd)\n \treturn result;\n }\n \n+#ifdef __linux__\n+\n+static int real_is_systemd_timer_available(void)\n+{\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\n+\tstrvec_pushl(&child.args, \"systemctl\", \"--user\", \"list-timers\", NULL);\n+\tchild.no_stdin = 1;\n+\tchild.no_stdout = 1;\n+\tchild.no_stderr = 1;\n+\tchild.silent_exec_failure = 1;\n+\n+\tif (start_command(&child))\n+\t\treturn 0;\n+\tif (finish_command(&child))\n+\t\treturn 0;\n+\treturn 1;\n+}\n+\n+#else\n+\n+static int real_is_systemd_timer_available(void)\n+{\n+\treturn 0;\n+}\n+\n+#endif\n+\n+static int is_systemd_timer_available(void)\n+{\n+\tconst char *cmd = \"systemctl\";\n+\tint is_available;\n+\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+\treturn real_is_systemd_timer_available();\n+}\n+\n+static char *xdg_config_home_systemd(const char *filename)\n+{\n+\treturn xdg_config_home_for(\"systemd/user\", filename);\n+}\n+\n+static int systemd_timer_enable_unit(int enable,\n+\t\t\t\t     enum schedule_priority schedule)\n+{\n+\tconst char *cmd = \"systemctl\";\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\tconst char *frequency = get_frequency(schedule);\n+\n+\t/*\n+\t * Disabling the systemd unit while it is already disabled makes\n+\t * systemctl print an error.\n+\t * Let's ignore it since it means we already are in the expected state:\n+\t * the unit is disabled.\n+\t *\n+\t * On the other hand, enabling a systemd unit which is already enabled\n+\t * produces no error.\n+\t */\n+\tif (!enable)\n+\t\tchild.no_stderr = 1;\n+\n+\tget_schedule_cmd(&cmd, NULL);\n+\tstrvec_split(&child.args, cmd);\n+\tstrvec_pushl(&child.args, \"--user\", enable ? \"enable\" : \"disable\",\n+\t\t     \"--now\", NULL);\n+\tstrvec_pushf(&child.args, \"git-maintenance@%s.timer\", frequency);\n+\n+\tif (start_command(&child))\n+\t\treturn error(_(\"failed to start systemctl\"));\n+\tif (finish_command(&child))\n+\t\t/*\n+\t\t * Disabling an already disabled systemd unit makes\n+\t\t * systemctl fail.\n+\t\t * Let's ignore this failure.\n+\t\t *\n+\t\t * Enabling an enabled systemd unit doesn't fail.\n+\t\t */\n+\t\tif (enable)\n+\t\t\treturn error(_(\"failed to run systemctl\"));\n+\treturn 0;\n+}\n+\n+static int systemd_timer_delete_unit_templates(void)\n+{\n+\tint ret = 0;\n+\tchar *filename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n+\tif (unlink(filename) && !is_missing_file_error(errno))\n+\t\tret = error_errno(_(\"failed to delete '%s'\"), filename);\n+\tFREE_AND_NULL(filename);\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n+\tif (unlink(filename) && !is_missing_file_error(errno))\n+\t\tret = error_errno(_(\"failed to delete '%s'\"), filename);\n+\n+\tfree(filename);\n+\treturn ret;\n+}\n+\n+static int systemd_timer_delete_units(void)\n+{\n+\treturn systemd_timer_enable_unit(0, SCHEDULE_HOURLY) ||\n+\t       systemd_timer_enable_unit(0, SCHEDULE_DAILY) ||\n+\t       systemd_timer_enable_unit(0, SCHEDULE_WEEKLY) ||\n+\t       systemd_timer_delete_unit_templates();\n+}\n+\n+static int systemd_timer_write_unit_templates(const char *exec_path)\n+{\n+\tchar *filename;\n+\tFILE *file;\n+\tconst char *unit;\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n+\tif (safe_create_leading_directories(filename)) {\n+\t\terror(_(\"failed to create directories for '%s'\"), filename);\n+\t\tgoto error;\n+\t}\n+\tfile = fopen_or_warn(filename, \"w\");\n+\tif (file == NULL)\n+\t\tgoto error;\n+\n+\tunit = \"# This file was created and is maintained by Git.\\n\"\n+\t       \"# Any edits made in this file might be replaced in the future\\n\"\n+\t       \"# by a Git command.\\n\"\n+\t       \"\\n\"\n+\t       \"[Unit]\\n\"\n+\t       \"Description=Optimize Git repositories data\\n\"\n+\t       \"\\n\"\n+\t       \"[Timer]\\n\"\n+\t       \"OnCalendar=%i\\n\"\n+\t       \"Persistent=true\\n\"\n+\t       \"\\n\"\n+\t       \"[Install]\\n\"\n+\t       \"WantedBy=timers.target\\n\";\n+\tif (fputs(unit, file) == EOF) {\n+\t\terror(_(\"failed to write to '%s'\"), filename);\n+\t\tfclose(file);\n+\t\tgoto error;\n+\t}\n+\tif (fclose(file) == EOF) {\n+\t\terror_errno(_(\"failed to flush '%s'\"), filename);\n+\t\tgoto error;\n+\t}\n+\tfree(filename);\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n+\tfile = fopen_or_warn(filename, \"w\");\n+\tif (file == NULL)\n+\t\tgoto error;\n+\n+\tunit = \"# This file was created and is maintained by Git.\\n\"\n+\t       \"# Any edits made in this file might be replaced in the future\\n\"\n+\t       \"# by a Git command.\\n\"\n+\t       \"\\n\"\n+\t       \"[Unit]\\n\"\n+\t       \"Description=Optimize Git repositories data\\n\"\n+\t       \"\\n\"\n+\t       \"[Service]\\n\"\n+\t       \"Type=oneshot\\n\"\n+\t       \"ExecStart=\\\"%s/git\\\" --exec-path=\\\"%s\\\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\\n\"\n+\t       \"LockPersonality=yes\\n\"\n+\t       \"MemoryDenyWriteExecute=yes\\n\"\n+\t       \"NoNewPrivileges=yes\\n\"\n+\t       \"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\\n\"\n+\t       \"RestrictNamespaces=yes\\n\"\n+\t       \"RestrictRealtime=yes\\n\"\n+\t       \"RestrictSUIDSGID=yes\\n\"\n+\t       \"SystemCallArchitectures=native\\n\"\n+\t       \"SystemCallFilter=@system-service\\n\";\n+\tif (fprintf(file, unit, exec_path, exec_path) < 0) {\n+\t\terror(_(\"failed to write to '%s'\"), filename);\n+\t\tfclose(file);\n+\t\tgoto error;\n+\t}\n+\tif (fclose(file) == EOF) {\n+\t\terror_errno(_(\"failed to flush '%s'\"), filename);\n+\t\tgoto error;\n+\t}\n+\tfree(filename);\n+\treturn 0;\n+\n+error:\n+\tfree(filename);\n+\tsystemd_timer_delete_unit_templates();\n+\treturn -1;\n+}\n+\n+static int systemd_timer_setup_units(void)\n+{\n+\tconst char *exec_path = git_exec_path();\n+\n+\tint ret = systemd_timer_write_unit_templates(exec_path) ||\n+\t          systemd_timer_enable_unit(1, SCHEDULE_HOURLY) ||\n+\t          systemd_timer_enable_unit(1, SCHEDULE_DAILY) ||\n+\t          systemd_timer_enable_unit(1, SCHEDULE_WEEKLY);\n+\tif (ret)\n+\t\tsystemd_timer_delete_units();\n+\treturn ret;\n+}\n+\n+static int systemd_timer_update_schedule(int run_maintenance, int fd)\n+{\n+\tif (run_maintenance)\n+\t\treturn systemd_timer_setup_units();\n+\telse\n+\t\treturn systemd_timer_delete_units();\n+}\n+\n enum scheduler {\n \tSCHEDULER_INVALID = -1,\n \tSCHEDULER_AUTO,\n \tSCHEDULER_CRON,\n+\tSCHEDULER_SYSTEMD,\n \tSCHEDULER_LAUNCHCTL,\n \tSCHEDULER_SCHTASKS,\n };\n@@ -2058,6 +2269,11 @@ static const struct {\n \t\t.is_available = is_crontab_available,\n \t\t.update_schedule = crontab_update_schedule,\n \t},\n+\t[SCHEDULER_SYSTEMD] = {\n+\t\t.name = \"systemctl\",\n+\t\t.is_available = is_systemd_timer_available,\n+\t\t.update_schedule = systemd_timer_update_schedule,\n+\t},\n \t[SCHEDULER_LAUNCHCTL] = {\n \t\t.name = \"launchctl\",\n \t\t.is_available = is_launchctl_available,\n@@ -2078,6 +2294,9 @@ static enum scheduler parse_scheduler(const char *value)\n \t\treturn SCHEDULER_AUTO;\n \telse if (!strcasecmp(value, \"cron\") || !strcasecmp(value, \"crontab\"))\n \t\treturn SCHEDULER_CRON;\n+\telse if (!strcasecmp(value, \"systemd\") ||\n+\t\t !strcasecmp(value, \"systemd-timer\"))\n+\t\treturn SCHEDULER_SYSTEMD;\n \telse if (!strcasecmp(value, \"launchctl\"))\n \t\treturn SCHEDULER_LAUNCHCTL;\n \telse if (!strcasecmp(value, \"schtasks\"))\n@@ -2116,6 +2335,15 @@ static void resolve_auto_scheduler(enum scheduler *scheduler)\n \t*scheduler = SCHEDULER_SCHTASKS;\n \treturn;\n \n+#elif defined(__linux__)\n+\tif (is_systemd_timer_available())\n+\t\t*scheduler = SCHEDULER_SYSTEMD;\n+\telse if (is_crontab_available())\n+\t\t*scheduler = SCHEDULER_CRON;\n+\telse\n+\t\tdie(_(\"neither systemd timers nor crontab are available\"));\n+\treturn;\n+\n #else\n \t*scheduler = SCHEDULER_CRON;\n \treturn;\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex b36b7f5fb0..b289cae6b9 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -20,6 +20,18 @@ test_xmllint () {\n \tfi\n }\n \n+test_lazy_prereq SYSTEMD_ANALYZE '\n+\tsystemd-analyze --help >out &&\n+\tgrep verify out\n+'\n+\n+test_systemd_analyze_verify () {\n+\tif test_have_prereq SYSTEMD_ANALYZE\n+\tthen\n+\t\tsystemd-analyze verify \"$@\"\n+\tfi\n+}\n+\n test_expect_success 'help text' '\n \ttest_expect_code 129 git maintenance -h 2>err &&\n \ttest_i18ngrep \"usage: git maintenance <subcommand>\" err &&\n@@ -634,15 +646,56 @@ test_expect_success 'start and stop Windows maintenance' '\n \ttest_cmp expect args\n '\n \n+test_expect_success 'start and stop Linux/systemd maintenance' '\n+\twrite_script print-args <<-\\EOF &&\n+\tprintf \"%s\\n\" \"$*\" >>args\n+\tEOF\n+\n+\tXDG_CONFIG_HOME=\"$PWD\" &&\n+\texport XDG_CONFIG_HOME &&\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args\" git maintenance start --scheduler=systemd-timer &&\n+\n+\t# start registers the repo\n+\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n+\n+\ttest_systemd_analyze_verify \"systemd/user/git-maintenance@.service\" &&\n+\n+\tprintf -- \"--user enable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args\" git maintenance stop &&\n+\n+\t# stop does not unregister the repo\n+\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n+\n+\ttest_path_is_missing \"systemd/user/git-maintenance@.timer\" &&\n+\ttest_path_is_missing \"systemd/user/git-maintenance@.service\" &&\n+\n+\tprintf -- \"--user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n+\ttest_cmp expect args\n+'\n+\n test_expect_success 'start and stop when several schedulers are available' '\n \twrite_script print-args <<-\\EOF &&\n \tprintf \"%s\\n\" \"$*\" | sed \"s:gui/[0-9][0-9]*:gui/[UID]:; s:\\(schtasks /create .* /xml\\).*:\\1:;\" >>args\n \tEOF\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n-\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=systemd-timer &&\n+\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n \t\thourly daily weekly >expect &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\tprintf -- \"systemctl --user enable --now git-maintenance@%s.timer\\n\" hourly daily weekly >>expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n+\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >>expect &&\n \tfor frequency in hourly daily weekly\n \tdo\n \t\tPLIST=\"$pfx/Library/LaunchAgents/org.git-scm.git.$frequency.plist\" &&\n@@ -652,17 +705,19 @@ test_expect_success 'start and stop when several schedulers are available' '\n \ttest_cmp expect args &&\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n+\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n \tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n-\t\thourly daily weekly >expect &&\n+\t\thourly daily weekly >>expect &&\n \tprintf \"schtasks /create /tn Git Maintenance (%s) /f /xml\\n\" \\\n \t\thourly daily weekly >>expect &&\n \ttest_cmp expect args &&\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n+\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n \tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n-\t\thourly daily weekly >expect &&\n+\t\thourly daily weekly >>expect &&\n \tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n \t\thourly daily weekly >>expect &&\n \ttest_cmp expect args\n-- \n2.32.0\n\n"},{"id":"427191","messageId":"20210612165043.165579-3-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210612165043.165579-1-lenaic@lhuard.fr","subject":"[PATCH v6 2/3] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-06-12T16:50:42Z","receivedAt":"2021-06-12T16:51:10Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Depending on the system, different schedulers can be used to schedule\nthe hourly, daily and weekly executions of `git maintenance run`:\n* `launchctl` for MacOS,\n* `schtasks` for Windows and\n* `crontab` for everything else.\n\n`git maintenance run` now has an option to let the end-user explicitly\nchoose which scheduler he wants to use:\n`--scheduler=auto|crontab|launchctl|schtasks`.\n\nWhen `git maintenance start --scheduler=XXX` is run, it not only\nregisters `git maintenance run` tasks in the scheduler XXX, it also\nremoves the `git maintenance run` tasks from all the other schedulers to\nensure we cannot have two schedulers launching concurrent identical\ntasks.\n\nThe default value is `auto` which chooses a suitable scheduler for the\nsystem.\n\n`git maintenance stop` doesn't have any `--scheduler` parameter because\nthis command will try to remove the `git maintenance run` tasks from all\nthe available schedulers.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n Documentation/git-maintenance.txt |  11 +\n builtin/gc.c                      | 334 +++++++++++++++++++++++-------\n t/t7900-maintenance.sh            |  55 ++++-\n 3 files changed, 325 insertions(+), 75 deletions(-)\n\ndiff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\nindex 1e738ad398..07065ed4f3 100644\n--- a/Documentation/git-maintenance.txt\n+++ b/Documentation/git-maintenance.txt\n@@ -179,6 +179,17 @@ OPTIONS\n \t`maintenance.<task>.enabled` configured as `true` are considered.\n \tSee the 'TASKS' section for the list of accepted `<task>` values.\n \n+--scheduler=auto|crontab|launchctl|schtasks::\n+\tWhen combined with the `start` subcommand, specify the scheduler\n+\tto use to run the hourly, daily and weekly executions of\n+\t`git maintenance run`.\n+\tThe possible values for `<scheduler>` depend on the system: `crontab`\n+\tis available on POSIX systems, `launchctl` is available on\n+\tMacOS and `schtasks` is available on Windows.\n+\tBy default or when `auto` is specified, a suitable scheduler for\n+\tthe system is used. On MacOS, `launchctl` is used. On Windows,\n+\t`schtasks` is used. On all other systems, `crontab` is used.\n+\n \n TROUBLESHOOTING\n ---------------\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex f05d2f0a1a..4a5ed7cb6f 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1529,6 +1529,59 @@ static const char *get_frequency(enum schedule_priority schedule)\n \t}\n }\n \n+static int get_schedule_cmd(const char **cmd, int *is_available)\n+{\n+\tchar *item;\n+\tchar *testing = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n+\n+\tif (!testing)\n+\t\treturn 0;\n+\n+\tif (is_available)\n+\t\t*is_available = 0;\n+\n+\tfor (item = testing;;) {\n+\t\tchar *sep;\n+\t\tchar *end_item = strchr(item, ',');\n+\t\tif (end_item)\n+\t\t\t*end_item = '\\0';\n+\n+\t\tsep = strchr(item, ':');\n+\t\tif (!sep)\n+\t\t\tdie(\"GIT_TEST_MAINT_SCHEDULER unparseable: %s\", testing);\n+\t\t*sep = '\\0';\n+\n+\t\tif (!strcmp(*cmd, item)) {\n+\t\t\t*cmd = sep + 1;\n+\t\t\tif (is_available)\n+\t\t\t\t*is_available = 1;\n+\t\t\tUNLEAK(testing);\n+\t\t\treturn 1;\n+\t\t}\n+\n+\t\tif (!end_item)\n+\t\t\tbreak;\n+\t\titem = end_item + 1;\n+\t}\n+\n+\tfree(testing);\n+\treturn 1;\n+}\n+\n+static int is_launchctl_available(void)\n+{\n+\tconst char *cmd = \"launchctl\";\n+\tint is_available;\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+#ifdef __APPLE__\n+\treturn 1;\n+#else\n+\treturn 0;\n+#endif\n+}\n+\n static char *launchctl_service_name(const char *frequency)\n {\n \tstruct strbuf label = STRBUF_INIT;\n@@ -1555,19 +1608,17 @@ static char *launchctl_get_uid(void)\n \treturn xstrfmt(\"gui/%d\", getuid());\n }\n \n-static int launchctl_boot_plist(int enable, const char *filename, const char *cmd)\n+static int launchctl_boot_plist(int enable, const char *filename)\n {\n+\tconst char *cmd = \"launchctl\";\n \tint result;\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tchar *uid = launchctl_get_uid();\n \n+\tget_schedule_cmd(&cmd, NULL);\n \tstrvec_split(&child.args, cmd);\n-\tif (enable)\n-\t\tstrvec_push(&child.args, \"bootstrap\");\n-\telse\n-\t\tstrvec_push(&child.args, \"bootout\");\n-\tstrvec_push(&child.args, uid);\n-\tstrvec_push(&child.args, filename);\n+\tstrvec_pushl(&child.args, enable ? \"bootstrap\" : \"bootout\", uid,\n+\t\t     filename, NULL);\n \n \tchild.no_stderr = 1;\n \tchild.no_stdout = 1;\n@@ -1581,26 +1632,26 @@ static int launchctl_boot_plist(int enable, const char *filename, const char *cm\n \treturn result;\n }\n \n-static int launchctl_remove_plist(enum schedule_priority schedule, const char *cmd)\n+static int launchctl_remove_plist(enum schedule_priority schedule)\n {\n \tconst char *frequency = get_frequency(schedule);\n \tchar *name = launchctl_service_name(frequency);\n \tchar *filename = launchctl_service_filename(name);\n-\tint result = launchctl_boot_plist(0, filename, cmd);\n+\tint result = launchctl_boot_plist(0, filename);\n \tunlink(filename);\n \tfree(filename);\n \tfree(name);\n \treturn result;\n }\n \n-static int launchctl_remove_plists(const char *cmd)\n+static int launchctl_remove_plists(void)\n {\n-\treturn launchctl_remove_plist(SCHEDULE_HOURLY, cmd) ||\n-\t\tlaunchctl_remove_plist(SCHEDULE_DAILY, cmd) ||\n-\t\tlaunchctl_remove_plist(SCHEDULE_WEEKLY, cmd);\n+\treturn launchctl_remove_plist(SCHEDULE_HOURLY) ||\n+\t       launchctl_remove_plist(SCHEDULE_DAILY) ||\n+\t       launchctl_remove_plist(SCHEDULE_WEEKLY);\n }\n \n-static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule, const char *cmd)\n+static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule)\n {\n \tFILE *plist;\n \tint i;\n@@ -1669,8 +1720,8 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n \tfclose(plist);\n \n \t/* bootout might fail if not already running, so ignore */\n-\tlaunchctl_boot_plist(0, filename, cmd);\n-\tif (launchctl_boot_plist(1, filename, cmd))\n+\tlaunchctl_boot_plist(0, filename);\n+\tif (launchctl_boot_plist(1, filename))\n \t\tdie(_(\"failed to bootstrap service %s\"), filename);\n \n \tfree(filename);\n@@ -1678,21 +1729,35 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n \treturn 0;\n }\n \n-static int launchctl_add_plists(const char *cmd)\n+static int launchctl_add_plists(void)\n {\n \tconst char *exec_path = git_exec_path();\n \n-\treturn launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY, cmd) ||\n-\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_DAILY, cmd) ||\n-\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY, cmd);\n+\treturn launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY) ||\n+\t       launchctl_schedule_plist(exec_path, SCHEDULE_DAILY) ||\n+\t       launchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY);\n }\n \n-static int launchctl_update_schedule(int run_maintenance, int fd, const char *cmd)\n+static int launchctl_update_schedule(int run_maintenance, int fd)\n {\n \tif (run_maintenance)\n-\t\treturn launchctl_add_plists(cmd);\n+\t\treturn launchctl_add_plists();\n \telse\n-\t\treturn launchctl_remove_plists(cmd);\n+\t\treturn launchctl_remove_plists();\n+}\n+\n+static int is_schtasks_available(void)\n+{\n+\tconst char *cmd = \"schtasks\";\n+\tint is_available;\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+#ifdef GIT_WINDOWS_NATIVE\n+\treturn 1;\n+#else\n+\treturn 0;\n+#endif\n }\n \n static char *schtasks_task_name(const char *frequency)\n@@ -1702,13 +1767,15 @@ static char *schtasks_task_name(const char *frequency)\n \treturn strbuf_detach(&label, NULL);\n }\n \n-static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd)\n+static int schtasks_remove_task(enum schedule_priority schedule)\n {\n+\tconst char *cmd = \"schtasks\";\n \tint result;\n \tstruct strvec args = STRVEC_INIT;\n \tconst char *frequency = get_frequency(schedule);\n \tchar *name = schtasks_task_name(frequency);\n \n+\tget_schedule_cmd(&cmd, NULL);\n \tstrvec_split(&args, cmd);\n \tstrvec_pushl(&args, \"/delete\", \"/tn\", name, \"/f\", NULL);\n \n@@ -1719,15 +1786,16 @@ static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd\n \treturn result;\n }\n \n-static int schtasks_remove_tasks(const char *cmd)\n+static int schtasks_remove_tasks(void)\n {\n-\treturn schtasks_remove_task(SCHEDULE_HOURLY, cmd) ||\n-\t\tschtasks_remove_task(SCHEDULE_DAILY, cmd) ||\n-\t\tschtasks_remove_task(SCHEDULE_WEEKLY, cmd);\n+\treturn schtasks_remove_task(SCHEDULE_HOURLY) ||\n+\t       schtasks_remove_task(SCHEDULE_DAILY) ||\n+\t       schtasks_remove_task(SCHEDULE_WEEKLY);\n }\n \n-static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule, const char *cmd)\n+static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule)\n {\n+\tconst char *cmd = \"schtasks\";\n \tint result;\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tconst char *xml;\n@@ -1736,6 +1804,8 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority\n \tchar *name = schtasks_task_name(frequency);\n \tstruct strbuf tfilename = STRBUF_INIT;\n \n+\tget_schedule_cmd(&cmd, NULL);\n+\n \tstrbuf_addf(&tfilename, \"%s/schedule_%s_XXXXXX\",\n \t\t    get_git_common_dir(), frequency);\n \ttfile = xmks_tempfile(tfilename.buf);\n@@ -1840,28 +1910,52 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority\n \treturn result;\n }\n \n-static int schtasks_schedule_tasks(const char *cmd)\n+static int schtasks_schedule_tasks(void)\n {\n \tconst char *exec_path = git_exec_path();\n \n-\treturn schtasks_schedule_task(exec_path, SCHEDULE_HOURLY, cmd) ||\n-\t\tschtasks_schedule_task(exec_path, SCHEDULE_DAILY, cmd) ||\n-\t\tschtasks_schedule_task(exec_path, SCHEDULE_WEEKLY, cmd);\n+\treturn schtasks_schedule_task(exec_path, SCHEDULE_HOURLY) ||\n+\t       schtasks_schedule_task(exec_path, SCHEDULE_DAILY) ||\n+\t       schtasks_schedule_task(exec_path, SCHEDULE_WEEKLY);\n }\n \n-static int schtasks_update_schedule(int run_maintenance, int fd, const char *cmd)\n+static int schtasks_update_schedule(int run_maintenance, int fd)\n {\n \tif (run_maintenance)\n-\t\treturn schtasks_schedule_tasks(cmd);\n+\t\treturn schtasks_schedule_tasks();\n \telse\n-\t\treturn schtasks_remove_tasks(cmd);\n+\t\treturn schtasks_remove_tasks();\n+}\n+\n+static int is_crontab_available(void)\n+{\n+\tconst char *cmd = \"crontab\";\n+\tint is_available;\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\n+\tif (get_schedule_cmd(&cmd, &is_available) && !is_available)\n+\t\treturn 0;\n+\n+\tstrvec_split(&child.args, cmd);\n+\tstrvec_push(&child.args, \"-l\");\n+\tchild.no_stdin = 1;\n+\tchild.no_stdout = 1;\n+\tchild.no_stderr = 1;\n+\tchild.silent_exec_failure = 1;\n+\n+\tif (start_command(&child))\n+\t\treturn 0;\n+\t/* Ignore exit code, as an empty crontab will return error. */\n+\tfinish_command(&child);\n+\treturn 1;\n }\n \n #define BEGIN_LINE \"# BEGIN GIT MAINTENANCE SCHEDULE\"\n #define END_LINE \"# END GIT MAINTENANCE SCHEDULE\"\n \n-static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n+static int crontab_update_schedule(int run_maintenance, int fd)\n {\n+\tconst char *cmd = \"crontab\";\n \tint result = 0;\n \tint in_old_region = 0;\n \tstruct child_process crontab_list = CHILD_PROCESS_INIT;\n@@ -1869,6 +1963,7 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n \tFILE *cron_list, *cron_in;\n \tstruct strbuf line = STRBUF_INIT;\n \n+\tget_schedule_cmd(&cmd, NULL);\n \tstrvec_split(&crontab_list.args, cmd);\n \tstrvec_push(&crontab_list.args, \"-l\");\n \tcrontab_list.in = -1;\n@@ -1945,66 +2040,163 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n \treturn result;\n }\n \n+enum scheduler {\n+\tSCHEDULER_INVALID = -1,\n+\tSCHEDULER_AUTO,\n+\tSCHEDULER_CRON,\n+\tSCHEDULER_LAUNCHCTL,\n+\tSCHEDULER_SCHTASKS,\n+};\n+\n+static const struct {\n+\tconst char *name;\n+\tint (*is_available)(void);\n+\tint (*update_schedule)(int run_maintenance, int fd);\n+} scheduler_fn[] = {\n+\t[SCHEDULER_CRON] = {\n+\t\t.name = \"crontab\",\n+\t\t.is_available = is_crontab_available,\n+\t\t.update_schedule = crontab_update_schedule,\n+\t},\n+\t[SCHEDULER_LAUNCHCTL] = {\n+\t\t.name = \"launchctl\",\n+\t\t.is_available = is_launchctl_available,\n+\t\t.update_schedule = launchctl_update_schedule,\n+\t},\n+\t[SCHEDULER_SCHTASKS] = {\n+\t\t.name = \"schtasks\",\n+\t\t.is_available = is_schtasks_available,\n+\t\t.update_schedule = schtasks_update_schedule,\n+\t},\n+};\n+\n+static enum scheduler parse_scheduler(const char *value)\n+{\n+\tif (!value)\n+\t\treturn SCHEDULER_INVALID;\n+\telse if (!strcasecmp(value, \"auto\"))\n+\t\treturn SCHEDULER_AUTO;\n+\telse if (!strcasecmp(value, \"cron\") || !strcasecmp(value, \"crontab\"))\n+\t\treturn SCHEDULER_CRON;\n+\telse if (!strcasecmp(value, \"launchctl\"))\n+\t\treturn SCHEDULER_LAUNCHCTL;\n+\telse if (!strcasecmp(value, \"schtasks\"))\n+\t\treturn SCHEDULER_SCHTASKS;\n+\telse\n+\t\treturn SCHEDULER_INVALID;\n+}\n+\n+static int maintenance_opt_scheduler(const struct option *opt, const char *arg,\n+\t\t\t\t     int unset)\n+{\n+\tenum scheduler *scheduler = opt->value;\n+\n+\tBUG_ON_OPT_NEG(unset);\n+\n+\t*scheduler = parse_scheduler(arg);\n+\tif (*scheduler == SCHEDULER_INVALID)\n+\t\treturn error(_(\"unrecognized --scheduler argument '%s'\"), arg);\n+\treturn 0;\n+}\n+\n+struct maintenance_start_opts {\n+\tenum scheduler scheduler;\n+};\n+\n+static void resolve_auto_scheduler(enum scheduler *scheduler)\n+{\n+\tif (*scheduler != SCHEDULER_AUTO)\n+\t\treturn;\n+\n #if defined(__APPLE__)\n-static const char platform_scheduler[] = \"launchctl\";\n+\t*scheduler = SCHEDULER_LAUNCHCTL;\n+\treturn;\n+\n #elif defined(GIT_WINDOWS_NATIVE)\n-static const char platform_scheduler[] = \"schtasks\";\n+\t*scheduler = SCHEDULER_SCHTASKS;\n+\treturn;\n+\n #else\n-static const char platform_scheduler[] = \"crontab\";\n+\t*scheduler = SCHEDULER_CRON;\n+\treturn;\n #endif\n+}\n \n-static int update_background_schedule(int enable)\n+static void validate_scheduler(enum scheduler scheduler)\n {\n-\tint result;\n-\tconst char *scheduler = platform_scheduler;\n-\tconst char *cmd = scheduler;\n-\tchar *testing;\n+\tif (scheduler == SCHEDULER_INVALID)\n+\t\tBUG(\"invalid scheduler\");\n+\tif (scheduler == SCHEDULER_AUTO)\n+\t\tBUG(\"resolve_auto_scheduler should have been called before\");\n+\n+\tif (!scheduler_fn[scheduler].is_available())\n+\t\tdie(_(\"%s scheduler is not available\"),\n+\t\t    scheduler_fn[scheduler].name);\n+}\n+\n+static int update_background_schedule(const struct maintenance_start_opts *opts,\n+\t\t\t\t      int enable)\n+{\n+\tunsigned int i;\n+\tint result = 0;\n \tstruct lock_file lk;\n \tchar *lock_path = xstrfmt(\"%s/schedule\", the_repository->objects->odb->path);\n \n-\ttesting = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n-\tif (testing) {\n-\t\tchar *sep = strchr(testing, ':');\n-\t\tif (!sep)\n-\t\t\tdie(\"GIT_TEST_MAINT_SCHEDULER unparseable: %s\", testing);\n-\t\t*sep = '\\0';\n-\t\tscheduler = testing;\n-\t\tcmd = sep + 1;\n+\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {\n+\t\tfree(lock_path);\n+\t\treturn error(_(\"another process is scheduling background maintenance\"));\n \t}\n \n-\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {\n-\t\tresult = error(_(\"another process is scheduling background maintenance\"));\n-\t\tgoto cleanup;\n+\tfor (i = 1; i < ARRAY_SIZE(scheduler_fn); i++) {\n+\t\tif (enable && opts->scheduler == i)\n+\t\t\tcontinue;\n+\t\tif (!scheduler_fn[i].is_available())\n+\t\t\tcontinue;\n+\t\tscheduler_fn[i].update_schedule(0, get_lock_file_fd(&lk));\n \t}\n \n-\tif (!strcmp(scheduler, \"launchctl\"))\n-\t\tresult = launchctl_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse if (!strcmp(scheduler, \"schtasks\"))\n-\t\tresult = schtasks_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse if (!strcmp(scheduler, \"crontab\"))\n-\t\tresult = crontab_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse\n-\t\tdie(\"unknown background scheduler: %s\", scheduler);\n+\tif (enable)\n+\t\tresult = scheduler_fn[opts->scheduler].update_schedule(\n+\t\t\t1, get_lock_file_fd(&lk));\n \n \trollback_lock_file(&lk);\n \n-cleanup:\n \tfree(lock_path);\n-\tfree(testing);\n \treturn result;\n }\n \n-static int maintenance_start(void)\n+static const char *const builtin_maintenance_start_usage[] = {\n+\tN_(\"git maintenance start [--scheduler=<scheduler>]\"),\n+\tNULL\n+};\n+\n+static int maintenance_start(int argc, const char **argv, const char *prefix)\n {\n+\tstruct maintenance_start_opts opts = { 0 };\n+\tstruct option options[] = {\n+\t\tOPT_CALLBACK_F(\n+\t\t\t0, \"scheduler\", &opts.scheduler, N_(\"scheduler\"),\n+\t\t\tN_(\"scheduler to use to trigger git maintenance run\"),\n+\t\t\tPARSE_OPT_NONEG, maintenance_opt_scheduler),\n+\t\tOPT_END()\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     builtin_maintenance_start_usage, 0);\n+\tif (argc)\n+\t\tusage_with_options(builtin_maintenance_start_usage, options);\n+\n+\tresolve_auto_scheduler(&opts.scheduler);\n+\tvalidate_scheduler(opts.scheduler);\n+\n \tif (maintenance_register())\n \t\twarning(_(\"failed to add repo to global config\"));\n-\n-\treturn update_background_schedule(1);\n+\treturn update_background_schedule(&opts, 1);\n }\n \n static int maintenance_stop(void)\n {\n-\treturn update_background_schedule(0);\n+\treturn update_background_schedule(NULL, 0);\n }\n \n static const char builtin_maintenance_usage[] =\tN_(\"git maintenance <subcommand> [<options>]\");\n@@ -2018,7 +2210,7 @@ int cmd_maintenance(int argc, const char **argv, const char *prefix)\n \tif (!strcmp(argv[1], \"run\"))\n \t\treturn maintenance_run(argc - 1, argv + 1, prefix);\n \tif (!strcmp(argv[1], \"start\"))\n-\t\treturn maintenance_start();\n+\t\treturn maintenance_start(argc - 1, argv + 1, prefix);\n \tif (!strcmp(argv[1], \"stop\"))\n \t\treturn maintenance_stop();\n \tif (!strcmp(argv[1], \"register\"))\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex b93ae014ee..b36b7f5fb0 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -494,8 +494,21 @@ test_expect_success !MINGW 'register and unregister with regex metacharacters' '\n \t\tmaintenance.repo \"$(pwd)/$META\"\n '\n \n+test_expect_success 'start --scheduler=<scheduler>' '\n+\ttest_expect_code 129 git maintenance start --scheduler=foo 2>err &&\n+\ttest_i18ngrep \"unrecognized --scheduler argument\" err &&\n+\n+\ttest_expect_code 129 git maintenance start --no-scheduler 2>err &&\n+\ttest_i18ngrep \"unknown option\" err &&\n+\n+\ttest_expect_code 128 \\\n+\t\tenv GIT_TEST_MAINT_SCHEDULER=\"launchctl:true,schtasks:true\" \\\n+\t\tgit maintenance start --scheduler=crontab 2>err &&\n+\ttest_i18ngrep \"fatal: crontab scheduler is not available\" err\n+'\n+\n test_expect_success 'start from empty cron table' '\n-\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start --scheduler=crontab &&\n \n \t# start registers the repo\n \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n@@ -518,7 +531,7 @@ test_expect_success 'stop from existing schedule' '\n \n test_expect_success 'start preserves existing schedule' '\n \techo \"Important information!\" >cron.txt &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start --scheduler=crontab &&\n \tgrep \"Important information!\" cron.txt\n '\n \n@@ -547,7 +560,7 @@ test_expect_success 'start and stop macOS maintenance' '\n \tEOF\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start --scheduler=launchctl &&\n \n \t# start registers the repo\n \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n@@ -598,7 +611,7 @@ test_expect_success 'start and stop Windows maintenance' '\n \tEOF\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"schtasks:./print-args\" git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"schtasks:./print-args\" git maintenance start --scheduler=schtasks &&\n \n \t# start registers the repo\n \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n@@ -621,6 +634,40 @@ test_expect_success 'start and stop Windows maintenance' '\n \ttest_cmp expect args\n '\n \n+test_expect_success 'start and stop when several schedulers are available' '\n+\twrite_script print-args <<-\\EOF &&\n+\tprintf \"%s\\n\" \"$*\" | sed \"s:gui/[0-9][0-9]*:gui/[UID]:; s:\\(schtasks /create .* /xml\\).*:\\1:;\" >>args\n+\tEOF\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >expect &&\n+\tfor frequency in hourly daily weekly\n+\tdo\n+\t\tPLIST=\"$pfx/Library/LaunchAgents/org.git-scm.git.$frequency.plist\" &&\n+\t\techo \"launchctl bootout gui/[UID] $PLIST\" >>expect &&\n+\t\techo \"launchctl bootstrap gui/[UID] $PLIST\" >>expect || return 1\n+\tdone &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n+\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n+\t\thourly daily weekly >expect &&\n+\tprintf \"schtasks /create /tn Git Maintenance (%s) /f /xml\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n+\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n+\t\thourly daily weekly >expect &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\ttest_cmp expect args\n+'\n+\n test_expect_success 'register preserves existing strategy' '\n \tgit config maintenance.strategy none &&\n \tgit maintenance register &&\n-- \n2.32.0\n\n"},{"id":"427268","messageId":"CAPig+cSLi7aN=6ahrHwy4fO-7JMBN3pmzfpWe5ZXOcC9j4+e+g@mail.gmail.com","threadId":"55603","inReplyTo":"20210612165043.165579-3-lenaic@lhuard.fr","subject":"Re: [PATCH v6 2/3] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2021-06-14T04:36:09Z","receivedAt":"2021-06-14T04:36:32Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Sat, Jun 12, 2021 at 12:51 PM Lénaïc Huard <lenaic@lhuard.fr> wrote:\n> Depending on the system, different schedulers can be used to schedule\n> the hourly, daily and weekly executions of `git maintenance run`:\n> * `launchctl` for MacOS,\n> * `schtasks` for Windows and\n> * `crontab` for everything else.\n>\n> `git maintenance run` now has an option to let the end-user explicitly\n> choose which scheduler he wants to use:\n> `--scheduler=auto|crontab|launchctl|schtasks`.\n>\n> When `git maintenance start --scheduler=XXX` is run, it not only\n> registers `git maintenance run` tasks in the scheduler XXX, it also\n> removes the `git maintenance run` tasks from all the other schedulers to\n> ensure we cannot have two schedulers launching concurrent identical\n> tasks.\n>\n> The default value is `auto` which chooses a suitable scheduler for the\n> system.\n>\n> `git maintenance stop` doesn't have any `--scheduler` parameter because\n> this command will try to remove the `git maintenance run` tasks from all\n> the available schedulers.\n>\n> Signed-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n\nThanks. Unfortunately, I haven't been following this series too\nclosely since I reviewed v1, so I set aside time to review v6, which I\nhave now done. The material in the cover letter and individual commit\nmessages was helpful in understanding the nuances of the changes, and\nthe series seems pretty well complete at this point. (If, however, you\ndo happen to re-roll for some reason, please consider using the\n--range-diff option of git-format-patch as an aid to reviewers.)\n\nI did leave a number of comments below regarding possible improvements\nto the code and documentation, however, they're probably mostly\nsubjective and don't necessarily warrant a re-roll; I'd have no\nproblem seeing this accepted as-is without the suggestions applied.\n(They can always be applied later on if someone considers them\nimportant enough.)\n\nI do, though, have one question (below) about is_crontab_available()\nfor which I could not figure out the answer.\n\n> ---\n> diff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\n> @@ -179,6 +179,17 @@ OPTIONS\n> +--scheduler=auto|crontab|launchctl|schtasks::\n> +       When combined with the `start` subcommand, specify the scheduler\n> +       to use to run the hourly, daily and weekly executions of\n> +       `git maintenance run`.\n> +       The possible values for `<scheduler>` depend on the system: `crontab`\n> +       is available on POSIX systems, `launchctl` is available on\n> +       MacOS and `schtasks` is available on Windows.\n> +       By default or when `auto` is specified, a suitable scheduler for\n> +       the system is used. On MacOS, `launchctl` is used. On Windows,\n> +       `schtasks` is used. On all other systems, `crontab` is used.\n\nThe above description is somewhat redundant. Another way to write it\nwithout the redundancy might be:\n\n    Specify the scheduler -- in combination with subcommand `start` --\n    for running the hourly, daily and weekly invocations of `git\n    maintenance run`. Possible values for `<scheduler>` are `auto`,\n    `crontab` (POSIX), `launchctl` (macOS), and `schtasks` (Windows).\n    When `auto` is specified, the appropriate platform-specific\n    scheduler is used. Default is `auto`.\n\n> diff --git a/builtin/gc.c b/builtin/gc.c\n> @@ -1529,6 +1529,59 @@ static const char *get_frequency(enum schedule_priority schedule)\n> +static int get_schedule_cmd(const char **cmd, int *is_available)\n> +{\n> +       char *item;\n> +       char *testing = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n> +\n> +       if (!testing)\n> +               return 0;\n> +\n> +       if (is_available)\n> +               *is_available = 0;\n> +\n> +       for (item = testing;;) {\n> +               char *sep;\n> +               char *end_item = strchr(item, ',');\n> +               if (end_item)\n> +                       *end_item = '\\0';\n> +\n> +               sep = strchr(item, ':');\n> +               if (!sep)\n> +                       die(\"GIT_TEST_MAINT_SCHEDULER unparseable: %s\", testing);\n> +               *sep = '\\0';\n> +\n> +               if (!strcmp(*cmd, item)) {\n> +                       *cmd = sep + 1;\n> +                       if (is_available)\n> +                               *is_available = 1;\n> +                       UNLEAK(testing);\n> +                       return 1;\n> +               }\n> +\n> +               if (!end_item)\n> +                       break;\n> +               item = end_item + 1;\n> +       }\n> +\n> +       free(testing);\n> +       return 1;\n> +}\n\nI ended up studying this implementation several times since I had to\ncome back to it repeatedly after reading calling code in order to (I\nhope) fully understand all the different conditions represented by its\nthree distinct return values (the function return value, and the\nvalues returned in **cmd and **is_available). That it required several\nreadings might warrant a comment block explaining what the function\ndoes and what the various return conditions mean. As a bonus, an\nexplanation of the value of GIT_TEST_MAINT_SCHEDULER -- a\ncomma-separated list of colon-delimited tuples, and what those tuples\nrepresent -- could be helpful.\n\n> +static int is_launchctl_available(void)\n> +{\n> +       const char *cmd = \"launchctl\";\n> +       int is_available;\n> +       if (get_schedule_cmd(&cmd, &is_available))\n> +               return is_available;\n> +\n> +#ifdef __APPLE__\n> +       return 1;\n> +#else\n> +       return 0;\n> +#endif\n> +}\n\nOn this project, we usually frown upon #if conditionals within\nfunctions since the code often can become unreadable. The usage in\nthis function doesn't suffer from that problem, however,\nresolve_auto_scheduler() is somewhat ugly. An alternative would be to\nset up these values outside of all functions, perhaps like this:\n\n    #ifdef __APPLE__\n    #define MAINT_SCHEDULER SCHEDULER_LAUNCHCTL\n    #elif GIT_WINDOWS_NATIVE\n    #define MAINT_SCHEDULER SCHEDULER_SCHTASKS\n    #else\n    #define MAINT_SCHEDULER SCHEDULER_CRON\n    #endif\n\nand then:\n\n    static int is_launchctl_available(void)\n    {\n        if (get_schedule_cmd(...))\n            return is_available;\n        return MAINT_SCHEDULER == SCHEDULER_LAUNCHCTL;\n    }\n\n    static void resolve_auto_scheduler(enum scheduler *scheduler)\n    {\n        if (*scheduler == SCHEDULER_AUTO)\n            *scheduler = MAINT_SCHEDULER;\n    }\n\n> +static int is_crontab_available(void)\n> +{\n> +       const char *cmd = \"crontab\";\n> +       int is_available;\n> +       struct child_process child = CHILD_PROCESS_INIT;\n> +\n> +       if (get_schedule_cmd(&cmd, &is_available) && !is_available)\n> +               return 0;\n> +\n> +       strvec_split(&child.args, cmd);\n> +       strvec_push(&child.args, \"-l\");\n> +       child.no_stdin = 1;\n> +       child.no_stdout = 1;\n> +       child.no_stderr = 1;\n> +       child.silent_exec_failure = 1;\n> +\n> +       if (start_command(&child))\n> +               return 0;\n> +       /* Ignore exit code, as an empty crontab will return error. */\n> +       finish_command(&child);\n> +       return 1;\n>  }\n\nIf I understand get_schedule_cmd() correctly, it will always return\ntrue if GIT_TEST_MAINT_SCHEDULER is present in the environment,\nhowever, it will only set `is_available` to true if\nGIT_TEST_MAINT_SCHEDULER contains a matching entry for `cmd` (which in\nthis case is \"crontab\"). Assuming this understanding is correct, then\nI'm having trouble understanding why this:\n\n    if (get_schedule_cmd(&cmd, &is_available) && !is_available)\n        return 0;\n\nisn't instead written like this:\n\n    if (get_schedule_cmd(&cmd, &is_available))\n        return is_available;\n\nThat is, why doesn't is_crontab_available() trust the result of\nget_schedule_cmd(), instead going ahead and trying to invoke `crontab`\nitself? Am I missing something which makes the `!is_available` case\nspecial?\n\n> +static void resolve_auto_scheduler(enum scheduler *scheduler)\n> +{\n> +       if (*scheduler != SCHEDULER_AUTO)\n> +               return;\n> +\n>  #if defined(__APPLE__)\n> +       *scheduler = SCHEDULER_LAUNCHCTL;\n> +       return;\n> +\n>  #elif defined(GIT_WINDOWS_NATIVE)\n> +       *scheduler = SCHEDULER_SCHTASKS;\n> +       return;\n> +\n>  #else\n> +       *scheduler = SCHEDULER_CRON;\n> +       return;\n>  #endif\n> +}\n\n(See above for a way to simplify this implementation.)\n\nIs there a strong reason which I'm missing that this function alters\nits argument rather than simply returning the resolved scheduler?\n\n    static enum scheduler resolve_scheduler(enum scheduler x) {...}\n\nOr is it just personal preference?\n\n(Minor: I took the liberty of shortening the function name since it\ndoesn't feel like the longer name adds much value.)\n\n> +static int maintenance_start(int argc, const char **argv, const char *prefix)\n>  {\n> +       struct maintenance_start_opts opts = { 0 };\n> +       struct option options[] = {\n> +               OPT_CALLBACK_F(\n> +                       0, \"scheduler\", &opts.scheduler, N_(\"scheduler\"),\n> +                       N_(\"scheduler to use to trigger git maintenance run\"),\n\nDropping \"to use\" would make this more concise without losing clarity:\n\n    \"scheduler to trigger git maintenance run\"\n\n> +                       PARSE_OPT_NONEG, maintenance_opt_scheduler),\n> +               OPT_END()\n> +       };\n> diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\n> @@ -494,8 +494,21 @@ test_expect_success !MINGW 'register and unregister with regex metacharacters' '\n> +test_expect_success 'start --scheduler=<scheduler>' '\n> +       test_expect_code 129 git maintenance start --scheduler=foo 2>err &&\n> +       test_i18ngrep \"unrecognized --scheduler argument\" err &&\n> +\n> +       test_expect_code 129 git maintenance start --no-scheduler 2>err &&\n> +       test_i18ngrep \"unknown option\" err &&\n> +\n> +       test_expect_code 128 \\\n> +               env GIT_TEST_MAINT_SCHEDULER=\"launchctl:true,schtasks:true\" \\\n> +               git maintenance start --scheduler=crontab 2>err &&\n> +       test_i18ngrep \"fatal: crontab scheduler is not available\" err\n> +'\n\nWhy does this test care about the exact exit codes rather than simply\nusing test_must_fail() as is typically done elsewhere in the test\nsuite, especially since we're also checking the error message itself?\nAm I missing some non-obvious property of the error codes?\n\nI don't see `auto` being tested anywhere. Do we want such a test? (It\nseems like it should be doable, though perhaps the complexity is too\nhigh -- I haven't thought it through fully.)\n"},{"id":"427628","messageId":"5c4366e4-a891-0a3a-2534-af48ab0097c2@gmail.com","threadId":"55603","inReplyTo":"CAPig+cSLi7aN=6ahrHwy4fO-7JMBN3pmzfpWe5ZXOcC9j4+e+g@mail.gmail.com","subject":"Re: [PATCH v6 2/3] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-06-16T18:12:33Z","receivedAt":"2021-06-16T18:12:39Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 6/14/2021 12:36 AM, Eric Sunshine wrote:\n> On Sat, Jun 12, 2021 at 12:51 PM Lénaïc Huard <lenaic@lhuard.fr> wrote:\n...\n>> diff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\n>> @@ -179,6 +179,17 @@ OPTIONS\n>> +--scheduler=auto|crontab|launchctl|schtasks::\n>> +       When combined with the `start` subcommand, specify the scheduler\n>> +       to use to run the hourly, daily and weekly executions of\n>> +       `git maintenance run`.\n>> +       The possible values for `<scheduler>` depend on the system: `crontab`\n>> +       is available on POSIX systems, `launchctl` is available on\n>> +       MacOS and `schtasks` is available on Windows.\n>> +       By default or when `auto` is specified, a suitable scheduler for\n>> +       the system is used. On MacOS, `launchctl` is used. On Windows,\n>> +       `schtasks` is used. On all other systems, `crontab` is used.\n> \n> The above description is somewhat redundant. Another way to write it\n> without the redundancy might be:\n> \n>     Specify the scheduler -- in combination with subcommand `start` --\n\nI think this change to the start is not contributing to the drop\nin redundancy, but _does_ break from the pattern of previous\noptions. These start with \"When combined with the `X` subcommand\"\nto clarify when they can be used.\n\nI'm not against improving that pattern. I'm just saying that we\nshould do it across the whole file if we do it at all.\n\n>     for running the hourly, daily and weekly invocations of `git\n>     maintenance run`. Possible values for `<scheduler>` are `auto`,\n>     `crontab` (POSIX), `launchctl` (macOS), and `schtasks` (Windows).\n>     When `auto` is specified, the appropriate platform-specific\n>     scheduler is used. Default is `auto`.\n\nI find this to be a good way to reorganize the paragraph to be\nvery clear. How do you propose adding `systemd-timer` in the next\npatch? Is it simply adding \"`systemd-timer (Linux)`\" or do we\nneed to be more careful using \"(when available)\"? Unlike the others,\nthe availability of that option is not as cut-and-dry.\n\n...\n\n> I don't see `auto` being tested anywhere. Do we want such a test? (It\n> seems like it should be doable, though perhaps the complexity is too\n> high -- I haven't thought it through fully.)\n\nUnfortunately, `auto` seems to live in a world where we need to\nactually run commands such as crontab and systemctl to determine\nif they are available, but that happens only when\nGIT_TEST_MAINT_SCHEDULER is unset. But, we don't want to actually\nedit the timing information for the test runner, so some other\nabstraction needs to be inserted at the proper layer.\n\nIt's worth a shot, but I expect it to be challenging to get right.\n\nThanks,\n-Stolee\n"},{"id":"427659","messageId":"CAPig+cSCWHh9eKs2Wj3N8OBJ5LzQk=4sCEu6=s0aR76nmo8H5g@mail.gmail.com","threadId":"55603","inReplyTo":"5c4366e4-a891-0a3a-2534-af48ab0097c2@gmail.com","subject":"Re: [PATCH v6 2/3] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2021-06-17T04:11:04Z","receivedAt":"2021-06-17T04:11:18Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Wed, Jun 16, 2021 at 8:48 PM Derrick Stolee <stolee@gmail.com> wrote:\n> On 6/14/2021 12:36 AM, Eric Sunshine wrote:\n> > On Sat, Jun 12, 2021 at 12:51 PM Lénaïc Huard <lenaic@lhuard.fr> wrote:\n> > The above description is somewhat redundant. Another way to write it\n> > without the redundancy might be:\n> >\n> >     Specify the scheduler -- in combination with subcommand `start` --\n>\n> I think this change to the start is not contributing to the drop\n> in redundancy, but _does_ break from the pattern of previous\n> options. These start with \"When combined with the `X` subcommand\"\n> to clarify when they can be used.\n>\n> I'm not against improving that pattern. I'm just saying that we\n> should do it across the whole file if we do it at all.\n\nIndeed, it's not a big deal. As mentioned in my review, all my\ncomments were subjective, and none of them should prevent this from\nbeing accepted as-is if everyone is happy with it.\n\n> >     for running the hourly, daily and weekly invocations of `git\n> >     maintenance run`. Possible values for `<scheduler>` are `auto`,\n> >     `crontab` (POSIX), `launchctl` (macOS), and `schtasks` (Windows).\n> >     When `auto` is specified, the appropriate platform-specific\n> >     scheduler is used. Default is `auto`.\n>\n> I find this to be a good way to reorganize the paragraph to be\n> very clear. How do you propose adding `systemd-timer` in the next\n> patch? Is it simply adding \"`systemd-timer (Linux)`\" or do we\n> need to be more careful using \"(when available)\"? Unlike the others,\n> the availability of that option is not as cut-and-dry.\n\nIt should be easy enough. For instance:\n\n    Possible values for `<scheduler>` are `auto`, `crontab` (POSIX),\n    `systemd-timer` (Linux), `launchctl` (macOS), and `schtasks`\n    (Windows). When `auto` is specified, the appropriate platform-\n    specific scheduler is used; on Linux, `systemd-timer` is used if\n    available, otherwise `crontab`. Default is `auto`.\n\nBut this sort of minor rewrite can always be done later.\n\n> > I don't see `auto` being tested anywhere. Do we want such a test? (It\n> > seems like it should be doable, though perhaps the complexity is too\n> > high -- I haven't thought it through fully.)\n>\n> Unfortunately, `auto` seems to live in a world where we need to\n> actually run commands such as crontab and systemctl to determine\n> if they are available, but that happens only when\n> GIT_TEST_MAINT_SCHEDULER is unset. But, we don't want to actually\n> edit the timing information for the test runner, so some other\n> abstraction needs to be inserted at the proper layer.\n>\n> It's worth a shot, but I expect it to be challenging to get right.\n\nI imagine that it can be mocked up with GIT_TEST_MAINT_SCHEDULER in\nsome fashion, but it's not worth holding up this series or expecting a\nre-roll for that one little question which popped out of my brain.\n\nAs I mentioned in my review, my comments were subjective, and I think\nthe series is in a state in which it can be accepted as-is without\nadditional re-rolls[1] if everyone is happy with it.\n\n[1]: Except for the one question I asked about is_crontab_available();\nI don't understand that one bit of logic, thus can't tell if the\nbehavior makes sense or not.\n"},{"id":"427750","messageId":"80d6050a-71d9-1278-e68f-91c3a1ca52e4@gmail.com","threadId":"55603","inReplyTo":"CAPig+cSLi7aN=6ahrHwy4fO-7JMBN3pmzfpWe5ZXOcC9j4+e+g@mail.gmail.com","subject":"Re: [PATCH v6 2/3] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2021-06-17T14:26:59Z","receivedAt":"2021-06-17T14:27:11Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 14/06/2021 05:36, Eric Sunshine wrote:\n> On Sat, Jun 12, 2021 at 12:51 PM Lénaïc Huard <lenaic@lhuard.fr> wrote:\n>> Depending on the system, different schedulers can be used to schedule\n>> the hourly, daily and weekly executions of `git maintenance run`:\n>> * `launchctl` for MacOS,\n>> * `schtasks` for Windows and\n>> * `crontab` for everything else.\n>>\n>> `git maintenance run` now has an option to let the end-user explicitly\n>> choose which scheduler he wants to use:\n>> `--scheduler=auto|crontab|launchctl|schtasks`.\n>>\n>> When `git maintenance start --scheduler=XXX` is run, it not only\n>> registers `git maintenance run` tasks in the scheduler XXX, it also\n>> removes the `git maintenance run` tasks from all the other schedulers to\n>> ensure we cannot have two schedulers launching concurrent identical\n>> tasks.\n>>\n>> The default value is `auto` which chooses a suitable scheduler for the\n>> system.\n>>\n>> `git maintenance stop` doesn't have any `--scheduler` parameter because\n>> this command will try to remove the `git maintenance run` tasks from all\n>> the available schedulers.\n>>\n>> Signed-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n> \n> Thanks. Unfortunately, I haven't been following this series too\n> closely since I reviewed v1, so I set aside time to review v6, which I\n> have now done. The material in the cover letter and individual commit\n> messages was helpful in understanding the nuances of the changes, and\n> the series seems pretty well complete at this point. (If, however, you\n> do happen to re-roll for some reason, please consider using the\n> --range-diff option of git-format-patch as an aid to reviewers.)\n> \n> I did leave a number of comments below regarding possible improvements\n> to the code and documentation, however, they're probably mostly\n> subjective and don't necessarily warrant a re-roll; I'd have no\n> problem seeing this accepted as-is without the suggestions applied.\n> (They can always be applied later on if someone considers them\n> important enough.)\n>\n> I do, though, have one question (below) about is_crontab_available()\n> for which I could not figure out the answer.\n\nI think that is a bug\n\n>> [...]\n>> diff --git a/builtin/gc.c b/builtin/gc.c\n>> @@ -1529,6 +1529,59 @@ static const char *get_frequency(enum schedule_priority schedule)\n>> +static int get_schedule_cmd(const char **cmd, int *is_available)\n>> +{\n>> +       char *item;\n>> +       char *testing = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n>> +\n>> +       if (!testing)\n>> +               return 0;\n>> +\n>> +       if (is_available)\n>> +               *is_available = 0;\n>> +\n>> +       for (item = testing;;) {\n>> +               char *sep;\n>> +               char *end_item = strchr(item, ',');\n>> +               if (end_item)\n>> +                       *end_item = '\\0';\n>> +\n>> +               sep = strchr(item, ':');\n>> +               if (!sep)\n>> +                       die(\"GIT_TEST_MAINT_SCHEDULER unparseable: %s\", testing);\n>> +               *sep = '\\0';\n>> +\n>> +               if (!strcmp(*cmd, item)) {\n>> +                       *cmd = sep + 1;\n>> +                       if (is_available)\n>> +                               *is_available = 1;\n>> +                       UNLEAK(testing);\n>> +                       return 1;\n>> +               }\n>> +\n>> +               if (!end_item)\n>> +                       break;\n>> +               item = end_item + 1;\n>> +       }\n>> +\n>> +       free(testing);\n>> +       return 1;\n>> +}\n> \n> I ended up studying this implementation several times since I had to\n> come back to it repeatedly after reading calling code in order to (I\n> hope) fully understand all the different conditions represented by its\n> three distinct return values (the function return value, and the\n> values returned in **cmd and **is_available). That it required several\n> readings might warrant a comment block explaining what the function\n> does and what the various return conditions mean. As a bonus, an\n> explanation of the value of GIT_TEST_MAINT_SCHEDULER -- a\n> comma-separated list of colon-delimited tuples, and what those tuples\n> represent -- could be helpful.\n\nI agree documenting GIT_TEST_MAINT_SCHEDULER would be useful\n\n>> +static int is_launchctl_available(void)\n>> +{\n>> +       const char *cmd = \"launchctl\";\n>> +       int is_available;\n>> +       if (get_schedule_cmd(&cmd, &is_available))\n>> +               return is_available;\n>> +\n>> +#ifdef __APPLE__\n>> +       return 1;\n>> +#else\n>> +       return 0;\n>> +#endif\n>> +}\n> \n> On this project, we usually frown upon #if conditionals within\n> functions since the code often can become unreadable. The usage in\n> this function doesn't suffer from that problem, however,\n> resolve_auto_scheduler() is somewhat ugly. An alternative would be to\n> set up these values outside of all functions, perhaps like this:\n> \n>      #ifdef __APPLE__\n>      #define MAINT_SCHEDULER SCHEDULER_LAUNCHCTL\n>      #elif GIT_WINDOWS_NATIVE\n>      #define MAINT_SCHEDULER SCHEDULER_SCHTASKS\n>      #else\n>      #define MAINT_SCHEDULER SCHEDULER_CRON\n>      #endif\n> \n> and then:\n> \n>      static int is_launchctl_available(void)\n>      {\n>          if (get_schedule_cmd(...))\n>              return is_available;\n>          return MAINT_SCHEDULER == SCHEDULER_LAUNCHCTL;\n>      }\n> \n>      static void resolve_auto_scheduler(enum scheduler *scheduler)\n>      {\n>          if (*scheduler == SCHEDULER_AUTO)\n>              *scheduler = MAINT_SCHEDULER;\n>      }\n> \n>> +static int is_crontab_available(void)\n>> +{\n>> +       const char *cmd = \"crontab\";\n>> +       int is_available;\n>> +       struct child_process child = CHILD_PROCESS_INIT;\n>> +\n>> +       if (get_schedule_cmd(&cmd, &is_available) && !is_available)\n>> +               return 0;\n>> +\n>> +       strvec_split(&child.args, cmd);\n>> +       strvec_push(&child.args, \"-l\");\n>> +       child.no_stdin = 1;\n>> +       child.no_stdout = 1;\n>> +       child.no_stderr = 1;\n>> +       child.silent_exec_failure = 1;\n>> +\n>> +       if (start_command(&child))\n>> +               return 0;\n>> +       /* Ignore exit code, as an empty crontab will return error. */\n>> +       finish_command(&child);\n>> +       return 1;\n>>   }\n> \n> If I understand get_schedule_cmd() correctly, it will always return\n> true if GIT_TEST_MAINT_SCHEDULER is present in the environment,\n> however, it will only set `is_available` to true if\n> GIT_TEST_MAINT_SCHEDULER contains a matching entry for `cmd` (which in\n> this case is \"crontab\"). Assuming this understanding is correct, then\n> I'm having trouble understanding why this:\n> \n>      if (get_schedule_cmd(&cmd, &is_available) && !is_available)\n>          return 0;\n> \n> isn't instead written like this:\n> \n>      if (get_schedule_cmd(&cmd, &is_available))\n>          return is_available;\n> \n> That is, why doesn't is_crontab_available() trust the result of\n> get_schedule_cmd(), instead going ahead and trying to invoke `crontab`\n> itself? Am I missing something which makes the `!is_available` case\n> special?\n\nI agree, I think we should be returning is_available irrespective of its \nvalue if get_schedule_cmd() returns true. This is what \nis_systemd_timer_available() does in the next patch. Well spotted.\n\nBest Wishes\n\nPhillip\n\n>> +static void resolve_auto_scheduler(enum scheduler *scheduler)\n>> +{\n>> +       if (*scheduler != SCHEDULER_AUTO)\n>> +               return;\n>> +\n>>   #if defined(__APPLE__)\n>> +       *scheduler = SCHEDULER_LAUNCHCTL;\n>> +       return;\n>> +\n>>   #elif defined(GIT_WINDOWS_NATIVE)\n>> +       *scheduler = SCHEDULER_SCHTASKS;\n>> +       return;\n>> +\n>>   #else\n>> +       *scheduler = SCHEDULER_CRON;\n>> +       return;\n>>   #endif\n>> +}\n> \n> (See above for a way to simplify this implementation.)\n> \n> Is there a strong reason which I'm missing that this function alters\n> its argument rather than simply returning the resolved scheduler?\n> \n>      static enum scheduler resolve_scheduler(enum scheduler x) {...}\n> \n> Or is it just personal preference?\n>\n> (Minor: I took the liberty of shortening the function name since it\n> doesn't feel like the longer name adds much value.)\n> \n>> +static int maintenance_start(int argc, const char **argv, const char *prefix)\n>>   {\n>> +       struct maintenance_start_opts opts = { 0 };\n>> +       struct option options[] = {\n>> +               OPT_CALLBACK_F(\n>> +                       0, \"scheduler\", &opts.scheduler, N_(\"scheduler\"),\n>> +                       N_(\"scheduler to use to trigger git maintenance run\"),\n> \n> Dropping \"to use\" would make this more concise without losing clarity:\n> \n>      \"scheduler to trigger git maintenance run\"\n> \n>> +                       PARSE_OPT_NONEG, maintenance_opt_scheduler),\n>> +               OPT_END()\n>> +       };\n>> diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\n>> @@ -494,8 +494,21 @@ test_expect_success !MINGW 'register and unregister with regex metacharacters' '\n>> +test_expect_success 'start --scheduler=<scheduler>' '\n>> +       test_expect_code 129 git maintenance start --scheduler=foo 2>err &&\n>> +       test_i18ngrep \"unrecognized --scheduler argument\" err &&\n>> +\n>> +       test_expect_code 129 git maintenance start --no-scheduler 2>err &&\n>> +       test_i18ngrep \"unknown option\" err &&\n>> +\n>> +       test_expect_code 128 \\\n>> +               env GIT_TEST_MAINT_SCHEDULER=\"launchctl:true,schtasks:true\" \\\n>> +               git maintenance start --scheduler=crontab 2>err &&\n>> +       test_i18ngrep \"fatal: crontab scheduler is not available\" err\n>> +'\n> \n> Why does this test care about the exact exit codes rather than simply\n> using test_must_fail() as is typically done elsewhere in the test\n> suite, especially since we're also checking the error message itself?\n> Am I missing some non-obvious property of the error codes?\n> \n> I don't see `auto` being tested anywhere. Do we want such a test? (It\n> seems like it should be doable, though perhaps the complexity is too\n> high -- I haven't thought it through fully.)\n> \n"},{"id":"429051","messageId":"20210702142556.99864-2-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210702142556.99864-1-lenaic@lhuard.fr","subject":"[PATCH v7 1/3] cache.h: Introduce a generic \"xdg_config_home_for(…)\" function","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-07-02T14:25:54Z","receivedAt":"2021-07-02T14:26:41Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Current implementation of `xdg_config_home(filename)` returns\n`$XDG_CONFIG_HOME/git/$filename`, with the `git` subdirectory inserted\nbetween the `XDG_CONFIG_HOME` environment variable and the parameter.\n\nThis patch introduces a `xdg_config_home_for(subdir, filename)` function\nwhich is more generic. It only concatenates \"$XDG_CONFIG_HOME\", or\n\"$HOME/.config\" if the former isn’t defined, with the parameters,\nwithout adding `git` in between.\n\n`xdg_config_home(filename)` is now implemented by calling\n`xdg_config_home_for(\"git\", filename)` but this new generic function can\nbe used to compute the configuration directory of other programs.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n cache.h |  7 +++++++\n path.c  | 13 ++++++++++---\n 2 files changed, 17 insertions(+), 3 deletions(-)\n\ndiff --git a/cache.h b/cache.h\nindex ba04ff8bd3..2a0fb3e4ba 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -1286,6 +1286,13 @@ int is_ntfs_dotmailmap(const char *name);\n  */\n int looks_like_command_line_option(const char *str);\n \n+/**\n+ * Return a newly allocated string with the evaluation of\n+ * \"$XDG_CONFIG_HOME/$subdir/$filename\" if $XDG_CONFIG_HOME is non-empty, otherwise\n+ * \"$HOME/.config/$subdir/$filename\". Return NULL upon error.\n+ */\n+char *xdg_config_home_for(const char *subdir, const char *filename);\n+\n /**\n  * Return a newly allocated string with the evaluation of\n  * \"$XDG_CONFIG_HOME/git/$filename\" if $XDG_CONFIG_HOME is non-empty, otherwise\ndiff --git a/path.c b/path.c\nindex 7bccd830e9..1b1de3be09 100644\n--- a/path.c\n+++ b/path.c\n@@ -1503,21 +1503,28 @@ int looks_like_command_line_option(const char *str)\n \treturn str && str[0] == '-';\n }\n \n-char *xdg_config_home(const char *filename)\n+char *xdg_config_home_for(const char *subdir, const char *filename)\n {\n \tconst char *home, *config_home;\n \n+\tassert(subdir);\n \tassert(filename);\n \tconfig_home = getenv(\"XDG_CONFIG_HOME\");\n \tif (config_home && *config_home)\n-\t\treturn mkpathdup(\"%s/git/%s\", config_home, filename);\n+\t\treturn mkpathdup(\"%s/%s/%s\", config_home, subdir, filename);\n \n \thome = getenv(\"HOME\");\n \tif (home)\n-\t\treturn mkpathdup(\"%s/.config/git/%s\", home, filename);\n+\t\treturn mkpathdup(\"%s/.config/%s/%s\", home, subdir, filename);\n+\n \treturn NULL;\n }\n \n+char *xdg_config_home(const char *filename)\n+{\n+\treturn xdg_config_home_for(\"git\", filename);\n+}\n+\n char *xdg_cache_home(const char *filename)\n {\n \tconst char *home, *cache_home;\n-- \n2.32.0\n\n"},{"id":"429052","messageId":"20210702142556.99864-1-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210612165043.165579-1-lenaic@lhuard.fr","subject":"[PATCH v7 0/3] maintenance: add support for systemd timers on Linux","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-07-02T14:25:53Z","receivedAt":"2021-07-02T14:26:46Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Hello,\n\nPlease find hereafter a new reroll of my patchset to add support for\nsystemd timers on Linux for the `git maintenance start` command.\n\nThe changes compared to the previous version address the remarks\nraised during the previous review.\n\nThe patches are:\n\n* cache.h: Introduce a generic \"xdg_config_home_for(…)\" function\n\n  This patch introduces a function to compute configuration files\n  paths inside $XDG_CONFIG_HOME.\n  It is used in the latest patch of this series to compute systemd\n  unit files location.\n\n  This patch is unchanged compared to its previous version.\n\n* maintenance: `git maintenance run` learned `--scheduler=<scheduler>`\n\n  This patch adds a new parameter to the `git maintenance run` to let\n  the user choose a scheduler.\n\n  This patch contains the following changes compared to its previous\n  version:\n\n  * `is_crontab_available` was not returning directly `is_available`\n    under tests condition (when `GIT_TEST_MAINT_SCHEDULER` is set).\n    This was indeed a bug as it means `cron` could be invoked by tests\n    whereas it should be mocked.\n    This is now fixed and `is_crontab_available` now has exactly the\n    same behavior as the `is_systemd_timer_available` function that is\n    introduced in the last patch.\n\n  * The `get_schedule_cmd` function that centralizes the testing logic\n    is now prefixed by a comment block describing its behavior and the\n    expected values for the `GIT_TEST_MAINT_SCHEDULER` environment\n    variable.\n\n  * The help message for the `--scheduler` option of the `git\n    maintenance start` command has been reworded following Eric’s\n    suggestion.\n    I’ve however kept the “When combined with the `start` subcommand…”\n    opening to keep consistency with the other options documented on\n    the same page.\n\n  * `resolve_auto_scheduler` function has been renamed\n    `resolve_scheduler` and it is now returning a value instead of\n    altering its parameter.\n\n* maintenance: add support for systemd timers on Linux\n\n  This patch implements the support of systemd timers on top of\n  crontab scheduler on Linux systems.\n\n  The changes in this patch are only followups of changes mentioned in\n  the previous patch:\n  * `resolve_scheduler` is now returning a value instead of altering\n  its parameter.\n\nBest wishes,\nLénaïc.\n\n\nLénaïc Huard (3):\n  cache.h: Introduce a generic \"xdg_config_home_for(…)\" function\n  maintenance: `git maintenance run` learned `--scheduler=<scheduler>`\n  maintenance: add support for systemd timers on Linux\n\n Documentation/git-maintenance.txt |  57 +++\n builtin/gc.c                      | 597 ++++++++++++++++++++++++++----\n cache.h                           |   7 +\n path.c                            |  13 +-\n t/t7900-maintenance.sh            | 110 +++++-\n 5 files changed, 706 insertions(+), 78 deletions(-)\n\nDiff-intervalle contre v6 :\n-:  ---------- > 1:  899b11ed5b cache.h: Introduce a generic \"xdg_config_home_for(…)\" function\n1:  604627f347 ! 2:  f3e2f0256b maintenance: `git maintenance run` learned `--scheduler=<scheduler>`\n    @@ Documentation/git-maintenance.txt: OPTIONS\n      \n     +--scheduler=auto|crontab|launchctl|schtasks::\n     +\tWhen combined with the `start` subcommand, specify the scheduler\n    -+\tto use to run the hourly, daily and weekly executions of\n    ++\tfor running the hourly, daily and weekly executions of\n     +\t`git maintenance run`.\n    -+\tThe possible values for `<scheduler>` depend on the system: `crontab`\n    -+\tis available on POSIX systems, `launchctl` is available on\n    -+\tMacOS and `schtasks` is available on Windows.\n    -+\tBy default or when `auto` is specified, a suitable scheduler for\n    -+\tthe system is used. On MacOS, `launchctl` is used. On Windows,\n    -+\t`schtasks` is used. On all other systems, `crontab` is used.\n    ++\tPossible values for `<scheduler>` are `auto`, `crontab` (POSIX),\n    ++\t`launchctl` (macOS), and `schtasks` (Windows).\n    ++\tWhen `auto` is specified, the appropriate platform-specific\n    ++\tscheduler is used. Default is `auto`.\n     +\n      \n      TROUBLESHOOTING\n    @@ builtin/gc.c: static const char *get_frequency(enum schedule_priority schedule)\n      \t}\n      }\n      \n    ++/*\n    ++ * get_schedule_cmd` reads the GIT_TEST_MAINT_SCHEDULER environment variable\n    ++ * to mock the schedulers that `git maintenance start` rely on.\n    ++ *\n    ++ * For test purpose, GIT_TEST_MAINT_SCHEDULER can be set to a comma-separated\n    ++ * list of colon-separated key/value pairs where each pair contains a scheduler\n    ++ * and its corresponding mock.\n    ++ *\n    ++ * * If $GET_TEST_MAINT_SCHEDULER is not set, return false and leave the\n    ++ *   arguments unmodified.\n    ++ *\n    ++ * * If $GET_TEST_MAINT_SCHEDULER is set, return true.\n    ++ *   In this case, the *cmd value is read as input.\n    ++ *\n    ++ *   * if the input value *cmd is the key of one of the comma-separated list\n    ++ *     item, then *is_available is set to true and *cmd is modified and becomes\n    ++ *     the mock command.\n    ++ *\n    ++ *   * if the input value *cmd isn’t the key of any of the comma-separated list\n    ++ *     item, then *is_available is set to false.\n    ++ *\n    ++ * Ex.:\n    ++ *   GIT_TEST_MAINT_SCHEDULER not set\n    ++ *     ┏━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n    ++ *     ┃ Input ┃                     Output                      ┃\n    ++ *     ┃ *cmd  ┃ return code │       *cmd        │ *is_available ┃\n    ++ *     ┣━━━━━━━╋━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━┫\n    ++ *     ┃ \"foo\" ┃    false    │ \"foo\" (unchanged) │  (unchanged)  ┃\n    ++ *     ┗━━━━━━━┻━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━┛\n    ++ *\n    ++ *   GIT_TEST_MAINT_SCHEDULER set to “foo:./mock_foo.sh,bar:./mock_bar.sh”\n    ++ *     ┏━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n    ++ *     ┃ Input ┃                     Output                      ┃\n    ++ *     ┃ *cmd  ┃ return code │       *cmd        │ *is_available ┃\n    ++ *     ┣━━━━━━━╋━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━┫\n    ++ *     ┃ \"foo\" ┃    true     │  \"./mock.foo.sh\"  │     true      ┃\n    ++ *     ┃ \"qux\" ┃    true     │ \"qux\" (unchanged) │     false     ┃\n    ++ *     ┗━━━━━━━┻━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━┛\n    ++ */\n     +static int get_schedule_cmd(const char **cmd, int *is_available)\n     +{\n     +\tchar *item;\n    @@ builtin/gc.c: static int schtasks_schedule_task(const char *exec_path, enum sche\n     +\tint is_available;\n     +\tstruct child_process child = CHILD_PROCESS_INIT;\n     +\n    -+\tif (get_schedule_cmd(&cmd, &is_available) && !is_available)\n    -+\t\treturn 0;\n    ++\tif (get_schedule_cmd(&cmd, &is_available))\n    ++\t\treturn is_available;\n     +\n     +\tstrvec_split(&child.args, cmd);\n     +\tstrvec_push(&child.args, \"-l\");\n    @@ builtin/gc.c: static int crontab_update_schedule(int run_maintenance, int fd, co\n     +\tenum scheduler scheduler;\n     +};\n     +\n    -+static void resolve_auto_scheduler(enum scheduler *scheduler)\n    ++static enum scheduler resolve_scheduler(enum scheduler scheduler)\n     +{\n    -+\tif (*scheduler != SCHEDULER_AUTO)\n    -+\t\treturn;\n    ++\tif (scheduler != SCHEDULER_AUTO)\n    ++\t\treturn scheduler;\n     +\n      #if defined(__APPLE__)\n     -static const char platform_scheduler[] = \"launchctl\";\n    -+\t*scheduler = SCHEDULER_LAUNCHCTL;\n    -+\treturn;\n    ++\treturn SCHEDULER_LAUNCHCTL;\n     +\n      #elif defined(GIT_WINDOWS_NATIVE)\n     -static const char platform_scheduler[] = \"schtasks\";\n    -+\t*scheduler = SCHEDULER_SCHTASKS;\n    -+\treturn;\n    ++\treturn SCHEDULER_SCHTASKS;\n     +\n      #else\n     -static const char platform_scheduler[] = \"crontab\";\n    -+\t*scheduler = SCHEDULER_CRON;\n    -+\treturn;\n    ++\treturn SCHEDULER_CRON;\n      #endif\n     +}\n      \n    @@ builtin/gc.c: static int crontab_update_schedule(int run_maintenance, int fd, co\n     +\tif (scheduler == SCHEDULER_INVALID)\n     +\t\tBUG(\"invalid scheduler\");\n     +\tif (scheduler == SCHEDULER_AUTO)\n    -+\t\tBUG(\"resolve_auto_scheduler should have been called before\");\n    ++\t\tBUG(\"resolve_scheduler should have been called before\");\n     +\n     +\tif (!scheduler_fn[scheduler].is_available())\n     +\t\tdie(_(\"%s scheduler is not available\"),\n    @@ builtin/gc.c: static int crontab_update_schedule(int run_maintenance, int fd, co\n     +\tstruct option options[] = {\n     +\t\tOPT_CALLBACK_F(\n     +\t\t\t0, \"scheduler\", &opts.scheduler, N_(\"scheduler\"),\n    -+\t\t\tN_(\"scheduler to use to trigger git maintenance run\"),\n    ++\t\t\tN_(\"scheduler to trigger git maintenance run\"),\n     +\t\t\tPARSE_OPT_NONEG, maintenance_opt_scheduler),\n     +\t\tOPT_END()\n     +\t};\n    @@ builtin/gc.c: static int crontab_update_schedule(int run_maintenance, int fd, co\n     +\tif (argc)\n     +\t\tusage_with_options(builtin_maintenance_start_usage, options);\n     +\n    -+\tresolve_auto_scheduler(&opts.scheduler);\n    ++\topts.scheduler = resolve_scheduler(opts.scheduler);\n     +\tvalidate_scheduler(opts.scheduler);\n     +\n      \tif (maintenance_register())\n2:  29628b5a92 ! 3:  0ea5b2fc45 maintenance: add support for systemd timers on Linux\n    @@ Documentation/git-maintenance.txt: OPTIONS\n     ---scheduler=auto|crontab|launchctl|schtasks::\n     +--scheduler=auto|crontab|systemd-timer|launchctl|schtasks::\n      \tWhen combined with the `start` subcommand, specify the scheduler\n    - \tto use to run the hourly, daily and weekly executions of\n    + \tfor running the hourly, daily and weekly executions of\n      \t`git maintenance run`.\n    - \tThe possible values for `<scheduler>` depend on the system: `crontab`\n    --\tis available on POSIX systems, `launchctl` is available on\n    --\tMacOS and `schtasks` is available on Windows.\n    -+\tis available on POSIX systems, `systemd-timer` is available on Linux\n    -+\tsystems, `launchctl` is available on MacOS and `schtasks` is available\n    -+\ton Windows.\n    - \tBy default or when `auto` is specified, a suitable scheduler for\n    - \tthe system is used. On MacOS, `launchctl` is used. On Windows,\n    --\t`schtasks` is used. On all other systems, `crontab` is used.\n    -+\t`schtasks` is used. On Linux, `systemd-timer` is used if user systemd\n    -+\ttimers are available, otherwise, `crontab` is used. On all other systems,\n    -+\t`crontab` is used.\n    +-\tPossible values for `<scheduler>` are `auto`, `crontab` (POSIX),\n    +-\t`launchctl` (macOS), and `schtasks` (Windows).\n    +-\tWhen `auto` is specified, the appropriate platform-specific\n    +-\tscheduler is used. Default is `auto`.\n    ++\tPossible values for `<scheduler>` are `auto`, `crontab`\n    ++\t(POSIX), `systemd-timer` (Linux), `launchctl` (macOS), and\n    ++\t`schtasks` (Windows). When `auto` is specified, the\n    ++\tappropriate platform-specific scheduler is used; on Linux,\n    ++\t`systemd-timer` is used if available, otherwise\n    ++\t`crontab`. Default is `auto`.\n      \n      \n      TROUBLESHOOTING\n    @@ builtin/gc.c: static enum scheduler parse_scheduler(const char *value)\n      \telse if (!strcasecmp(value, \"launchctl\"))\n      \t\treturn SCHEDULER_LAUNCHCTL;\n      \telse if (!strcasecmp(value, \"schtasks\"))\n    -@@ builtin/gc.c: static void resolve_auto_scheduler(enum scheduler *scheduler)\n    - \t*scheduler = SCHEDULER_SCHTASKS;\n    - \treturn;\n    +@@ builtin/gc.c: static enum scheduler resolve_scheduler(enum scheduler scheduler)\n    + #elif defined(GIT_WINDOWS_NATIVE)\n    + \treturn SCHEDULER_SCHTASKS;\n      \n     +#elif defined(__linux__)\n     +\tif (is_systemd_timer_available())\n    -+\t\t*scheduler = SCHEDULER_SYSTEMD;\n    ++\t\treturn SCHEDULER_SYSTEMD;\n     +\telse if (is_crontab_available())\n    -+\t\t*scheduler = SCHEDULER_CRON;\n    ++\t\treturn SCHEDULER_CRON;\n     +\telse\n     +\t\tdie(_(\"neither systemd timers nor crontab are available\"));\n    -+\treturn;\n     +\n      #else\n    - \t*scheduler = SCHEDULER_CRON;\n    - \treturn;\n    + \treturn SCHEDULER_CRON;\n    + #endif\n     \n      ## t/t7900-maintenance.sh ##\n     @@ t/t7900-maintenance.sh: test_xmllint () {\n-- \n2.32.0\n\n"},{"id":"429053","messageId":"20210702142556.99864-4-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210702142556.99864-1-lenaic@lhuard.fr","subject":"[PATCH v7 3/3] maintenance: add support for systemd timers on Linux","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-07-02T14:25:56Z","receivedAt":"2021-07-02T14:26:50Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"The existing mechanism for scheduling background maintenance is done\nthrough cron. On Linux systems managed by systemd, systemd provides an\nalternative to schedule recurring tasks: systemd timers.\n\nThe main motivations to implement systemd timers in addition to cron\nare:\n* cron is optional and Linux systems running systemd might not have it\n  installed.\n* The execution of `crontab -l` can tell us if cron is installed but not\n  if the daemon is actually running.\n* With systemd, each service is run in its own cgroup and its logs are\n  tagged by the service inside journald. With cron, all scheduled tasks\n  are running in the cron daemon cgroup and all the logs of the\n  user-scheduled tasks are pretended to belong to the system cron\n  service.\n  Concretely, a user that doesn’t have access to the system logs won’t\n  have access to the log of their own tasks scheduled by cron whereas\n  they will have access to the log of their own tasks scheduled by\n  systemd timer.\n  Although `cron` attempts to send email, that email may go unseen by\n  the user because these days, local mailboxes are not heavily used\n  anymore.\n\nIn order to schedule git maintenance, we need two unit template files:\n* ~/.config/systemd/user/git-maintenance@.service\n  to define the command to be started by systemd and\n* ~/.config/systemd/user/git-maintenance@.timer\n  to define the schedule at which the command should be run.\n\nThose units are templates that are parameterized by the frequency.\n\nBased on those templates, 3 timers are started:\n* git-maintenance@hourly.timer\n* git-maintenance@daily.timer\n* git-maintenance@weekly.timer\n\nThe command launched by those three timers are the same as with the\nother scheduling methods:\n\n/path/to/git for-each-repo --exec-path=/path/to\n--config=maintenance.repo maintenance run --schedule=%i\n\nwith the full path for git to ensure that the version of git launched\nfor the scheduled maintenance is the same as the one used to run\n`maintenance start`.\n\nThe timer unit contains `Persistent=true` so that, if the computer is\npowered down when a maintenance task should run, the task will be run\nwhen the computer is back powered on.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n Documentation/git-maintenance.txt |  58 +++++++-\n builtin/gc.c                      | 227 ++++++++++++++++++++++++++++++\n t/t7900-maintenance.sh            |  67 ++++++++-\n 3 files changed, 341 insertions(+), 11 deletions(-)\n\ndiff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\nindex 576290b5c6..e2cfb68ab5 100644\n--- a/Documentation/git-maintenance.txt\n+++ b/Documentation/git-maintenance.txt\n@@ -179,14 +179,16 @@ OPTIONS\n \t`maintenance.<task>.enabled` configured as `true` are considered.\n \tSee the 'TASKS' section for the list of accepted `<task>` values.\n \n---scheduler=auto|crontab|launchctl|schtasks::\n+--scheduler=auto|crontab|systemd-timer|launchctl|schtasks::\n \tWhen combined with the `start` subcommand, specify the scheduler\n \tfor running the hourly, daily and weekly executions of\n \t`git maintenance run`.\n-\tPossible values for `<scheduler>` are `auto`, `crontab` (POSIX),\n-\t`launchctl` (macOS), and `schtasks` (Windows).\n-\tWhen `auto` is specified, the appropriate platform-specific\n-\tscheduler is used. Default is `auto`.\n+\tPossible values for `<scheduler>` are `auto`, `crontab`\n+\t(POSIX), `systemd-timer` (Linux), `launchctl` (macOS), and\n+\t`schtasks` (Windows). When `auto` is specified, the\n+\tappropriate platform-specific scheduler is used; on Linux,\n+\t`systemd-timer` is used if available, otherwise\n+\t`crontab`. Default is `auto`.\n \n \n TROUBLESHOOTING\n@@ -286,6 +288,52 @@ schedule to ensure you are executing the correct binaries in your\n schedule.\n \n \n+BACKGROUND MAINTENANCE ON LINUX SYSTEMD SYSTEMS\n+-----------------------------------------------\n+\n+While Linux supports `cron`, depending on the distribution, `cron` may\n+be an optional package not necessarily installed. On modern Linux\n+distributions, systemd timers are superseding it.\n+\n+If user systemd timers are available, they will be used as a replacement\n+of `cron`.\n+\n+In this case, `git maintenance start` will create user systemd timer units\n+and start the timers. The current list of user-scheduled tasks can be found\n+by running `systemctl --user list-timers`. The timers written by `git\n+maintenance start` are similar to this:\n+\n+-----------------------------------------------------------------------\n+$ systemctl --user list-timers\n+NEXT                         LEFT          LAST                         PASSED     UNIT                         ACTIVATES\n+Thu 2021-04-29 19:00:00 CEST 42min left    Thu 2021-04-29 18:00:11 CEST 17min ago  git-maintenance@hourly.timer git-maintenance@hourly.service\n+Fri 2021-04-30 00:00:00 CEST 5h 42min left Thu 2021-04-29 00:00:11 CEST 18h ago    git-maintenance@daily.timer  git-maintenance@daily.service\n+Mon 2021-05-03 00:00:00 CEST 3 days left   Mon 2021-04-26 00:00:11 CEST 3 days ago git-maintenance@weekly.timer git-maintenance@weekly.service\n+-----------------------------------------------------------------------\n+\n+One timer is registered for each `--schedule=<frequency>` option.\n+\n+The definition of the systemd units can be inspected in the following files:\n+\n+-----------------------------------------------------------------------\n+~/.config/systemd/user/git-maintenance@.timer\n+~/.config/systemd/user/git-maintenance@.service\n+~/.config/systemd/user/timers.target.wants/git-maintenance@hourly.timer\n+~/.config/systemd/user/timers.target.wants/git-maintenance@daily.timer\n+~/.config/systemd/user/timers.target.wants/git-maintenance@weekly.timer\n+-----------------------------------------------------------------------\n+\n+`git maintenance start` will overwrite these files and start the timer\n+again with `systemctl --user`, so any customization should be done by\n+creating a drop-in file, i.e. a `.conf` suffixed file in the\n+`~/.config/systemd/user/git-maintenance@.service.d` directory.\n+\n+`git maintenance stop` will stop the user systemd timers and delete\n+the above mentioned files.\n+\n+For more details, see `systemd.timer(5)`.\n+\n+\n BACKGROUND MAINTENANCE ON MACOS SYSTEMS\n ---------------------------------------\n \ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 96a43f99b4..0cc02fa79e 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -2079,10 +2079,221 @@ static int crontab_update_schedule(int run_maintenance, int fd)\n \treturn result;\n }\n \n+#ifdef __linux__\n+\n+static int real_is_systemd_timer_available(void)\n+{\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\n+\tstrvec_pushl(&child.args, \"systemctl\", \"--user\", \"list-timers\", NULL);\n+\tchild.no_stdin = 1;\n+\tchild.no_stdout = 1;\n+\tchild.no_stderr = 1;\n+\tchild.silent_exec_failure = 1;\n+\n+\tif (start_command(&child))\n+\t\treturn 0;\n+\tif (finish_command(&child))\n+\t\treturn 0;\n+\treturn 1;\n+}\n+\n+#else\n+\n+static int real_is_systemd_timer_available(void)\n+{\n+\treturn 0;\n+}\n+\n+#endif\n+\n+static int is_systemd_timer_available(void)\n+{\n+\tconst char *cmd = \"systemctl\";\n+\tint is_available;\n+\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+\treturn real_is_systemd_timer_available();\n+}\n+\n+static char *xdg_config_home_systemd(const char *filename)\n+{\n+\treturn xdg_config_home_for(\"systemd/user\", filename);\n+}\n+\n+static int systemd_timer_enable_unit(int enable,\n+\t\t\t\t     enum schedule_priority schedule)\n+{\n+\tconst char *cmd = \"systemctl\";\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\tconst char *frequency = get_frequency(schedule);\n+\n+\t/*\n+\t * Disabling the systemd unit while it is already disabled makes\n+\t * systemctl print an error.\n+\t * Let's ignore it since it means we already are in the expected state:\n+\t * the unit is disabled.\n+\t *\n+\t * On the other hand, enabling a systemd unit which is already enabled\n+\t * produces no error.\n+\t */\n+\tif (!enable)\n+\t\tchild.no_stderr = 1;\n+\n+\tget_schedule_cmd(&cmd, NULL);\n+\tstrvec_split(&child.args, cmd);\n+\tstrvec_pushl(&child.args, \"--user\", enable ? \"enable\" : \"disable\",\n+\t\t     \"--now\", NULL);\n+\tstrvec_pushf(&child.args, \"git-maintenance@%s.timer\", frequency);\n+\n+\tif (start_command(&child))\n+\t\treturn error(_(\"failed to start systemctl\"));\n+\tif (finish_command(&child))\n+\t\t/*\n+\t\t * Disabling an already disabled systemd unit makes\n+\t\t * systemctl fail.\n+\t\t * Let's ignore this failure.\n+\t\t *\n+\t\t * Enabling an enabled systemd unit doesn't fail.\n+\t\t */\n+\t\tif (enable)\n+\t\t\treturn error(_(\"failed to run systemctl\"));\n+\treturn 0;\n+}\n+\n+static int systemd_timer_delete_unit_templates(void)\n+{\n+\tint ret = 0;\n+\tchar *filename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n+\tif (unlink(filename) && !is_missing_file_error(errno))\n+\t\tret = error_errno(_(\"failed to delete '%s'\"), filename);\n+\tFREE_AND_NULL(filename);\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n+\tif (unlink(filename) && !is_missing_file_error(errno))\n+\t\tret = error_errno(_(\"failed to delete '%s'\"), filename);\n+\n+\tfree(filename);\n+\treturn ret;\n+}\n+\n+static int systemd_timer_delete_units(void)\n+{\n+\treturn systemd_timer_enable_unit(0, SCHEDULE_HOURLY) ||\n+\t       systemd_timer_enable_unit(0, SCHEDULE_DAILY) ||\n+\t       systemd_timer_enable_unit(0, SCHEDULE_WEEKLY) ||\n+\t       systemd_timer_delete_unit_templates();\n+}\n+\n+static int systemd_timer_write_unit_templates(const char *exec_path)\n+{\n+\tchar *filename;\n+\tFILE *file;\n+\tconst char *unit;\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n+\tif (safe_create_leading_directories(filename)) {\n+\t\terror(_(\"failed to create directories for '%s'\"), filename);\n+\t\tgoto error;\n+\t}\n+\tfile = fopen_or_warn(filename, \"w\");\n+\tif (file == NULL)\n+\t\tgoto error;\n+\n+\tunit = \"# This file was created and is maintained by Git.\\n\"\n+\t       \"# Any edits made in this file might be replaced in the future\\n\"\n+\t       \"# by a Git command.\\n\"\n+\t       \"\\n\"\n+\t       \"[Unit]\\n\"\n+\t       \"Description=Optimize Git repositories data\\n\"\n+\t       \"\\n\"\n+\t       \"[Timer]\\n\"\n+\t       \"OnCalendar=%i\\n\"\n+\t       \"Persistent=true\\n\"\n+\t       \"\\n\"\n+\t       \"[Install]\\n\"\n+\t       \"WantedBy=timers.target\\n\";\n+\tif (fputs(unit, file) == EOF) {\n+\t\terror(_(\"failed to write to '%s'\"), filename);\n+\t\tfclose(file);\n+\t\tgoto error;\n+\t}\n+\tif (fclose(file) == EOF) {\n+\t\terror_errno(_(\"failed to flush '%s'\"), filename);\n+\t\tgoto error;\n+\t}\n+\tfree(filename);\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n+\tfile = fopen_or_warn(filename, \"w\");\n+\tif (file == NULL)\n+\t\tgoto error;\n+\n+\tunit = \"# This file was created and is maintained by Git.\\n\"\n+\t       \"# Any edits made in this file might be replaced in the future\\n\"\n+\t       \"# by a Git command.\\n\"\n+\t       \"\\n\"\n+\t       \"[Unit]\\n\"\n+\t       \"Description=Optimize Git repositories data\\n\"\n+\t       \"\\n\"\n+\t       \"[Service]\\n\"\n+\t       \"Type=oneshot\\n\"\n+\t       \"ExecStart=\\\"%s/git\\\" --exec-path=\\\"%s\\\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\\n\"\n+\t       \"LockPersonality=yes\\n\"\n+\t       \"MemoryDenyWriteExecute=yes\\n\"\n+\t       \"NoNewPrivileges=yes\\n\"\n+\t       \"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\\n\"\n+\t       \"RestrictNamespaces=yes\\n\"\n+\t       \"RestrictRealtime=yes\\n\"\n+\t       \"RestrictSUIDSGID=yes\\n\"\n+\t       \"SystemCallArchitectures=native\\n\"\n+\t       \"SystemCallFilter=@system-service\\n\";\n+\tif (fprintf(file, unit, exec_path, exec_path) < 0) {\n+\t\terror(_(\"failed to write to '%s'\"), filename);\n+\t\tfclose(file);\n+\t\tgoto error;\n+\t}\n+\tif (fclose(file) == EOF) {\n+\t\terror_errno(_(\"failed to flush '%s'\"), filename);\n+\t\tgoto error;\n+\t}\n+\tfree(filename);\n+\treturn 0;\n+\n+error:\n+\tfree(filename);\n+\tsystemd_timer_delete_unit_templates();\n+\treturn -1;\n+}\n+\n+static int systemd_timer_setup_units(void)\n+{\n+\tconst char *exec_path = git_exec_path();\n+\n+\tint ret = systemd_timer_write_unit_templates(exec_path) ||\n+\t          systemd_timer_enable_unit(1, SCHEDULE_HOURLY) ||\n+\t          systemd_timer_enable_unit(1, SCHEDULE_DAILY) ||\n+\t          systemd_timer_enable_unit(1, SCHEDULE_WEEKLY);\n+\tif (ret)\n+\t\tsystemd_timer_delete_units();\n+\treturn ret;\n+}\n+\n+static int systemd_timer_update_schedule(int run_maintenance, int fd)\n+{\n+\tif (run_maintenance)\n+\t\treturn systemd_timer_setup_units();\n+\telse\n+\t\treturn systemd_timer_delete_units();\n+}\n+\n enum scheduler {\n \tSCHEDULER_INVALID = -1,\n \tSCHEDULER_AUTO,\n \tSCHEDULER_CRON,\n+\tSCHEDULER_SYSTEMD,\n \tSCHEDULER_LAUNCHCTL,\n \tSCHEDULER_SCHTASKS,\n };\n@@ -2097,6 +2308,11 @@ static const struct {\n \t\t.is_available = is_crontab_available,\n \t\t.update_schedule = crontab_update_schedule,\n \t},\n+\t[SCHEDULER_SYSTEMD] = {\n+\t\t.name = \"systemctl\",\n+\t\t.is_available = is_systemd_timer_available,\n+\t\t.update_schedule = systemd_timer_update_schedule,\n+\t},\n \t[SCHEDULER_LAUNCHCTL] = {\n \t\t.name = \"launchctl\",\n \t\t.is_available = is_launchctl_available,\n@@ -2117,6 +2333,9 @@ static enum scheduler parse_scheduler(const char *value)\n \t\treturn SCHEDULER_AUTO;\n \telse if (!strcasecmp(value, \"cron\") || !strcasecmp(value, \"crontab\"))\n \t\treturn SCHEDULER_CRON;\n+\telse if (!strcasecmp(value, \"systemd\") ||\n+\t\t !strcasecmp(value, \"systemd-timer\"))\n+\t\treturn SCHEDULER_SYSTEMD;\n \telse if (!strcasecmp(value, \"launchctl\"))\n \t\treturn SCHEDULER_LAUNCHCTL;\n \telse if (!strcasecmp(value, \"schtasks\"))\n@@ -2153,6 +2372,14 @@ static enum scheduler resolve_scheduler(enum scheduler scheduler)\n #elif defined(GIT_WINDOWS_NATIVE)\n \treturn SCHEDULER_SCHTASKS;\n \n+#elif defined(__linux__)\n+\tif (is_systemd_timer_available())\n+\t\treturn SCHEDULER_SYSTEMD;\n+\telse if (is_crontab_available())\n+\t\treturn SCHEDULER_CRON;\n+\telse\n+\t\tdie(_(\"neither systemd timers nor crontab are available\"));\n+\n #else\n \treturn SCHEDULER_CRON;\n #endif\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex b36b7f5fb0..b289cae6b9 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -20,6 +20,18 @@ test_xmllint () {\n \tfi\n }\n \n+test_lazy_prereq SYSTEMD_ANALYZE '\n+\tsystemd-analyze --help >out &&\n+\tgrep verify out\n+'\n+\n+test_systemd_analyze_verify () {\n+\tif test_have_prereq SYSTEMD_ANALYZE\n+\tthen\n+\t\tsystemd-analyze verify \"$@\"\n+\tfi\n+}\n+\n test_expect_success 'help text' '\n \ttest_expect_code 129 git maintenance -h 2>err &&\n \ttest_i18ngrep \"usage: git maintenance <subcommand>\" err &&\n@@ -634,15 +646,56 @@ test_expect_success 'start and stop Windows maintenance' '\n \ttest_cmp expect args\n '\n \n+test_expect_success 'start and stop Linux/systemd maintenance' '\n+\twrite_script print-args <<-\\EOF &&\n+\tprintf \"%s\\n\" \"$*\" >>args\n+\tEOF\n+\n+\tXDG_CONFIG_HOME=\"$PWD\" &&\n+\texport XDG_CONFIG_HOME &&\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args\" git maintenance start --scheduler=systemd-timer &&\n+\n+\t# start registers the repo\n+\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n+\n+\ttest_systemd_analyze_verify \"systemd/user/git-maintenance@.service\" &&\n+\n+\tprintf -- \"--user enable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args\" git maintenance stop &&\n+\n+\t# stop does not unregister the repo\n+\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n+\n+\ttest_path_is_missing \"systemd/user/git-maintenance@.timer\" &&\n+\ttest_path_is_missing \"systemd/user/git-maintenance@.service\" &&\n+\n+\tprintf -- \"--user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n+\ttest_cmp expect args\n+'\n+\n test_expect_success 'start and stop when several schedulers are available' '\n \twrite_script print-args <<-\\EOF &&\n \tprintf \"%s\\n\" \"$*\" | sed \"s:gui/[0-9][0-9]*:gui/[UID]:; s:\\(schtasks /create .* /xml\\).*:\\1:;\" >>args\n \tEOF\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n-\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=systemd-timer &&\n+\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n \t\thourly daily weekly >expect &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\tprintf -- \"systemctl --user enable --now git-maintenance@%s.timer\\n\" hourly daily weekly >>expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n+\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >>expect &&\n \tfor frequency in hourly daily weekly\n \tdo\n \t\tPLIST=\"$pfx/Library/LaunchAgents/org.git-scm.git.$frequency.plist\" &&\n@@ -652,17 +705,19 @@ test_expect_success 'start and stop when several schedulers are available' '\n \ttest_cmp expect args &&\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n+\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n \tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n-\t\thourly daily weekly >expect &&\n+\t\thourly daily weekly >>expect &&\n \tprintf \"schtasks /create /tn Git Maintenance (%s) /f /xml\\n\" \\\n \t\thourly daily weekly >>expect &&\n \ttest_cmp expect args &&\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n+\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n \tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n-\t\thourly daily weekly >expect &&\n+\t\thourly daily weekly >>expect &&\n \tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n \t\thourly daily weekly >>expect &&\n \ttest_cmp expect args\n-- \n2.32.0\n\n"},{"id":"429054","messageId":"20210702142556.99864-3-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210702142556.99864-1-lenaic@lhuard.fr","subject":"[PATCH v7 2/3] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-07-02T14:25:55Z","receivedAt":"2021-07-02T14:27:12Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Depending on the system, different schedulers can be used to schedule\nthe hourly, daily and weekly executions of `git maintenance run`:\n* `launchctl` for MacOS,\n* `schtasks` for Windows and\n* `crontab` for everything else.\n\n`git maintenance run` now has an option to let the end-user explicitly\nchoose which scheduler he wants to use:\n`--scheduler=auto|crontab|launchctl|schtasks`.\n\nWhen `git maintenance start --scheduler=XXX` is run, it not only\nregisters `git maintenance run` tasks in the scheduler XXX, it also\nremoves the `git maintenance run` tasks from all the other schedulers to\nensure we cannot have two schedulers launching concurrent identical\ntasks.\n\nThe default value is `auto` which chooses a suitable scheduler for the\nsystem.\n\n`git maintenance stop` doesn't have any `--scheduler` parameter because\nthis command will try to remove the `git maintenance run` tasks from all\nthe available schedulers.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n Documentation/git-maintenance.txt |   9 +\n builtin/gc.c                      | 370 ++++++++++++++++++++++++------\n t/t7900-maintenance.sh            |  55 ++++-\n 3 files changed, 359 insertions(+), 75 deletions(-)\n\ndiff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\nindex 1e738ad398..576290b5c6 100644\n--- a/Documentation/git-maintenance.txt\n+++ b/Documentation/git-maintenance.txt\n@@ -179,6 +179,15 @@ OPTIONS\n \t`maintenance.<task>.enabled` configured as `true` are considered.\n \tSee the 'TASKS' section for the list of accepted `<task>` values.\n \n+--scheduler=auto|crontab|launchctl|schtasks::\n+\tWhen combined with the `start` subcommand, specify the scheduler\n+\tfor running the hourly, daily and weekly executions of\n+\t`git maintenance run`.\n+\tPossible values for `<scheduler>` are `auto`, `crontab` (POSIX),\n+\t`launchctl` (macOS), and `schtasks` (Windows).\n+\tWhen `auto` is specified, the appropriate platform-specific\n+\tscheduler is used. Default is `auto`.\n+\n \n TROUBLESHOOTING\n ---------------\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex f05d2f0a1a..96a43f99b4 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1529,6 +1529,98 @@ static const char *get_frequency(enum schedule_priority schedule)\n \t}\n }\n \n+/*\n+ * get_schedule_cmd` reads the GIT_TEST_MAINT_SCHEDULER environment variable\n+ * to mock the schedulers that `git maintenance start` rely on.\n+ *\n+ * For test purpose, GIT_TEST_MAINT_SCHEDULER can be set to a comma-separated\n+ * list of colon-separated key/value pairs where each pair contains a scheduler\n+ * and its corresponding mock.\n+ *\n+ * * If $GET_TEST_MAINT_SCHEDULER is not set, return false and leave the\n+ *   arguments unmodified.\n+ *\n+ * * If $GET_TEST_MAINT_SCHEDULER is set, return true.\n+ *   In this case, the *cmd value is read as input.\n+ *\n+ *   * if the input value *cmd is the key of one of the comma-separated list\n+ *     item, then *is_available is set to true and *cmd is modified and becomes\n+ *     the mock command.\n+ *\n+ *   * if the input value *cmd isn’t the key of any of the comma-separated list\n+ *     item, then *is_available is set to false.\n+ *\n+ * Ex.:\n+ *   GIT_TEST_MAINT_SCHEDULER not set\n+ *     ┏━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n+ *     ┃ Input ┃                     Output                      ┃\n+ *     ┃ *cmd  ┃ return code │       *cmd        │ *is_available ┃\n+ *     ┣━━━━━━━╋━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━┫\n+ *     ┃ \"foo\" ┃    false    │ \"foo\" (unchanged) │  (unchanged)  ┃\n+ *     ┗━━━━━━━┻━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━┛\n+ *\n+ *   GIT_TEST_MAINT_SCHEDULER set to “foo:./mock_foo.sh,bar:./mock_bar.sh”\n+ *     ┏━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n+ *     ┃ Input ┃                     Output                      ┃\n+ *     ┃ *cmd  ┃ return code │       *cmd        │ *is_available ┃\n+ *     ┣━━━━━━━╋━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━┫\n+ *     ┃ \"foo\" ┃    true     │  \"./mock.foo.sh\"  │     true      ┃\n+ *     ┃ \"qux\" ┃    true     │ \"qux\" (unchanged) │     false     ┃\n+ *     ┗━━━━━━━┻━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━┛\n+ */\n+static int get_schedule_cmd(const char **cmd, int *is_available)\n+{\n+\tchar *item;\n+\tchar *testing = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n+\n+\tif (!testing)\n+\t\treturn 0;\n+\n+\tif (is_available)\n+\t\t*is_available = 0;\n+\n+\tfor (item = testing;;) {\n+\t\tchar *sep;\n+\t\tchar *end_item = strchr(item, ',');\n+\t\tif (end_item)\n+\t\t\t*end_item = '\\0';\n+\n+\t\tsep = strchr(item, ':');\n+\t\tif (!sep)\n+\t\t\tdie(\"GIT_TEST_MAINT_SCHEDULER unparseable: %s\", testing);\n+\t\t*sep = '\\0';\n+\n+\t\tif (!strcmp(*cmd, item)) {\n+\t\t\t*cmd = sep + 1;\n+\t\t\tif (is_available)\n+\t\t\t\t*is_available = 1;\n+\t\t\tUNLEAK(testing);\n+\t\t\treturn 1;\n+\t\t}\n+\n+\t\tif (!end_item)\n+\t\t\tbreak;\n+\t\titem = end_item + 1;\n+\t}\n+\n+\tfree(testing);\n+\treturn 1;\n+}\n+\n+static int is_launchctl_available(void)\n+{\n+\tconst char *cmd = \"launchctl\";\n+\tint is_available;\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+#ifdef __APPLE__\n+\treturn 1;\n+#else\n+\treturn 0;\n+#endif\n+}\n+\n static char *launchctl_service_name(const char *frequency)\n {\n \tstruct strbuf label = STRBUF_INIT;\n@@ -1555,19 +1647,17 @@ static char *launchctl_get_uid(void)\n \treturn xstrfmt(\"gui/%d\", getuid());\n }\n \n-static int launchctl_boot_plist(int enable, const char *filename, const char *cmd)\n+static int launchctl_boot_plist(int enable, const char *filename)\n {\n+\tconst char *cmd = \"launchctl\";\n \tint result;\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tchar *uid = launchctl_get_uid();\n \n+\tget_schedule_cmd(&cmd, NULL);\n \tstrvec_split(&child.args, cmd);\n-\tif (enable)\n-\t\tstrvec_push(&child.args, \"bootstrap\");\n-\telse\n-\t\tstrvec_push(&child.args, \"bootout\");\n-\tstrvec_push(&child.args, uid);\n-\tstrvec_push(&child.args, filename);\n+\tstrvec_pushl(&child.args, enable ? \"bootstrap\" : \"bootout\", uid,\n+\t\t     filename, NULL);\n \n \tchild.no_stderr = 1;\n \tchild.no_stdout = 1;\n@@ -1581,26 +1671,26 @@ static int launchctl_boot_plist(int enable, const char *filename, const char *cm\n \treturn result;\n }\n \n-static int launchctl_remove_plist(enum schedule_priority schedule, const char *cmd)\n+static int launchctl_remove_plist(enum schedule_priority schedule)\n {\n \tconst char *frequency = get_frequency(schedule);\n \tchar *name = launchctl_service_name(frequency);\n \tchar *filename = launchctl_service_filename(name);\n-\tint result = launchctl_boot_plist(0, filename, cmd);\n+\tint result = launchctl_boot_plist(0, filename);\n \tunlink(filename);\n \tfree(filename);\n \tfree(name);\n \treturn result;\n }\n \n-static int launchctl_remove_plists(const char *cmd)\n+static int launchctl_remove_plists(void)\n {\n-\treturn launchctl_remove_plist(SCHEDULE_HOURLY, cmd) ||\n-\t\tlaunchctl_remove_plist(SCHEDULE_DAILY, cmd) ||\n-\t\tlaunchctl_remove_plist(SCHEDULE_WEEKLY, cmd);\n+\treturn launchctl_remove_plist(SCHEDULE_HOURLY) ||\n+\t       launchctl_remove_plist(SCHEDULE_DAILY) ||\n+\t       launchctl_remove_plist(SCHEDULE_WEEKLY);\n }\n \n-static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule, const char *cmd)\n+static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule)\n {\n \tFILE *plist;\n \tint i;\n@@ -1669,8 +1759,8 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n \tfclose(plist);\n \n \t/* bootout might fail if not already running, so ignore */\n-\tlaunchctl_boot_plist(0, filename, cmd);\n-\tif (launchctl_boot_plist(1, filename, cmd))\n+\tlaunchctl_boot_plist(0, filename);\n+\tif (launchctl_boot_plist(1, filename))\n \t\tdie(_(\"failed to bootstrap service %s\"), filename);\n \n \tfree(filename);\n@@ -1678,21 +1768,35 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n \treturn 0;\n }\n \n-static int launchctl_add_plists(const char *cmd)\n+static int launchctl_add_plists(void)\n {\n \tconst char *exec_path = git_exec_path();\n \n-\treturn launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY, cmd) ||\n-\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_DAILY, cmd) ||\n-\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY, cmd);\n+\treturn launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY) ||\n+\t       launchctl_schedule_plist(exec_path, SCHEDULE_DAILY) ||\n+\t       launchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY);\n }\n \n-static int launchctl_update_schedule(int run_maintenance, int fd, const char *cmd)\n+static int launchctl_update_schedule(int run_maintenance, int fd)\n {\n \tif (run_maintenance)\n-\t\treturn launchctl_add_plists(cmd);\n+\t\treturn launchctl_add_plists();\n \telse\n-\t\treturn launchctl_remove_plists(cmd);\n+\t\treturn launchctl_remove_plists();\n+}\n+\n+static int is_schtasks_available(void)\n+{\n+\tconst char *cmd = \"schtasks\";\n+\tint is_available;\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+#ifdef GIT_WINDOWS_NATIVE\n+\treturn 1;\n+#else\n+\treturn 0;\n+#endif\n }\n \n static char *schtasks_task_name(const char *frequency)\n@@ -1702,13 +1806,15 @@ static char *schtasks_task_name(const char *frequency)\n \treturn strbuf_detach(&label, NULL);\n }\n \n-static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd)\n+static int schtasks_remove_task(enum schedule_priority schedule)\n {\n+\tconst char *cmd = \"schtasks\";\n \tint result;\n \tstruct strvec args = STRVEC_INIT;\n \tconst char *frequency = get_frequency(schedule);\n \tchar *name = schtasks_task_name(frequency);\n \n+\tget_schedule_cmd(&cmd, NULL);\n \tstrvec_split(&args, cmd);\n \tstrvec_pushl(&args, \"/delete\", \"/tn\", name, \"/f\", NULL);\n \n@@ -1719,15 +1825,16 @@ static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd\n \treturn result;\n }\n \n-static int schtasks_remove_tasks(const char *cmd)\n+static int schtasks_remove_tasks(void)\n {\n-\treturn schtasks_remove_task(SCHEDULE_HOURLY, cmd) ||\n-\t\tschtasks_remove_task(SCHEDULE_DAILY, cmd) ||\n-\t\tschtasks_remove_task(SCHEDULE_WEEKLY, cmd);\n+\treturn schtasks_remove_task(SCHEDULE_HOURLY) ||\n+\t       schtasks_remove_task(SCHEDULE_DAILY) ||\n+\t       schtasks_remove_task(SCHEDULE_WEEKLY);\n }\n \n-static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule, const char *cmd)\n+static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule)\n {\n+\tconst char *cmd = \"schtasks\";\n \tint result;\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tconst char *xml;\n@@ -1736,6 +1843,8 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority\n \tchar *name = schtasks_task_name(frequency);\n \tstruct strbuf tfilename = STRBUF_INIT;\n \n+\tget_schedule_cmd(&cmd, NULL);\n+\n \tstrbuf_addf(&tfilename, \"%s/schedule_%s_XXXXXX\",\n \t\t    get_git_common_dir(), frequency);\n \ttfile = xmks_tempfile(tfilename.buf);\n@@ -1840,28 +1949,52 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority\n \treturn result;\n }\n \n-static int schtasks_schedule_tasks(const char *cmd)\n+static int schtasks_schedule_tasks(void)\n {\n \tconst char *exec_path = git_exec_path();\n \n-\treturn schtasks_schedule_task(exec_path, SCHEDULE_HOURLY, cmd) ||\n-\t\tschtasks_schedule_task(exec_path, SCHEDULE_DAILY, cmd) ||\n-\t\tschtasks_schedule_task(exec_path, SCHEDULE_WEEKLY, cmd);\n+\treturn schtasks_schedule_task(exec_path, SCHEDULE_HOURLY) ||\n+\t       schtasks_schedule_task(exec_path, SCHEDULE_DAILY) ||\n+\t       schtasks_schedule_task(exec_path, SCHEDULE_WEEKLY);\n }\n \n-static int schtasks_update_schedule(int run_maintenance, int fd, const char *cmd)\n+static int schtasks_update_schedule(int run_maintenance, int fd)\n {\n \tif (run_maintenance)\n-\t\treturn schtasks_schedule_tasks(cmd);\n+\t\treturn schtasks_schedule_tasks();\n \telse\n-\t\treturn schtasks_remove_tasks(cmd);\n+\t\treturn schtasks_remove_tasks();\n+}\n+\n+static int is_crontab_available(void)\n+{\n+\tconst char *cmd = \"crontab\";\n+\tint is_available;\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+\tstrvec_split(&child.args, cmd);\n+\tstrvec_push(&child.args, \"-l\");\n+\tchild.no_stdin = 1;\n+\tchild.no_stdout = 1;\n+\tchild.no_stderr = 1;\n+\tchild.silent_exec_failure = 1;\n+\n+\tif (start_command(&child))\n+\t\treturn 0;\n+\t/* Ignore exit code, as an empty crontab will return error. */\n+\tfinish_command(&child);\n+\treturn 1;\n }\n \n #define BEGIN_LINE \"# BEGIN GIT MAINTENANCE SCHEDULE\"\n #define END_LINE \"# END GIT MAINTENANCE SCHEDULE\"\n \n-static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n+static int crontab_update_schedule(int run_maintenance, int fd)\n {\n+\tconst char *cmd = \"crontab\";\n \tint result = 0;\n \tint in_old_region = 0;\n \tstruct child_process crontab_list = CHILD_PROCESS_INIT;\n@@ -1869,6 +2002,7 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n \tFILE *cron_list, *cron_in;\n \tstruct strbuf line = STRBUF_INIT;\n \n+\tget_schedule_cmd(&cmd, NULL);\n \tstrvec_split(&crontab_list.args, cmd);\n \tstrvec_push(&crontab_list.args, \"-l\");\n \tcrontab_list.in = -1;\n@@ -1945,66 +2079,160 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n \treturn result;\n }\n \n+enum scheduler {\n+\tSCHEDULER_INVALID = -1,\n+\tSCHEDULER_AUTO,\n+\tSCHEDULER_CRON,\n+\tSCHEDULER_LAUNCHCTL,\n+\tSCHEDULER_SCHTASKS,\n+};\n+\n+static const struct {\n+\tconst char *name;\n+\tint (*is_available)(void);\n+\tint (*update_schedule)(int run_maintenance, int fd);\n+} scheduler_fn[] = {\n+\t[SCHEDULER_CRON] = {\n+\t\t.name = \"crontab\",\n+\t\t.is_available = is_crontab_available,\n+\t\t.update_schedule = crontab_update_schedule,\n+\t},\n+\t[SCHEDULER_LAUNCHCTL] = {\n+\t\t.name = \"launchctl\",\n+\t\t.is_available = is_launchctl_available,\n+\t\t.update_schedule = launchctl_update_schedule,\n+\t},\n+\t[SCHEDULER_SCHTASKS] = {\n+\t\t.name = \"schtasks\",\n+\t\t.is_available = is_schtasks_available,\n+\t\t.update_schedule = schtasks_update_schedule,\n+\t},\n+};\n+\n+static enum scheduler parse_scheduler(const char *value)\n+{\n+\tif (!value)\n+\t\treturn SCHEDULER_INVALID;\n+\telse if (!strcasecmp(value, \"auto\"))\n+\t\treturn SCHEDULER_AUTO;\n+\telse if (!strcasecmp(value, \"cron\") || !strcasecmp(value, \"crontab\"))\n+\t\treturn SCHEDULER_CRON;\n+\telse if (!strcasecmp(value, \"launchctl\"))\n+\t\treturn SCHEDULER_LAUNCHCTL;\n+\telse if (!strcasecmp(value, \"schtasks\"))\n+\t\treturn SCHEDULER_SCHTASKS;\n+\telse\n+\t\treturn SCHEDULER_INVALID;\n+}\n+\n+static int maintenance_opt_scheduler(const struct option *opt, const char *arg,\n+\t\t\t\t     int unset)\n+{\n+\tenum scheduler *scheduler = opt->value;\n+\n+\tBUG_ON_OPT_NEG(unset);\n+\n+\t*scheduler = parse_scheduler(arg);\n+\tif (*scheduler == SCHEDULER_INVALID)\n+\t\treturn error(_(\"unrecognized --scheduler argument '%s'\"), arg);\n+\treturn 0;\n+}\n+\n+struct maintenance_start_opts {\n+\tenum scheduler scheduler;\n+};\n+\n+static enum scheduler resolve_scheduler(enum scheduler scheduler)\n+{\n+\tif (scheduler != SCHEDULER_AUTO)\n+\t\treturn scheduler;\n+\n #if defined(__APPLE__)\n-static const char platform_scheduler[] = \"launchctl\";\n+\treturn SCHEDULER_LAUNCHCTL;\n+\n #elif defined(GIT_WINDOWS_NATIVE)\n-static const char platform_scheduler[] = \"schtasks\";\n+\treturn SCHEDULER_SCHTASKS;\n+\n #else\n-static const char platform_scheduler[] = \"crontab\";\n+\treturn SCHEDULER_CRON;\n #endif\n+}\n \n-static int update_background_schedule(int enable)\n+static void validate_scheduler(enum scheduler scheduler)\n {\n-\tint result;\n-\tconst char *scheduler = platform_scheduler;\n-\tconst char *cmd = scheduler;\n-\tchar *testing;\n+\tif (scheduler == SCHEDULER_INVALID)\n+\t\tBUG(\"invalid scheduler\");\n+\tif (scheduler == SCHEDULER_AUTO)\n+\t\tBUG(\"resolve_scheduler should have been called before\");\n+\n+\tif (!scheduler_fn[scheduler].is_available())\n+\t\tdie(_(\"%s scheduler is not available\"),\n+\t\t    scheduler_fn[scheduler].name);\n+}\n+\n+static int update_background_schedule(const struct maintenance_start_opts *opts,\n+\t\t\t\t      int enable)\n+{\n+\tunsigned int i;\n+\tint result = 0;\n \tstruct lock_file lk;\n \tchar *lock_path = xstrfmt(\"%s/schedule\", the_repository->objects->odb->path);\n \n-\ttesting = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n-\tif (testing) {\n-\t\tchar *sep = strchr(testing, ':');\n-\t\tif (!sep)\n-\t\t\tdie(\"GIT_TEST_MAINT_SCHEDULER unparseable: %s\", testing);\n-\t\t*sep = '\\0';\n-\t\tscheduler = testing;\n-\t\tcmd = sep + 1;\n+\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {\n+\t\tfree(lock_path);\n+\t\treturn error(_(\"another process is scheduling background maintenance\"));\n \t}\n \n-\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {\n-\t\tresult = error(_(\"another process is scheduling background maintenance\"));\n-\t\tgoto cleanup;\n+\tfor (i = 1; i < ARRAY_SIZE(scheduler_fn); i++) {\n+\t\tif (enable && opts->scheduler == i)\n+\t\t\tcontinue;\n+\t\tif (!scheduler_fn[i].is_available())\n+\t\t\tcontinue;\n+\t\tscheduler_fn[i].update_schedule(0, get_lock_file_fd(&lk));\n \t}\n \n-\tif (!strcmp(scheduler, \"launchctl\"))\n-\t\tresult = launchctl_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse if (!strcmp(scheduler, \"schtasks\"))\n-\t\tresult = schtasks_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse if (!strcmp(scheduler, \"crontab\"))\n-\t\tresult = crontab_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse\n-\t\tdie(\"unknown background scheduler: %s\", scheduler);\n+\tif (enable)\n+\t\tresult = scheduler_fn[opts->scheduler].update_schedule(\n+\t\t\t1, get_lock_file_fd(&lk));\n \n \trollback_lock_file(&lk);\n \n-cleanup:\n \tfree(lock_path);\n-\tfree(testing);\n \treturn result;\n }\n \n-static int maintenance_start(void)\n+static const char *const builtin_maintenance_start_usage[] = {\n+\tN_(\"git maintenance start [--scheduler=<scheduler>]\"),\n+\tNULL\n+};\n+\n+static int maintenance_start(int argc, const char **argv, const char *prefix)\n {\n+\tstruct maintenance_start_opts opts = { 0 };\n+\tstruct option options[] = {\n+\t\tOPT_CALLBACK_F(\n+\t\t\t0, \"scheduler\", &opts.scheduler, N_(\"scheduler\"),\n+\t\t\tN_(\"scheduler to trigger git maintenance run\"),\n+\t\t\tPARSE_OPT_NONEG, maintenance_opt_scheduler),\n+\t\tOPT_END()\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     builtin_maintenance_start_usage, 0);\n+\tif (argc)\n+\t\tusage_with_options(builtin_maintenance_start_usage, options);\n+\n+\topts.scheduler = resolve_scheduler(opts.scheduler);\n+\tvalidate_scheduler(opts.scheduler);\n+\n \tif (maintenance_register())\n \t\twarning(_(\"failed to add repo to global config\"));\n-\n-\treturn update_background_schedule(1);\n+\treturn update_background_schedule(&opts, 1);\n }\n \n static int maintenance_stop(void)\n {\n-\treturn update_background_schedule(0);\n+\treturn update_background_schedule(NULL, 0);\n }\n \n static const char builtin_maintenance_usage[] =\tN_(\"git maintenance <subcommand> [<options>]\");\n@@ -2018,7 +2246,7 @@ int cmd_maintenance(int argc, const char **argv, const char *prefix)\n \tif (!strcmp(argv[1], \"run\"))\n \t\treturn maintenance_run(argc - 1, argv + 1, prefix);\n \tif (!strcmp(argv[1], \"start\"))\n-\t\treturn maintenance_start();\n+\t\treturn maintenance_start(argc - 1, argv + 1, prefix);\n \tif (!strcmp(argv[1], \"stop\"))\n \t\treturn maintenance_stop();\n \tif (!strcmp(argv[1], \"register\"))\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex b93ae014ee..b36b7f5fb0 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -494,8 +494,21 @@ test_expect_success !MINGW 'register and unregister with regex metacharacters' '\n \t\tmaintenance.repo \"$(pwd)/$META\"\n '\n \n+test_expect_success 'start --scheduler=<scheduler>' '\n+\ttest_expect_code 129 git maintenance start --scheduler=foo 2>err &&\n+\ttest_i18ngrep \"unrecognized --scheduler argument\" err &&\n+\n+\ttest_expect_code 129 git maintenance start --no-scheduler 2>err &&\n+\ttest_i18ngrep \"unknown option\" err &&\n+\n+\ttest_expect_code 128 \\\n+\t\tenv GIT_TEST_MAINT_SCHEDULER=\"launchctl:true,schtasks:true\" \\\n+\t\tgit maintenance start --scheduler=crontab 2>err &&\n+\ttest_i18ngrep \"fatal: crontab scheduler is not available\" err\n+'\n+\n test_expect_success 'start from empty cron table' '\n-\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start --scheduler=crontab &&\n \n \t# start registers the repo\n \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n@@ -518,7 +531,7 @@ test_expect_success 'stop from existing schedule' '\n \n test_expect_success 'start preserves existing schedule' '\n \techo \"Important information!\" >cron.txt &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start --scheduler=crontab &&\n \tgrep \"Important information!\" cron.txt\n '\n \n@@ -547,7 +560,7 @@ test_expect_success 'start and stop macOS maintenance' '\n \tEOF\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start --scheduler=launchctl &&\n \n \t# start registers the repo\n \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n@@ -598,7 +611,7 @@ test_expect_success 'start and stop Windows maintenance' '\n \tEOF\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"schtasks:./print-args\" git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"schtasks:./print-args\" git maintenance start --scheduler=schtasks &&\n \n \t# start registers the repo\n \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n@@ -621,6 +634,40 @@ test_expect_success 'start and stop Windows maintenance' '\n \ttest_cmp expect args\n '\n \n+test_expect_success 'start and stop when several schedulers are available' '\n+\twrite_script print-args <<-\\EOF &&\n+\tprintf \"%s\\n\" \"$*\" | sed \"s:gui/[0-9][0-9]*:gui/[UID]:; s:\\(schtasks /create .* /xml\\).*:\\1:;\" >>args\n+\tEOF\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >expect &&\n+\tfor frequency in hourly daily weekly\n+\tdo\n+\t\tPLIST=\"$pfx/Library/LaunchAgents/org.git-scm.git.$frequency.plist\" &&\n+\t\techo \"launchctl bootout gui/[UID] $PLIST\" >>expect &&\n+\t\techo \"launchctl bootstrap gui/[UID] $PLIST\" >>expect || return 1\n+\tdone &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n+\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n+\t\thourly daily weekly >expect &&\n+\tprintf \"schtasks /create /tn Git Maintenance (%s) /f /xml\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n+\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n+\t\thourly daily weekly >expect &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\ttest_cmp expect args\n+'\n+\n test_expect_success 'register preserves existing strategy' '\n \tgit config maintenance.strategy none &&\n \tgit maintenance register &&\n-- \n2.32.0\n\n"},{"id":"429055","messageId":"2080051.vtJqlmqO2r@coruscant.lhuard.fr","threadId":"55603","inReplyTo":"CAPig+cSLi7aN=6ahrHwy4fO-7JMBN3pmzfpWe5ZXOcC9j4+e+g@mail.gmail.com","subject":"Re: [PATCH v6 2/3] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-07-02T15:04:10Z","receivedAt":"2021-07-02T15:04:28Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Le lundi 14 juin 2021, 06:36:09 CEST Eric Sunshine a écrit :\n\n> Thanks. Unfortunately, I haven't been following this series too\n> closely since I reviewed v1, so I set aside time to review v6, which I\n> have now done.\n> …\n> I do, though, have one question (below) about is_crontab_available()\n> for which I could not figure out the answer.\n\nThank you very much for your review.\nI’ve just submitted a new re-roll that should address the points you raised \nand in particular the `is_crontab_available` unexpected behavior.\n\n…\n> > +static int is_launchctl_available(void)\n> > +{\n> > +       const char *cmd = \"launchctl\";\n> > +       int is_available;\n> > +       if (get_schedule_cmd(&cmd, &is_available))\n> > +               return is_available;\n> > +\n> > +#ifdef __APPLE__\n> > +       return 1;\n> > +#else\n> > +       return 0;\n> > +#endif\n> > +}\n> \n> On this project, we usually frown upon #if conditionals within\n> functions since the code often can become unreadable. The usage in\n> this function doesn't suffer from that problem, however,\n> resolve_auto_scheduler() is somewhat ugly. An alternative would be to\n> set up these values outside of all functions, perhaps like this:\n> \n>     #ifdef __APPLE__\n>     #define MAINT_SCHEDULER SCHEDULER_LAUNCHCTL\n>     #elif GIT_WINDOWS_NATIVE\n>     #define MAINT_SCHEDULER SCHEDULER_SCHTASKS\n>     #else\n>     #define MAINT_SCHEDULER SCHEDULER_CRON\n>     #endif\n> \n> and then:\n> \n>     static int is_launchctl_available(void)\n>     {\n>         if (get_schedule_cmd(...))\n>             return is_available;\n>         return MAINT_SCHEDULER == SCHEDULER_LAUNCHCTL;\n>     }\n> \n>     static void resolve_auto_scheduler(enum scheduler *scheduler)\n>     {\n>         if (*scheduler == SCHEDULER_AUTO)\n>             *scheduler = MAINT_SCHEDULER;\n>     }\n> \n\nThis approach would unfortunately work only for the second patch of this \nseries where a single scheduler is available on each platform.\nWith the third patch of this series, `resolve_auto_scheduler` doesn’t return a \nvalue that is fully determined at compilation time anymore.\nOn Linux, both `crontab` and `systemd-timers` are susceptible to be available \nand this is checked at runtime.\nSo, with the third patch of this series, it wouldn’t be possible anymore to \ndefine a single value for `MAINT_SCHEDULER` and to base \n`resolve_auto_scheduler` on it.\n\n…\n> > +                       PARSE_OPT_NONEG, maintenance_opt_scheduler),\n> > +               OPT_END()\n> > +       };\n> > diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\n> > @@ -494,8 +494,21 @@ test_expect_success !MINGW 'register and unregister\n> > with regex metacharacters' ' +test_expect_success 'start\n> > --scheduler=<scheduler>' '\n> > +       test_expect_code 129 git maintenance start --scheduler=foo 2>err\n> > &&\n> > +       test_i18ngrep \"unrecognized --scheduler argument\" err &&\n> > +\n> > +       test_expect_code 129 git maintenance start --no-scheduler 2>err &&\n> > +       test_i18ngrep \"unknown option\" err &&\n> > +\n> > +       test_expect_code 128 \\\n> > +               env\n> > GIT_TEST_MAINT_SCHEDULER=\"launchctl:true,schtasks:true\" \\ +              \n> > git maintenance start --scheduler=crontab 2>err && +       test_i18ngrep\n> > \"fatal: crontab scheduler is not available\" err +'\n> \n> Why does this test care about the exact exit codes rather than simply\n> using test_must_fail() as is typically done elsewhere in the test\n> suite, especially since we're also checking the error message itself?\n> Am I missing some non-obvious property of the error codes?\n\nI have no strong opinion on this.\nI only mimicked the `help text` test that is at the top of the `t7900-\nmaintenance.sh` file as it was also testing invalid commands and checking the \nresulting error message.\n\n> I don't see `auto` being tested anywhere. Do we want such a test? (It\n> seems like it should be doable, though perhaps the complexity is too\n> high -- I haven't thought it through fully.)\n\nMy main problem with the `auto` is that a big part of its logic is determined \nat compilation time with `#if` statements based on the platform.\nAnd it seems that the tests are designed so far to test the same things on all \nplatforms.\nA solution could be to completely get rid of the platform `#if` statements and \nto turn all the detection logic as runtime tests.\nBut it would mean that, for ex., the git binary for Linux would systematically \ncheck if the MacOS and Windows specific schedulers are available.\n\nCheers,\nLénaïc.\n\n\n"},{"id":"429065","messageId":"xmqq8s2opoj8.fsf@gitster.g","threadId":"55603","inReplyTo":"20210702142556.99864-1-lenaic@lhuard.fr","subject":"Re: [PATCH v7 0/3] maintenance: add support for systemd timers on Linux","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-07-02T18:18:51Z","receivedAt":"2021-07-02T18:18:53Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Lénaïc Huard <lenaic@lhuard.fr> writes:\n\n> Hello,\n>\n> Please find hereafter a new reroll of my patchset to add support for\n> systemd timers on Linux for the `git maintenance start` command.\n>\n> The changes compared to the previous version address the remarks\n> raised during the previous review.\n\nwhich are...?\n\n>\n> The patches are:\n>\n> * cache.h: Introduce a generic \"xdg_config_home_for(…)\" function\n> * maintenance: `git maintenance run` learned `--scheduler=<scheduler>`\n> * maintenance: add support for systemd timers on Linux\n\nThanks, will replace.\n"},{"id":"429285","messageId":"387ad277-a75c-92ca-b7af-163d10d3b4a4@gmail.com","threadId":"55603","inReplyTo":"20210702142556.99864-1-lenaic@lhuard.fr","subject":"Re: [PATCH v7 0/3] maintenance: add support for systemd timers on Linux","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2021-07-06T13:18:44Z","receivedAt":"2021-07-06T13:18:54Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi  Lénaïc\n\nOn 02/07/2021 15:25, Lénaïc Huard wrote:\n> Hello,\n> \n> Please find hereafter a new reroll of my patchset to add support for\n> systemd timers on Linux for the `git maintenance start` command.\n> \n> The changes compared to the previous version address the remarks\n> raised during the previous review.\n> ... \n> Diff-intervalle contre v6 :\n> -:  ---------- > 1:  899b11ed5b cache.h: Introduce a generic \"xdg_config_home_for(…)\" function\n> 1:  604627f347 ! 2:  f3e2f0256b maintenance: `git maintenance run` learned `--scheduler=<scheduler>`\n>      @@ Documentation/git-maintenance.txt: OPTIONS\n>        \n>       +--scheduler=auto|crontab|launchctl|schtasks::\n>       +\tWhen combined with the `start` subcommand, specify the scheduler\n>      -+\tto use to run the hourly, daily and weekly executions of\n>      ++\tfor running the hourly, daily and weekly executions of\n>       +\t`git maintenance run`.\n>      -+\tThe possible values for `<scheduler>` depend on the system: `crontab`\n>      -+\tis available on POSIX systems, `launchctl` is available on\n>      -+\tMacOS and `schtasks` is available on Windows.\n>      -+\tBy default or when `auto` is specified, a suitable scheduler for\n>      -+\tthe system is used. On MacOS, `launchctl` is used. On Windows,\n>      -+\t`schtasks` is used. On all other systems, `crontab` is used.\n>      ++\tPossible values for `<scheduler>` are `auto`, `crontab` (POSIX),\n>      ++\t`launchctl` (macOS), and `schtasks` (Windows).\n>      ++\tWhen `auto` is specified, the appropriate platform-specific\n>      ++\tscheduler is used. Default is `auto`.\n>       +\n>        \n>        TROUBLESHOOTING\n>      @@ builtin/gc.c: static const char *get_frequency(enum schedule_priority schedule)\n>        \t}\n>        }\n>        \n>      ++/*\n>      ++ * get_schedule_cmd` reads the GIT_TEST_MAINT_SCHEDULER environment variable\n>      ++ * to mock the schedulers that `git maintenance start` rely on.\n>      ++ *\n>      ++ * For test purpose, GIT_TEST_MAINT_SCHEDULER can be set to a comma-separated\n>      ++ * list of colon-separated key/value pairs where each pair contains a scheduler\n>      ++ * and its corresponding mock.\n>      ++ *\n>      ++ * * If $GET_TEST_MAINT_SCHEDULER is not set, return false and leave the\n>      ++ *   arguments unmodified.\n>      ++ *\n>      ++ * * If $GET_TEST_MAINT_SCHEDULER is set, return true.\n>      ++ *   In this case, the *cmd value is read as input.\n>      ++ *\n>      ++ *   * if the input value *cmd is the key of one of the comma-separated list\n>      ++ *     item, then *is_available is set to true and *cmd is modified and becomes\n>      ++ *     the mock command.\n>      ++ *\n>      ++ *   * if the input value *cmd isn’t the key of any of the comma-separated list\n>      ++ *     item, then *is_available is set to false.\n>      ++ *\n>      ++ * Ex.:\n>      ++ *   GIT_TEST_MAINT_SCHEDULER not set\n>      ++ *     ┏━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n>      ++ *     ┃ Input ┃                     Output                      ┃\n>      ++ *     ┃ *cmd  ┃ return code │       *cmd        │ *is_available ┃\n>      ++ *     ┣━━━━━━━╋━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━┫\n>      ++ *     ┃ \"foo\" ┃    false    │ \"foo\" (unchanged) │  (unchanged)  ┃\n>      ++ *     ┗━━━━━━━┻━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━┛\n>      ++ *\n>      ++ *   GIT_TEST_MAINT_SCHEDULER set to “foo:./mock_foo.sh,bar:./mock_bar.sh”\n>      ++ *     ┏━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n>      ++ *     ┃ Input ┃                     Output                      ┃\n>      ++ *     ┃ *cmd  ┃ return code │       *cmd        │ *is_available ┃\n>      ++ *     ┣━━━━━━━╋━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━┫\n>      ++ *     ┃ \"foo\" ┃    true     │  \"./mock.foo.sh\"  │     true      ┃\n>      ++ *     ┃ \"qux\" ┃    true     │ \"qux\" (unchanged) │     false     ┃\n>      ++ *     ┗━━━━━━━┻━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━┛\n>      ++ */\n>       +static int get_schedule_cmd(const char **cmd, int *is_available)\n>       +{\n>       +\tchar *item;\n>      @@ builtin/gc.c: static int schtasks_schedule_task(const char *exec_path, enum sche\n>       +\tint is_available;\n>       +\tstruct child_process child = CHILD_PROCESS_INIT;\n>       +\n>      -+\tif (get_schedule_cmd(&cmd, &is_available) && !is_available)\n>      -+\t\treturn 0;\n>      ++\tif (get_schedule_cmd(&cmd, &is_available))\n>      ++\t\treturn is_available;\n\nThis fixes the bug that Eric found with the last version - excellent.\n\n>       +\n>       +\tstrvec_split(&child.args, cmd);\n>       +\tstrvec_push(&child.args, \"-l\");\n>      @@ builtin/gc.c: static int crontab_update_schedule(int run_maintenance, int fd, co\n>       +\tenum scheduler scheduler;\n>       +};\n>       +\n>      -+static void resolve_auto_scheduler(enum scheduler *scheduler)\n>      ++static enum scheduler resolve_scheduler(enum scheduler scheduler)\n>       +{\n>      -+\tif (*scheduler != SCHEDULER_AUTO)\n>      -+\t\treturn;\n>      ++\tif (scheduler != SCHEDULER_AUTO)\n>      ++\t\treturn scheduler;\n>       +\n>        #if defined(__APPLE__)\n>       -static const char platform_scheduler[] = \"launchctl\";\n>      -+\t*scheduler = SCHEDULER_LAUNCHCTL;\n>      -+\treturn;\n>      ++\treturn SCHEDULER_LAUNCHCTL;\n>       +\n>        #elif defined(GIT_WINDOWS_NATIVE)\n>       -static const char platform_scheduler[] = \"schtasks\";\n>      -+\t*scheduler = SCHEDULER_SCHTASKS;\n>      -+\treturn;\n>      ++\treturn SCHEDULER_SCHTASKS;\n>       +\n>        #else\n>       -static const char platform_scheduler[] = \"crontab\";\n>      -+\t*scheduler = SCHEDULER_CRON;\n>      -+\treturn;\n>      ++\treturn SCHEDULER_CRON;\n>        #endif\n>       +}\n\nThis is one of the changes that Eric suggested, I agree it improves the \ncode.\n\nThanks for your work on these patches, I've scanned the rest of the \nrange-diff and I'd be happy to see this version merged\n\nBest Wishes\n\nPhillip\n\n>      @@ builtin/gc.c: static int crontab_update_schedule(int run_maintenance, int fd, co\n>       +\tif (scheduler == SCHEDULER_INVALID)\n>       +\t\tBUG(\"invalid scheduler\");\n>       +\tif (scheduler == SCHEDULER_AUTO)\n>      -+\t\tBUG(\"resolve_auto_scheduler should have been called before\");\n>      ++\t\tBUG(\"resolve_scheduler should have been called before\");\n>       +\n>       +\tif (!scheduler_fn[scheduler].is_available())\n>       +\t\tdie(_(\"%s scheduler is not available\"),\n>      @@ builtin/gc.c: static int crontab_update_schedule(int run_maintenance, int fd, co\n>       +\tstruct option options[] = {\n>       +\t\tOPT_CALLBACK_F(\n>       +\t\t\t0, \"scheduler\", &opts.scheduler, N_(\"scheduler\"),\n>      -+\t\t\tN_(\"scheduler to use to trigger git maintenance run\"),\n>      ++\t\t\tN_(\"scheduler to trigger git maintenance run\"),\n>       +\t\t\tPARSE_OPT_NONEG, maintenance_opt_scheduler),\n>       +\t\tOPT_END()\n>       +\t};\n>      @@ builtin/gc.c: static int crontab_update_schedule(int run_maintenance, int fd, co\n>       +\tif (argc)\n>       +\t\tusage_with_options(builtin_maintenance_start_usage, options);\n>       +\n>      -+\tresolve_auto_scheduler(&opts.scheduler);\n>      ++\topts.scheduler = resolve_scheduler(opts.scheduler);\n>       +\tvalidate_scheduler(opts.scheduler);\n>       +\n>        \tif (maintenance_register())\n> 2:  29628b5a92 ! 3:  0ea5b2fc45 maintenance: add support for systemd timers on Linux\n>      @@ Documentation/git-maintenance.txt: OPTIONS\n>       ---scheduler=auto|crontab|launchctl|schtasks::\n>       +--scheduler=auto|crontab|systemd-timer|launchctl|schtasks::\n>        \tWhen combined with the `start` subcommand, specify the scheduler\n>      - \tto use to run the hourly, daily and weekly executions of\n>      + \tfor running the hourly, daily and weekly executions of\n>        \t`git maintenance run`.\n>      - \tThe possible values for `<scheduler>` depend on the system: `crontab`\n>      --\tis available on POSIX systems, `launchctl` is available on\n>      --\tMacOS and `schtasks` is available on Windows.\n>      -+\tis available on POSIX systems, `systemd-timer` is available on Linux\n>      -+\tsystems, `launchctl` is available on MacOS and `schtasks` is available\n>      -+\ton Windows.\n>      - \tBy default or when `auto` is specified, a suitable scheduler for\n>      - \tthe system is used. On MacOS, `launchctl` is used. On Windows,\n>      --\t`schtasks` is used. On all other systems, `crontab` is used.\n>      -+\t`schtasks` is used. On Linux, `systemd-timer` is used if user systemd\n>      -+\ttimers are available, otherwise, `crontab` is used. On all other systems,\n>      -+\t`crontab` is used.\n>      +-\tPossible values for `<scheduler>` are `auto`, `crontab` (POSIX),\n>      +-\t`launchctl` (macOS), and `schtasks` (Windows).\n>      +-\tWhen `auto` is specified, the appropriate platform-specific\n>      +-\tscheduler is used. Default is `auto`.\n>      ++\tPossible values for `<scheduler>` are `auto`, `crontab`\n>      ++\t(POSIX), `systemd-timer` (Linux), `launchctl` (macOS), and\n>      ++\t`schtasks` (Windows). When `auto` is specified, the\n>      ++\tappropriate platform-specific scheduler is used; on Linux,\n>      ++\t`systemd-timer` is used if available, otherwise\n>      ++\t`crontab`. Default is `auto`.\n>        \n>        \n>        TROUBLESHOOTING\n>      @@ builtin/gc.c: static enum scheduler parse_scheduler(const char *value)\n>        \telse if (!strcasecmp(value, \"launchctl\"))\n>        \t\treturn SCHEDULER_LAUNCHCTL;\n>        \telse if (!strcasecmp(value, \"schtasks\"))\n>      -@@ builtin/gc.c: static void resolve_auto_scheduler(enum scheduler *scheduler)\n>      - \t*scheduler = SCHEDULER_SCHTASKS;\n>      - \treturn;\n>      +@@ builtin/gc.c: static enum scheduler resolve_scheduler(enum scheduler scheduler)\n>      + #elif defined(GIT_WINDOWS_NATIVE)\n>      + \treturn SCHEDULER_SCHTASKS;\n>        \n>       +#elif defined(__linux__)\n>       +\tif (is_systemd_timer_available())\n>      -+\t\t*scheduler = SCHEDULER_SYSTEMD;\n>      ++\t\treturn SCHEDULER_SYSTEMD;\n>       +\telse if (is_crontab_available())\n>      -+\t\t*scheduler = SCHEDULER_CRON;\n>      ++\t\treturn SCHEDULER_CRON;\n>       +\telse\n>       +\t\tdie(_(\"neither systemd timers nor crontab are available\"));\n>      -+\treturn;\n>       +\n>        #else\n>      - \t*scheduler = SCHEDULER_CRON;\n>      - \treturn;\n>      + \treturn SCHEDULER_CRON;\n>      + #endif\n>       \n>        ## t/t7900-maintenance.sh ##\n>       @@ t/t7900-maintenance.sh: test_xmllint () {\n> \n\n"},{"id":"429335","messageId":"87h7h75hzz.fsf@evledraar.gmail.com","threadId":"55603","inReplyTo":"20210702142556.99864-3-lenaic@lhuard.fr","subject":"Re: [PATCH v7 2/3] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-07-06T19:56:38Z","receivedAt":"2021-07-06T20:01:42Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Jul 02 2021, Lénaïc Huard wrote:\n\n> + *     ┏━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n> + *     ┃ Input ┃                     Output                      ┃\n> + *     ┃ *cmd  ┃ return code │       *cmd        │ *is_available ┃\n> + *     ┣━━━━━━━╋━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━┫\n> + *     ┃ \"foo\" ┃    false    │ \"foo\" (unchanged) │  (unchanged)  ┃\n> + *     ┗━━━━━━━┻━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━┛\n\nI wonder if we have developers for whom the non-ASCII here is an issue.\n\n> +static int get_schedule_cmd(const char **cmd, int *is_available)\n> +{\n> +\tchar *item;\n> +\tchar *testing = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n> +\n> +\tif (!testing)\n> +\t\treturn 0;\n> +\n> +\tif (is_available)\n> +\t\t*is_available = 0;\n> +\n> +\tfor (item = testing;;) {\n> +\t\tchar *sep;\n> +\t\tchar *end_item = strchr(item, ',');\n> +\t\tif (end_item)\n> +\t\t\t*end_item = '\\0';\n> +\n> +\t\tsep = strchr(item, ':');\n> +\t\tif (!sep)\n> +\t\t\tdie(\"GIT_TEST_MAINT_SCHEDULER unparseable: %s\", testing);\n> +\t\t*sep = '\\0';\n> +\n> +\t\tif (!strcmp(*cmd, item)) {\n> +\t\t\t*cmd = sep + 1;\n> +\t\t\tif (is_available)\n> +\t\t\t\t*is_available = 1;\n> +\t\t\tUNLEAK(testing);\n> +\t\t\treturn 1;\n> +\t\t}\n\nThis sort of code is much more pleseant to read and work with if you use\nstrbuf_split_buf(). This isn't performance sensitive, so a few more\nallocations is fine.\n\n> +#ifdef __APPLE__\n> +\treturn 1;\n> +#else\n> +\treturn 0;\n> +#endif\n> +}\n\nI see this is partially a pre-existing thing in the file, but we have an\n__APPLE__ already in cache.h. Perhaps define a iLAUNCHCTL_AVAILABLE\nthere. See e.g. 62e5ee81a39 (read-cache.c: remove #ifdef NO_PTHREADS,\n2018-11-03).\n"},{"id":"429336","messageId":"87eecb5hth.fsf@evledraar.gmail.com","threadId":"55603","inReplyTo":"20210702142556.99864-4-lenaic@lhuard.fr","subject":"Re: [PATCH v7 3/3] maintenance: add support for systemd timers on Linux","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-07-06T20:03:44Z","receivedAt":"2021-07-06T20:05:36Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Jul 02 2021, Lénaïc Huard wrote:\n\n[nits]\n\n> +#ifdef __linux__\n> +\n> +static int real_is_systemd_timer_available(void)\n> +{\n> +\tstruct child_process child = CHILD_PROCESS_INIT;\n> +\n> +\tstrvec_pushl(&child.args, \"systemctl\", \"--user\", \"list-timers\", NULL);\n> +\tchild.no_stdin = 1;\n> +\tchild.no_stdout = 1;\n> +\tchild.no_stderr = 1;\n> +\tchild.silent_exec_failure = 1;\n> +\n> +\tif (start_command(&child))\n> +\t\treturn 0;\n> +\tif (finish_command(&child))\n> +\t\treturn 0;\n> +\treturn 1;\n> +}\n> +\n> +#else\n> +\n> +static int real_is_systemd_timer_available(void)\n> +{\n> +\treturn 0;\n> +}\n> +\n> +#endif\n\nDitto the Apple macro, i.e. if most/all of the code complies better to\njust compile it on all platforms.\n\nIn 2/3 we have:\n\t\n\t+struct maintenance_start_opts {\n\t+\tenum scheduler scheduler;\n\t+};\n\t\nIt's not mentioned (perhaps discussed in previous rounds) but I assumed\nthe struct would grow in 3/3, but it didn't. Why not pass \"enum\nscheduler \" around directly?\n"},{"id":"429348","messageId":"xmqq5yxni2rn.fsf@gitster.g","threadId":"55603","inReplyTo":"87h7h75hzz.fsf@evledraar.gmail.com","subject":"Re: [PATCH v7 2/3] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-07-06T20:52:12Z","receivedAt":"2021-07-06T20:52:15Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n> On Fri, Jul 02 2021, Lénaïc Huard wrote:\n>\n>> + *     ┏━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n>> + *     ┃ Input ┃                     Output                      ┃\n>> + *     ┃ *cmd  ┃ return code │       *cmd        │ *is_available ┃\n>> + *     ┣━━━━━━━╋━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━┫\n>> + *     ┃ \"foo\" ┃    false    │ \"foo\" (unchanged) │  (unchanged)  ┃\n>> + *     ┗━━━━━━━┻━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━┛\n>\n> I wonder if we have developers for whom the non-ASCII here is an issue.\n\nI do have an issue myself ;-) but I can survive.  I do not know\nabout others.\n\n> This sort of code is much more pleseant to read and work with if you use\n> strbuf_split_buf(). This isn't performance sensitive, so a few more\n> allocations is fine.\n\nPlease do not encourage use of strbuf_split_buf().  It is a\nmisdesigned API as it rarely is justifyable to have an array, each\nelement of which can be independently tweaked by being strbuf.  We\nare not implementing a text editor after all ;-)\n\nA helper function that takes a string and returns a strvec would be\na good fit, though.\n\n>> +#ifdef __APPLE__\n>> +\treturn 1;\n>> +#else\n>> +\treturn 0;\n>> +#endif\n>> +}\n>\n> I see this is partially a pre-existing thing in the file, but we have an\n> __APPLE__ already in cache.h. Perhaps define a iLAUNCHCTL_AVAILABLE\n> there. See e.g. 62e5ee81a39 (read-cache.c: remove #ifdef NO_PTHREADS,\n> 2018-11-03).\n\nExcellent suggestion.\n"},{"id":"429358","messageId":"60e4c8b0c734b_1c428120895@natae.notmuch","threadId":"55603","inReplyTo":"87h7h75hzz.fsf@evledraar.gmail.com","subject":"Re: [PATCH v7 2/3] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2021-07-06T21:18:40Z","receivedAt":"2021-07-06T21:18:45Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Ævar Arnfjörð Bjarmason wrote:\n> \n> On Fri, Jul 02 2021, Lénaïc Huard wrote:\n> \n> > + *     ┏━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n> > + *     ┃ Input ┃                     Output                      ┃\n> > + *     ┃ *cmd  ┃ return code │       *cmd        │ *is_available ┃\n> > + *     ┣━━━━━━━╋━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━┫\n> > + *     ┃ \"foo\" ┃    false    │ \"foo\" (unchanged) │  (unchanged)  ┃\n> > + *     ┗━━━━━━━┻━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━┛\n> \n> I wonder if we have developers for whom the non-ASCII here is an issue.\n\nIt's not an issue per se for me, it just looks weird to my eyes, like\nseeing an emoji in the middle of a text file.\n\nIt's not wrong, I'm just not used to it. Really not used to it.\n\n-- \nFelipe Contreras"},{"id":"429856","messageId":"YOzbO1/mfL8hKhBT@coredump.intra.peff.net","threadId":"55603","inReplyTo":"xmqq5yxni2rn.fsf@gitster.g","subject":"Re: [PATCH v7 2/3] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2021-07-13T00:15:55Z","receivedAt":"2021-07-13T00:15:57Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Tue, Jul 06, 2021 at 01:52:12PM -0700, Junio C Hamano wrote:\n\n> > This sort of code is much more pleseant to read and work with if you use\n> > strbuf_split_buf(). This isn't performance sensitive, so a few more\n> > allocations is fine.\n> \n> Please do not encourage use of strbuf_split_buf().  It is a\n> misdesigned API as it rarely is justifyable to have an array, each\n> element of which can be independently tweaked by being strbuf.  We\n> are not implementing a text editor after all ;-)\n\nVery much agreed on avoiding strbuf_split_buf(). My usual go-to is\nstring_list_split(), which I think would work here for splitting on \":\".\n\n> A helper function that takes a string and returns a strvec would be\n> a good fit, though.\n\nI was going to second that, but I see we already have one. :) Dscho\nintroduced it in c5aa6db64f (argv_array: offer to split a string by\nwhitespace, 2018-04-25), and then it later became strvec_split().\n\nAnd indeed, Lénaïc's patches use it elsewhere. I think it doesn't work\nin this instance because it can't take an arbitrary delimiter. But I\nwouldn't at all mind seeing it grow that feature (and I suspect it could\neven share some code with string_list_split(), but didn't look).\n\n-Peff\n"},{"id":"429861","messageId":"CAPig+cT-x4_YuxmmoFw62jFqKRFJrS_UkyNOkdQz9-Gwec3QCQ@mail.gmail.com","threadId":"55603","inReplyTo":"YOzbO1/mfL8hKhBT@coredump.intra.peff.net","subject":"Re: [PATCH v7 2/3] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2021-07-13T02:22:22Z","receivedAt":"2021-07-13T02:22:36Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Mon, Jul 12, 2021 at 8:16 PM Jeff King <peff@peff.net> wrote:\n> On Tue, Jul 06, 2021 at 01:52:12PM -0700, Junio C Hamano wrote:\n> > A helper function that takes a string and returns a strvec would be\n> > a good fit, though.\n>\n> I was going to second that, but I see we already have one. :) Dscho\n> introduced it in c5aa6db64f (argv_array: offer to split a string by\n> whitespace, 2018-04-25), and then it later became strvec_split().\n>\n> And indeed, Lénaïc's patches use it elsewhere. I think it doesn't work\n> in this instance because it can't take an arbitrary delimiter. But I\n> wouldn't at all mind seeing it grow that feature (and I suspect it could\n> even share some code with string_list_split(), but didn't look).\n\nSince Lénaïc is a relative newcomer to the project, can we, as\nreviewers, be clear that we don't expect him to perform the task of\ngeneralizing strvec_split() just to get this series -- which is\nalready at v7 -- landed? I gave the previous round a pretty thorough\ngoing-over and -- aside from one minor test-time bug -- didn't find\nany show-stoppers which should prevent it from landing. While it may\nbe the case that the series has a superficial wart here and there\n(such as #ifdef's in function bodies, and non-ASCII fancy comment\nboxes), the review comments on the latest round have pretty much all\nbeen subjective; I haven't seen any outright actionable observations.\nExtra polishing based upon the subjective review comments can always\nbe done later atop Lénaïc's series (if someone -- not necessarily\nLénaïc -- wants to do so) without asking him for endless re-rolls.\n"},{"id":"429864","messageId":"YO0O9JHtnYrk9qRm@coredump.intra.peff.net","threadId":"55603","inReplyTo":"CAPig+cT-x4_YuxmmoFw62jFqKRFJrS_UkyNOkdQz9-Gwec3QCQ@mail.gmail.com","subject":"Re: [PATCH v7 2/3] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2021-07-13T03:56:36Z","receivedAt":"2021-07-13T03:56:53Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Mon, Jul 12, 2021 at 10:22:22PM -0400, Eric Sunshine wrote:\n\n> On Mon, Jul 12, 2021 at 8:16 PM Jeff King <peff@peff.net> wrote:\n> > On Tue, Jul 06, 2021 at 01:52:12PM -0700, Junio C Hamano wrote:\n> > > A helper function that takes a string and returns a strvec would be\n> > > a good fit, though.\n> >\n> > I was going to second that, but I see we already have one. :) Dscho\n> > introduced it in c5aa6db64f (argv_array: offer to split a string by\n> > whitespace, 2018-04-25), and then it later became strvec_split().\n> >\n> > And indeed, Lénaïc's patches use it elsewhere. I think it doesn't work\n> > in this instance because it can't take an arbitrary delimiter. But I\n> > wouldn't at all mind seeing it grow that feature (and I suspect it could\n> > even share some code with string_list_split(), but didn't look).\n> \n> Since Lénaïc is a relative newcomer to the project, can we, as\n> reviewers, be clear that we don't expect him to perform the task of\n> generalizing strvec_split() just to get this series -- which is\n> already at v7 -- landed?\n\nYeah, sorry if I was unclear on that. That is absolutely not a\nrequirement for this series.\n\n(I do not have an opinion on Ævar's original question about using a\nsplit function rather than open-coding. _If_ we were to do that,\nstring_list_split() would be the sensible existing mechanism. But it is\nalso not worth derailing the series for).\n\n-Peff\n"},{"id":"429865","messageId":"CAPig+cQFHw3qGdFLPuu6zq8zQy2h25Nkk6T1u0gBTQd-LCXt1w@mail.gmail.com","threadId":"55603","inReplyTo":"YO0O9JHtnYrk9qRm@coredump.intra.peff.net","subject":"Re: [PATCH v7 2/3] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2021-07-13T05:17:45Z","receivedAt":"2021-07-13T05:17:59Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Mon, Jul 12, 2021 at 11:56 PM Jeff King <peff@peff.net> wrote:\n> On Mon, Jul 12, 2021 at 10:22:22PM -0400, Eric Sunshine wrote:\n> > On Mon, Jul 12, 2021 at 8:16 PM Jeff King <peff@peff.net> wrote:\n> > > And indeed, Lénaïc's patches use it elsewhere. I think it doesn't work\n> > > in this instance because it can't take an arbitrary delimiter. But I\n> > > wouldn't at all mind seeing it grow that feature (and I suspect it could\n> > > even share some code with string_list_split(), but didn't look).\n> >\n> > Since Lénaïc is a relative newcomer to the project, can we, as\n> > reviewers, be clear that we don't expect him to perform the task of\n> > generalizing strvec_split() just to get this series -- which is\n> > already at v7 -- landed?\n>\n> Yeah, sorry if I was unclear on that. That is absolutely not a\n> requirement for this series.\n\nThanks, and sorry if my response seemed to be aimed only at your\nreply. It wasn't. It was a reaction to a number of recent reviews --\nin general -- containing only subjective comments or a mix of\nsubjective comments and genuine actionable items without necessarily\nmaking it clear which is which. Such ambiguity is hard on newcomers,\nwho may end up doing unnecessary work trying to get their patches\naccepted, or might scare them away altogether.\n"},{"id":"429866","messageId":"80ad532f-bcbe-2af2-27e3-0f503209b300@gmail.com","threadId":"55603","inReplyTo":"CAPig+cT-x4_YuxmmoFw62jFqKRFJrS_UkyNOkdQz9-Gwec3QCQ@mail.gmail.com","subject":"Re: [PATCH v7 2/3] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Bagas Sanjaya","fromEmail":"bagasdotme@gmail.com","sentAt":"2021-07-13T07:04:41Z","receivedAt":"2021-07-13T07:04:50Z","isPatch":true,"sender":{"key":"bagasdotme@gmail.com","avatar":"https://avatars.githubusercontent.com/u/40219486?v=4"},"body":"On 13/07/21 09.22, Eric Sunshine wrote:\n> Since Lénaïc is a relative newcomer to the project, can we, as\n> reviewers, be clear that we don't expect him to perform the task of\n> generalizing strvec_split() just to get this series -- which is\n> already at v7 -- landed? I gave the previous round a pretty thorough\n> going-over and -- aside from one minor test-time bug -- didn't find\n> any show-stoppers which should prevent it from landing. While it may\n> be the case that the series has a superficial wart here and there\n> (such as #ifdef's in function bodies, and non-ASCII fancy comment\n> boxes), the review comments on the latest round have pretty much all\n> been subjective; I haven't seen any outright actionable observations.\n> Extra polishing based upon the subjective review comments can always\n> be done later atop Lénaïc's series (if someone -- not necessarily\n> Lénaïc -- wants to do so) without asking him for endless re-rolls.\n> \n\nIn such situation when there is endless re-roll of patch series due to \nsubjective reviews, we can ask for final call from the maintainer, right?\n\n-- \nAn old man doll... just what I always wanted! - Clara\n"},{"id":"433003","messageId":"8eb18679-f6d5-b97e-f417-3747bb8309c3@gmail.com","threadId":"55603","inReplyTo":"20210608134000.663398-1-lenaic@lhuard.fr","subject":"Re: [PATCH v5 0/3] add support for systemd timers on Linux","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-08-17T17:22:05Z","receivedAt":"2021-08-17T17:22:09Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 6/8/2021 9:39 AM, Lénaïc Huard wrote:\n> Hello,\n> \n> I’ve reworked this submission based on the valuable feedback I’ve received.\n> Thanks again for it!\n\nHi Lénaïc!\n\nI'm replying to your series because it appears you did not see our discussion\nof this topic in the What's Cooking thread a couple weeks ago [1] and might\nmiss the discussion that began today [2].\n\n[1] https://lore.kernel.org/git/4aed0293-6a48-d370-3b72-496b7c631cb5@gmail.com/\n[2] https://lore.kernel.org/git/7a4b1238-5c3b-4c08-0e9d-511f857f9c38@gmail.com/\n\nThe proposal I give in [2] is that I can forward-fix the remaining comments\nOR re-submit the patches with a new patch and some edits to your current patches.\n(You would remain author of the patches you wrote.)\n\nNone of that is important if you plan to submit a v6 that responds to the\nremaining feedback (summarized in [1]).\n\nI'll hold off for a couple days to give you a chance to read and respond.\n\nThanks,\n-Stolee\n"},{"id":"433023","messageId":"86d8fede-5ae6-6c5c-05dd-91373cb9f4c3@gmail.com","threadId":"55603","inReplyTo":"8eb18679-f6d5-b97e-f417-3747bb8309c3@gmail.com","subject":"Re: [PATCH v5 0/3] add support for systemd timers on Linux","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2021-08-17T19:43:12Z","receivedAt":"2021-08-17T19:43:24Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 17/08/2021 18:22, Derrick Stolee wrote:\n> On 6/8/2021 9:39 AM, Lénaïc Huard wrote:\n>> Hello,\n>>\n>> I’ve reworked this submission based on the valuable feedback I’ve received.\n>> Thanks again for it!\n> \n> Hi Lénaïc!\n> \n> I'm replying to your series because it appears you did not see our discussion\n> of this topic in the What's Cooking thread a couple weeks ago [1] and might\n> miss the discussion that began today [2].\n> \n> [1] https://lore.kernel.org/git/4aed0293-6a48-d370-3b72-496b7c631cb5@gmail.com/\n> [2] https://lore.kernel.org/git/7a4b1238-5c3b-4c08-0e9d-511f857f9c38@gmail.com/\n> \n> The proposal I give in [2] is that I can forward-fix the remaining comments\n> OR re-submit the patches with a new patch and some edits to your current patches.\n> (You would remain author of the patches you wrote.)\n> \n> None of that is important if you plan to submit a v6 that responds to the\n> remaining feedback (summarized in [1]).\n\nI think you mean v8, v7[1] is what is in seen at the moment. There was a \nsuggestion at the time that a v8 would not be needed[2]\n\nBest Wishes\n\nPhillip\n\n[1] https://public-inbox.org/git/20210702142556.99864-1-lenaic@lhuard.fr/\n[2] https://public-inbox.org/git/YO0O9JHtnYrk9qRm@coredump.intra.peff.net/\n\n> I'll hold off for a couple days to give you a chance to read and respond.\n> \n> Thanks,\n> -Stolee\n> \n"},{"id":"433028","messageId":"363a0e7d-ad30-5481-2bd5-c83420a84070@gmail.com","threadId":"55603","inReplyTo":"86d8fede-5ae6-6c5c-05dd-91373cb9f4c3@gmail.com","subject":"Re: [PATCH v5 0/3] add support for systemd timers on Linux","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-08-17T20:29:37Z","receivedAt":"2021-08-17T20:29:43Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 8/17/2021 3:43 PM, Phillip Wood wrote:\n> On 17/08/2021 18:22, Derrick Stolee wrote:\n>> On 6/8/2021 9:39 AM, Lénaïc Huard wrote:\n>>> Hello,\n>>>\n>>> I’ve reworked this submission based on the valuable feedback I’ve received.\n>>> Thanks again for it!\n>>\n>> Hi Lénaïc!\n>>\n>> I'm replying to your series because it appears you did not see our discussion\n>> of this topic in the What's Cooking thread a couple weeks ago [1] and might\n>> miss the discussion that began today [2].\n>>\n>> [1] https://lore.kernel.org/git/4aed0293-6a48-d370-3b72-496b7c631cb5@gmail.com/\n>> [2] https://lore.kernel.org/git/7a4b1238-5c3b-4c08-0e9d-511f857f9c38@gmail.com/\n>>\n>> The proposal I give in [2] is that I can forward-fix the remaining comments\n>> OR re-submit the patches with a new patch and some edits to your current patches.\n>> (You would remain author of the patches you wrote.)\n>>\n>> None of that is important if you plan to submit a v6 that responds to the\n>> remaining feedback (summarized in [1]).\n> \n> I think you mean v8, v7[1] is what is in seen at the moment. There was a suggestion at the time that a v8 would not be needed[2]\n \nI do, thanks. Sorry for picking the wrong version to start\nthis discussion.\n\nThanks,\n-Stolee\n"},{"id":"433045","messageId":"1931213.f5cRXtTDAC@coruscant.lhuard.fr","threadId":"55603","inReplyTo":"8eb18679-f6d5-b97e-f417-3747bb8309c3@gmail.com","subject":"Re: [PATCH v5 0/3] add support for systemd timers on Linux","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-08-18T05:56:35Z","receivedAt":"2021-08-18T06:08:17Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Le mardi 17 août 2021, 19:22:05 CEST Derrick Stolee a écrit :\n> On 6/8/2021 9:39 AM, Lénaïc Huard wrote:\n> > Hello,\n> > \n> > I’ve reworked this submission based on the valuable feedback I’ve\n> > received.\n> > Thanks again for it!\n> \n> Hi Lénaïc!\n> \n> I'm replying to your series because it appears you did not see our\n> discussion of this topic in the What's Cooking thread a couple weeks ago\n> [1] and might miss the discussion that began today [2].\n> \n> [1]\n> https://lore.kernel.org/git/4aed0293-6a48-d370-3b72-496b7c631cb5@gmail.com/\n> [2]\n> https://lore.kernel.org/git/7a4b1238-5c3b-4c08-0e9d-511f857f9c38@gmail.com/\n> \n> The proposal I give in [2] is that I can forward-fix the remaining comments\n> OR re-submit the patches with a new patch and some edits to your current\n> patches. (You would remain author of the patches you wrote.)\n> \n> None of that is important if you plan to submit a v6 that responds to the\n> remaining feedback (summarized in [1]).\n> \n> I'll hold off for a couple days to give you a chance to read and respond.\n> \n> Thanks,\n> -Stolee\n\nHello,\n\nSorry for the silence. I just happened to be in holiday for the past few weeks \nand did not have access to my mails.\nI can catch up the discussions I missed and try to address the remaining \nconcerns in a new re-roll.\n\nCheers,\nLénaïc.\n\n\n\n"},{"id":"433069","messageId":"64e595d5-3492-2d3a-eef9-bc6ae881db1c@gmail.com","threadId":"55603","inReplyTo":"1931213.f5cRXtTDAC@coruscant.lhuard.fr","subject":"Re: [PATCH v5 0/3] add support for systemd timers on Linux","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-08-18T13:28:43Z","receivedAt":"2021-08-18T13:29:08Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 8/18/2021 1:56 AM, Lénaïc Huard wrote:\n> Le mardi 17 août 2021, 19:22:05 CEST Derrick Stolee a écrit :\n>> None of that is important if you plan to submit a v6 that responds to the\n>> remaining feedback (summarized in [1]).\n>>\n>> I'll hold off for a couple days to give you a chance to read and respond.\n...\n> Hello,\n> \n> Sorry for the silence. I just happened to be in holiday for the past few weeks \n> and did not have access to my mails.\n> I can catch up the discussions I missed and try to address the remaining \n> concerns in a new re-roll.\n\nPerfect! Welcome back!\n\n-Stolee\n"},{"id":"433074","messageId":"xmqqsfz6628i.fsf@gitster.g","threadId":"55603","inReplyTo":"64e595d5-3492-2d3a-eef9-bc6ae881db1c@gmail.com","subject":"Re: [PATCH v5 0/3] add support for systemd timers on Linux","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-08-18T18:23:41Z","receivedAt":"2021-08-18T18:23:48Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Derrick Stolee <stolee@gmail.com> writes:\n\n> On 8/18/2021 1:56 AM, Lénaïc Huard wrote:\n>> Le mardi 17 août 2021, 19:22:05 CEST Derrick Stolee a écrit :\n>>> None of that is important if you plan to submit a v6 that responds to the\n>>> remaining feedback (summarized in [1]).\n>>>\n>>> I'll hold off for a couple days to give you a chance to read and respond.\n> ...\n>> Hello,\n>> \n>> Sorry for the silence. I just happened to be in holiday for the past few weeks \n>> and did not have access to my mails.\n>> I can catch up the discussions I missed and try to address the remaining \n>> concerns in a new re-roll.\n>\n> Perfect! Welcome back!\n\nThanks, both.  Looking forward to see this topic reignited and see\nits happy ending ;-)\n\n\n"},{"id":"433487","messageId":"3218082.ccbTtk1zYS@coruscant.lhuard.fr","threadId":"55603","inReplyTo":"87h7h75hzz.fsf@evledraar.gmail.com","subject":"Re: [PATCH v7 2/3] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-08-23T20:06:15Z","receivedAt":"2021-08-23T20:07:04Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Hello,\n\nSorry to come back after such a long time. I just resumed working on that \nseries and would have a question about a review comment.\n\nLe mardi 6 juillet 2021, 21:56:38 CEST Ævar Arnfjörð Bjarmason a écrit :\n> On Fri, Jul 02 2021, Lénaïc Huard wrote:\n\n> > +#ifdef __APPLE__\n> > +\treturn 1;\n> > +#else\n> > +\treturn 0;\n> > +#endif\n> > +}\n> \n> \n> I see this is partially a pre-existing thing in the file, but we have an\n> __APPLE__ already in cache.h. Perhaps define a iLAUNCHCTL_AVAILABLE\n> there. See e.g. 62e5ee81a39 (read-cache.c: remove #ifdef NO_PTHREADS,\n> 2018-11-03).\n\nIs the suggestion to replace\n\n+#ifdef __APPLE__\n+\treturn 1;\n+#else\n+\treturn 0;\n+#endif\n+}\n\nby\n\n+ return IS_LAUNCHCTL_AVAILABLE;\n\nand to add\n\n#ifdef __APPLE__\n#define IS_LAUNCHCTL_AVAILABLE 1\n#else\n#define IS_LAUNCHCTL_AVAILABLE 0\n#endif\n\nsomewhere else like at the top of builtin/gc.c ?\n\nAlso, do we agree this shouldn’t be defined in cache.h ? I’m a little bit \nconfused.\n\nKind regards,\nLénaïc.\n\n\n\n\n"},{"id":"433491","messageId":"20210823204011.87023-4-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210823204011.87023-1-lenaic@lhuard.fr","subject":"[PATCH v8 3/3] maintenance: add support for systemd timers on Linux","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-08-23T20:40:11Z","receivedAt":"2021-08-23T20:41:05Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"The existing mechanism for scheduling background maintenance is done\nthrough cron. On Linux systems managed by systemd, systemd provides an\nalternative to schedule recurring tasks: systemd timers.\n\nThe main motivations to implement systemd timers in addition to cron\nare:\n* cron is optional and Linux systems running systemd might not have it\n  installed.\n* The execution of `crontab -l` can tell us if cron is installed but not\n  if the daemon is actually running.\n* With systemd, each service is run in its own cgroup and its logs are\n  tagged by the service inside journald. With cron, all scheduled tasks\n  are running in the cron daemon cgroup and all the logs of the\n  user-scheduled tasks are pretended to belong to the system cron\n  service.\n  Concretely, a user that doesn’t have access to the system logs won’t\n  have access to the log of their own tasks scheduled by cron whereas\n  they will have access to the log of their own tasks scheduled by\n  systemd timer.\n  Although `cron` attempts to send email, that email may go unseen by\n  the user because these days, local mailboxes are not heavily used\n  anymore.\n\nIn order to schedule git maintenance, we need two unit template files:\n* ~/.config/systemd/user/git-maintenance@.service\n  to define the command to be started by systemd and\n* ~/.config/systemd/user/git-maintenance@.timer\n  to define the schedule at which the command should be run.\n\nThose units are templates that are parameterized by the frequency.\n\nBased on those templates, 3 timers are started:\n* git-maintenance@hourly.timer\n* git-maintenance@daily.timer\n* git-maintenance@weekly.timer\n\nThe command launched by those three timers are the same as with the\nother scheduling methods:\n\n/path/to/git for-each-repo --exec-path=/path/to\n--config=maintenance.repo maintenance run --schedule=%i\n\nwith the full path for git to ensure that the version of git launched\nfor the scheduled maintenance is the same as the one used to run\n`maintenance start`.\n\nThe timer unit contains `Persistent=true` so that, if the computer is\npowered down when a maintenance task should run, the task will be run\nwhen the computer is back powered on.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n Documentation/git-maintenance.txt |  58 +++++++-\n builtin/gc.c                      | 227 ++++++++++++++++++++++++++++++\n t/t7900-maintenance.sh            |  67 ++++++++-\n 3 files changed, 341 insertions(+), 11 deletions(-)\n\ndiff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\nindex 576290b5c6..e2cfb68ab5 100644\n--- a/Documentation/git-maintenance.txt\n+++ b/Documentation/git-maintenance.txt\n@@ -179,14 +179,16 @@ OPTIONS\n \t`maintenance.<task>.enabled` configured as `true` are considered.\n \tSee the 'TASKS' section for the list of accepted `<task>` values.\n \n---scheduler=auto|crontab|launchctl|schtasks::\n+--scheduler=auto|crontab|systemd-timer|launchctl|schtasks::\n \tWhen combined with the `start` subcommand, specify the scheduler\n \tfor running the hourly, daily and weekly executions of\n \t`git maintenance run`.\n-\tPossible values for `<scheduler>` are `auto`, `crontab` (POSIX),\n-\t`launchctl` (macOS), and `schtasks` (Windows).\n-\tWhen `auto` is specified, the appropriate platform-specific\n-\tscheduler is used. Default is `auto`.\n+\tPossible values for `<scheduler>` are `auto`, `crontab`\n+\t(POSIX), `systemd-timer` (Linux), `launchctl` (macOS), and\n+\t`schtasks` (Windows). When `auto` is specified, the\n+\tappropriate platform-specific scheduler is used; on Linux,\n+\t`systemd-timer` is used if available, otherwise\n+\t`crontab`. Default is `auto`.\n \n \n TROUBLESHOOTING\n@@ -286,6 +288,52 @@ schedule to ensure you are executing the correct binaries in your\n schedule.\n \n \n+BACKGROUND MAINTENANCE ON LINUX SYSTEMD SYSTEMS\n+-----------------------------------------------\n+\n+While Linux supports `cron`, depending on the distribution, `cron` may\n+be an optional package not necessarily installed. On modern Linux\n+distributions, systemd timers are superseding it.\n+\n+If user systemd timers are available, they will be used as a replacement\n+of `cron`.\n+\n+In this case, `git maintenance start` will create user systemd timer units\n+and start the timers. The current list of user-scheduled tasks can be found\n+by running `systemctl --user list-timers`. The timers written by `git\n+maintenance start` are similar to this:\n+\n+-----------------------------------------------------------------------\n+$ systemctl --user list-timers\n+NEXT                         LEFT          LAST                         PASSED     UNIT                         ACTIVATES\n+Thu 2021-04-29 19:00:00 CEST 42min left    Thu 2021-04-29 18:00:11 CEST 17min ago  git-maintenance@hourly.timer git-maintenance@hourly.service\n+Fri 2021-04-30 00:00:00 CEST 5h 42min left Thu 2021-04-29 00:00:11 CEST 18h ago    git-maintenance@daily.timer  git-maintenance@daily.service\n+Mon 2021-05-03 00:00:00 CEST 3 days left   Mon 2021-04-26 00:00:11 CEST 3 days ago git-maintenance@weekly.timer git-maintenance@weekly.service\n+-----------------------------------------------------------------------\n+\n+One timer is registered for each `--schedule=<frequency>` option.\n+\n+The definition of the systemd units can be inspected in the following files:\n+\n+-----------------------------------------------------------------------\n+~/.config/systemd/user/git-maintenance@.timer\n+~/.config/systemd/user/git-maintenance@.service\n+~/.config/systemd/user/timers.target.wants/git-maintenance@hourly.timer\n+~/.config/systemd/user/timers.target.wants/git-maintenance@daily.timer\n+~/.config/systemd/user/timers.target.wants/git-maintenance@weekly.timer\n+-----------------------------------------------------------------------\n+\n+`git maintenance start` will overwrite these files and start the timer\n+again with `systemctl --user`, so any customization should be done by\n+creating a drop-in file, i.e. a `.conf` suffixed file in the\n+`~/.config/systemd/user/git-maintenance@.service.d` directory.\n+\n+`git maintenance stop` will stop the user systemd timers and delete\n+the above mentioned files.\n+\n+For more details, see `systemd.timer(5)`.\n+\n+\n BACKGROUND MAINTENANCE ON MACOS SYSTEMS\n ---------------------------------------\n \ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 9e464d4a10..a3b2fc55d2 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -2074,10 +2074,221 @@ static int crontab_update_schedule(int run_maintenance, int fd)\n \treturn result;\n }\n \n+#ifdef __linux__\n+\n+static int real_is_systemd_timer_available(void)\n+{\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\n+\tstrvec_pushl(&child.args, \"systemctl\", \"--user\", \"list-timers\", NULL);\n+\tchild.no_stdin = 1;\n+\tchild.no_stdout = 1;\n+\tchild.no_stderr = 1;\n+\tchild.silent_exec_failure = 1;\n+\n+\tif (start_command(&child))\n+\t\treturn 0;\n+\tif (finish_command(&child))\n+\t\treturn 0;\n+\treturn 1;\n+}\n+\n+#else\n+\n+static int real_is_systemd_timer_available(void)\n+{\n+\treturn 0;\n+}\n+\n+#endif\n+\n+static int is_systemd_timer_available(void)\n+{\n+\tconst char *cmd = \"systemctl\";\n+\tint is_available;\n+\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+\treturn real_is_systemd_timer_available();\n+}\n+\n+static char *xdg_config_home_systemd(const char *filename)\n+{\n+\treturn xdg_config_home_for(\"systemd/user\", filename);\n+}\n+\n+static int systemd_timer_enable_unit(int enable,\n+\t\t\t\t     enum schedule_priority schedule)\n+{\n+\tconst char *cmd = \"systemctl\";\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\tconst char *frequency = get_frequency(schedule);\n+\n+\t/*\n+\t * Disabling the systemd unit while it is already disabled makes\n+\t * systemctl print an error.\n+\t * Let's ignore it since it means we already are in the expected state:\n+\t * the unit is disabled.\n+\t *\n+\t * On the other hand, enabling a systemd unit which is already enabled\n+\t * produces no error.\n+\t */\n+\tif (!enable)\n+\t\tchild.no_stderr = 1;\n+\n+\tget_schedule_cmd(&cmd, NULL);\n+\tstrvec_split(&child.args, cmd);\n+\tstrvec_pushl(&child.args, \"--user\", enable ? \"enable\" : \"disable\",\n+\t\t     \"--now\", NULL);\n+\tstrvec_pushf(&child.args, \"git-maintenance@%s.timer\", frequency);\n+\n+\tif (start_command(&child))\n+\t\treturn error(_(\"failed to start systemctl\"));\n+\tif (finish_command(&child))\n+\t\t/*\n+\t\t * Disabling an already disabled systemd unit makes\n+\t\t * systemctl fail.\n+\t\t * Let's ignore this failure.\n+\t\t *\n+\t\t * Enabling an enabled systemd unit doesn't fail.\n+\t\t */\n+\t\tif (enable)\n+\t\t\treturn error(_(\"failed to run systemctl\"));\n+\treturn 0;\n+}\n+\n+static int systemd_timer_delete_unit_templates(void)\n+{\n+\tint ret = 0;\n+\tchar *filename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n+\tif (unlink(filename) && !is_missing_file_error(errno))\n+\t\tret = error_errno(_(\"failed to delete '%s'\"), filename);\n+\tFREE_AND_NULL(filename);\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n+\tif (unlink(filename) && !is_missing_file_error(errno))\n+\t\tret = error_errno(_(\"failed to delete '%s'\"), filename);\n+\n+\tfree(filename);\n+\treturn ret;\n+}\n+\n+static int systemd_timer_delete_units(void)\n+{\n+\treturn systemd_timer_enable_unit(0, SCHEDULE_HOURLY) ||\n+\t       systemd_timer_enable_unit(0, SCHEDULE_DAILY) ||\n+\t       systemd_timer_enable_unit(0, SCHEDULE_WEEKLY) ||\n+\t       systemd_timer_delete_unit_templates();\n+}\n+\n+static int systemd_timer_write_unit_templates(const char *exec_path)\n+{\n+\tchar *filename;\n+\tFILE *file;\n+\tconst char *unit;\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n+\tif (safe_create_leading_directories(filename)) {\n+\t\terror(_(\"failed to create directories for '%s'\"), filename);\n+\t\tgoto error;\n+\t}\n+\tfile = fopen_or_warn(filename, \"w\");\n+\tif (file == NULL)\n+\t\tgoto error;\n+\n+\tunit = \"# This file was created and is maintained by Git.\\n\"\n+\t       \"# Any edits made in this file might be replaced in the future\\n\"\n+\t       \"# by a Git command.\\n\"\n+\t       \"\\n\"\n+\t       \"[Unit]\\n\"\n+\t       \"Description=Optimize Git repositories data\\n\"\n+\t       \"\\n\"\n+\t       \"[Timer]\\n\"\n+\t       \"OnCalendar=%i\\n\"\n+\t       \"Persistent=true\\n\"\n+\t       \"\\n\"\n+\t       \"[Install]\\n\"\n+\t       \"WantedBy=timers.target\\n\";\n+\tif (fputs(unit, file) == EOF) {\n+\t\terror(_(\"failed to write to '%s'\"), filename);\n+\t\tfclose(file);\n+\t\tgoto error;\n+\t}\n+\tif (fclose(file) == EOF) {\n+\t\terror_errno(_(\"failed to flush '%s'\"), filename);\n+\t\tgoto error;\n+\t}\n+\tfree(filename);\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n+\tfile = fopen_or_warn(filename, \"w\");\n+\tif (file == NULL)\n+\t\tgoto error;\n+\n+\tunit = \"# This file was created and is maintained by Git.\\n\"\n+\t       \"# Any edits made in this file might be replaced in the future\\n\"\n+\t       \"# by a Git command.\\n\"\n+\t       \"\\n\"\n+\t       \"[Unit]\\n\"\n+\t       \"Description=Optimize Git repositories data\\n\"\n+\t       \"\\n\"\n+\t       \"[Service]\\n\"\n+\t       \"Type=oneshot\\n\"\n+\t       \"ExecStart=\\\"%s/git\\\" --exec-path=\\\"%s\\\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\\n\"\n+\t       \"LockPersonality=yes\\n\"\n+\t       \"MemoryDenyWriteExecute=yes\\n\"\n+\t       \"NoNewPrivileges=yes\\n\"\n+\t       \"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\\n\"\n+\t       \"RestrictNamespaces=yes\\n\"\n+\t       \"RestrictRealtime=yes\\n\"\n+\t       \"RestrictSUIDSGID=yes\\n\"\n+\t       \"SystemCallArchitectures=native\\n\"\n+\t       \"SystemCallFilter=@system-service\\n\";\n+\tif (fprintf(file, unit, exec_path, exec_path) < 0) {\n+\t\terror(_(\"failed to write to '%s'\"), filename);\n+\t\tfclose(file);\n+\t\tgoto error;\n+\t}\n+\tif (fclose(file) == EOF) {\n+\t\terror_errno(_(\"failed to flush '%s'\"), filename);\n+\t\tgoto error;\n+\t}\n+\tfree(filename);\n+\treturn 0;\n+\n+error:\n+\tfree(filename);\n+\tsystemd_timer_delete_unit_templates();\n+\treturn -1;\n+}\n+\n+static int systemd_timer_setup_units(void)\n+{\n+\tconst char *exec_path = git_exec_path();\n+\n+\tint ret = systemd_timer_write_unit_templates(exec_path) ||\n+\t          systemd_timer_enable_unit(1, SCHEDULE_HOURLY) ||\n+\t          systemd_timer_enable_unit(1, SCHEDULE_DAILY) ||\n+\t          systemd_timer_enable_unit(1, SCHEDULE_WEEKLY);\n+\tif (ret)\n+\t\tsystemd_timer_delete_units();\n+\treturn ret;\n+}\n+\n+static int systemd_timer_update_schedule(int run_maintenance, int fd)\n+{\n+\tif (run_maintenance)\n+\t\treturn systemd_timer_setup_units();\n+\telse\n+\t\treturn systemd_timer_delete_units();\n+}\n+\n enum scheduler {\n \tSCHEDULER_INVALID = -1,\n \tSCHEDULER_AUTO,\n \tSCHEDULER_CRON,\n+\tSCHEDULER_SYSTEMD,\n \tSCHEDULER_LAUNCHCTL,\n \tSCHEDULER_SCHTASKS,\n };\n@@ -2092,6 +2303,11 @@ static const struct {\n \t\t.is_available = is_crontab_available,\n \t\t.update_schedule = crontab_update_schedule,\n \t},\n+\t[SCHEDULER_SYSTEMD] = {\n+\t\t.name = \"systemctl\",\n+\t\t.is_available = is_systemd_timer_available,\n+\t\t.update_schedule = systemd_timer_update_schedule,\n+\t},\n \t[SCHEDULER_LAUNCHCTL] = {\n \t\t.name = \"launchctl\",\n \t\t.is_available = is_launchctl_available,\n@@ -2112,6 +2328,9 @@ static enum scheduler parse_scheduler(const char *value)\n \t\treturn SCHEDULER_AUTO;\n \telse if (!strcasecmp(value, \"cron\") || !strcasecmp(value, \"crontab\"))\n \t\treturn SCHEDULER_CRON;\n+\telse if (!strcasecmp(value, \"systemd\") ||\n+\t\t !strcasecmp(value, \"systemd-timer\"))\n+\t\treturn SCHEDULER_SYSTEMD;\n \telse if (!strcasecmp(value, \"launchctl\"))\n \t\treturn SCHEDULER_LAUNCHCTL;\n \telse if (!strcasecmp(value, \"schtasks\"))\n@@ -2148,6 +2367,14 @@ static enum scheduler resolve_scheduler(enum scheduler scheduler)\n #elif defined(GIT_WINDOWS_NATIVE)\n \treturn SCHEDULER_SCHTASKS;\n \n+#elif defined(__linux__)\n+\tif (is_systemd_timer_available())\n+\t\treturn SCHEDULER_SYSTEMD;\n+\telse if (is_crontab_available())\n+\t\treturn SCHEDULER_CRON;\n+\telse\n+\t\tdie(_(\"neither systemd timers nor crontab are available\"));\n+\n #else\n \treturn SCHEDULER_CRON;\n #endif\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 27bce7992c..265f7793f5 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -20,6 +20,18 @@ test_xmllint () {\n \tfi\n }\n \n+test_lazy_prereq SYSTEMD_ANALYZE '\n+\tsystemd-analyze --help >out &&\n+\tgrep verify out\n+'\n+\n+test_systemd_analyze_verify () {\n+\tif test_have_prereq SYSTEMD_ANALYZE\n+\tthen\n+\t\tsystemd-analyze verify \"$@\"\n+\tfi\n+}\n+\n test_expect_success 'help text' '\n \ttest_expect_code 129 git maintenance -h 2>err &&\n \ttest_i18ngrep \"usage: git maintenance <subcommand>\" err &&\n@@ -632,15 +644,56 @@ test_expect_success 'start and stop Windows maintenance' '\n \ttest_cmp expect args\n '\n \n+test_expect_success 'start and stop Linux/systemd maintenance' '\n+\twrite_script print-args <<-\\EOF &&\n+\tprintf \"%s\\n\" \"$*\" >>args\n+\tEOF\n+\n+\tXDG_CONFIG_HOME=\"$PWD\" &&\n+\texport XDG_CONFIG_HOME &&\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args\" git maintenance start --scheduler=systemd-timer &&\n+\n+\t# start registers the repo\n+\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n+\n+\ttest_systemd_analyze_verify \"systemd/user/git-maintenance@.service\" &&\n+\n+\tprintf -- \"--user enable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args\" git maintenance stop &&\n+\n+\t# stop does not unregister the repo\n+\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n+\n+\ttest_path_is_missing \"systemd/user/git-maintenance@.timer\" &&\n+\ttest_path_is_missing \"systemd/user/git-maintenance@.service\" &&\n+\n+\tprintf -- \"--user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n+\ttest_cmp expect args\n+'\n+\n test_expect_success 'start and stop when several schedulers are available' '\n \twrite_script print-args <<-\\EOF &&\n \tprintf \"%s\\n\" \"$*\" | sed \"s:gui/[0-9][0-9]*:gui/[UID]:; s:\\(schtasks /create .* /xml\\).*:\\1:;\" >>args\n \tEOF\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n-\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=systemd-timer &&\n+\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n \t\thourly daily weekly >expect &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\tprintf -- \"systemctl --user enable --now git-maintenance@%s.timer\\n\" hourly daily weekly >>expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n+\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >>expect &&\n \tfor frequency in hourly daily weekly\n \tdo\n \t\tPLIST=\"$pfx/Library/LaunchAgents/org.git-scm.git.$frequency.plist\" &&\n@@ -650,17 +703,19 @@ test_expect_success 'start and stop when several schedulers are available' '\n \ttest_cmp expect args &&\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n+\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n \tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n-\t\thourly daily weekly >expect &&\n+\t\thourly daily weekly >>expect &&\n \tprintf \"schtasks /create /tn Git Maintenance (%s) /f /xml\\n\" \\\n \t\thourly daily weekly >>expect &&\n \ttest_cmp expect args &&\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n+\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n \tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n-\t\thourly daily weekly >expect &&\n+\t\thourly daily weekly >>expect &&\n \tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n \t\thourly daily weekly >>expect &&\n \ttest_cmp expect args\n-- \n2.33.0\n\n"},{"id":"433492","messageId":"20210823204011.87023-1-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210702142556.99864-1-lenaic@lhuard.fr","subject":"[PATCH v8 0/3] maintenance: add support for systemd timers on Linux","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-08-23T20:40:08Z","receivedAt":"2021-08-23T20:41:21Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Hello,\n\nI’ve just resumed working on my patchset to add support for systemd\ntimers on Linux for the `git maintenance start` command.\n\nThe patches are:\n\n* cache.h: Introduce a generic \"xdg_config_home_for(…)\" function\n\n  This patch introduces a function to compute configuration files\n  paths inside $XDG_CONFIG_HOME.\n  It is used in the latest patch of this series to compute systemd\n  unit files location.\n\n  This patch is unchanged compared to its previous version.\n\n* maintenance: `git maintenance run` learned `--scheduler=<scheduler>`\n\n  This patch adds a new parameter to the `git maintenance run` to let\n  the user choose a scheduler.\n\n  This patch contains the following changes compared to its previous\n  version:\n\n  * Remove some UTF-8 characters in a code comment and replace them by\n    ASCII ones.\n\n  * Leverage `string_list_split` in `get_schedule_cmd` to parse the\n    comma-separated list of colon-separated pairs in\n    GIT_TEST_MAINT_SCHEDULER environment variable.\n\n* maintenance: add support for systemd timers on Linux\n\n  This patch implements the support of systemd timers on top of\n  crontab scheduler on Linux systems.\n\n  This patch is unchanged compared to its previous version.\n\n\nAccording to [1], there were 3 changes awaited in this v8:\n* The two already mentionned above (utf-8 characters and\n  `string_list_split` thing)\n* An improvement around the #ifdef.\n\nI must admit I haven’t touched anything around the #ifdef in this v8\nbecause I’m not sure what to do. I’ve just asked for some more details\nin [2].\n\n[1] https://lore.kernel.org/git/4aed0293-6a48-d370-3b72-496b7c631cb5@gmail.com/\n[2] https://lore.kernel.org/git/3218082.ccbTtk1zYS@coruscant.lhuard.fr/\n\n\nBest wishes,\nLénaïc.\n\n\nLénaïc Huard (3):\n  cache.h: Introduce a generic \"xdg_config_home_for(…)\" function\n  maintenance: `git maintenance run` learned `--scheduler=<scheduler>`\n  maintenance: add support for systemd timers on Linux\n\n Documentation/git-maintenance.txt |  57 +++\n builtin/gc.c                      | 592 ++++++++++++++++++++++++++----\n cache.h                           |   7 +\n path.c                            |  13 +-\n t/t7900-maintenance.sh            | 110 +++++-\n 5 files changed, 701 insertions(+), 78 deletions(-)\n\nDiff-intervalle contre v7 :\n1:  899b11ed5b = 1:  1639bd151c cache.h: Introduce a generic \"xdg_config_home_for(…)\" function\n2:  f3e2f0256b ! 2:  ea5568269c maintenance: `git maintenance run` learned `--scheduler=<scheduler>`\n    @@ builtin/gc.c: static const char *get_frequency(enum schedule_priority schedule)\n     + *\n     + * Ex.:\n     + *   GIT_TEST_MAINT_SCHEDULER not set\n    -+ *     ┏━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n    -+ *     ┃ Input ┃                     Output                      ┃\n    -+ *     ┃ *cmd  ┃ return code │       *cmd        │ *is_available ┃\n    -+ *     ┣━━━━━━━╋━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━┫\n    -+ *     ┃ \"foo\" ┃    false    │ \"foo\" (unchanged) │  (unchanged)  ┃\n    -+ *     ┗━━━━━━━┻━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━┛\n    ++ *     +-------+-------------------------------------------------+\n    ++ *     | Input |                     Output                      |\n    ++ *     | *cmd  | return code |       *cmd        | *is_available |\n    ++ *     +-------+-------------+-------------------+---------------+\n    ++ *     | \"foo\" |    false    | \"foo\" (unchanged) |  (unchanged)  |\n    ++ *     +-------+-------------+-------------------+---------------+\n     + *\n     + *   GIT_TEST_MAINT_SCHEDULER set to “foo:./mock_foo.sh,bar:./mock_bar.sh”\n    -+ *     ┏━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n    -+ *     ┃ Input ┃                     Output                      ┃\n    -+ *     ┃ *cmd  ┃ return code │       *cmd        │ *is_available ┃\n    -+ *     ┣━━━━━━━╋━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━┫\n    -+ *     ┃ \"foo\" ┃    true     │  \"./mock.foo.sh\"  │     true      ┃\n    -+ *     ┃ \"qux\" ┃    true     │ \"qux\" (unchanged) │     false     ┃\n    -+ *     ┗━━━━━━━┻━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━┛\n    ++ *     +-------+-------------------------------------------------+\n    ++ *     | Input |                     Output                      |\n    ++ *     | *cmd  | return code |       *cmd        | *is_available |\n    ++ *     +-------+-------------+-------------------+---------------+\n    ++ *     | \"foo\" |    true     |  \"./mock.foo.sh\"  |     true      |\n    ++ *     | \"qux\" |    true     | \"qux\" (unchanged) |     false     |\n    ++ *     +-------+-------------+-------------------+---------------+\n     + */\n     +static int get_schedule_cmd(const char **cmd, int *is_available)\n     +{\n    -+\tchar *item;\n     +\tchar *testing = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n    ++\tstruct string_list_item *item;\n    ++\tstruct string_list list = STRING_LIST_INIT_NODUP;\n     +\n     +\tif (!testing)\n     +\t\treturn 0;\n    @@ builtin/gc.c: static const char *get_frequency(enum schedule_priority schedule)\n     +\tif (is_available)\n     +\t\t*is_available = 0;\n     +\n    -+\tfor (item = testing;;) {\n    -+\t\tchar *sep;\n    -+\t\tchar *end_item = strchr(item, ',');\n    -+\t\tif (end_item)\n    -+\t\t\t*end_item = '\\0';\n    ++\tstring_list_split_in_place(&list, testing, ',', -1);\n    ++\tfor_each_string_list_item(item, &list) {\n    ++\t\tstruct string_list pair = STRING_LIST_INIT_NODUP;\n     +\n    -+\t\tsep = strchr(item, ':');\n    -+\t\tif (!sep)\n    -+\t\t\tdie(\"GIT_TEST_MAINT_SCHEDULER unparseable: %s\", testing);\n    -+\t\t*sep = '\\0';\n    ++\t\tif (string_list_split_in_place(&pair, item->string, ':', 2) != 2)\n    ++\t\t\tcontinue;\n     +\n    -+\t\tif (!strcmp(*cmd, item)) {\n    -+\t\t\t*cmd = sep + 1;\n    ++\t\tif (!strcmp(*cmd, pair.items[0].string)) {\n    ++\t\t\t*cmd = pair.items[1].string;\n     +\t\t\tif (is_available)\n     +\t\t\t\t*is_available = 1;\n    ++\t\t\tstring_list_clear(&list, 0);\n     +\t\t\tUNLEAK(testing);\n     +\t\t\treturn 1;\n     +\t\t}\n    -+\n    -+\t\tif (!end_item)\n    -+\t\t\tbreak;\n    -+\t\titem = end_item + 1;\n     +\t}\n     +\n    ++\tstring_list_clear(&list, 0);\n     +\tfree(testing);\n     +\treturn 1;\n     +}\n3:  0ea5b2fc45 = 3:  faf56c078f maintenance: add support for systemd timers on Linux\n-- \n2.33.0\n\n"},{"id":"433493","messageId":"20210823204011.87023-2-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210823204011.87023-1-lenaic@lhuard.fr","subject":"[PATCH v8 1/3] cache.h: Introduce a generic \"xdg_config_home_for(…)\" function","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-08-23T20:40:09Z","receivedAt":"2021-08-23T20:41:26Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Current implementation of `xdg_config_home(filename)` returns\n`$XDG_CONFIG_HOME/git/$filename`, with the `git` subdirectory inserted\nbetween the `XDG_CONFIG_HOME` environment variable and the parameter.\n\nThis patch introduces a `xdg_config_home_for(subdir, filename)` function\nwhich is more generic. It only concatenates \"$XDG_CONFIG_HOME\", or\n\"$HOME/.config\" if the former isn’t defined, with the parameters,\nwithout adding `git` in between.\n\n`xdg_config_home(filename)` is now implemented by calling\n`xdg_config_home_for(\"git\", filename)` but this new generic function can\nbe used to compute the configuration directory of other programs.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n cache.h |  7 +++++++\n path.c  | 13 ++++++++++---\n 2 files changed, 17 insertions(+), 3 deletions(-)\n\ndiff --git a/cache.h b/cache.h\nindex bd4869beee..79c4e17ea4 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -1295,6 +1295,13 @@ int is_ntfs_dotmailmap(const char *name);\n  */\n int looks_like_command_line_option(const char *str);\n \n+/**\n+ * Return a newly allocated string with the evaluation of\n+ * \"$XDG_CONFIG_HOME/$subdir/$filename\" if $XDG_CONFIG_HOME is non-empty, otherwise\n+ * \"$HOME/.config/$subdir/$filename\". Return NULL upon error.\n+ */\n+char *xdg_config_home_for(const char *subdir, const char *filename);\n+\n /**\n  * Return a newly allocated string with the evaluation of\n  * \"$XDG_CONFIG_HOME/git/$filename\" if $XDG_CONFIG_HOME is non-empty, otherwise\ndiff --git a/path.c b/path.c\nindex 7bccd830e9..1b1de3be09 100644\n--- a/path.c\n+++ b/path.c\n@@ -1503,21 +1503,28 @@ int looks_like_command_line_option(const char *str)\n \treturn str && str[0] == '-';\n }\n \n-char *xdg_config_home(const char *filename)\n+char *xdg_config_home_for(const char *subdir, const char *filename)\n {\n \tconst char *home, *config_home;\n \n+\tassert(subdir);\n \tassert(filename);\n \tconfig_home = getenv(\"XDG_CONFIG_HOME\");\n \tif (config_home && *config_home)\n-\t\treturn mkpathdup(\"%s/git/%s\", config_home, filename);\n+\t\treturn mkpathdup(\"%s/%s/%s\", config_home, subdir, filename);\n \n \thome = getenv(\"HOME\");\n \tif (home)\n-\t\treturn mkpathdup(\"%s/.config/git/%s\", home, filename);\n+\t\treturn mkpathdup(\"%s/.config/%s/%s\", home, subdir, filename);\n+\n \treturn NULL;\n }\n \n+char *xdg_config_home(const char *filename)\n+{\n+\treturn xdg_config_home_for(\"git\", filename);\n+}\n+\n char *xdg_cache_home(const char *filename)\n {\n \tconst char *home, *cache_home;\n-- \n2.33.0\n\n"},{"id":"433494","messageId":"20210823204011.87023-3-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210823204011.87023-1-lenaic@lhuard.fr","subject":"[PATCH v8 2/3] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-08-23T20:40:10Z","receivedAt":"2021-08-23T20:41:38Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Depending on the system, different schedulers can be used to schedule\nthe hourly, daily and weekly executions of `git maintenance run`:\n* `launchctl` for MacOS,\n* `schtasks` for Windows and\n* `crontab` for everything else.\n\n`git maintenance run` now has an option to let the end-user explicitly\nchoose which scheduler he wants to use:\n`--scheduler=auto|crontab|launchctl|schtasks`.\n\nWhen `git maintenance start --scheduler=XXX` is run, it not only\nregisters `git maintenance run` tasks in the scheduler XXX, it also\nremoves the `git maintenance run` tasks from all the other schedulers to\nensure we cannot have two schedulers launching concurrent identical\ntasks.\n\nThe default value is `auto` which chooses a suitable scheduler for the\nsystem.\n\n`git maintenance stop` doesn't have any `--scheduler` parameter because\nthis command will try to remove the `git maintenance run` tasks from all\nthe available schedulers.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n Documentation/git-maintenance.txt |   9 +\n builtin/gc.c                      | 365 ++++++++++++++++++++++++------\n t/t7900-maintenance.sh            |  55 ++++-\n 3 files changed, 354 insertions(+), 75 deletions(-)\n\ndiff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\nindex 1e738ad398..576290b5c6 100644\n--- a/Documentation/git-maintenance.txt\n+++ b/Documentation/git-maintenance.txt\n@@ -179,6 +179,15 @@ OPTIONS\n \t`maintenance.<task>.enabled` configured as `true` are considered.\n \tSee the 'TASKS' section for the list of accepted `<task>` values.\n \n+--scheduler=auto|crontab|launchctl|schtasks::\n+\tWhen combined with the `start` subcommand, specify the scheduler\n+\tfor running the hourly, daily and weekly executions of\n+\t`git maintenance run`.\n+\tPossible values for `<scheduler>` are `auto`, `crontab` (POSIX),\n+\t`launchctl` (macOS), and `schtasks` (Windows).\n+\tWhen `auto` is specified, the appropriate platform-specific\n+\tscheduler is used. Default is `auto`.\n+\n \n TROUBLESHOOTING\n ---------------\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex f05d2f0a1a..9e464d4a10 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1529,6 +1529,93 @@ static const char *get_frequency(enum schedule_priority schedule)\n \t}\n }\n \n+/*\n+ * get_schedule_cmd` reads the GIT_TEST_MAINT_SCHEDULER environment variable\n+ * to mock the schedulers that `git maintenance start` rely on.\n+ *\n+ * For test purpose, GIT_TEST_MAINT_SCHEDULER can be set to a comma-separated\n+ * list of colon-separated key/value pairs where each pair contains a scheduler\n+ * and its corresponding mock.\n+ *\n+ * * If $GET_TEST_MAINT_SCHEDULER is not set, return false and leave the\n+ *   arguments unmodified.\n+ *\n+ * * If $GET_TEST_MAINT_SCHEDULER is set, return true.\n+ *   In this case, the *cmd value is read as input.\n+ *\n+ *   * if the input value *cmd is the key of one of the comma-separated list\n+ *     item, then *is_available is set to true and *cmd is modified and becomes\n+ *     the mock command.\n+ *\n+ *   * if the input value *cmd isn’t the key of any of the comma-separated list\n+ *     item, then *is_available is set to false.\n+ *\n+ * Ex.:\n+ *   GIT_TEST_MAINT_SCHEDULER not set\n+ *     +-------+-------------------------------------------------+\n+ *     | Input |                     Output                      |\n+ *     | *cmd  | return code |       *cmd        | *is_available |\n+ *     +-------+-------------+-------------------+---------------+\n+ *     | \"foo\" |    false    | \"foo\" (unchanged) |  (unchanged)  |\n+ *     +-------+-------------+-------------------+---------------+\n+ *\n+ *   GIT_TEST_MAINT_SCHEDULER set to “foo:./mock_foo.sh,bar:./mock_bar.sh”\n+ *     +-------+-------------------------------------------------+\n+ *     | Input |                     Output                      |\n+ *     | *cmd  | return code |       *cmd        | *is_available |\n+ *     +-------+-------------+-------------------+---------------+\n+ *     | \"foo\" |    true     |  \"./mock.foo.sh\"  |     true      |\n+ *     | \"qux\" |    true     | \"qux\" (unchanged) |     false     |\n+ *     +-------+-------------+-------------------+---------------+\n+ */\n+static int get_schedule_cmd(const char **cmd, int *is_available)\n+{\n+\tchar *testing = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n+\tstruct string_list_item *item;\n+\tstruct string_list list = STRING_LIST_INIT_NODUP;\n+\n+\tif (!testing)\n+\t\treturn 0;\n+\n+\tif (is_available)\n+\t\t*is_available = 0;\n+\n+\tstring_list_split_in_place(&list, testing, ',', -1);\n+\tfor_each_string_list_item(item, &list) {\n+\t\tstruct string_list pair = STRING_LIST_INIT_NODUP;\n+\n+\t\tif (string_list_split_in_place(&pair, item->string, ':', 2) != 2)\n+\t\t\tcontinue;\n+\n+\t\tif (!strcmp(*cmd, pair.items[0].string)) {\n+\t\t\t*cmd = pair.items[1].string;\n+\t\t\tif (is_available)\n+\t\t\t\t*is_available = 1;\n+\t\t\tstring_list_clear(&list, 0);\n+\t\t\tUNLEAK(testing);\n+\t\t\treturn 1;\n+\t\t}\n+\t}\n+\n+\tstring_list_clear(&list, 0);\n+\tfree(testing);\n+\treturn 1;\n+}\n+\n+static int is_launchctl_available(void)\n+{\n+\tconst char *cmd = \"launchctl\";\n+\tint is_available;\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+#ifdef __APPLE__\n+\treturn 1;\n+#else\n+\treturn 0;\n+#endif\n+}\n+\n static char *launchctl_service_name(const char *frequency)\n {\n \tstruct strbuf label = STRBUF_INIT;\n@@ -1555,19 +1642,17 @@ static char *launchctl_get_uid(void)\n \treturn xstrfmt(\"gui/%d\", getuid());\n }\n \n-static int launchctl_boot_plist(int enable, const char *filename, const char *cmd)\n+static int launchctl_boot_plist(int enable, const char *filename)\n {\n+\tconst char *cmd = \"launchctl\";\n \tint result;\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tchar *uid = launchctl_get_uid();\n \n+\tget_schedule_cmd(&cmd, NULL);\n \tstrvec_split(&child.args, cmd);\n-\tif (enable)\n-\t\tstrvec_push(&child.args, \"bootstrap\");\n-\telse\n-\t\tstrvec_push(&child.args, \"bootout\");\n-\tstrvec_push(&child.args, uid);\n-\tstrvec_push(&child.args, filename);\n+\tstrvec_pushl(&child.args, enable ? \"bootstrap\" : \"bootout\", uid,\n+\t\t     filename, NULL);\n \n \tchild.no_stderr = 1;\n \tchild.no_stdout = 1;\n@@ -1581,26 +1666,26 @@ static int launchctl_boot_plist(int enable, const char *filename, const char *cm\n \treturn result;\n }\n \n-static int launchctl_remove_plist(enum schedule_priority schedule, const char *cmd)\n+static int launchctl_remove_plist(enum schedule_priority schedule)\n {\n \tconst char *frequency = get_frequency(schedule);\n \tchar *name = launchctl_service_name(frequency);\n \tchar *filename = launchctl_service_filename(name);\n-\tint result = launchctl_boot_plist(0, filename, cmd);\n+\tint result = launchctl_boot_plist(0, filename);\n \tunlink(filename);\n \tfree(filename);\n \tfree(name);\n \treturn result;\n }\n \n-static int launchctl_remove_plists(const char *cmd)\n+static int launchctl_remove_plists(void)\n {\n-\treturn launchctl_remove_plist(SCHEDULE_HOURLY, cmd) ||\n-\t\tlaunchctl_remove_plist(SCHEDULE_DAILY, cmd) ||\n-\t\tlaunchctl_remove_plist(SCHEDULE_WEEKLY, cmd);\n+\treturn launchctl_remove_plist(SCHEDULE_HOURLY) ||\n+\t       launchctl_remove_plist(SCHEDULE_DAILY) ||\n+\t       launchctl_remove_plist(SCHEDULE_WEEKLY);\n }\n \n-static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule, const char *cmd)\n+static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule)\n {\n \tFILE *plist;\n \tint i;\n@@ -1669,8 +1754,8 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n \tfclose(plist);\n \n \t/* bootout might fail if not already running, so ignore */\n-\tlaunchctl_boot_plist(0, filename, cmd);\n-\tif (launchctl_boot_plist(1, filename, cmd))\n+\tlaunchctl_boot_plist(0, filename);\n+\tif (launchctl_boot_plist(1, filename))\n \t\tdie(_(\"failed to bootstrap service %s\"), filename);\n \n \tfree(filename);\n@@ -1678,21 +1763,35 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n \treturn 0;\n }\n \n-static int launchctl_add_plists(const char *cmd)\n+static int launchctl_add_plists(void)\n {\n \tconst char *exec_path = git_exec_path();\n \n-\treturn launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY, cmd) ||\n-\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_DAILY, cmd) ||\n-\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY, cmd);\n+\treturn launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY) ||\n+\t       launchctl_schedule_plist(exec_path, SCHEDULE_DAILY) ||\n+\t       launchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY);\n }\n \n-static int launchctl_update_schedule(int run_maintenance, int fd, const char *cmd)\n+static int launchctl_update_schedule(int run_maintenance, int fd)\n {\n \tif (run_maintenance)\n-\t\treturn launchctl_add_plists(cmd);\n+\t\treturn launchctl_add_plists();\n \telse\n-\t\treturn launchctl_remove_plists(cmd);\n+\t\treturn launchctl_remove_plists();\n+}\n+\n+static int is_schtasks_available(void)\n+{\n+\tconst char *cmd = \"schtasks\";\n+\tint is_available;\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+#ifdef GIT_WINDOWS_NATIVE\n+\treturn 1;\n+#else\n+\treturn 0;\n+#endif\n }\n \n static char *schtasks_task_name(const char *frequency)\n@@ -1702,13 +1801,15 @@ static char *schtasks_task_name(const char *frequency)\n \treturn strbuf_detach(&label, NULL);\n }\n \n-static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd)\n+static int schtasks_remove_task(enum schedule_priority schedule)\n {\n+\tconst char *cmd = \"schtasks\";\n \tint result;\n \tstruct strvec args = STRVEC_INIT;\n \tconst char *frequency = get_frequency(schedule);\n \tchar *name = schtasks_task_name(frequency);\n \n+\tget_schedule_cmd(&cmd, NULL);\n \tstrvec_split(&args, cmd);\n \tstrvec_pushl(&args, \"/delete\", \"/tn\", name, \"/f\", NULL);\n \n@@ -1719,15 +1820,16 @@ static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd\n \treturn result;\n }\n \n-static int schtasks_remove_tasks(const char *cmd)\n+static int schtasks_remove_tasks(void)\n {\n-\treturn schtasks_remove_task(SCHEDULE_HOURLY, cmd) ||\n-\t\tschtasks_remove_task(SCHEDULE_DAILY, cmd) ||\n-\t\tschtasks_remove_task(SCHEDULE_WEEKLY, cmd);\n+\treturn schtasks_remove_task(SCHEDULE_HOURLY) ||\n+\t       schtasks_remove_task(SCHEDULE_DAILY) ||\n+\t       schtasks_remove_task(SCHEDULE_WEEKLY);\n }\n \n-static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule, const char *cmd)\n+static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule)\n {\n+\tconst char *cmd = \"schtasks\";\n \tint result;\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tconst char *xml;\n@@ -1736,6 +1838,8 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority\n \tchar *name = schtasks_task_name(frequency);\n \tstruct strbuf tfilename = STRBUF_INIT;\n \n+\tget_schedule_cmd(&cmd, NULL);\n+\n \tstrbuf_addf(&tfilename, \"%s/schedule_%s_XXXXXX\",\n \t\t    get_git_common_dir(), frequency);\n \ttfile = xmks_tempfile(tfilename.buf);\n@@ -1840,28 +1944,52 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority\n \treturn result;\n }\n \n-static int schtasks_schedule_tasks(const char *cmd)\n+static int schtasks_schedule_tasks(void)\n {\n \tconst char *exec_path = git_exec_path();\n \n-\treturn schtasks_schedule_task(exec_path, SCHEDULE_HOURLY, cmd) ||\n-\t\tschtasks_schedule_task(exec_path, SCHEDULE_DAILY, cmd) ||\n-\t\tschtasks_schedule_task(exec_path, SCHEDULE_WEEKLY, cmd);\n+\treturn schtasks_schedule_task(exec_path, SCHEDULE_HOURLY) ||\n+\t       schtasks_schedule_task(exec_path, SCHEDULE_DAILY) ||\n+\t       schtasks_schedule_task(exec_path, SCHEDULE_WEEKLY);\n }\n \n-static int schtasks_update_schedule(int run_maintenance, int fd, const char *cmd)\n+static int schtasks_update_schedule(int run_maintenance, int fd)\n {\n \tif (run_maintenance)\n-\t\treturn schtasks_schedule_tasks(cmd);\n+\t\treturn schtasks_schedule_tasks();\n \telse\n-\t\treturn schtasks_remove_tasks(cmd);\n+\t\treturn schtasks_remove_tasks();\n+}\n+\n+static int is_crontab_available(void)\n+{\n+\tconst char *cmd = \"crontab\";\n+\tint is_available;\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+\tstrvec_split(&child.args, cmd);\n+\tstrvec_push(&child.args, \"-l\");\n+\tchild.no_stdin = 1;\n+\tchild.no_stdout = 1;\n+\tchild.no_stderr = 1;\n+\tchild.silent_exec_failure = 1;\n+\n+\tif (start_command(&child))\n+\t\treturn 0;\n+\t/* Ignore exit code, as an empty crontab will return error. */\n+\tfinish_command(&child);\n+\treturn 1;\n }\n \n #define BEGIN_LINE \"# BEGIN GIT MAINTENANCE SCHEDULE\"\n #define END_LINE \"# END GIT MAINTENANCE SCHEDULE\"\n \n-static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n+static int crontab_update_schedule(int run_maintenance, int fd)\n {\n+\tconst char *cmd = \"crontab\";\n \tint result = 0;\n \tint in_old_region = 0;\n \tstruct child_process crontab_list = CHILD_PROCESS_INIT;\n@@ -1869,6 +1997,7 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n \tFILE *cron_list, *cron_in;\n \tstruct strbuf line = STRBUF_INIT;\n \n+\tget_schedule_cmd(&cmd, NULL);\n \tstrvec_split(&crontab_list.args, cmd);\n \tstrvec_push(&crontab_list.args, \"-l\");\n \tcrontab_list.in = -1;\n@@ -1945,66 +2074,160 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n \treturn result;\n }\n \n+enum scheduler {\n+\tSCHEDULER_INVALID = -1,\n+\tSCHEDULER_AUTO,\n+\tSCHEDULER_CRON,\n+\tSCHEDULER_LAUNCHCTL,\n+\tSCHEDULER_SCHTASKS,\n+};\n+\n+static const struct {\n+\tconst char *name;\n+\tint (*is_available)(void);\n+\tint (*update_schedule)(int run_maintenance, int fd);\n+} scheduler_fn[] = {\n+\t[SCHEDULER_CRON] = {\n+\t\t.name = \"crontab\",\n+\t\t.is_available = is_crontab_available,\n+\t\t.update_schedule = crontab_update_schedule,\n+\t},\n+\t[SCHEDULER_LAUNCHCTL] = {\n+\t\t.name = \"launchctl\",\n+\t\t.is_available = is_launchctl_available,\n+\t\t.update_schedule = launchctl_update_schedule,\n+\t},\n+\t[SCHEDULER_SCHTASKS] = {\n+\t\t.name = \"schtasks\",\n+\t\t.is_available = is_schtasks_available,\n+\t\t.update_schedule = schtasks_update_schedule,\n+\t},\n+};\n+\n+static enum scheduler parse_scheduler(const char *value)\n+{\n+\tif (!value)\n+\t\treturn SCHEDULER_INVALID;\n+\telse if (!strcasecmp(value, \"auto\"))\n+\t\treturn SCHEDULER_AUTO;\n+\telse if (!strcasecmp(value, \"cron\") || !strcasecmp(value, \"crontab\"))\n+\t\treturn SCHEDULER_CRON;\n+\telse if (!strcasecmp(value, \"launchctl\"))\n+\t\treturn SCHEDULER_LAUNCHCTL;\n+\telse if (!strcasecmp(value, \"schtasks\"))\n+\t\treturn SCHEDULER_SCHTASKS;\n+\telse\n+\t\treturn SCHEDULER_INVALID;\n+}\n+\n+static int maintenance_opt_scheduler(const struct option *opt, const char *arg,\n+\t\t\t\t     int unset)\n+{\n+\tenum scheduler *scheduler = opt->value;\n+\n+\tBUG_ON_OPT_NEG(unset);\n+\n+\t*scheduler = parse_scheduler(arg);\n+\tif (*scheduler == SCHEDULER_INVALID)\n+\t\treturn error(_(\"unrecognized --scheduler argument '%s'\"), arg);\n+\treturn 0;\n+}\n+\n+struct maintenance_start_opts {\n+\tenum scheduler scheduler;\n+};\n+\n+static enum scheduler resolve_scheduler(enum scheduler scheduler)\n+{\n+\tif (scheduler != SCHEDULER_AUTO)\n+\t\treturn scheduler;\n+\n #if defined(__APPLE__)\n-static const char platform_scheduler[] = \"launchctl\";\n+\treturn SCHEDULER_LAUNCHCTL;\n+\n #elif defined(GIT_WINDOWS_NATIVE)\n-static const char platform_scheduler[] = \"schtasks\";\n+\treturn SCHEDULER_SCHTASKS;\n+\n #else\n-static const char platform_scheduler[] = \"crontab\";\n+\treturn SCHEDULER_CRON;\n #endif\n+}\n \n-static int update_background_schedule(int enable)\n+static void validate_scheduler(enum scheduler scheduler)\n {\n-\tint result;\n-\tconst char *scheduler = platform_scheduler;\n-\tconst char *cmd = scheduler;\n-\tchar *testing;\n+\tif (scheduler == SCHEDULER_INVALID)\n+\t\tBUG(\"invalid scheduler\");\n+\tif (scheduler == SCHEDULER_AUTO)\n+\t\tBUG(\"resolve_scheduler should have been called before\");\n+\n+\tif (!scheduler_fn[scheduler].is_available())\n+\t\tdie(_(\"%s scheduler is not available\"),\n+\t\t    scheduler_fn[scheduler].name);\n+}\n+\n+static int update_background_schedule(const struct maintenance_start_opts *opts,\n+\t\t\t\t      int enable)\n+{\n+\tunsigned int i;\n+\tint result = 0;\n \tstruct lock_file lk;\n \tchar *lock_path = xstrfmt(\"%s/schedule\", the_repository->objects->odb->path);\n \n-\ttesting = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n-\tif (testing) {\n-\t\tchar *sep = strchr(testing, ':');\n-\t\tif (!sep)\n-\t\t\tdie(\"GIT_TEST_MAINT_SCHEDULER unparseable: %s\", testing);\n-\t\t*sep = '\\0';\n-\t\tscheduler = testing;\n-\t\tcmd = sep + 1;\n+\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {\n+\t\tfree(lock_path);\n+\t\treturn error(_(\"another process is scheduling background maintenance\"));\n \t}\n \n-\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {\n-\t\tresult = error(_(\"another process is scheduling background maintenance\"));\n-\t\tgoto cleanup;\n+\tfor (i = 1; i < ARRAY_SIZE(scheduler_fn); i++) {\n+\t\tif (enable && opts->scheduler == i)\n+\t\t\tcontinue;\n+\t\tif (!scheduler_fn[i].is_available())\n+\t\t\tcontinue;\n+\t\tscheduler_fn[i].update_schedule(0, get_lock_file_fd(&lk));\n \t}\n \n-\tif (!strcmp(scheduler, \"launchctl\"))\n-\t\tresult = launchctl_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse if (!strcmp(scheduler, \"schtasks\"))\n-\t\tresult = schtasks_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse if (!strcmp(scheduler, \"crontab\"))\n-\t\tresult = crontab_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse\n-\t\tdie(\"unknown background scheduler: %s\", scheduler);\n+\tif (enable)\n+\t\tresult = scheduler_fn[opts->scheduler].update_schedule(\n+\t\t\t1, get_lock_file_fd(&lk));\n \n \trollback_lock_file(&lk);\n \n-cleanup:\n \tfree(lock_path);\n-\tfree(testing);\n \treturn result;\n }\n \n-static int maintenance_start(void)\n+static const char *const builtin_maintenance_start_usage[] = {\n+\tN_(\"git maintenance start [--scheduler=<scheduler>]\"),\n+\tNULL\n+};\n+\n+static int maintenance_start(int argc, const char **argv, const char *prefix)\n {\n+\tstruct maintenance_start_opts opts = { 0 };\n+\tstruct option options[] = {\n+\t\tOPT_CALLBACK_F(\n+\t\t\t0, \"scheduler\", &opts.scheduler, N_(\"scheduler\"),\n+\t\t\tN_(\"scheduler to trigger git maintenance run\"),\n+\t\t\tPARSE_OPT_NONEG, maintenance_opt_scheduler),\n+\t\tOPT_END()\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     builtin_maintenance_start_usage, 0);\n+\tif (argc)\n+\t\tusage_with_options(builtin_maintenance_start_usage, options);\n+\n+\topts.scheduler = resolve_scheduler(opts.scheduler);\n+\tvalidate_scheduler(opts.scheduler);\n+\n \tif (maintenance_register())\n \t\twarning(_(\"failed to add repo to global config\"));\n-\n-\treturn update_background_schedule(1);\n+\treturn update_background_schedule(&opts, 1);\n }\n \n static int maintenance_stop(void)\n {\n-\treturn update_background_schedule(0);\n+\treturn update_background_schedule(NULL, 0);\n }\n \n static const char builtin_maintenance_usage[] =\tN_(\"git maintenance <subcommand> [<options>]\");\n@@ -2018,7 +2241,7 @@ int cmd_maintenance(int argc, const char **argv, const char *prefix)\n \tif (!strcmp(argv[1], \"run\"))\n \t\treturn maintenance_run(argc - 1, argv + 1, prefix);\n \tif (!strcmp(argv[1], \"start\"))\n-\t\treturn maintenance_start();\n+\t\treturn maintenance_start(argc - 1, argv + 1, prefix);\n \tif (!strcmp(argv[1], \"stop\"))\n \t\treturn maintenance_stop();\n \tif (!strcmp(argv[1], \"register\"))\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 58f46c77e6..27bce7992c 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -492,8 +492,21 @@ test_expect_success !MINGW 'register and unregister with regex metacharacters' '\n \t\tmaintenance.repo \"$(pwd)/$META\"\n '\n \n+test_expect_success 'start --scheduler=<scheduler>' '\n+\ttest_expect_code 129 git maintenance start --scheduler=foo 2>err &&\n+\ttest_i18ngrep \"unrecognized --scheduler argument\" err &&\n+\n+\ttest_expect_code 129 git maintenance start --no-scheduler 2>err &&\n+\ttest_i18ngrep \"unknown option\" err &&\n+\n+\ttest_expect_code 128 \\\n+\t\tenv GIT_TEST_MAINT_SCHEDULER=\"launchctl:true,schtasks:true\" \\\n+\t\tgit maintenance start --scheduler=crontab 2>err &&\n+\ttest_i18ngrep \"fatal: crontab scheduler is not available\" err\n+'\n+\n test_expect_success 'start from empty cron table' '\n-\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start --scheduler=crontab &&\n \n \t# start registers the repo\n \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n@@ -516,7 +529,7 @@ test_expect_success 'stop from existing schedule' '\n \n test_expect_success 'start preserves existing schedule' '\n \techo \"Important information!\" >cron.txt &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start --scheduler=crontab &&\n \tgrep \"Important information!\" cron.txt\n '\n \n@@ -545,7 +558,7 @@ test_expect_success 'start and stop macOS maintenance' '\n \tEOF\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start --scheduler=launchctl &&\n \n \t# start registers the repo\n \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n@@ -596,7 +609,7 @@ test_expect_success 'start and stop Windows maintenance' '\n \tEOF\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"schtasks:./print-args\" git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"schtasks:./print-args\" git maintenance start --scheduler=schtasks &&\n \n \t# start registers the repo\n \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n@@ -619,6 +632,40 @@ test_expect_success 'start and stop Windows maintenance' '\n \ttest_cmp expect args\n '\n \n+test_expect_success 'start and stop when several schedulers are available' '\n+\twrite_script print-args <<-\\EOF &&\n+\tprintf \"%s\\n\" \"$*\" | sed \"s:gui/[0-9][0-9]*:gui/[UID]:; s:\\(schtasks /create .* /xml\\).*:\\1:;\" >>args\n+\tEOF\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >expect &&\n+\tfor frequency in hourly daily weekly\n+\tdo\n+\t\tPLIST=\"$pfx/Library/LaunchAgents/org.git-scm.git.$frequency.plist\" &&\n+\t\techo \"launchctl bootout gui/[UID] $PLIST\" >>expect &&\n+\t\techo \"launchctl bootstrap gui/[UID] $PLIST\" >>expect || return 1\n+\tdone &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n+\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n+\t\thourly daily weekly >expect &&\n+\tprintf \"schtasks /create /tn Git Maintenance (%s) /f /xml\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n+\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n+\t\thourly daily weekly >expect &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\ttest_cmp expect args\n+'\n+\n test_expect_success 'register preserves existing strategy' '\n \tgit config maintenance.strategy none &&\n \tgit maintenance register &&\n-- \n2.33.0\n\n"},{"id":"433501","messageId":"xmqqk0kbpzf0.fsf@gitster.g","threadId":"55603","inReplyTo":"3218082.ccbTtk1zYS@coruscant.lhuard.fr","subject":"Re: [PATCH v7 2/3] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-08-23T22:30:11Z","receivedAt":"2021-08-23T22:30:18Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Lénaïc Huard <lenaic@lhuard.fr> writes:\n\n> Is the suggestion to replace\n>\n> +#ifdef __APPLE__\n> +\treturn 1;\n> +#else\n> +\treturn 0;\n> +#endif\n> +}\n>\n> by\n>\n> + return IS_LAUNCHCTL_AVAILABLE;\n>\n> and to add\n>\n> #ifdef __APPLE__\n> #define IS_LAUNCHCTL_AVAILABLE 1\n> #else\n> #define IS_LAUNCHCTL_AVAILABLE 0\n> #endif\n>\n> somewhere else like at the top of builtin/gc.c ?\n\nI wasn't the one who suggested it, but the suggestion reads as such\nto me.\n\n> Also, do we agree this shouldn’t be defined in cache.h ? I’m a little bit \n> confused.\n\nThe audience of \"cache.h\" (or more precisely, \"git-compat-util.h\" is\nwhere these come from by including system headers) is much wider\nthan those narrow users who care about launchtrl or cron, so\nlimiting it in builtin/gc.c would make more sense, I would think.\n\nThanks.\n\n\n\n"},{"id":"433602","messageId":"bd93fdca-3b37-2d71-2e6c-6e0e38ad098f@gmail.com","threadId":"55603","inReplyTo":"20210823204011.87023-3-lenaic@lhuard.fr","subject":"Re: [PATCH v8 2/3] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-08-24T17:45:55Z","receivedAt":"2021-08-24T18:01:16Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 8/23/2021 4:40 PM, Lénaïc Huard wrote:> + * Ex.:\n> + *   GIT_TEST_MAINT_SCHEDULER not set\n> + *     +-------+-------------------------------------------------+\n> + *     | Input |                     Output                      |\n> + *     | *cmd  | return code |       *cmd        | *is_available |\n> + *     +-------+-------------+-------------------+---------------+\n> + *     | \"foo\" |    false    | \"foo\" (unchanged) |  (unchanged)  |\n> + *     +-------+-------------+-------------------+---------------+\n> + *\n> + *   GIT_TEST_MAINT_SCHEDULER set to “foo:./mock_foo.sh,bar:./mock_bar.sh”\n> + *     +-------+-------------------------------------------------+\n> + *     | Input |                     Output                      |\n> + *     | *cmd  | return code |       *cmd        | *is_available |\n> + *     +-------+-------------+-------------------+---------------+\n> + *     | \"foo\" |    true     |  \"./mock.foo.sh\"  |     true      |\n> + *     | \"qux\" |    true     | \"qux\" (unchanged) |     false     |\n> + *     +-------+-------------+-------------------+---------------+\n> + */\n\nThank you for updating to this ASCII table. It has the same amount\nof visual information without requiring special characters.\n\n> +static int is_launchctl_available(void)\n> +{\n> +\tconst char *cmd = \"launchctl\";\n> +\tint is_available;\n> +\tif (get_schedule_cmd(&cmd, &is_available))\n> +\t\treturn is_available;\n> +\n> +#ifdef __APPLE__\n> +\treturn 1;\n> +#else\n> +\treturn 0;\n> +#endif\n> +}\n\nI find this use of #ifdef to be perfectly fine. Adding a layer of\nindirection into the compat layer through another macro is\nunnecessary, in my opinion.\n\nThanks,\n-Stolee\n"},{"id":"433601","messageId":"66b2961f-19c8-d308-c9d2-eb8d05599a84@gmail.com","threadId":"55603","inReplyTo":"20210823204011.87023-4-lenaic@lhuard.fr","subject":"Re: [PATCH v8 3/3] maintenance: add support for systemd timers on Linux","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-08-24T17:45:37Z","receivedAt":"2021-08-24T18:01:17Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 8/23/2021 4:40 PM, Lénaïc Huard wrote:\n> +#ifdef __linux__\n> +\n> +static int real_is_systemd_timer_available(void)\n> +{\n> +\tstruct child_process child = CHILD_PROCESS_INIT;\n> +\n> +\tstrvec_pushl(&child.args, \"systemctl\", \"--user\", \"list-timers\", NULL);\n> +\tchild.no_stdin = 1;\n> +\tchild.no_stdout = 1;\n> +\tchild.no_stderr = 1;\n> +\tchild.silent_exec_failure = 1;\n> +\n> +\tif (start_command(&child))\n> +\t\treturn 0;\n> +\tif (finish_command(&child))\n> +\t\treturn 0;\n> +\treturn 1;\n> +}\n> +\n> +#else\n> +\n> +static int real_is_systemd_timer_available(void)\n> +{\n> +\treturn 0;\n> +}\n> +\n> +#endif\n\nThis #ifdef option is one that could be changed. There is a lot\nof code inside the #ifdef that would be nice to compile on all\nplatforms.\n\nTechnically, we could drop all conditionals here and rely on\nthe start_command() and finish_command() to tell us that systemd\nis or is not installed. This would allow a potential future where\nmaybe macOS supports systemd (or users install a version\nthemselves).\n\nAnother option would be to compile in a conditional early return\ninside real_is_systemd_timer_available() such as\n\nstatic int real_is_systemd_timer_available(void)\n{\n\tstruct child_process child = CHILD_PROCESS_INIT;\n\n#ifndef __linux__\n\tif (1)\n\t\treturn 0;\n#endif\n\n\tstrvec_pushl(&child.args, \"systemctl\", \"--user\", \"list-timers\", NULL);\n\tchild.no_stdin = 1;\n\tchild.no_stdout = 1;\n\tchild.no_stderr = 1;\n\tchild.silent_exec_failure = 1;\n\n\tif (start_command(&child))\n\t\treturn 0;\n\tif (finish_command(&child))\n\t\treturn 0;\n\treturn 1;\n}\n\n...but this also looks a bit awkward in order to avoid compilers\ncomplaining about unreachable code (and some might still rightly\nwarn about unreachable code).\n\n> +static int systemd_timer_setup_units(void)\n> +{\n> +\tconst char *exec_path = git_exec_path();\n> +\n> +\tint ret = systemd_timer_write_unit_templates(exec_path) ||\n> +\t          systemd_timer_enable_unit(1, SCHEDULE_HOURLY) ||\n> +\t          systemd_timer_enable_unit(1, SCHEDULE_DAILY) ||\n> +\t          systemd_timer_enable_unit(1, SCHEDULE_WEEKLY);\n\nThese lines are incorrectly tabbed with spaces. Here is a corrected\nversion:\n\n+\tint ret = systemd_timer_write_unit_templates(exec_path) ||\n+\t\t  systemd_timer_enable_unit(1, SCHEDULE_HOURLY) ||\n+\t\t  systemd_timer_enable_unit(1, SCHEDULE_DAILY) ||\n+\t\t  systemd_timer_enable_unit(1, SCHEDULE_WEEKLY);\n\n'git rebase --whitespace=fix <base>' will also fix these issues. You\ncan discover if they exist using 'git log --check'.\n\nThanks,\n-Stolee\n"},{"id":"433603","messageId":"44904983-a6a8-d72f-24db-50bf112c585b@gmail.com","threadId":"55603","inReplyTo":"20210823204011.87023-1-lenaic@lhuard.fr","subject":"Re: [PATCH v8 0/3] maintenance: add support for systemd timers on Linux","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-08-24T17:47:44Z","receivedAt":"2021-08-24T18:01:32Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 8/23/2021 4:40 PM, Lénaïc Huard wrote:\n> According to [1], there were 3 changes awaited in this v8:\n> * The two already mentionned above (utf-8 characters and\n>   `string_list_split` thing)\n> * An improvement around the #ifdef.\n> \n> I must admit I haven’t touched anything around the #ifdef in this v8\n> because I’m not sure what to do. I’ve just asked for some more details\n> in [2].\n\nI commented on the relevant patches, but in summary:\n\n1. I think your #ifdef __APPLE__ is fine in patch 2.\n\n2. The #ifdef __linux__ in patch 3 could be removed\n   entirely.\n\nThen there is a nit about whitespace, but that is easily\nrectified with 'git rebase --whitespace=fix'.\n\nThanks,\n-Stolee\n"},{"id":"433955","messageId":"20210827210255.12565-1-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210823204011.87023-1-lenaic@lhuard.fr","subject":"[PATCH v9 0/3] maintenance: add support for systemd timers on Linux","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-08-27T21:02:52Z","receivedAt":"2021-08-27T21:03:41Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Hello,\n\nPlease find hereafter my updated patchset to add support for systemd\ntimers on Linux for the `git maintenance start` command.\n\nThe goal of this re-roll is to address Derrick’s remarks [1]\nNamely:\n* Get rid of the #ifdef around real_is_systemd_timer_available\n* Fix the whitespace issue in systemd_timer_setup_units\n\n[1] https://lore.kernel.org/git/44904983-a6a8-d72f-24db-50bf112c585b@gmail.com/\n\nThe patches are:\n\n* cache.h: Introduce a generic \"xdg_config_home_for(…)\" function\n\n  This patch introduces a function to compute configuration files\n  paths inside $XDG_CONFIG_HOME.\n\n  This patch is unchanged compared to its previous version.\n\n* maintenance: `git maintenance run` learned `--scheduler=<scheduler>`\n\n  This patch adds a new parameter to the `git maintenance run` to let\n  the user choose a scheduler.\n\n  This patch is unchanged compared to its previous version.\n\n* maintenance: add support for systemd timers on Linux\n\n  This patch implements the support of systemd timers on top of\n  crontab scheduler on Linux systems.\n\n  * The whitespace nit has been fixed with `git rebase\n    --whitespace=fix`\n\n  * The #ifdef around real_is_systemd_timer_available has been dropped\n    so that its code is compiled on every platform.\n\n    This means that:\n    * The availability of `launchctl` and `schtasks` are completely\n      determined at compilation time by the platform.\n    * The availability of `cron` and `systemd timers` are fully\n      determined at runtime even if today only Linux might have systemd.\n\nBest wishes,\nLénaïc.\n\n\nLénaïc Huard (3):\n  cache.h: Introduce a generic \"xdg_config_home_for(…)\" function\n  maintenance: `git maintenance run` learned `--scheduler=<scheduler>`\n  maintenance: add support for systemd timers on Linux\n\n Documentation/git-maintenance.txt |  57 +++\n builtin/gc.c                      | 581 ++++++++++++++++++++++++++----\n cache.h                           |   7 +\n path.c                            |  13 +-\n t/t7900-maintenance.sh            | 110 +++++-\n 5 files changed, 690 insertions(+), 78 deletions(-)\n\nDiff-intervalle contre v8 :\n-:  ---------- > 1:  1639bd151c cache.h: Introduce a generic \"xdg_config_home_for(…)\" function\n-:  ---------- > 2:  ea5568269c maintenance: `git maintenance run` learned `--scheduler=<scheduler>`\n1:  faf56c078f ! 3:  8c25ebd3be maintenance: add support for systemd timers on Linux\n    @@ builtin/gc.c: static int crontab_update_schedule(int run_maintenance, int fd)\n      \treturn result;\n      }\n      \n    -+#ifdef __linux__\n    -+\n     +static int real_is_systemd_timer_available(void)\n     +{\n     +\tstruct child_process child = CHILD_PROCESS_INIT;\n    @@ builtin/gc.c: static int crontab_update_schedule(int run_maintenance, int fd)\n     +\treturn 1;\n     +}\n     +\n    -+#else\n    -+\n    -+static int real_is_systemd_timer_available(void)\n    -+{\n    -+\treturn 0;\n    -+}\n    -+\n    -+#endif\n    -+\n     +static int is_systemd_timer_available(void)\n     +{\n     +\tconst char *cmd = \"systemctl\";\n    @@ builtin/gc.c: static int crontab_update_schedule(int run_maintenance, int fd)\n     +\tconst char *exec_path = git_exec_path();\n     +\n     +\tint ret = systemd_timer_write_unit_templates(exec_path) ||\n    -+\t          systemd_timer_enable_unit(1, SCHEDULE_HOURLY) ||\n    -+\t          systemd_timer_enable_unit(1, SCHEDULE_DAILY) ||\n    -+\t          systemd_timer_enable_unit(1, SCHEDULE_WEEKLY);\n    ++\t\t  systemd_timer_enable_unit(1, SCHEDULE_HOURLY) ||\n    ++\t\t  systemd_timer_enable_unit(1, SCHEDULE_DAILY) ||\n    ++\t\t  systemd_timer_enable_unit(1, SCHEDULE_WEEKLY);\n     +\tif (ret)\n     +\t\tsystemd_timer_delete_units();\n     +\treturn ret;\n-- \n2.33.0\n\n"},{"id":"433956","messageId":"20210827210255.12565-2-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210827210255.12565-1-lenaic@lhuard.fr","subject":"[PATCH v9 1/3] cache.h: Introduce a generic \"xdg_config_home_for(…)\" function","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-08-27T21:02:53Z","receivedAt":"2021-08-27T21:03:43Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Current implementation of `xdg_config_home(filename)` returns\n`$XDG_CONFIG_HOME/git/$filename`, with the `git` subdirectory inserted\nbetween the `XDG_CONFIG_HOME` environment variable and the parameter.\n\nThis patch introduces a `xdg_config_home_for(subdir, filename)` function\nwhich is more generic. It only concatenates \"$XDG_CONFIG_HOME\", or\n\"$HOME/.config\" if the former isn’t defined, with the parameters,\nwithout adding `git` in between.\n\n`xdg_config_home(filename)` is now implemented by calling\n`xdg_config_home_for(\"git\", filename)` but this new generic function can\nbe used to compute the configuration directory of other programs.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n cache.h |  7 +++++++\n path.c  | 13 ++++++++++---\n 2 files changed, 17 insertions(+), 3 deletions(-)\n\ndiff --git a/cache.h b/cache.h\nindex bd4869beee..79c4e17ea4 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -1295,6 +1295,13 @@ int is_ntfs_dotmailmap(const char *name);\n  */\n int looks_like_command_line_option(const char *str);\n \n+/**\n+ * Return a newly allocated string with the evaluation of\n+ * \"$XDG_CONFIG_HOME/$subdir/$filename\" if $XDG_CONFIG_HOME is non-empty, otherwise\n+ * \"$HOME/.config/$subdir/$filename\". Return NULL upon error.\n+ */\n+char *xdg_config_home_for(const char *subdir, const char *filename);\n+\n /**\n  * Return a newly allocated string with the evaluation of\n  * \"$XDG_CONFIG_HOME/git/$filename\" if $XDG_CONFIG_HOME is non-empty, otherwise\ndiff --git a/path.c b/path.c\nindex 7bccd830e9..1b1de3be09 100644\n--- a/path.c\n+++ b/path.c\n@@ -1503,21 +1503,28 @@ int looks_like_command_line_option(const char *str)\n \treturn str && str[0] == '-';\n }\n \n-char *xdg_config_home(const char *filename)\n+char *xdg_config_home_for(const char *subdir, const char *filename)\n {\n \tconst char *home, *config_home;\n \n+\tassert(subdir);\n \tassert(filename);\n \tconfig_home = getenv(\"XDG_CONFIG_HOME\");\n \tif (config_home && *config_home)\n-\t\treturn mkpathdup(\"%s/git/%s\", config_home, filename);\n+\t\treturn mkpathdup(\"%s/%s/%s\", config_home, subdir, filename);\n \n \thome = getenv(\"HOME\");\n \tif (home)\n-\t\treturn mkpathdup(\"%s/.config/git/%s\", home, filename);\n+\t\treturn mkpathdup(\"%s/.config/%s/%s\", home, subdir, filename);\n+\n \treturn NULL;\n }\n \n+char *xdg_config_home(const char *filename)\n+{\n+\treturn xdg_config_home_for(\"git\", filename);\n+}\n+\n char *xdg_cache_home(const char *filename)\n {\n \tconst char *home, *cache_home;\n-- \n2.33.0\n\n"},{"id":"433957","messageId":"20210827210255.12565-3-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210827210255.12565-1-lenaic@lhuard.fr","subject":"[PATCH v9 2/3] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-08-27T21:02:54Z","receivedAt":"2021-08-27T21:03:44Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Depending on the system, different schedulers can be used to schedule\nthe hourly, daily and weekly executions of `git maintenance run`:\n* `launchctl` for MacOS,\n* `schtasks` for Windows and\n* `crontab` for everything else.\n\n`git maintenance run` now has an option to let the end-user explicitly\nchoose which scheduler he wants to use:\n`--scheduler=auto|crontab|launchctl|schtasks`.\n\nWhen `git maintenance start --scheduler=XXX` is run, it not only\nregisters `git maintenance run` tasks in the scheduler XXX, it also\nremoves the `git maintenance run` tasks from all the other schedulers to\nensure we cannot have two schedulers launching concurrent identical\ntasks.\n\nThe default value is `auto` which chooses a suitable scheduler for the\nsystem.\n\n`git maintenance stop` doesn't have any `--scheduler` parameter because\nthis command will try to remove the `git maintenance run` tasks from all\nthe available schedulers.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n Documentation/git-maintenance.txt |   9 +\n builtin/gc.c                      | 365 ++++++++++++++++++++++++------\n t/t7900-maintenance.sh            |  55 ++++-\n 3 files changed, 354 insertions(+), 75 deletions(-)\n\ndiff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\nindex 1e738ad398..576290b5c6 100644\n--- a/Documentation/git-maintenance.txt\n+++ b/Documentation/git-maintenance.txt\n@@ -179,6 +179,15 @@ OPTIONS\n \t`maintenance.<task>.enabled` configured as `true` are considered.\n \tSee the 'TASKS' section for the list of accepted `<task>` values.\n \n+--scheduler=auto|crontab|launchctl|schtasks::\n+\tWhen combined with the `start` subcommand, specify the scheduler\n+\tfor running the hourly, daily and weekly executions of\n+\t`git maintenance run`.\n+\tPossible values for `<scheduler>` are `auto`, `crontab` (POSIX),\n+\t`launchctl` (macOS), and `schtasks` (Windows).\n+\tWhen `auto` is specified, the appropriate platform-specific\n+\tscheduler is used. Default is `auto`.\n+\n \n TROUBLESHOOTING\n ---------------\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex f05d2f0a1a..9e464d4a10 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1529,6 +1529,93 @@ static const char *get_frequency(enum schedule_priority schedule)\n \t}\n }\n \n+/*\n+ * get_schedule_cmd` reads the GIT_TEST_MAINT_SCHEDULER environment variable\n+ * to mock the schedulers that `git maintenance start` rely on.\n+ *\n+ * For test purpose, GIT_TEST_MAINT_SCHEDULER can be set to a comma-separated\n+ * list of colon-separated key/value pairs where each pair contains a scheduler\n+ * and its corresponding mock.\n+ *\n+ * * If $GET_TEST_MAINT_SCHEDULER is not set, return false and leave the\n+ *   arguments unmodified.\n+ *\n+ * * If $GET_TEST_MAINT_SCHEDULER is set, return true.\n+ *   In this case, the *cmd value is read as input.\n+ *\n+ *   * if the input value *cmd is the key of one of the comma-separated list\n+ *     item, then *is_available is set to true and *cmd is modified and becomes\n+ *     the mock command.\n+ *\n+ *   * if the input value *cmd isn’t the key of any of the comma-separated list\n+ *     item, then *is_available is set to false.\n+ *\n+ * Ex.:\n+ *   GIT_TEST_MAINT_SCHEDULER not set\n+ *     +-------+-------------------------------------------------+\n+ *     | Input |                     Output                      |\n+ *     | *cmd  | return code |       *cmd        | *is_available |\n+ *     +-------+-------------+-------------------+---------------+\n+ *     | \"foo\" |    false    | \"foo\" (unchanged) |  (unchanged)  |\n+ *     +-------+-------------+-------------------+---------------+\n+ *\n+ *   GIT_TEST_MAINT_SCHEDULER set to “foo:./mock_foo.sh,bar:./mock_bar.sh”\n+ *     +-------+-------------------------------------------------+\n+ *     | Input |                     Output                      |\n+ *     | *cmd  | return code |       *cmd        | *is_available |\n+ *     +-------+-------------+-------------------+---------------+\n+ *     | \"foo\" |    true     |  \"./mock.foo.sh\"  |     true      |\n+ *     | \"qux\" |    true     | \"qux\" (unchanged) |     false     |\n+ *     +-------+-------------+-------------------+---------------+\n+ */\n+static int get_schedule_cmd(const char **cmd, int *is_available)\n+{\n+\tchar *testing = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n+\tstruct string_list_item *item;\n+\tstruct string_list list = STRING_LIST_INIT_NODUP;\n+\n+\tif (!testing)\n+\t\treturn 0;\n+\n+\tif (is_available)\n+\t\t*is_available = 0;\n+\n+\tstring_list_split_in_place(&list, testing, ',', -1);\n+\tfor_each_string_list_item(item, &list) {\n+\t\tstruct string_list pair = STRING_LIST_INIT_NODUP;\n+\n+\t\tif (string_list_split_in_place(&pair, item->string, ':', 2) != 2)\n+\t\t\tcontinue;\n+\n+\t\tif (!strcmp(*cmd, pair.items[0].string)) {\n+\t\t\t*cmd = pair.items[1].string;\n+\t\t\tif (is_available)\n+\t\t\t\t*is_available = 1;\n+\t\t\tstring_list_clear(&list, 0);\n+\t\t\tUNLEAK(testing);\n+\t\t\treturn 1;\n+\t\t}\n+\t}\n+\n+\tstring_list_clear(&list, 0);\n+\tfree(testing);\n+\treturn 1;\n+}\n+\n+static int is_launchctl_available(void)\n+{\n+\tconst char *cmd = \"launchctl\";\n+\tint is_available;\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+#ifdef __APPLE__\n+\treturn 1;\n+#else\n+\treturn 0;\n+#endif\n+}\n+\n static char *launchctl_service_name(const char *frequency)\n {\n \tstruct strbuf label = STRBUF_INIT;\n@@ -1555,19 +1642,17 @@ static char *launchctl_get_uid(void)\n \treturn xstrfmt(\"gui/%d\", getuid());\n }\n \n-static int launchctl_boot_plist(int enable, const char *filename, const char *cmd)\n+static int launchctl_boot_plist(int enable, const char *filename)\n {\n+\tconst char *cmd = \"launchctl\";\n \tint result;\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tchar *uid = launchctl_get_uid();\n \n+\tget_schedule_cmd(&cmd, NULL);\n \tstrvec_split(&child.args, cmd);\n-\tif (enable)\n-\t\tstrvec_push(&child.args, \"bootstrap\");\n-\telse\n-\t\tstrvec_push(&child.args, \"bootout\");\n-\tstrvec_push(&child.args, uid);\n-\tstrvec_push(&child.args, filename);\n+\tstrvec_pushl(&child.args, enable ? \"bootstrap\" : \"bootout\", uid,\n+\t\t     filename, NULL);\n \n \tchild.no_stderr = 1;\n \tchild.no_stdout = 1;\n@@ -1581,26 +1666,26 @@ static int launchctl_boot_plist(int enable, const char *filename, const char *cm\n \treturn result;\n }\n \n-static int launchctl_remove_plist(enum schedule_priority schedule, const char *cmd)\n+static int launchctl_remove_plist(enum schedule_priority schedule)\n {\n \tconst char *frequency = get_frequency(schedule);\n \tchar *name = launchctl_service_name(frequency);\n \tchar *filename = launchctl_service_filename(name);\n-\tint result = launchctl_boot_plist(0, filename, cmd);\n+\tint result = launchctl_boot_plist(0, filename);\n \tunlink(filename);\n \tfree(filename);\n \tfree(name);\n \treturn result;\n }\n \n-static int launchctl_remove_plists(const char *cmd)\n+static int launchctl_remove_plists(void)\n {\n-\treturn launchctl_remove_plist(SCHEDULE_HOURLY, cmd) ||\n-\t\tlaunchctl_remove_plist(SCHEDULE_DAILY, cmd) ||\n-\t\tlaunchctl_remove_plist(SCHEDULE_WEEKLY, cmd);\n+\treturn launchctl_remove_plist(SCHEDULE_HOURLY) ||\n+\t       launchctl_remove_plist(SCHEDULE_DAILY) ||\n+\t       launchctl_remove_plist(SCHEDULE_WEEKLY);\n }\n \n-static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule, const char *cmd)\n+static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule)\n {\n \tFILE *plist;\n \tint i;\n@@ -1669,8 +1754,8 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n \tfclose(plist);\n \n \t/* bootout might fail if not already running, so ignore */\n-\tlaunchctl_boot_plist(0, filename, cmd);\n-\tif (launchctl_boot_plist(1, filename, cmd))\n+\tlaunchctl_boot_plist(0, filename);\n+\tif (launchctl_boot_plist(1, filename))\n \t\tdie(_(\"failed to bootstrap service %s\"), filename);\n \n \tfree(filename);\n@@ -1678,21 +1763,35 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n \treturn 0;\n }\n \n-static int launchctl_add_plists(const char *cmd)\n+static int launchctl_add_plists(void)\n {\n \tconst char *exec_path = git_exec_path();\n \n-\treturn launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY, cmd) ||\n-\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_DAILY, cmd) ||\n-\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY, cmd);\n+\treturn launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY) ||\n+\t       launchctl_schedule_plist(exec_path, SCHEDULE_DAILY) ||\n+\t       launchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY);\n }\n \n-static int launchctl_update_schedule(int run_maintenance, int fd, const char *cmd)\n+static int launchctl_update_schedule(int run_maintenance, int fd)\n {\n \tif (run_maintenance)\n-\t\treturn launchctl_add_plists(cmd);\n+\t\treturn launchctl_add_plists();\n \telse\n-\t\treturn launchctl_remove_plists(cmd);\n+\t\treturn launchctl_remove_plists();\n+}\n+\n+static int is_schtasks_available(void)\n+{\n+\tconst char *cmd = \"schtasks\";\n+\tint is_available;\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+#ifdef GIT_WINDOWS_NATIVE\n+\treturn 1;\n+#else\n+\treturn 0;\n+#endif\n }\n \n static char *schtasks_task_name(const char *frequency)\n@@ -1702,13 +1801,15 @@ static char *schtasks_task_name(const char *frequency)\n \treturn strbuf_detach(&label, NULL);\n }\n \n-static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd)\n+static int schtasks_remove_task(enum schedule_priority schedule)\n {\n+\tconst char *cmd = \"schtasks\";\n \tint result;\n \tstruct strvec args = STRVEC_INIT;\n \tconst char *frequency = get_frequency(schedule);\n \tchar *name = schtasks_task_name(frequency);\n \n+\tget_schedule_cmd(&cmd, NULL);\n \tstrvec_split(&args, cmd);\n \tstrvec_pushl(&args, \"/delete\", \"/tn\", name, \"/f\", NULL);\n \n@@ -1719,15 +1820,16 @@ static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd\n \treturn result;\n }\n \n-static int schtasks_remove_tasks(const char *cmd)\n+static int schtasks_remove_tasks(void)\n {\n-\treturn schtasks_remove_task(SCHEDULE_HOURLY, cmd) ||\n-\t\tschtasks_remove_task(SCHEDULE_DAILY, cmd) ||\n-\t\tschtasks_remove_task(SCHEDULE_WEEKLY, cmd);\n+\treturn schtasks_remove_task(SCHEDULE_HOURLY) ||\n+\t       schtasks_remove_task(SCHEDULE_DAILY) ||\n+\t       schtasks_remove_task(SCHEDULE_WEEKLY);\n }\n \n-static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule, const char *cmd)\n+static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule)\n {\n+\tconst char *cmd = \"schtasks\";\n \tint result;\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tconst char *xml;\n@@ -1736,6 +1838,8 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority\n \tchar *name = schtasks_task_name(frequency);\n \tstruct strbuf tfilename = STRBUF_INIT;\n \n+\tget_schedule_cmd(&cmd, NULL);\n+\n \tstrbuf_addf(&tfilename, \"%s/schedule_%s_XXXXXX\",\n \t\t    get_git_common_dir(), frequency);\n \ttfile = xmks_tempfile(tfilename.buf);\n@@ -1840,28 +1944,52 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority\n \treturn result;\n }\n \n-static int schtasks_schedule_tasks(const char *cmd)\n+static int schtasks_schedule_tasks(void)\n {\n \tconst char *exec_path = git_exec_path();\n \n-\treturn schtasks_schedule_task(exec_path, SCHEDULE_HOURLY, cmd) ||\n-\t\tschtasks_schedule_task(exec_path, SCHEDULE_DAILY, cmd) ||\n-\t\tschtasks_schedule_task(exec_path, SCHEDULE_WEEKLY, cmd);\n+\treturn schtasks_schedule_task(exec_path, SCHEDULE_HOURLY) ||\n+\t       schtasks_schedule_task(exec_path, SCHEDULE_DAILY) ||\n+\t       schtasks_schedule_task(exec_path, SCHEDULE_WEEKLY);\n }\n \n-static int schtasks_update_schedule(int run_maintenance, int fd, const char *cmd)\n+static int schtasks_update_schedule(int run_maintenance, int fd)\n {\n \tif (run_maintenance)\n-\t\treturn schtasks_schedule_tasks(cmd);\n+\t\treturn schtasks_schedule_tasks();\n \telse\n-\t\treturn schtasks_remove_tasks(cmd);\n+\t\treturn schtasks_remove_tasks();\n+}\n+\n+static int is_crontab_available(void)\n+{\n+\tconst char *cmd = \"crontab\";\n+\tint is_available;\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+\tstrvec_split(&child.args, cmd);\n+\tstrvec_push(&child.args, \"-l\");\n+\tchild.no_stdin = 1;\n+\tchild.no_stdout = 1;\n+\tchild.no_stderr = 1;\n+\tchild.silent_exec_failure = 1;\n+\n+\tif (start_command(&child))\n+\t\treturn 0;\n+\t/* Ignore exit code, as an empty crontab will return error. */\n+\tfinish_command(&child);\n+\treturn 1;\n }\n \n #define BEGIN_LINE \"# BEGIN GIT MAINTENANCE SCHEDULE\"\n #define END_LINE \"# END GIT MAINTENANCE SCHEDULE\"\n \n-static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n+static int crontab_update_schedule(int run_maintenance, int fd)\n {\n+\tconst char *cmd = \"crontab\";\n \tint result = 0;\n \tint in_old_region = 0;\n \tstruct child_process crontab_list = CHILD_PROCESS_INIT;\n@@ -1869,6 +1997,7 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n \tFILE *cron_list, *cron_in;\n \tstruct strbuf line = STRBUF_INIT;\n \n+\tget_schedule_cmd(&cmd, NULL);\n \tstrvec_split(&crontab_list.args, cmd);\n \tstrvec_push(&crontab_list.args, \"-l\");\n \tcrontab_list.in = -1;\n@@ -1945,66 +2074,160 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n \treturn result;\n }\n \n+enum scheduler {\n+\tSCHEDULER_INVALID = -1,\n+\tSCHEDULER_AUTO,\n+\tSCHEDULER_CRON,\n+\tSCHEDULER_LAUNCHCTL,\n+\tSCHEDULER_SCHTASKS,\n+};\n+\n+static const struct {\n+\tconst char *name;\n+\tint (*is_available)(void);\n+\tint (*update_schedule)(int run_maintenance, int fd);\n+} scheduler_fn[] = {\n+\t[SCHEDULER_CRON] = {\n+\t\t.name = \"crontab\",\n+\t\t.is_available = is_crontab_available,\n+\t\t.update_schedule = crontab_update_schedule,\n+\t},\n+\t[SCHEDULER_LAUNCHCTL] = {\n+\t\t.name = \"launchctl\",\n+\t\t.is_available = is_launchctl_available,\n+\t\t.update_schedule = launchctl_update_schedule,\n+\t},\n+\t[SCHEDULER_SCHTASKS] = {\n+\t\t.name = \"schtasks\",\n+\t\t.is_available = is_schtasks_available,\n+\t\t.update_schedule = schtasks_update_schedule,\n+\t},\n+};\n+\n+static enum scheduler parse_scheduler(const char *value)\n+{\n+\tif (!value)\n+\t\treturn SCHEDULER_INVALID;\n+\telse if (!strcasecmp(value, \"auto\"))\n+\t\treturn SCHEDULER_AUTO;\n+\telse if (!strcasecmp(value, \"cron\") || !strcasecmp(value, \"crontab\"))\n+\t\treturn SCHEDULER_CRON;\n+\telse if (!strcasecmp(value, \"launchctl\"))\n+\t\treturn SCHEDULER_LAUNCHCTL;\n+\telse if (!strcasecmp(value, \"schtasks\"))\n+\t\treturn SCHEDULER_SCHTASKS;\n+\telse\n+\t\treturn SCHEDULER_INVALID;\n+}\n+\n+static int maintenance_opt_scheduler(const struct option *opt, const char *arg,\n+\t\t\t\t     int unset)\n+{\n+\tenum scheduler *scheduler = opt->value;\n+\n+\tBUG_ON_OPT_NEG(unset);\n+\n+\t*scheduler = parse_scheduler(arg);\n+\tif (*scheduler == SCHEDULER_INVALID)\n+\t\treturn error(_(\"unrecognized --scheduler argument '%s'\"), arg);\n+\treturn 0;\n+}\n+\n+struct maintenance_start_opts {\n+\tenum scheduler scheduler;\n+};\n+\n+static enum scheduler resolve_scheduler(enum scheduler scheduler)\n+{\n+\tif (scheduler != SCHEDULER_AUTO)\n+\t\treturn scheduler;\n+\n #if defined(__APPLE__)\n-static const char platform_scheduler[] = \"launchctl\";\n+\treturn SCHEDULER_LAUNCHCTL;\n+\n #elif defined(GIT_WINDOWS_NATIVE)\n-static const char platform_scheduler[] = \"schtasks\";\n+\treturn SCHEDULER_SCHTASKS;\n+\n #else\n-static const char platform_scheduler[] = \"crontab\";\n+\treturn SCHEDULER_CRON;\n #endif\n+}\n \n-static int update_background_schedule(int enable)\n+static void validate_scheduler(enum scheduler scheduler)\n {\n-\tint result;\n-\tconst char *scheduler = platform_scheduler;\n-\tconst char *cmd = scheduler;\n-\tchar *testing;\n+\tif (scheduler == SCHEDULER_INVALID)\n+\t\tBUG(\"invalid scheduler\");\n+\tif (scheduler == SCHEDULER_AUTO)\n+\t\tBUG(\"resolve_scheduler should have been called before\");\n+\n+\tif (!scheduler_fn[scheduler].is_available())\n+\t\tdie(_(\"%s scheduler is not available\"),\n+\t\t    scheduler_fn[scheduler].name);\n+}\n+\n+static int update_background_schedule(const struct maintenance_start_opts *opts,\n+\t\t\t\t      int enable)\n+{\n+\tunsigned int i;\n+\tint result = 0;\n \tstruct lock_file lk;\n \tchar *lock_path = xstrfmt(\"%s/schedule\", the_repository->objects->odb->path);\n \n-\ttesting = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n-\tif (testing) {\n-\t\tchar *sep = strchr(testing, ':');\n-\t\tif (!sep)\n-\t\t\tdie(\"GIT_TEST_MAINT_SCHEDULER unparseable: %s\", testing);\n-\t\t*sep = '\\0';\n-\t\tscheduler = testing;\n-\t\tcmd = sep + 1;\n+\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {\n+\t\tfree(lock_path);\n+\t\treturn error(_(\"another process is scheduling background maintenance\"));\n \t}\n \n-\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {\n-\t\tresult = error(_(\"another process is scheduling background maintenance\"));\n-\t\tgoto cleanup;\n+\tfor (i = 1; i < ARRAY_SIZE(scheduler_fn); i++) {\n+\t\tif (enable && opts->scheduler == i)\n+\t\t\tcontinue;\n+\t\tif (!scheduler_fn[i].is_available())\n+\t\t\tcontinue;\n+\t\tscheduler_fn[i].update_schedule(0, get_lock_file_fd(&lk));\n \t}\n \n-\tif (!strcmp(scheduler, \"launchctl\"))\n-\t\tresult = launchctl_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse if (!strcmp(scheduler, \"schtasks\"))\n-\t\tresult = schtasks_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse if (!strcmp(scheduler, \"crontab\"))\n-\t\tresult = crontab_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse\n-\t\tdie(\"unknown background scheduler: %s\", scheduler);\n+\tif (enable)\n+\t\tresult = scheduler_fn[opts->scheduler].update_schedule(\n+\t\t\t1, get_lock_file_fd(&lk));\n \n \trollback_lock_file(&lk);\n \n-cleanup:\n \tfree(lock_path);\n-\tfree(testing);\n \treturn result;\n }\n \n-static int maintenance_start(void)\n+static const char *const builtin_maintenance_start_usage[] = {\n+\tN_(\"git maintenance start [--scheduler=<scheduler>]\"),\n+\tNULL\n+};\n+\n+static int maintenance_start(int argc, const char **argv, const char *prefix)\n {\n+\tstruct maintenance_start_opts opts = { 0 };\n+\tstruct option options[] = {\n+\t\tOPT_CALLBACK_F(\n+\t\t\t0, \"scheduler\", &opts.scheduler, N_(\"scheduler\"),\n+\t\t\tN_(\"scheduler to trigger git maintenance run\"),\n+\t\t\tPARSE_OPT_NONEG, maintenance_opt_scheduler),\n+\t\tOPT_END()\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     builtin_maintenance_start_usage, 0);\n+\tif (argc)\n+\t\tusage_with_options(builtin_maintenance_start_usage, options);\n+\n+\topts.scheduler = resolve_scheduler(opts.scheduler);\n+\tvalidate_scheduler(opts.scheduler);\n+\n \tif (maintenance_register())\n \t\twarning(_(\"failed to add repo to global config\"));\n-\n-\treturn update_background_schedule(1);\n+\treturn update_background_schedule(&opts, 1);\n }\n \n static int maintenance_stop(void)\n {\n-\treturn update_background_schedule(0);\n+\treturn update_background_schedule(NULL, 0);\n }\n \n static const char builtin_maintenance_usage[] =\tN_(\"git maintenance <subcommand> [<options>]\");\n@@ -2018,7 +2241,7 @@ int cmd_maintenance(int argc, const char **argv, const char *prefix)\n \tif (!strcmp(argv[1], \"run\"))\n \t\treturn maintenance_run(argc - 1, argv + 1, prefix);\n \tif (!strcmp(argv[1], \"start\"))\n-\t\treturn maintenance_start();\n+\t\treturn maintenance_start(argc - 1, argv + 1, prefix);\n \tif (!strcmp(argv[1], \"stop\"))\n \t\treturn maintenance_stop();\n \tif (!strcmp(argv[1], \"register\"))\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 58f46c77e6..27bce7992c 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -492,8 +492,21 @@ test_expect_success !MINGW 'register and unregister with regex metacharacters' '\n \t\tmaintenance.repo \"$(pwd)/$META\"\n '\n \n+test_expect_success 'start --scheduler=<scheduler>' '\n+\ttest_expect_code 129 git maintenance start --scheduler=foo 2>err &&\n+\ttest_i18ngrep \"unrecognized --scheduler argument\" err &&\n+\n+\ttest_expect_code 129 git maintenance start --no-scheduler 2>err &&\n+\ttest_i18ngrep \"unknown option\" err &&\n+\n+\ttest_expect_code 128 \\\n+\t\tenv GIT_TEST_MAINT_SCHEDULER=\"launchctl:true,schtasks:true\" \\\n+\t\tgit maintenance start --scheduler=crontab 2>err &&\n+\ttest_i18ngrep \"fatal: crontab scheduler is not available\" err\n+'\n+\n test_expect_success 'start from empty cron table' '\n-\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start --scheduler=crontab &&\n \n \t# start registers the repo\n \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n@@ -516,7 +529,7 @@ test_expect_success 'stop from existing schedule' '\n \n test_expect_success 'start preserves existing schedule' '\n \techo \"Important information!\" >cron.txt &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start --scheduler=crontab &&\n \tgrep \"Important information!\" cron.txt\n '\n \n@@ -545,7 +558,7 @@ test_expect_success 'start and stop macOS maintenance' '\n \tEOF\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start --scheduler=launchctl &&\n \n \t# start registers the repo\n \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n@@ -596,7 +609,7 @@ test_expect_success 'start and stop Windows maintenance' '\n \tEOF\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"schtasks:./print-args\" git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"schtasks:./print-args\" git maintenance start --scheduler=schtasks &&\n \n \t# start registers the repo\n \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n@@ -619,6 +632,40 @@ test_expect_success 'start and stop Windows maintenance' '\n \ttest_cmp expect args\n '\n \n+test_expect_success 'start and stop when several schedulers are available' '\n+\twrite_script print-args <<-\\EOF &&\n+\tprintf \"%s\\n\" \"$*\" | sed \"s:gui/[0-9][0-9]*:gui/[UID]:; s:\\(schtasks /create .* /xml\\).*:\\1:;\" >>args\n+\tEOF\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >expect &&\n+\tfor frequency in hourly daily weekly\n+\tdo\n+\t\tPLIST=\"$pfx/Library/LaunchAgents/org.git-scm.git.$frequency.plist\" &&\n+\t\techo \"launchctl bootout gui/[UID] $PLIST\" >>expect &&\n+\t\techo \"launchctl bootstrap gui/[UID] $PLIST\" >>expect || return 1\n+\tdone &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n+\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n+\t\thourly daily weekly >expect &&\n+\tprintf \"schtasks /create /tn Git Maintenance (%s) /f /xml\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n+\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n+\t\thourly daily weekly >expect &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\ttest_cmp expect args\n+'\n+\n test_expect_success 'register preserves existing strategy' '\n \tgit config maintenance.strategy none &&\n \tgit maintenance register &&\n-- \n2.33.0\n\n"},{"id":"433958","messageId":"20210827210255.12565-4-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210827210255.12565-1-lenaic@lhuard.fr","subject":"[PATCH v9 3/3] maintenance: add support for systemd timers on Linux","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-08-27T21:02:55Z","receivedAt":"2021-08-27T21:04:12Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"The existing mechanism for scheduling background maintenance is done\nthrough cron. On Linux systems managed by systemd, systemd provides an\nalternative to schedule recurring tasks: systemd timers.\n\nThe main motivations to implement systemd timers in addition to cron\nare:\n* cron is optional and Linux systems running systemd might not have it\n  installed.\n* The execution of `crontab -l` can tell us if cron is installed but not\n  if the daemon is actually running.\n* With systemd, each service is run in its own cgroup and its logs are\n  tagged by the service inside journald. With cron, all scheduled tasks\n  are running in the cron daemon cgroup and all the logs of the\n  user-scheduled tasks are pretended to belong to the system cron\n  service.\n  Concretely, a user that doesn’t have access to the system logs won’t\n  have access to the log of their own tasks scheduled by cron whereas\n  they will have access to the log of their own tasks scheduled by\n  systemd timer.\n  Although `cron` attempts to send email, that email may go unseen by\n  the user because these days, local mailboxes are not heavily used\n  anymore.\n\nIn order to schedule git maintenance, we need two unit template files:\n* ~/.config/systemd/user/git-maintenance@.service\n  to define the command to be started by systemd and\n* ~/.config/systemd/user/git-maintenance@.timer\n  to define the schedule at which the command should be run.\n\nThose units are templates that are parameterized by the frequency.\n\nBased on those templates, 3 timers are started:\n* git-maintenance@hourly.timer\n* git-maintenance@daily.timer\n* git-maintenance@weekly.timer\n\nThe command launched by those three timers are the same as with the\nother scheduling methods:\n\n/path/to/git for-each-repo --exec-path=/path/to\n--config=maintenance.repo maintenance run --schedule=%i\n\nwith the full path for git to ensure that the version of git launched\nfor the scheduled maintenance is the same as the one used to run\n`maintenance start`.\n\nThe timer unit contains `Persistent=true` so that, if the computer is\npowered down when a maintenance task should run, the task will be run\nwhen the computer is back powered on.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n Documentation/git-maintenance.txt |  58 +++++++-\n builtin/gc.c                      | 216 ++++++++++++++++++++++++++++++\n t/t7900-maintenance.sh            |  67 ++++++++-\n 3 files changed, 330 insertions(+), 11 deletions(-)\n\ndiff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\nindex 576290b5c6..e2cfb68ab5 100644\n--- a/Documentation/git-maintenance.txt\n+++ b/Documentation/git-maintenance.txt\n@@ -179,14 +179,16 @@ OPTIONS\n \t`maintenance.<task>.enabled` configured as `true` are considered.\n \tSee the 'TASKS' section for the list of accepted `<task>` values.\n \n---scheduler=auto|crontab|launchctl|schtasks::\n+--scheduler=auto|crontab|systemd-timer|launchctl|schtasks::\n \tWhen combined with the `start` subcommand, specify the scheduler\n \tfor running the hourly, daily and weekly executions of\n \t`git maintenance run`.\n-\tPossible values for `<scheduler>` are `auto`, `crontab` (POSIX),\n-\t`launchctl` (macOS), and `schtasks` (Windows).\n-\tWhen `auto` is specified, the appropriate platform-specific\n-\tscheduler is used. Default is `auto`.\n+\tPossible values for `<scheduler>` are `auto`, `crontab`\n+\t(POSIX), `systemd-timer` (Linux), `launchctl` (macOS), and\n+\t`schtasks` (Windows). When `auto` is specified, the\n+\tappropriate platform-specific scheduler is used; on Linux,\n+\t`systemd-timer` is used if available, otherwise\n+\t`crontab`. Default is `auto`.\n \n \n TROUBLESHOOTING\n@@ -286,6 +288,52 @@ schedule to ensure you are executing the correct binaries in your\n schedule.\n \n \n+BACKGROUND MAINTENANCE ON LINUX SYSTEMD SYSTEMS\n+-----------------------------------------------\n+\n+While Linux supports `cron`, depending on the distribution, `cron` may\n+be an optional package not necessarily installed. On modern Linux\n+distributions, systemd timers are superseding it.\n+\n+If user systemd timers are available, they will be used as a replacement\n+of `cron`.\n+\n+In this case, `git maintenance start` will create user systemd timer units\n+and start the timers. The current list of user-scheduled tasks can be found\n+by running `systemctl --user list-timers`. The timers written by `git\n+maintenance start` are similar to this:\n+\n+-----------------------------------------------------------------------\n+$ systemctl --user list-timers\n+NEXT                         LEFT          LAST                         PASSED     UNIT                         ACTIVATES\n+Thu 2021-04-29 19:00:00 CEST 42min left    Thu 2021-04-29 18:00:11 CEST 17min ago  git-maintenance@hourly.timer git-maintenance@hourly.service\n+Fri 2021-04-30 00:00:00 CEST 5h 42min left Thu 2021-04-29 00:00:11 CEST 18h ago    git-maintenance@daily.timer  git-maintenance@daily.service\n+Mon 2021-05-03 00:00:00 CEST 3 days left   Mon 2021-04-26 00:00:11 CEST 3 days ago git-maintenance@weekly.timer git-maintenance@weekly.service\n+-----------------------------------------------------------------------\n+\n+One timer is registered for each `--schedule=<frequency>` option.\n+\n+The definition of the systemd units can be inspected in the following files:\n+\n+-----------------------------------------------------------------------\n+~/.config/systemd/user/git-maintenance@.timer\n+~/.config/systemd/user/git-maintenance@.service\n+~/.config/systemd/user/timers.target.wants/git-maintenance@hourly.timer\n+~/.config/systemd/user/timers.target.wants/git-maintenance@daily.timer\n+~/.config/systemd/user/timers.target.wants/git-maintenance@weekly.timer\n+-----------------------------------------------------------------------\n+\n+`git maintenance start` will overwrite these files and start the timer\n+again with `systemctl --user`, so any customization should be done by\n+creating a drop-in file, i.e. a `.conf` suffixed file in the\n+`~/.config/systemd/user/git-maintenance@.service.d` directory.\n+\n+`git maintenance stop` will stop the user systemd timers and delete\n+the above mentioned files.\n+\n+For more details, see `systemd.timer(5)`.\n+\n+\n BACKGROUND MAINTENANCE ON MACOS SYSTEMS\n ---------------------------------------\n \ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 9e464d4a10..d97bfc44ae 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -2074,10 +2074,210 @@ static int crontab_update_schedule(int run_maintenance, int fd)\n \treturn result;\n }\n \n+static int real_is_systemd_timer_available(void)\n+{\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\n+\tstrvec_pushl(&child.args, \"systemctl\", \"--user\", \"list-timers\", NULL);\n+\tchild.no_stdin = 1;\n+\tchild.no_stdout = 1;\n+\tchild.no_stderr = 1;\n+\tchild.silent_exec_failure = 1;\n+\n+\tif (start_command(&child))\n+\t\treturn 0;\n+\tif (finish_command(&child))\n+\t\treturn 0;\n+\treturn 1;\n+}\n+\n+static int is_systemd_timer_available(void)\n+{\n+\tconst char *cmd = \"systemctl\";\n+\tint is_available;\n+\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+\treturn real_is_systemd_timer_available();\n+}\n+\n+static char *xdg_config_home_systemd(const char *filename)\n+{\n+\treturn xdg_config_home_for(\"systemd/user\", filename);\n+}\n+\n+static int systemd_timer_enable_unit(int enable,\n+\t\t\t\t     enum schedule_priority schedule)\n+{\n+\tconst char *cmd = \"systemctl\";\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\tconst char *frequency = get_frequency(schedule);\n+\n+\t/*\n+\t * Disabling the systemd unit while it is already disabled makes\n+\t * systemctl print an error.\n+\t * Let's ignore it since it means we already are in the expected state:\n+\t * the unit is disabled.\n+\t *\n+\t * On the other hand, enabling a systemd unit which is already enabled\n+\t * produces no error.\n+\t */\n+\tif (!enable)\n+\t\tchild.no_stderr = 1;\n+\n+\tget_schedule_cmd(&cmd, NULL);\n+\tstrvec_split(&child.args, cmd);\n+\tstrvec_pushl(&child.args, \"--user\", enable ? \"enable\" : \"disable\",\n+\t\t     \"--now\", NULL);\n+\tstrvec_pushf(&child.args, \"git-maintenance@%s.timer\", frequency);\n+\n+\tif (start_command(&child))\n+\t\treturn error(_(\"failed to start systemctl\"));\n+\tif (finish_command(&child))\n+\t\t/*\n+\t\t * Disabling an already disabled systemd unit makes\n+\t\t * systemctl fail.\n+\t\t * Let's ignore this failure.\n+\t\t *\n+\t\t * Enabling an enabled systemd unit doesn't fail.\n+\t\t */\n+\t\tif (enable)\n+\t\t\treturn error(_(\"failed to run systemctl\"));\n+\treturn 0;\n+}\n+\n+static int systemd_timer_delete_unit_templates(void)\n+{\n+\tint ret = 0;\n+\tchar *filename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n+\tif (unlink(filename) && !is_missing_file_error(errno))\n+\t\tret = error_errno(_(\"failed to delete '%s'\"), filename);\n+\tFREE_AND_NULL(filename);\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n+\tif (unlink(filename) && !is_missing_file_error(errno))\n+\t\tret = error_errno(_(\"failed to delete '%s'\"), filename);\n+\n+\tfree(filename);\n+\treturn ret;\n+}\n+\n+static int systemd_timer_delete_units(void)\n+{\n+\treturn systemd_timer_enable_unit(0, SCHEDULE_HOURLY) ||\n+\t       systemd_timer_enable_unit(0, SCHEDULE_DAILY) ||\n+\t       systemd_timer_enable_unit(0, SCHEDULE_WEEKLY) ||\n+\t       systemd_timer_delete_unit_templates();\n+}\n+\n+static int systemd_timer_write_unit_templates(const char *exec_path)\n+{\n+\tchar *filename;\n+\tFILE *file;\n+\tconst char *unit;\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n+\tif (safe_create_leading_directories(filename)) {\n+\t\terror(_(\"failed to create directories for '%s'\"), filename);\n+\t\tgoto error;\n+\t}\n+\tfile = fopen_or_warn(filename, \"w\");\n+\tif (file == NULL)\n+\t\tgoto error;\n+\n+\tunit = \"# This file was created and is maintained by Git.\\n\"\n+\t       \"# Any edits made in this file might be replaced in the future\\n\"\n+\t       \"# by a Git command.\\n\"\n+\t       \"\\n\"\n+\t       \"[Unit]\\n\"\n+\t       \"Description=Optimize Git repositories data\\n\"\n+\t       \"\\n\"\n+\t       \"[Timer]\\n\"\n+\t       \"OnCalendar=%i\\n\"\n+\t       \"Persistent=true\\n\"\n+\t       \"\\n\"\n+\t       \"[Install]\\n\"\n+\t       \"WantedBy=timers.target\\n\";\n+\tif (fputs(unit, file) == EOF) {\n+\t\terror(_(\"failed to write to '%s'\"), filename);\n+\t\tfclose(file);\n+\t\tgoto error;\n+\t}\n+\tif (fclose(file) == EOF) {\n+\t\terror_errno(_(\"failed to flush '%s'\"), filename);\n+\t\tgoto error;\n+\t}\n+\tfree(filename);\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n+\tfile = fopen_or_warn(filename, \"w\");\n+\tif (file == NULL)\n+\t\tgoto error;\n+\n+\tunit = \"# This file was created and is maintained by Git.\\n\"\n+\t       \"# Any edits made in this file might be replaced in the future\\n\"\n+\t       \"# by a Git command.\\n\"\n+\t       \"\\n\"\n+\t       \"[Unit]\\n\"\n+\t       \"Description=Optimize Git repositories data\\n\"\n+\t       \"\\n\"\n+\t       \"[Service]\\n\"\n+\t       \"Type=oneshot\\n\"\n+\t       \"ExecStart=\\\"%s/git\\\" --exec-path=\\\"%s\\\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\\n\"\n+\t       \"LockPersonality=yes\\n\"\n+\t       \"MemoryDenyWriteExecute=yes\\n\"\n+\t       \"NoNewPrivileges=yes\\n\"\n+\t       \"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\\n\"\n+\t       \"RestrictNamespaces=yes\\n\"\n+\t       \"RestrictRealtime=yes\\n\"\n+\t       \"RestrictSUIDSGID=yes\\n\"\n+\t       \"SystemCallArchitectures=native\\n\"\n+\t       \"SystemCallFilter=@system-service\\n\";\n+\tif (fprintf(file, unit, exec_path, exec_path) < 0) {\n+\t\terror(_(\"failed to write to '%s'\"), filename);\n+\t\tfclose(file);\n+\t\tgoto error;\n+\t}\n+\tif (fclose(file) == EOF) {\n+\t\terror_errno(_(\"failed to flush '%s'\"), filename);\n+\t\tgoto error;\n+\t}\n+\tfree(filename);\n+\treturn 0;\n+\n+error:\n+\tfree(filename);\n+\tsystemd_timer_delete_unit_templates();\n+\treturn -1;\n+}\n+\n+static int systemd_timer_setup_units(void)\n+{\n+\tconst char *exec_path = git_exec_path();\n+\n+\tint ret = systemd_timer_write_unit_templates(exec_path) ||\n+\t\t  systemd_timer_enable_unit(1, SCHEDULE_HOURLY) ||\n+\t\t  systemd_timer_enable_unit(1, SCHEDULE_DAILY) ||\n+\t\t  systemd_timer_enable_unit(1, SCHEDULE_WEEKLY);\n+\tif (ret)\n+\t\tsystemd_timer_delete_units();\n+\treturn ret;\n+}\n+\n+static int systemd_timer_update_schedule(int run_maintenance, int fd)\n+{\n+\tif (run_maintenance)\n+\t\treturn systemd_timer_setup_units();\n+\telse\n+\t\treturn systemd_timer_delete_units();\n+}\n+\n enum scheduler {\n \tSCHEDULER_INVALID = -1,\n \tSCHEDULER_AUTO,\n \tSCHEDULER_CRON,\n+\tSCHEDULER_SYSTEMD,\n \tSCHEDULER_LAUNCHCTL,\n \tSCHEDULER_SCHTASKS,\n };\n@@ -2092,6 +2292,11 @@ static const struct {\n \t\t.is_available = is_crontab_available,\n \t\t.update_schedule = crontab_update_schedule,\n \t},\n+\t[SCHEDULER_SYSTEMD] = {\n+\t\t.name = \"systemctl\",\n+\t\t.is_available = is_systemd_timer_available,\n+\t\t.update_schedule = systemd_timer_update_schedule,\n+\t},\n \t[SCHEDULER_LAUNCHCTL] = {\n \t\t.name = \"launchctl\",\n \t\t.is_available = is_launchctl_available,\n@@ -2112,6 +2317,9 @@ static enum scheduler parse_scheduler(const char *value)\n \t\treturn SCHEDULER_AUTO;\n \telse if (!strcasecmp(value, \"cron\") || !strcasecmp(value, \"crontab\"))\n \t\treturn SCHEDULER_CRON;\n+\telse if (!strcasecmp(value, \"systemd\") ||\n+\t\t !strcasecmp(value, \"systemd-timer\"))\n+\t\treturn SCHEDULER_SYSTEMD;\n \telse if (!strcasecmp(value, \"launchctl\"))\n \t\treturn SCHEDULER_LAUNCHCTL;\n \telse if (!strcasecmp(value, \"schtasks\"))\n@@ -2148,6 +2356,14 @@ static enum scheduler resolve_scheduler(enum scheduler scheduler)\n #elif defined(GIT_WINDOWS_NATIVE)\n \treturn SCHEDULER_SCHTASKS;\n \n+#elif defined(__linux__)\n+\tif (is_systemd_timer_available())\n+\t\treturn SCHEDULER_SYSTEMD;\n+\telse if (is_crontab_available())\n+\t\treturn SCHEDULER_CRON;\n+\telse\n+\t\tdie(_(\"neither systemd timers nor crontab are available\"));\n+\n #else\n \treturn SCHEDULER_CRON;\n #endif\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 27bce7992c..265f7793f5 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -20,6 +20,18 @@ test_xmllint () {\n \tfi\n }\n \n+test_lazy_prereq SYSTEMD_ANALYZE '\n+\tsystemd-analyze --help >out &&\n+\tgrep verify out\n+'\n+\n+test_systemd_analyze_verify () {\n+\tif test_have_prereq SYSTEMD_ANALYZE\n+\tthen\n+\t\tsystemd-analyze verify \"$@\"\n+\tfi\n+}\n+\n test_expect_success 'help text' '\n \ttest_expect_code 129 git maintenance -h 2>err &&\n \ttest_i18ngrep \"usage: git maintenance <subcommand>\" err &&\n@@ -632,15 +644,56 @@ test_expect_success 'start and stop Windows maintenance' '\n \ttest_cmp expect args\n '\n \n+test_expect_success 'start and stop Linux/systemd maintenance' '\n+\twrite_script print-args <<-\\EOF &&\n+\tprintf \"%s\\n\" \"$*\" >>args\n+\tEOF\n+\n+\tXDG_CONFIG_HOME=\"$PWD\" &&\n+\texport XDG_CONFIG_HOME &&\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args\" git maintenance start --scheduler=systemd-timer &&\n+\n+\t# start registers the repo\n+\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n+\n+\ttest_systemd_analyze_verify \"systemd/user/git-maintenance@.service\" &&\n+\n+\tprintf -- \"--user enable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args\" git maintenance stop &&\n+\n+\t# stop does not unregister the repo\n+\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n+\n+\ttest_path_is_missing \"systemd/user/git-maintenance@.timer\" &&\n+\ttest_path_is_missing \"systemd/user/git-maintenance@.service\" &&\n+\n+\tprintf -- \"--user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n+\ttest_cmp expect args\n+'\n+\n test_expect_success 'start and stop when several schedulers are available' '\n \twrite_script print-args <<-\\EOF &&\n \tprintf \"%s\\n\" \"$*\" | sed \"s:gui/[0-9][0-9]*:gui/[UID]:; s:\\(schtasks /create .* /xml\\).*:\\1:;\" >>args\n \tEOF\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n-\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=systemd-timer &&\n+\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n \t\thourly daily weekly >expect &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\tprintf -- \"systemctl --user enable --now git-maintenance@%s.timer\\n\" hourly daily weekly >>expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n+\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >>expect &&\n \tfor frequency in hourly daily weekly\n \tdo\n \t\tPLIST=\"$pfx/Library/LaunchAgents/org.git-scm.git.$frequency.plist\" &&\n@@ -650,17 +703,19 @@ test_expect_success 'start and stop when several schedulers are available' '\n \ttest_cmp expect args &&\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n+\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n \tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n-\t\thourly daily weekly >expect &&\n+\t\thourly daily weekly >>expect &&\n \tprintf \"schtasks /create /tn Git Maintenance (%s) /f /xml\\n\" \\\n \t\thourly daily weekly >>expect &&\n \ttest_cmp expect args &&\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n+\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n \tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n-\t\thourly daily weekly >expect &&\n+\t\thourly daily weekly >>expect &&\n \tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n \t\thourly daily weekly >>expect &&\n \ttest_cmp expect args\n-- \n2.33.0\n\n"},{"id":"433987","messageId":"51246c10-fe0b-b8e5-cdc3-54bdc6c8054e@ramsayjones.plus.com","threadId":"55603","inReplyTo":"20210827210255.12565-3-lenaic@lhuard.fr","subject":"Re: [PATCH v9 2/3] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Ramsay Jones","fromEmail":"ramsay@ramsayjones.plus.com","sentAt":"2021-08-27T23:54:48Z","receivedAt":"2021-08-28T00:02:41Z","isPatch":true,"sender":{"key":"ramsay@ramsayjones.plus.com","avatar":"https://avatars.githubusercontent.com/u/33702710?v=4"},"body":"Hi Lénaïc,\n\nI noticed a couple of typos while tracking down a test failure\non the previous version of this series:\n\nOn 27/08/2021 22:02, Lénaïc Huard wrote:\n> Depending on the system, different schedulers can be used to schedule\n> the hourly, daily and weekly executions of `git maintenance run`:\n> * `launchctl` for MacOS,\n> * `schtasks` for Windows and\n> * `crontab` for everything else.\n> \n> `git maintenance run` now has an option to let the end-user explicitly\n> choose which scheduler he wants to use:\n> `--scheduler=auto|crontab|launchctl|schtasks`.\n> \n> When `git maintenance start --scheduler=XXX` is run, it not only\n> registers `git maintenance run` tasks in the scheduler XXX, it also\n> removes the `git maintenance run` tasks from all the other schedulers to\n> ensure we cannot have two schedulers launching concurrent identical\n> tasks.\n> \n> The default value is `auto` which chooses a suitable scheduler for the\n> system.\n> \n> `git maintenance stop` doesn't have any `--scheduler` parameter because\n> this command will try to remove the `git maintenance run` tasks from all\n> the available schedulers.\n> \n> Signed-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n> ---\n>  Documentation/git-maintenance.txt |   9 +\n>  builtin/gc.c                      | 365 ++++++++++++++++++++++++------\n>  t/t7900-maintenance.sh            |  55 ++++-\n>  3 files changed, 354 insertions(+), 75 deletions(-)\n> \n> diff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\n> index 1e738ad398..576290b5c6 100644\n> --- a/Documentation/git-maintenance.txt\n> +++ b/Documentation/git-maintenance.txt\n> @@ -179,6 +179,15 @@ OPTIONS\n>  \t`maintenance.<task>.enabled` configured as `true` are considered.\n>  \tSee the 'TASKS' section for the list of accepted `<task>` values.\n>  \n> +--scheduler=auto|crontab|launchctl|schtasks::\n> +\tWhen combined with the `start` subcommand, specify the scheduler\n> +\tfor running the hourly, daily and weekly executions of\n> +\t`git maintenance run`.\n> +\tPossible values for `<scheduler>` are `auto`, `crontab` (POSIX),\n> +\t`launchctl` (macOS), and `schtasks` (Windows).\n> +\tWhen `auto` is specified, the appropriate platform-specific\n> +\tscheduler is used. Default is `auto`.\n> +\n>  \n>  TROUBLESHOOTING\n>  ---------------\n> diff --git a/builtin/gc.c b/builtin/gc.c\n> index f05d2f0a1a..9e464d4a10 100644\n> --- a/builtin/gc.c\n> +++ b/builtin/gc.c\n> @@ -1529,6 +1529,93 @@ static const char *get_frequency(enum schedule_priority schedule)\n>  \t}\n>  }\n>  \n> +/*\n> + * get_schedule_cmd` reads the GIT_TEST_MAINT_SCHEDULER environment variable\n> + * to mock the schedulers that `git maintenance start` rely on.\n> + *\n> + * For test purpose, GIT_TEST_MAINT_SCHEDULER can be set to a comma-separated\n> + * list of colon-separated key/value pairs where each pair contains a scheduler\n> + * and its corresponding mock.\n> + *\n> + * * If $GET_TEST_MAINT_SCHEDULER is not set, return false and leave the\n\ns/GET/GIT/\n\n> + *   arguments unmodified.\n> + *\n> + * * If $GET_TEST_MAINT_SCHEDULER is set, return true.\n\ns/GET/GIT/\n\nATB,\nRamsay Jones\n\n> + *   In this case, the *cmd value is read as input.\n> + *\n> + *   * if the input value *cmd is the key of one of the comma-separated list\n> + *     item, then *is_available is set to true and *cmd is modified and becomes\n> + *     the mock command.\n> + *\n> + *   * if the input value *cmd isn’t the key of any of the comma-separated list\n> + *     item, then *is_available is set to false.\n> + *\n> + * Ex.:\n> + *   GIT_TEST_MAINT_SCHEDULER not set\n> + *     +-------+-------------------------------------------------+\n> + *     | Input |                     Output                      |\n> + *     | *cmd  | return code |       *cmd        | *is_available |\n> + *     +-------+-------------+-------------------+---------------+\n> + *     | \"foo\" |    false    | \"foo\" (unchanged) |  (unchanged)  |\n> + *     +-------+-------------+-------------------+---------------+\n> + *\n> + *   GIT_TEST_MAINT_SCHEDULER set to “foo:./mock_foo.sh,bar:./mock_bar.sh”\n> + *     +-------+-------------------------------------------------+\n> + *     | Input |                     Output                      |\n> + *     | *cmd  | return code |       *cmd        | *is_available |\n> + *     +-------+-------------+-------------------+---------------+\n> + *     | \"foo\" |    true     |  \"./mock.foo.sh\"  |     true      |\n> + *     | \"qux\" |    true     | \"qux\" (unchanged) |     false     |\n> + *     +-------+-------------+-------------------+---------------+\n> + */\n> +static int get_schedule_cmd(const char **cmd, int *is_available)\n> +{\n> +\tchar *testing = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n> +\tstruct string_list_item *item;\n> +\tstruct string_list list = STRING_LIST_INIT_NODUP;\n> +\n> +\tif (!testing)\n> +\t\treturn 0;\n> +\n> +\tif (is_available)\n> +\t\t*is_available = 0;\n> +\n> +\tstring_list_split_in_place(&list, testing, ',', -1);\n> +\tfor_each_string_list_item(item, &list) {\n> +\t\tstruct string_list pair = STRING_LIST_INIT_NODUP;\n> +\n> +\t\tif (string_list_split_in_place(&pair, item->string, ':', 2) != 2)\n> +\t\t\tcontinue;\n> +\n> +\t\tif (!strcmp(*cmd, pair.items[0].string)) {\n> +\t\t\t*cmd = pair.items[1].string;\n> +\t\t\tif (is_available)\n> +\t\t\t\t*is_available = 1;\n> +\t\t\tstring_list_clear(&list, 0);\n> +\t\t\tUNLEAK(testing);\n> +\t\t\treturn 1;\n> +\t\t}\n> +\t}\n> +\n> +\tstring_list_clear(&list, 0);\n> +\tfree(testing);\n> +\treturn 1;\n> +}\n> +\n> +static int is_launchctl_available(void)\n> +{\n> +\tconst char *cmd = \"launchctl\";\n> +\tint is_available;\n> +\tif (get_schedule_cmd(&cmd, &is_available))\n> +\t\treturn is_available;\n> +\n> +#ifdef __APPLE__\n> +\treturn 1;\n> +#else\n> +\treturn 0;\n> +#endif\n> +}\n> +\n>  static char *launchctl_service_name(const char *frequency)\n>  {\n>  \tstruct strbuf label = STRBUF_INIT;\n> @@ -1555,19 +1642,17 @@ static char *launchctl_get_uid(void)\n>  \treturn xstrfmt(\"gui/%d\", getuid());\n>  }\n>  \n> -static int launchctl_boot_plist(int enable, const char *filename, const char *cmd)\n> +static int launchctl_boot_plist(int enable, const char *filename)\n>  {\n> +\tconst char *cmd = \"launchctl\";\n>  \tint result;\n>  \tstruct child_process child = CHILD_PROCESS_INIT;\n>  \tchar *uid = launchctl_get_uid();\n>  \n> +\tget_schedule_cmd(&cmd, NULL);\n>  \tstrvec_split(&child.args, cmd);\n> -\tif (enable)\n> -\t\tstrvec_push(&child.args, \"bootstrap\");\n> -\telse\n> -\t\tstrvec_push(&child.args, \"bootout\");\n> -\tstrvec_push(&child.args, uid);\n> -\tstrvec_push(&child.args, filename);\n> +\tstrvec_pushl(&child.args, enable ? \"bootstrap\" : \"bootout\", uid,\n> +\t\t     filename, NULL);\n>  \n>  \tchild.no_stderr = 1;\n>  \tchild.no_stdout = 1;\n> @@ -1581,26 +1666,26 @@ static int launchctl_boot_plist(int enable, const char *filename, const char *cm\n>  \treturn result;\n>  }\n>  \n> -static int launchctl_remove_plist(enum schedule_priority schedule, const char *cmd)\n> +static int launchctl_remove_plist(enum schedule_priority schedule)\n>  {\n>  \tconst char *frequency = get_frequency(schedule);\n>  \tchar *name = launchctl_service_name(frequency);\n>  \tchar *filename = launchctl_service_filename(name);\n> -\tint result = launchctl_boot_plist(0, filename, cmd);\n> +\tint result = launchctl_boot_plist(0, filename);\n>  \tunlink(filename);\n>  \tfree(filename);\n>  \tfree(name);\n>  \treturn result;\n>  }\n>  \n> -static int launchctl_remove_plists(const char *cmd)\n> +static int launchctl_remove_plists(void)\n>  {\n> -\treturn launchctl_remove_plist(SCHEDULE_HOURLY, cmd) ||\n> -\t\tlaunchctl_remove_plist(SCHEDULE_DAILY, cmd) ||\n> -\t\tlaunchctl_remove_plist(SCHEDULE_WEEKLY, cmd);\n> +\treturn launchctl_remove_plist(SCHEDULE_HOURLY) ||\n> +\t       launchctl_remove_plist(SCHEDULE_DAILY) ||\n> +\t       launchctl_remove_plist(SCHEDULE_WEEKLY);\n>  }\n>  \n> -static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule, const char *cmd)\n> +static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule)\n>  {\n>  \tFILE *plist;\n>  \tint i;\n> @@ -1669,8 +1754,8 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n>  \tfclose(plist);\n>  \n>  \t/* bootout might fail if not already running, so ignore */\n> -\tlaunchctl_boot_plist(0, filename, cmd);\n> -\tif (launchctl_boot_plist(1, filename, cmd))\n> +\tlaunchctl_boot_plist(0, filename);\n> +\tif (launchctl_boot_plist(1, filename))\n>  \t\tdie(_(\"failed to bootstrap service %s\"), filename);\n>  \n>  \tfree(filename);\n> @@ -1678,21 +1763,35 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n>  \treturn 0;\n>  }\n>  \n> -static int launchctl_add_plists(const char *cmd)\n> +static int launchctl_add_plists(void)\n>  {\n>  \tconst char *exec_path = git_exec_path();\n>  \n> -\treturn launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY, cmd) ||\n> -\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_DAILY, cmd) ||\n> -\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY, cmd);\n> +\treturn launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY) ||\n> +\t       launchctl_schedule_plist(exec_path, SCHEDULE_DAILY) ||\n> +\t       launchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY);\n>  }\n>  \n> -static int launchctl_update_schedule(int run_maintenance, int fd, const char *cmd)\n> +static int launchctl_update_schedule(int run_maintenance, int fd)\n>  {\n>  \tif (run_maintenance)\n> -\t\treturn launchctl_add_plists(cmd);\n> +\t\treturn launchctl_add_plists();\n>  \telse\n> -\t\treturn launchctl_remove_plists(cmd);\n> +\t\treturn launchctl_remove_plists();\n> +}\n> +\n> +static int is_schtasks_available(void)\n> +{\n> +\tconst char *cmd = \"schtasks\";\n> +\tint is_available;\n> +\tif (get_schedule_cmd(&cmd, &is_available))\n> +\t\treturn is_available;\n> +\n> +#ifdef GIT_WINDOWS_NATIVE\n> +\treturn 1;\n> +#else\n> +\treturn 0;\n> +#endif\n>  }\n>  \n>  static char *schtasks_task_name(const char *frequency)\n> @@ -1702,13 +1801,15 @@ static char *schtasks_task_name(const char *frequency)\n>  \treturn strbuf_detach(&label, NULL);\n>  }\n>  \n> -static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd)\n> +static int schtasks_remove_task(enum schedule_priority schedule)\n>  {\n> +\tconst char *cmd = \"schtasks\";\n>  \tint result;\n>  \tstruct strvec args = STRVEC_INIT;\n>  \tconst char *frequency = get_frequency(schedule);\n>  \tchar *name = schtasks_task_name(frequency);\n>  \n> +\tget_schedule_cmd(&cmd, NULL);\n>  \tstrvec_split(&args, cmd);\n>  \tstrvec_pushl(&args, \"/delete\", \"/tn\", name, \"/f\", NULL);\n>  \n> @@ -1719,15 +1820,16 @@ static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd\n>  \treturn result;\n>  }\n>  \n> -static int schtasks_remove_tasks(const char *cmd)\n> +static int schtasks_remove_tasks(void)\n>  {\n> -\treturn schtasks_remove_task(SCHEDULE_HOURLY, cmd) ||\n> -\t\tschtasks_remove_task(SCHEDULE_DAILY, cmd) ||\n> -\t\tschtasks_remove_task(SCHEDULE_WEEKLY, cmd);\n> +\treturn schtasks_remove_task(SCHEDULE_HOURLY) ||\n> +\t       schtasks_remove_task(SCHEDULE_DAILY) ||\n> +\t       schtasks_remove_task(SCHEDULE_WEEKLY);\n>  }\n>  \n> -static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule, const char *cmd)\n> +static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule)\n>  {\n> +\tconst char *cmd = \"schtasks\";\n>  \tint result;\n>  \tstruct child_process child = CHILD_PROCESS_INIT;\n>  \tconst char *xml;\n> @@ -1736,6 +1838,8 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority\n>  \tchar *name = schtasks_task_name(frequency);\n>  \tstruct strbuf tfilename = STRBUF_INIT;\n>  \n> +\tget_schedule_cmd(&cmd, NULL);\n> +\n>  \tstrbuf_addf(&tfilename, \"%s/schedule_%s_XXXXXX\",\n>  \t\t    get_git_common_dir(), frequency);\n>  \ttfile = xmks_tempfile(tfilename.buf);\n> @@ -1840,28 +1944,52 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority\n>  \treturn result;\n>  }\n>  \n> -static int schtasks_schedule_tasks(const char *cmd)\n> +static int schtasks_schedule_tasks(void)\n>  {\n>  \tconst char *exec_path = git_exec_path();\n>  \n> -\treturn schtasks_schedule_task(exec_path, SCHEDULE_HOURLY, cmd) ||\n> -\t\tschtasks_schedule_task(exec_path, SCHEDULE_DAILY, cmd) ||\n> -\t\tschtasks_schedule_task(exec_path, SCHEDULE_WEEKLY, cmd);\n> +\treturn schtasks_schedule_task(exec_path, SCHEDULE_HOURLY) ||\n> +\t       schtasks_schedule_task(exec_path, SCHEDULE_DAILY) ||\n> +\t       schtasks_schedule_task(exec_path, SCHEDULE_WEEKLY);\n>  }\n>  \n> -static int schtasks_update_schedule(int run_maintenance, int fd, const char *cmd)\n> +static int schtasks_update_schedule(int run_maintenance, int fd)\n>  {\n>  \tif (run_maintenance)\n> -\t\treturn schtasks_schedule_tasks(cmd);\n> +\t\treturn schtasks_schedule_tasks();\n>  \telse\n> -\t\treturn schtasks_remove_tasks(cmd);\n> +\t\treturn schtasks_remove_tasks();\n> +}\n> +\n> +static int is_crontab_available(void)\n> +{\n> +\tconst char *cmd = \"crontab\";\n> +\tint is_available;\n> +\tstruct child_process child = CHILD_PROCESS_INIT;\n> +\n> +\tif (get_schedule_cmd(&cmd, &is_available))\n> +\t\treturn is_available;\n> +\n> +\tstrvec_split(&child.args, cmd);\n> +\tstrvec_push(&child.args, \"-l\");\n> +\tchild.no_stdin = 1;\n> +\tchild.no_stdout = 1;\n> +\tchild.no_stderr = 1;\n> +\tchild.silent_exec_failure = 1;\n> +\n> +\tif (start_command(&child))\n> +\t\treturn 0;\n> +\t/* Ignore exit code, as an empty crontab will return error. */\n> +\tfinish_command(&child);\n> +\treturn 1;\n>  }\n>  \n>  #define BEGIN_LINE \"# BEGIN GIT MAINTENANCE SCHEDULE\"\n>  #define END_LINE \"# END GIT MAINTENANCE SCHEDULE\"\n>  \n> -static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n> +static int crontab_update_schedule(int run_maintenance, int fd)\n>  {\n> +\tconst char *cmd = \"crontab\";\n>  \tint result = 0;\n>  \tint in_old_region = 0;\n>  \tstruct child_process crontab_list = CHILD_PROCESS_INIT;\n> @@ -1869,6 +1997,7 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n>  \tFILE *cron_list, *cron_in;\n>  \tstruct strbuf line = STRBUF_INIT;\n>  \n> +\tget_schedule_cmd(&cmd, NULL);\n>  \tstrvec_split(&crontab_list.args, cmd);\n>  \tstrvec_push(&crontab_list.args, \"-l\");\n>  \tcrontab_list.in = -1;\n> @@ -1945,66 +2074,160 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n>  \treturn result;\n>  }\n>  \n> +enum scheduler {\n> +\tSCHEDULER_INVALID = -1,\n> +\tSCHEDULER_AUTO,\n> +\tSCHEDULER_CRON,\n> +\tSCHEDULER_LAUNCHCTL,\n> +\tSCHEDULER_SCHTASKS,\n> +};\n> +\n> +static const struct {\n> +\tconst char *name;\n> +\tint (*is_available)(void);\n> +\tint (*update_schedule)(int run_maintenance, int fd);\n> +} scheduler_fn[] = {\n> +\t[SCHEDULER_CRON] = {\n> +\t\t.name = \"crontab\",\n> +\t\t.is_available = is_crontab_available,\n> +\t\t.update_schedule = crontab_update_schedule,\n> +\t},\n> +\t[SCHEDULER_LAUNCHCTL] = {\n> +\t\t.name = \"launchctl\",\n> +\t\t.is_available = is_launchctl_available,\n> +\t\t.update_schedule = launchctl_update_schedule,\n> +\t},\n> +\t[SCHEDULER_SCHTASKS] = {\n> +\t\t.name = \"schtasks\",\n> +\t\t.is_available = is_schtasks_available,\n> +\t\t.update_schedule = schtasks_update_schedule,\n> +\t},\n> +};\n> +\n> +static enum scheduler parse_scheduler(const char *value)\n> +{\n> +\tif (!value)\n> +\t\treturn SCHEDULER_INVALID;\n> +\telse if (!strcasecmp(value, \"auto\"))\n> +\t\treturn SCHEDULER_AUTO;\n> +\telse if (!strcasecmp(value, \"cron\") || !strcasecmp(value, \"crontab\"))\n> +\t\treturn SCHEDULER_CRON;\n> +\telse if (!strcasecmp(value, \"launchctl\"))\n> +\t\treturn SCHEDULER_LAUNCHCTL;\n> +\telse if (!strcasecmp(value, \"schtasks\"))\n> +\t\treturn SCHEDULER_SCHTASKS;\n> +\telse\n> +\t\treturn SCHEDULER_INVALID;\n> +}\n> +\n> +static int maintenance_opt_scheduler(const struct option *opt, const char *arg,\n> +\t\t\t\t     int unset)\n> +{\n> +\tenum scheduler *scheduler = opt->value;\n> +\n> +\tBUG_ON_OPT_NEG(unset);\n> +\n> +\t*scheduler = parse_scheduler(arg);\n> +\tif (*scheduler == SCHEDULER_INVALID)\n> +\t\treturn error(_(\"unrecognized --scheduler argument '%s'\"), arg);\n> +\treturn 0;\n> +}\n> +\n> +struct maintenance_start_opts {\n> +\tenum scheduler scheduler;\n> +};\n> +\n> +static enum scheduler resolve_scheduler(enum scheduler scheduler)\n> +{\n> +\tif (scheduler != SCHEDULER_AUTO)\n> +\t\treturn scheduler;\n> +\n>  #if defined(__APPLE__)\n> -static const char platform_scheduler[] = \"launchctl\";\n> +\treturn SCHEDULER_LAUNCHCTL;\n> +\n>  #elif defined(GIT_WINDOWS_NATIVE)\n> -static const char platform_scheduler[] = \"schtasks\";\n> +\treturn SCHEDULER_SCHTASKS;\n> +\n>  #else\n> -static const char platform_scheduler[] = \"crontab\";\n> +\treturn SCHEDULER_CRON;\n>  #endif\n> +}\n>  \n> -static int update_background_schedule(int enable)\n> +static void validate_scheduler(enum scheduler scheduler)\n>  {\n> -\tint result;\n> -\tconst char *scheduler = platform_scheduler;\n> -\tconst char *cmd = scheduler;\n> -\tchar *testing;\n> +\tif (scheduler == SCHEDULER_INVALID)\n> +\t\tBUG(\"invalid scheduler\");\n> +\tif (scheduler == SCHEDULER_AUTO)\n> +\t\tBUG(\"resolve_scheduler should have been called before\");\n> +\n> +\tif (!scheduler_fn[scheduler].is_available())\n> +\t\tdie(_(\"%s scheduler is not available\"),\n> +\t\t    scheduler_fn[scheduler].name);\n> +}\n> +\n> +static int update_background_schedule(const struct maintenance_start_opts *opts,\n> +\t\t\t\t      int enable)\n> +{\n> +\tunsigned int i;\n> +\tint result = 0;\n>  \tstruct lock_file lk;\n>  \tchar *lock_path = xstrfmt(\"%s/schedule\", the_repository->objects->odb->path);\n>  \n> -\ttesting = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n> -\tif (testing) {\n> -\t\tchar *sep = strchr(testing, ':');\n> -\t\tif (!sep)\n> -\t\t\tdie(\"GIT_TEST_MAINT_SCHEDULER unparseable: %s\", testing);\n> -\t\t*sep = '\\0';\n> -\t\tscheduler = testing;\n> -\t\tcmd = sep + 1;\n> +\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {\n> +\t\tfree(lock_path);\n> +\t\treturn error(_(\"another process is scheduling background maintenance\"));\n>  \t}\n>  \n> -\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {\n> -\t\tresult = error(_(\"another process is scheduling background maintenance\"));\n> -\t\tgoto cleanup;\n> +\tfor (i = 1; i < ARRAY_SIZE(scheduler_fn); i++) {\n> +\t\tif (enable && opts->scheduler == i)\n> +\t\t\tcontinue;\n> +\t\tif (!scheduler_fn[i].is_available())\n> +\t\t\tcontinue;\n> +\t\tscheduler_fn[i].update_schedule(0, get_lock_file_fd(&lk));\n>  \t}\n>  \n> -\tif (!strcmp(scheduler, \"launchctl\"))\n> -\t\tresult = launchctl_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n> -\telse if (!strcmp(scheduler, \"schtasks\"))\n> -\t\tresult = schtasks_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n> -\telse if (!strcmp(scheduler, \"crontab\"))\n> -\t\tresult = crontab_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n> -\telse\n> -\t\tdie(\"unknown background scheduler: %s\", scheduler);\n> +\tif (enable)\n> +\t\tresult = scheduler_fn[opts->scheduler].update_schedule(\n> +\t\t\t1, get_lock_file_fd(&lk));\n>  \n>  \trollback_lock_file(&lk);\n>  \n> -cleanup:\n>  \tfree(lock_path);\n> -\tfree(testing);\n>  \treturn result;\n>  }\n>  \n> -static int maintenance_start(void)\n> +static const char *const builtin_maintenance_start_usage[] = {\n> +\tN_(\"git maintenance start [--scheduler=<scheduler>]\"),\n> +\tNULL\n> +};\n> +\n> +static int maintenance_start(int argc, const char **argv, const char *prefix)\n>  {\n> +\tstruct maintenance_start_opts opts = { 0 };\n> +\tstruct option options[] = {\n> +\t\tOPT_CALLBACK_F(\n> +\t\t\t0, \"scheduler\", &opts.scheduler, N_(\"scheduler\"),\n> +\t\t\tN_(\"scheduler to trigger git maintenance run\"),\n> +\t\t\tPARSE_OPT_NONEG, maintenance_opt_scheduler),\n> +\t\tOPT_END()\n> +\t};\n> +\n> +\targc = parse_options(argc, argv, prefix, options,\n> +\t\t\t     builtin_maintenance_start_usage, 0);\n> +\tif (argc)\n> +\t\tusage_with_options(builtin_maintenance_start_usage, options);\n> +\n> +\topts.scheduler = resolve_scheduler(opts.scheduler);\n> +\tvalidate_scheduler(opts.scheduler);\n> +\n>  \tif (maintenance_register())\n>  \t\twarning(_(\"failed to add repo to global config\"));\n> -\n> -\treturn update_background_schedule(1);\n> +\treturn update_background_schedule(&opts, 1);\n>  }\n>  \n>  static int maintenance_stop(void)\n>  {\n> -\treturn update_background_schedule(0);\n> +\treturn update_background_schedule(NULL, 0);\n>  }\n>  \n>  static const char builtin_maintenance_usage[] =\tN_(\"git maintenance <subcommand> [<options>]\");\n> @@ -2018,7 +2241,7 @@ int cmd_maintenance(int argc, const char **argv, const char *prefix)\n>  \tif (!strcmp(argv[1], \"run\"))\n>  \t\treturn maintenance_run(argc - 1, argv + 1, prefix);\n>  \tif (!strcmp(argv[1], \"start\"))\n> -\t\treturn maintenance_start();\n> +\t\treturn maintenance_start(argc - 1, argv + 1, prefix);\n>  \tif (!strcmp(argv[1], \"stop\"))\n>  \t\treturn maintenance_stop();\n>  \tif (!strcmp(argv[1], \"register\"))\n> diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\n> index 58f46c77e6..27bce7992c 100755\n> --- a/t/t7900-maintenance.sh\n> +++ b/t/t7900-maintenance.sh\n> @@ -492,8 +492,21 @@ test_expect_success !MINGW 'register and unregister with regex metacharacters' '\n>  \t\tmaintenance.repo \"$(pwd)/$META\"\n>  '\n>  \n> +test_expect_success 'start --scheduler=<scheduler>' '\n> +\ttest_expect_code 129 git maintenance start --scheduler=foo 2>err &&\n> +\ttest_i18ngrep \"unrecognized --scheduler argument\" err &&\n> +\n> +\ttest_expect_code 129 git maintenance start --no-scheduler 2>err &&\n> +\ttest_i18ngrep \"unknown option\" err &&\n> +\n> +\ttest_expect_code 128 \\\n> +\t\tenv GIT_TEST_MAINT_SCHEDULER=\"launchctl:true,schtasks:true\" \\\n> +\t\tgit maintenance start --scheduler=crontab 2>err &&\n> +\ttest_i18ngrep \"fatal: crontab scheduler is not available\" err\n> +'\n> +\n>  test_expect_success 'start from empty cron table' '\n> -\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start --scheduler=crontab &&\n>  \n>  \t# start registers the repo\n>  \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n> @@ -516,7 +529,7 @@ test_expect_success 'stop from existing schedule' '\n>  \n>  test_expect_success 'start preserves existing schedule' '\n>  \techo \"Important information!\" >cron.txt &&\n> -\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start --scheduler=crontab &&\n>  \tgrep \"Important information!\" cron.txt\n>  '\n>  \n> @@ -545,7 +558,7 @@ test_expect_success 'start and stop macOS maintenance' '\n>  \tEOF\n>  \n>  \trm -f args &&\n> -\tGIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start &&\n> +\tGIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start --scheduler=launchctl &&\n>  \n>  \t# start registers the repo\n>  \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n> @@ -596,7 +609,7 @@ test_expect_success 'start and stop Windows maintenance' '\n>  \tEOF\n>  \n>  \trm -f args &&\n> -\tGIT_TEST_MAINT_SCHEDULER=\"schtasks:./print-args\" git maintenance start &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"schtasks:./print-args\" git maintenance start --scheduler=schtasks &&\n>  \n>  \t# start registers the repo\n>  \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n> @@ -619,6 +632,40 @@ test_expect_success 'start and stop Windows maintenance' '\n>  \ttest_cmp expect args\n>  '\n>  \n> +test_expect_success 'start and stop when several schedulers are available' '\n> +\twrite_script print-args <<-\\EOF &&\n> +\tprintf \"%s\\n\" \"$*\" | sed \"s:gui/[0-9][0-9]*:gui/[UID]:; s:\\(schtasks /create .* /xml\\).*:\\1:;\" >>args\n> +\tEOF\n> +\n> +\trm -f args &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n> +\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n> +\t\thourly daily weekly >expect &&\n> +\tfor frequency in hourly daily weekly\n> +\tdo\n> +\t\tPLIST=\"$pfx/Library/LaunchAgents/org.git-scm.git.$frequency.plist\" &&\n> +\t\techo \"launchctl bootout gui/[UID] $PLIST\" >>expect &&\n> +\t\techo \"launchctl bootstrap gui/[UID] $PLIST\" >>expect || return 1\n> +\tdone &&\n> +\ttest_cmp expect args &&\n> +\n> +\trm -f args &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n> +\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n> +\t\thourly daily weekly >expect &&\n> +\tprintf \"schtasks /create /tn Git Maintenance (%s) /f /xml\\n\" \\\n> +\t\thourly daily weekly >>expect &&\n> +\ttest_cmp expect args &&\n> +\n> +\trm -f args &&\n> +\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n> +\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n> +\t\thourly daily weekly >expect &&\n> +\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n> +\t\thourly daily weekly >>expect &&\n> +\ttest_cmp expect args\n> +'\n> +\n>  test_expect_success 'register preserves existing strategy' '\n>  \tgit config maintenance.strategy none &&\n>  \tgit maintenance register &&\n> \n"},{"id":"434705","messageId":"20210904205500.13074-2-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210904205500.13074-1-lenaic@lhuard.fr","subject":"[PATCH v10 1/3] cache.h: Introduce a generic \"xdg_config_home_for(…)\" function","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-09-04T20:54:58Z","receivedAt":"2021-09-04T20:55:44Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Current implementation of `xdg_config_home(filename)` returns\n`$XDG_CONFIG_HOME/git/$filename`, with the `git` subdirectory inserted\nbetween the `XDG_CONFIG_HOME` environment variable and the parameter.\n\nThis patch introduces a `xdg_config_home_for(subdir, filename)` function\nwhich is more generic. It only concatenates \"$XDG_CONFIG_HOME\", or\n\"$HOME/.config\" if the former isn’t defined, with the parameters,\nwithout adding `git` in between.\n\n`xdg_config_home(filename)` is now implemented by calling\n`xdg_config_home_for(\"git\", filename)` but this new generic function can\nbe used to compute the configuration directory of other programs.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n cache.h |  7 +++++++\n path.c  | 13 ++++++++++---\n 2 files changed, 17 insertions(+), 3 deletions(-)\n\ndiff --git a/cache.h b/cache.h\nindex d23de69368..72410b85d7 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -1297,6 +1297,13 @@ int is_ntfs_dotmailmap(const char *name);\n  */\n int looks_like_command_line_option(const char *str);\n \n+/**\n+ * Return a newly allocated string with the evaluation of\n+ * \"$XDG_CONFIG_HOME/$subdir/$filename\" if $XDG_CONFIG_HOME is non-empty, otherwise\n+ * \"$HOME/.config/$subdir/$filename\". Return NULL upon error.\n+ */\n+char *xdg_config_home_for(const char *subdir, const char *filename);\n+\n /**\n  * Return a newly allocated string with the evaluation of\n  * \"$XDG_CONFIG_HOME/git/$filename\" if $XDG_CONFIG_HOME is non-empty, otherwise\ndiff --git a/path.c b/path.c\nindex 0bc788ea40..2c895471d9 100644\n--- a/path.c\n+++ b/path.c\n@@ -1510,21 +1510,28 @@ int looks_like_command_line_option(const char *str)\n \treturn str && str[0] == '-';\n }\n \n-char *xdg_config_home(const char *filename)\n+char *xdg_config_home_for(const char *subdir, const char *filename)\n {\n \tconst char *home, *config_home;\n \n+\tassert(subdir);\n \tassert(filename);\n \tconfig_home = getenv(\"XDG_CONFIG_HOME\");\n \tif (config_home && *config_home)\n-\t\treturn mkpathdup(\"%s/git/%s\", config_home, filename);\n+\t\treturn mkpathdup(\"%s/%s/%s\", config_home, subdir, filename);\n \n \thome = getenv(\"HOME\");\n \tif (home)\n-\t\treturn mkpathdup(\"%s/.config/git/%s\", home, filename);\n+\t\treturn mkpathdup(\"%s/.config/%s/%s\", home, subdir, filename);\n+\n \treturn NULL;\n }\n \n+char *xdg_config_home(const char *filename)\n+{\n+\treturn xdg_config_home_for(\"git\", filename);\n+}\n+\n char *xdg_cache_home(const char *filename)\n {\n \tconst char *home, *cache_home;\n-- \n2.33.0\n\n"},{"id":"434706","messageId":"20210904205500.13074-4-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210904205500.13074-1-lenaic@lhuard.fr","subject":"[PATCH v10 3/3] maintenance: add support for systemd timers on Linux","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-09-04T20:55:00Z","receivedAt":"2021-09-04T20:55:44Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"The existing mechanism for scheduling background maintenance is done\nthrough cron. On Linux systems managed by systemd, systemd provides an\nalternative to schedule recurring tasks: systemd timers.\n\nThe main motivations to implement systemd timers in addition to cron\nare:\n* cron is optional and Linux systems running systemd might not have it\n  installed.\n* The execution of `crontab -l` can tell us if cron is installed but not\n  if the daemon is actually running.\n* With systemd, each service is run in its own cgroup and its logs are\n  tagged by the service inside journald. With cron, all scheduled tasks\n  are running in the cron daemon cgroup and all the logs of the\n  user-scheduled tasks are pretended to belong to the system cron\n  service.\n  Concretely, a user that doesn’t have access to the system logs won’t\n  have access to the log of their own tasks scheduled by cron whereas\n  they will have access to the log of their own tasks scheduled by\n  systemd timer.\n  Although `cron` attempts to send email, that email may go unseen by\n  the user because these days, local mailboxes are not heavily used\n  anymore.\n\nIn order to schedule git maintenance, we need two unit template files:\n* ~/.config/systemd/user/git-maintenance@.service\n  to define the command to be started by systemd and\n* ~/.config/systemd/user/git-maintenance@.timer\n  to define the schedule at which the command should be run.\n\nThose units are templates that are parameterized by the frequency.\n\nBased on those templates, 3 timers are started:\n* git-maintenance@hourly.timer\n* git-maintenance@daily.timer\n* git-maintenance@weekly.timer\n\nThe command launched by those three timers are the same as with the\nother scheduling methods:\n\n/path/to/git for-each-repo --exec-path=/path/to\n--config=maintenance.repo maintenance run --schedule=%i\n\nwith the full path for git to ensure that the version of git launched\nfor the scheduled maintenance is the same as the one used to run\n`maintenance start`.\n\nThe timer unit contains `Persistent=true` so that, if the computer is\npowered down when a maintenance task should run, the task will be run\nwhen the computer is back powered on.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n Documentation/git-maintenance.txt |  58 +++++++-\n builtin/gc.c                      | 216 ++++++++++++++++++++++++++++++\n t/t7900-maintenance.sh            |  67 ++++++++-\n 3 files changed, 330 insertions(+), 11 deletions(-)\n\ndiff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\nindex 576290b5c6..e2cfb68ab5 100644\n--- a/Documentation/git-maintenance.txt\n+++ b/Documentation/git-maintenance.txt\n@@ -179,14 +179,16 @@ OPTIONS\n \t`maintenance.<task>.enabled` configured as `true` are considered.\n \tSee the 'TASKS' section for the list of accepted `<task>` values.\n \n---scheduler=auto|crontab|launchctl|schtasks::\n+--scheduler=auto|crontab|systemd-timer|launchctl|schtasks::\n \tWhen combined with the `start` subcommand, specify the scheduler\n \tfor running the hourly, daily and weekly executions of\n \t`git maintenance run`.\n-\tPossible values for `<scheduler>` are `auto`, `crontab` (POSIX),\n-\t`launchctl` (macOS), and `schtasks` (Windows).\n-\tWhen `auto` is specified, the appropriate platform-specific\n-\tscheduler is used. Default is `auto`.\n+\tPossible values for `<scheduler>` are `auto`, `crontab`\n+\t(POSIX), `systemd-timer` (Linux), `launchctl` (macOS), and\n+\t`schtasks` (Windows). When `auto` is specified, the\n+\tappropriate platform-specific scheduler is used; on Linux,\n+\t`systemd-timer` is used if available, otherwise\n+\t`crontab`. Default is `auto`.\n \n \n TROUBLESHOOTING\n@@ -286,6 +288,52 @@ schedule to ensure you are executing the correct binaries in your\n schedule.\n \n \n+BACKGROUND MAINTENANCE ON LINUX SYSTEMD SYSTEMS\n+-----------------------------------------------\n+\n+While Linux supports `cron`, depending on the distribution, `cron` may\n+be an optional package not necessarily installed. On modern Linux\n+distributions, systemd timers are superseding it.\n+\n+If user systemd timers are available, they will be used as a replacement\n+of `cron`.\n+\n+In this case, `git maintenance start` will create user systemd timer units\n+and start the timers. The current list of user-scheduled tasks can be found\n+by running `systemctl --user list-timers`. The timers written by `git\n+maintenance start` are similar to this:\n+\n+-----------------------------------------------------------------------\n+$ systemctl --user list-timers\n+NEXT                         LEFT          LAST                         PASSED     UNIT                         ACTIVATES\n+Thu 2021-04-29 19:00:00 CEST 42min left    Thu 2021-04-29 18:00:11 CEST 17min ago  git-maintenance@hourly.timer git-maintenance@hourly.service\n+Fri 2021-04-30 00:00:00 CEST 5h 42min left Thu 2021-04-29 00:00:11 CEST 18h ago    git-maintenance@daily.timer  git-maintenance@daily.service\n+Mon 2021-05-03 00:00:00 CEST 3 days left   Mon 2021-04-26 00:00:11 CEST 3 days ago git-maintenance@weekly.timer git-maintenance@weekly.service\n+-----------------------------------------------------------------------\n+\n+One timer is registered for each `--schedule=<frequency>` option.\n+\n+The definition of the systemd units can be inspected in the following files:\n+\n+-----------------------------------------------------------------------\n+~/.config/systemd/user/git-maintenance@.timer\n+~/.config/systemd/user/git-maintenance@.service\n+~/.config/systemd/user/timers.target.wants/git-maintenance@hourly.timer\n+~/.config/systemd/user/timers.target.wants/git-maintenance@daily.timer\n+~/.config/systemd/user/timers.target.wants/git-maintenance@weekly.timer\n+-----------------------------------------------------------------------\n+\n+`git maintenance start` will overwrite these files and start the timer\n+again with `systemctl --user`, so any customization should be done by\n+creating a drop-in file, i.e. a `.conf` suffixed file in the\n+`~/.config/systemd/user/git-maintenance@.service.d` directory.\n+\n+`git maintenance stop` will stop the user systemd timers and delete\n+the above mentioned files.\n+\n+For more details, see `systemd.timer(5)`.\n+\n+\n BACKGROUND MAINTENANCE ON MACOS SYSTEMS\n ---------------------------------------\n \ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 4e3bee5c9a..59b714f610 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -2074,10 +2074,210 @@ static int crontab_update_schedule(int run_maintenance, int fd)\n \treturn result;\n }\n \n+static int real_is_systemd_timer_available(void)\n+{\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\n+\tstrvec_pushl(&child.args, \"systemctl\", \"--user\", \"list-timers\", NULL);\n+\tchild.no_stdin = 1;\n+\tchild.no_stdout = 1;\n+\tchild.no_stderr = 1;\n+\tchild.silent_exec_failure = 1;\n+\n+\tif (start_command(&child))\n+\t\treturn 0;\n+\tif (finish_command(&child))\n+\t\treturn 0;\n+\treturn 1;\n+}\n+\n+static int is_systemd_timer_available(void)\n+{\n+\tconst char *cmd = \"systemctl\";\n+\tint is_available;\n+\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+\treturn real_is_systemd_timer_available();\n+}\n+\n+static char *xdg_config_home_systemd(const char *filename)\n+{\n+\treturn xdg_config_home_for(\"systemd/user\", filename);\n+}\n+\n+static int systemd_timer_enable_unit(int enable,\n+\t\t\t\t     enum schedule_priority schedule)\n+{\n+\tconst char *cmd = \"systemctl\";\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\tconst char *frequency = get_frequency(schedule);\n+\n+\t/*\n+\t * Disabling the systemd unit while it is already disabled makes\n+\t * systemctl print an error.\n+\t * Let's ignore it since it means we already are in the expected state:\n+\t * the unit is disabled.\n+\t *\n+\t * On the other hand, enabling a systemd unit which is already enabled\n+\t * produces no error.\n+\t */\n+\tif (!enable)\n+\t\tchild.no_stderr = 1;\n+\n+\tget_schedule_cmd(&cmd, NULL);\n+\tstrvec_split(&child.args, cmd);\n+\tstrvec_pushl(&child.args, \"--user\", enable ? \"enable\" : \"disable\",\n+\t\t     \"--now\", NULL);\n+\tstrvec_pushf(&child.args, \"git-maintenance@%s.timer\", frequency);\n+\n+\tif (start_command(&child))\n+\t\treturn error(_(\"failed to start systemctl\"));\n+\tif (finish_command(&child))\n+\t\t/*\n+\t\t * Disabling an already disabled systemd unit makes\n+\t\t * systemctl fail.\n+\t\t * Let's ignore this failure.\n+\t\t *\n+\t\t * Enabling an enabled systemd unit doesn't fail.\n+\t\t */\n+\t\tif (enable)\n+\t\t\treturn error(_(\"failed to run systemctl\"));\n+\treturn 0;\n+}\n+\n+static int systemd_timer_delete_unit_templates(void)\n+{\n+\tint ret = 0;\n+\tchar *filename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n+\tif (unlink(filename) && !is_missing_file_error(errno))\n+\t\tret = error_errno(_(\"failed to delete '%s'\"), filename);\n+\tFREE_AND_NULL(filename);\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n+\tif (unlink(filename) && !is_missing_file_error(errno))\n+\t\tret = error_errno(_(\"failed to delete '%s'\"), filename);\n+\n+\tfree(filename);\n+\treturn ret;\n+}\n+\n+static int systemd_timer_delete_units(void)\n+{\n+\treturn systemd_timer_enable_unit(0, SCHEDULE_HOURLY) ||\n+\t       systemd_timer_enable_unit(0, SCHEDULE_DAILY) ||\n+\t       systemd_timer_enable_unit(0, SCHEDULE_WEEKLY) ||\n+\t       systemd_timer_delete_unit_templates();\n+}\n+\n+static int systemd_timer_write_unit_templates(const char *exec_path)\n+{\n+\tchar *filename;\n+\tFILE *file;\n+\tconst char *unit;\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.timer\");\n+\tif (safe_create_leading_directories(filename)) {\n+\t\terror(_(\"failed to create directories for '%s'\"), filename);\n+\t\tgoto error;\n+\t}\n+\tfile = fopen_or_warn(filename, \"w\");\n+\tif (file == NULL)\n+\t\tgoto error;\n+\n+\tunit = \"# This file was created and is maintained by Git.\\n\"\n+\t       \"# Any edits made in this file might be replaced in the future\\n\"\n+\t       \"# by a Git command.\\n\"\n+\t       \"\\n\"\n+\t       \"[Unit]\\n\"\n+\t       \"Description=Optimize Git repositories data\\n\"\n+\t       \"\\n\"\n+\t       \"[Timer]\\n\"\n+\t       \"OnCalendar=%i\\n\"\n+\t       \"Persistent=true\\n\"\n+\t       \"\\n\"\n+\t       \"[Install]\\n\"\n+\t       \"WantedBy=timers.target\\n\";\n+\tif (fputs(unit, file) == EOF) {\n+\t\terror(_(\"failed to write to '%s'\"), filename);\n+\t\tfclose(file);\n+\t\tgoto error;\n+\t}\n+\tif (fclose(file) == EOF) {\n+\t\terror_errno(_(\"failed to flush '%s'\"), filename);\n+\t\tgoto error;\n+\t}\n+\tfree(filename);\n+\n+\tfilename = xdg_config_home_systemd(\"git-maintenance@.service\");\n+\tfile = fopen_or_warn(filename, \"w\");\n+\tif (file == NULL)\n+\t\tgoto error;\n+\n+\tunit = \"# This file was created and is maintained by Git.\\n\"\n+\t       \"# Any edits made in this file might be replaced in the future\\n\"\n+\t       \"# by a Git command.\\n\"\n+\t       \"\\n\"\n+\t       \"[Unit]\\n\"\n+\t       \"Description=Optimize Git repositories data\\n\"\n+\t       \"\\n\"\n+\t       \"[Service]\\n\"\n+\t       \"Type=oneshot\\n\"\n+\t       \"ExecStart=\\\"%s/git\\\" --exec-path=\\\"%s\\\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\\n\"\n+\t       \"LockPersonality=yes\\n\"\n+\t       \"MemoryDenyWriteExecute=yes\\n\"\n+\t       \"NoNewPrivileges=yes\\n\"\n+\t       \"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\\n\"\n+\t       \"RestrictNamespaces=yes\\n\"\n+\t       \"RestrictRealtime=yes\\n\"\n+\t       \"RestrictSUIDSGID=yes\\n\"\n+\t       \"SystemCallArchitectures=native\\n\"\n+\t       \"SystemCallFilter=@system-service\\n\";\n+\tif (fprintf(file, unit, exec_path, exec_path) < 0) {\n+\t\terror(_(\"failed to write to '%s'\"), filename);\n+\t\tfclose(file);\n+\t\tgoto error;\n+\t}\n+\tif (fclose(file) == EOF) {\n+\t\terror_errno(_(\"failed to flush '%s'\"), filename);\n+\t\tgoto error;\n+\t}\n+\tfree(filename);\n+\treturn 0;\n+\n+error:\n+\tfree(filename);\n+\tsystemd_timer_delete_unit_templates();\n+\treturn -1;\n+}\n+\n+static int systemd_timer_setup_units(void)\n+{\n+\tconst char *exec_path = git_exec_path();\n+\n+\tint ret = systemd_timer_write_unit_templates(exec_path) ||\n+\t\t  systemd_timer_enable_unit(1, SCHEDULE_HOURLY) ||\n+\t\t  systemd_timer_enable_unit(1, SCHEDULE_DAILY) ||\n+\t\t  systemd_timer_enable_unit(1, SCHEDULE_WEEKLY);\n+\tif (ret)\n+\t\tsystemd_timer_delete_units();\n+\treturn ret;\n+}\n+\n+static int systemd_timer_update_schedule(int run_maintenance, int fd)\n+{\n+\tif (run_maintenance)\n+\t\treturn systemd_timer_setup_units();\n+\telse\n+\t\treturn systemd_timer_delete_units();\n+}\n+\n enum scheduler {\n \tSCHEDULER_INVALID = -1,\n \tSCHEDULER_AUTO,\n \tSCHEDULER_CRON,\n+\tSCHEDULER_SYSTEMD,\n \tSCHEDULER_LAUNCHCTL,\n \tSCHEDULER_SCHTASKS,\n };\n@@ -2092,6 +2292,11 @@ static const struct {\n \t\t.is_available = is_crontab_available,\n \t\t.update_schedule = crontab_update_schedule,\n \t},\n+\t[SCHEDULER_SYSTEMD] = {\n+\t\t.name = \"systemctl\",\n+\t\t.is_available = is_systemd_timer_available,\n+\t\t.update_schedule = systemd_timer_update_schedule,\n+\t},\n \t[SCHEDULER_LAUNCHCTL] = {\n \t\t.name = \"launchctl\",\n \t\t.is_available = is_launchctl_available,\n@@ -2112,6 +2317,9 @@ static enum scheduler parse_scheduler(const char *value)\n \t\treturn SCHEDULER_AUTO;\n \telse if (!strcasecmp(value, \"cron\") || !strcasecmp(value, \"crontab\"))\n \t\treturn SCHEDULER_CRON;\n+\telse if (!strcasecmp(value, \"systemd\") ||\n+\t\t !strcasecmp(value, \"systemd-timer\"))\n+\t\treturn SCHEDULER_SYSTEMD;\n \telse if (!strcasecmp(value, \"launchctl\"))\n \t\treturn SCHEDULER_LAUNCHCTL;\n \telse if (!strcasecmp(value, \"schtasks\"))\n@@ -2148,6 +2356,14 @@ static enum scheduler resolve_scheduler(enum scheduler scheduler)\n #elif defined(GIT_WINDOWS_NATIVE)\n \treturn SCHEDULER_SCHTASKS;\n \n+#elif defined(__linux__)\n+\tif (is_systemd_timer_available())\n+\t\treturn SCHEDULER_SYSTEMD;\n+\telse if (is_crontab_available())\n+\t\treturn SCHEDULER_CRON;\n+\telse\n+\t\tdie(_(\"neither systemd timers nor crontab are available\"));\n+\n #else\n \treturn SCHEDULER_CRON;\n #endif\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 27bce7992c..265f7793f5 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -20,6 +20,18 @@ test_xmllint () {\n \tfi\n }\n \n+test_lazy_prereq SYSTEMD_ANALYZE '\n+\tsystemd-analyze --help >out &&\n+\tgrep verify out\n+'\n+\n+test_systemd_analyze_verify () {\n+\tif test_have_prereq SYSTEMD_ANALYZE\n+\tthen\n+\t\tsystemd-analyze verify \"$@\"\n+\tfi\n+}\n+\n test_expect_success 'help text' '\n \ttest_expect_code 129 git maintenance -h 2>err &&\n \ttest_i18ngrep \"usage: git maintenance <subcommand>\" err &&\n@@ -632,15 +644,56 @@ test_expect_success 'start and stop Windows maintenance' '\n \ttest_cmp expect args\n '\n \n+test_expect_success 'start and stop Linux/systemd maintenance' '\n+\twrite_script print-args <<-\\EOF &&\n+\tprintf \"%s\\n\" \"$*\" >>args\n+\tEOF\n+\n+\tXDG_CONFIG_HOME=\"$PWD\" &&\n+\texport XDG_CONFIG_HOME &&\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args\" git maintenance start --scheduler=systemd-timer &&\n+\n+\t# start registers the repo\n+\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n+\n+\ttest_systemd_analyze_verify \"systemd/user/git-maintenance@.service\" &&\n+\n+\tprintf -- \"--user enable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args\" git maintenance stop &&\n+\n+\t# stop does not unregister the repo\n+\tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n+\n+\ttest_path_is_missing \"systemd/user/git-maintenance@.timer\" &&\n+\ttest_path_is_missing \"systemd/user/git-maintenance@.service\" &&\n+\n+\tprintf -- \"--user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n+\ttest_cmp expect args\n+'\n+\n test_expect_success 'start and stop when several schedulers are available' '\n \twrite_script print-args <<-\\EOF &&\n \tprintf \"%s\\n\" \"$*\" | sed \"s:gui/[0-9][0-9]*:gui/[UID]:; s:\\(schtasks /create .* /xml\\).*:\\1:;\" >>args\n \tEOF\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n-\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=systemd-timer &&\n+\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n \t\thourly daily weekly >expect &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\tprintf -- \"systemctl --user enable --now git-maintenance@%s.timer\\n\" hourly daily weekly >>expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n+\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >>expect &&\n \tfor frequency in hourly daily weekly\n \tdo\n \t\tPLIST=\"$pfx/Library/LaunchAgents/org.git-scm.git.$frequency.plist\" &&\n@@ -650,17 +703,19 @@ test_expect_success 'start and stop when several schedulers are available' '\n \ttest_cmp expect args &&\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n+\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n \tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n-\t\thourly daily weekly >expect &&\n+\t\thourly daily weekly >>expect &&\n \tprintf \"schtasks /create /tn Git Maintenance (%s) /f /xml\\n\" \\\n \t\thourly daily weekly >>expect &&\n \ttest_cmp expect args &&\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"systemctl:./print-args systemctl,launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n+\tprintf -- \"systemctl --user disable --now git-maintenance@%s.timer\\n\" hourly daily weekly >expect &&\n \tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n-\t\thourly daily weekly >expect &&\n+\t\thourly daily weekly >>expect &&\n \tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n \t\thourly daily weekly >>expect &&\n \ttest_cmp expect args\n-- \n2.33.0\n\n"},{"id":"434707","messageId":"20210904205500.13074-1-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210827210255.12565-1-lenaic@lhuard.fr","subject":"[PATCH v10 0/3] maintenance: add support for systemd timers on Linux","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-09-04T20:54:57Z","receivedAt":"2021-09-04T20:56:07Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Hello,\n\nPlease find hereafter my updated patchset to add support for systemd\ntimers on Linux for the `git maintenance start` command.\n\nThe only changes compared to the previous version are fixes for the\ntwo typos in a comment that Ramsay Jones pointed out [1]\n\n[1] https://lore.kernel.org/git/51246c10-fe0b-b8e5-cdc3-54bdc6c8054e@ramsayjones.plus.com/\n\n\nThe patches are:\n\n* cache.h: Introduce a generic \"xdg_config_home_for(…)\" function\n\n  This patch introduces a function to compute configuration files\n  paths inside $XDG_CONFIG_HOME.\n\n* maintenance: `git maintenance run` learned `--scheduler=<scheduler>`\n\n  This patch adds a new parameter to the `git maintenance run` to let\n  the user choose a scheduler.\n\n* maintenance: add support for systemd timers on Linux\n\n  This patch implements the support of systemd timers on top of\n  crontab scheduler on Linux systems.\n\nBest wishes,\nLénaïc.\n\nLénaïc Huard (3):\n  cache.h: Introduce a generic \"xdg_config_home_for(…)\" function\n  maintenance: `git maintenance run` learned `--scheduler=<scheduler>`\n  maintenance: add support for systemd timers on Linux\n\n Documentation/git-maintenance.txt |  57 +++\n builtin/gc.c                      | 581 ++++++++++++++++++++++++++----\n cache.h                           |   7 +\n path.c                            |  13 +-\n t/t7900-maintenance.sh            | 110 +++++-\n 5 files changed, 690 insertions(+), 78 deletions(-)\n\nDiff-intervalle contre v9 :\n1:  1639bd151c = 1:  0c0362d4ec cache.h: Introduce a generic \"xdg_config_home_for(…)\" function\n2:  ea5568269c ! 2:  5fb061ca9d maintenance: `git maintenance run` learned `--scheduler=<scheduler>`\n    @@ builtin/gc.c: static const char *get_frequency(enum schedule_priority schedule)\n     + * list of colon-separated key/value pairs where each pair contains a scheduler\n     + * and its corresponding mock.\n     + *\n    -+ * * If $GET_TEST_MAINT_SCHEDULER is not set, return false and leave the\n    ++ * * If $GIT_TEST_MAINT_SCHEDULER is not set, return false and leave the\n     + *   arguments unmodified.\n     + *\n    -+ * * If $GET_TEST_MAINT_SCHEDULER is set, return true.\n    ++ * * If $GIT_TEST_MAINT_SCHEDULER is set, return true.\n     + *   In this case, the *cmd value is read as input.\n     + *\n     + *   * if the input value *cmd is the key of one of the comma-separated list\n3:  8c25ebd3be = 3:  b0f2f6df0e maintenance: add support for systemd timers on Linux\n-- \n2.33.0\n\n"},{"id":"434708","messageId":"20210904205500.13074-3-lenaic@lhuard.fr","threadId":"55603","inReplyTo":"20210904205500.13074-1-lenaic@lhuard.fr","subject":"[PATCH v10 2/3] maintenance: `git maintenance run` learned `--scheduler=<scheduler>`","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-09-04T20:54:59Z","receivedAt":"2021-09-04T20:56:07Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Depending on the system, different schedulers can be used to schedule\nthe hourly, daily and weekly executions of `git maintenance run`:\n* `launchctl` for MacOS,\n* `schtasks` for Windows and\n* `crontab` for everything else.\n\n`git maintenance run` now has an option to let the end-user explicitly\nchoose which scheduler he wants to use:\n`--scheduler=auto|crontab|launchctl|schtasks`.\n\nWhen `git maintenance start --scheduler=XXX` is run, it not only\nregisters `git maintenance run` tasks in the scheduler XXX, it also\nremoves the `git maintenance run` tasks from all the other schedulers to\nensure we cannot have two schedulers launching concurrent identical\ntasks.\n\nThe default value is `auto` which chooses a suitable scheduler for the\nsystem.\n\n`git maintenance stop` doesn't have any `--scheduler` parameter because\nthis command will try to remove the `git maintenance run` tasks from all\nthe available schedulers.\n\nSigned-off-by: Lénaïc Huard <lenaic@lhuard.fr>\n---\n Documentation/git-maintenance.txt |   9 +\n builtin/gc.c                      | 365 ++++++++++++++++++++++++------\n t/t7900-maintenance.sh            |  55 ++++-\n 3 files changed, 354 insertions(+), 75 deletions(-)\n\ndiff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt\nindex 1e738ad398..576290b5c6 100644\n--- a/Documentation/git-maintenance.txt\n+++ b/Documentation/git-maintenance.txt\n@@ -179,6 +179,15 @@ OPTIONS\n \t`maintenance.<task>.enabled` configured as `true` are considered.\n \tSee the 'TASKS' section for the list of accepted `<task>` values.\n \n+--scheduler=auto|crontab|launchctl|schtasks::\n+\tWhen combined with the `start` subcommand, specify the scheduler\n+\tfor running the hourly, daily and weekly executions of\n+\t`git maintenance run`.\n+\tPossible values for `<scheduler>` are `auto`, `crontab` (POSIX),\n+\t`launchctl` (macOS), and `schtasks` (Windows).\n+\tWhen `auto` is specified, the appropriate platform-specific\n+\tscheduler is used. Default is `auto`.\n+\n \n TROUBLESHOOTING\n ---------------\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 6ce5ca4512..4e3bee5c9a 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1529,6 +1529,93 @@ static const char *get_frequency(enum schedule_priority schedule)\n \t}\n }\n \n+/*\n+ * get_schedule_cmd` reads the GIT_TEST_MAINT_SCHEDULER environment variable\n+ * to mock the schedulers that `git maintenance start` rely on.\n+ *\n+ * For test purpose, GIT_TEST_MAINT_SCHEDULER can be set to a comma-separated\n+ * list of colon-separated key/value pairs where each pair contains a scheduler\n+ * and its corresponding mock.\n+ *\n+ * * If $GIT_TEST_MAINT_SCHEDULER is not set, return false and leave the\n+ *   arguments unmodified.\n+ *\n+ * * If $GIT_TEST_MAINT_SCHEDULER is set, return true.\n+ *   In this case, the *cmd value is read as input.\n+ *\n+ *   * if the input value *cmd is the key of one of the comma-separated list\n+ *     item, then *is_available is set to true and *cmd is modified and becomes\n+ *     the mock command.\n+ *\n+ *   * if the input value *cmd isn’t the key of any of the comma-separated list\n+ *     item, then *is_available is set to false.\n+ *\n+ * Ex.:\n+ *   GIT_TEST_MAINT_SCHEDULER not set\n+ *     +-------+-------------------------------------------------+\n+ *     | Input |                     Output                      |\n+ *     | *cmd  | return code |       *cmd        | *is_available |\n+ *     +-------+-------------+-------------------+---------------+\n+ *     | \"foo\" |    false    | \"foo\" (unchanged) |  (unchanged)  |\n+ *     +-------+-------------+-------------------+---------------+\n+ *\n+ *   GIT_TEST_MAINT_SCHEDULER set to “foo:./mock_foo.sh,bar:./mock_bar.sh”\n+ *     +-------+-------------------------------------------------+\n+ *     | Input |                     Output                      |\n+ *     | *cmd  | return code |       *cmd        | *is_available |\n+ *     +-------+-------------+-------------------+---------------+\n+ *     | \"foo\" |    true     |  \"./mock.foo.sh\"  |     true      |\n+ *     | \"qux\" |    true     | \"qux\" (unchanged) |     false     |\n+ *     +-------+-------------+-------------------+---------------+\n+ */\n+static int get_schedule_cmd(const char **cmd, int *is_available)\n+{\n+\tchar *testing = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n+\tstruct string_list_item *item;\n+\tstruct string_list list = STRING_LIST_INIT_NODUP;\n+\n+\tif (!testing)\n+\t\treturn 0;\n+\n+\tif (is_available)\n+\t\t*is_available = 0;\n+\n+\tstring_list_split_in_place(&list, testing, ',', -1);\n+\tfor_each_string_list_item(item, &list) {\n+\t\tstruct string_list pair = STRING_LIST_INIT_NODUP;\n+\n+\t\tif (string_list_split_in_place(&pair, item->string, ':', 2) != 2)\n+\t\t\tcontinue;\n+\n+\t\tif (!strcmp(*cmd, pair.items[0].string)) {\n+\t\t\t*cmd = pair.items[1].string;\n+\t\t\tif (is_available)\n+\t\t\t\t*is_available = 1;\n+\t\t\tstring_list_clear(&list, 0);\n+\t\t\tUNLEAK(testing);\n+\t\t\treturn 1;\n+\t\t}\n+\t}\n+\n+\tstring_list_clear(&list, 0);\n+\tfree(testing);\n+\treturn 1;\n+}\n+\n+static int is_launchctl_available(void)\n+{\n+\tconst char *cmd = \"launchctl\";\n+\tint is_available;\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+#ifdef __APPLE__\n+\treturn 1;\n+#else\n+\treturn 0;\n+#endif\n+}\n+\n static char *launchctl_service_name(const char *frequency)\n {\n \tstruct strbuf label = STRBUF_INIT;\n@@ -1555,19 +1642,17 @@ static char *launchctl_get_uid(void)\n \treturn xstrfmt(\"gui/%d\", getuid());\n }\n \n-static int launchctl_boot_plist(int enable, const char *filename, const char *cmd)\n+static int launchctl_boot_plist(int enable, const char *filename)\n {\n+\tconst char *cmd = \"launchctl\";\n \tint result;\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tchar *uid = launchctl_get_uid();\n \n+\tget_schedule_cmd(&cmd, NULL);\n \tstrvec_split(&child.args, cmd);\n-\tif (enable)\n-\t\tstrvec_push(&child.args, \"bootstrap\");\n-\telse\n-\t\tstrvec_push(&child.args, \"bootout\");\n-\tstrvec_push(&child.args, uid);\n-\tstrvec_push(&child.args, filename);\n+\tstrvec_pushl(&child.args, enable ? \"bootstrap\" : \"bootout\", uid,\n+\t\t     filename, NULL);\n \n \tchild.no_stderr = 1;\n \tchild.no_stdout = 1;\n@@ -1581,26 +1666,26 @@ static int launchctl_boot_plist(int enable, const char *filename, const char *cm\n \treturn result;\n }\n \n-static int launchctl_remove_plist(enum schedule_priority schedule, const char *cmd)\n+static int launchctl_remove_plist(enum schedule_priority schedule)\n {\n \tconst char *frequency = get_frequency(schedule);\n \tchar *name = launchctl_service_name(frequency);\n \tchar *filename = launchctl_service_filename(name);\n-\tint result = launchctl_boot_plist(0, filename, cmd);\n+\tint result = launchctl_boot_plist(0, filename);\n \tunlink(filename);\n \tfree(filename);\n \tfree(name);\n \treturn result;\n }\n \n-static int launchctl_remove_plists(const char *cmd)\n+static int launchctl_remove_plists(void)\n {\n-\treturn launchctl_remove_plist(SCHEDULE_HOURLY, cmd) ||\n-\t\tlaunchctl_remove_plist(SCHEDULE_DAILY, cmd) ||\n-\t\tlaunchctl_remove_plist(SCHEDULE_WEEKLY, cmd);\n+\treturn launchctl_remove_plist(SCHEDULE_HOURLY) ||\n+\t       launchctl_remove_plist(SCHEDULE_DAILY) ||\n+\t       launchctl_remove_plist(SCHEDULE_WEEKLY);\n }\n \n-static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule, const char *cmd)\n+static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule)\n {\n \tFILE *plist;\n \tint i;\n@@ -1669,8 +1754,8 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n \tfclose(plist);\n \n \t/* bootout might fail if not already running, so ignore */\n-\tlaunchctl_boot_plist(0, filename, cmd);\n-\tif (launchctl_boot_plist(1, filename, cmd))\n+\tlaunchctl_boot_plist(0, filename);\n+\tif (launchctl_boot_plist(1, filename))\n \t\tdie(_(\"failed to bootstrap service %s\"), filename);\n \n \tfree(filename);\n@@ -1678,21 +1763,35 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n \treturn 0;\n }\n \n-static int launchctl_add_plists(const char *cmd)\n+static int launchctl_add_plists(void)\n {\n \tconst char *exec_path = git_exec_path();\n \n-\treturn launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY, cmd) ||\n-\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_DAILY, cmd) ||\n-\t\tlaunchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY, cmd);\n+\treturn launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY) ||\n+\t       launchctl_schedule_plist(exec_path, SCHEDULE_DAILY) ||\n+\t       launchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY);\n }\n \n-static int launchctl_update_schedule(int run_maintenance, int fd, const char *cmd)\n+static int launchctl_update_schedule(int run_maintenance, int fd)\n {\n \tif (run_maintenance)\n-\t\treturn launchctl_add_plists(cmd);\n+\t\treturn launchctl_add_plists();\n \telse\n-\t\treturn launchctl_remove_plists(cmd);\n+\t\treturn launchctl_remove_plists();\n+}\n+\n+static int is_schtasks_available(void)\n+{\n+\tconst char *cmd = \"schtasks\";\n+\tint is_available;\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+#ifdef GIT_WINDOWS_NATIVE\n+\treturn 1;\n+#else\n+\treturn 0;\n+#endif\n }\n \n static char *schtasks_task_name(const char *frequency)\n@@ -1702,13 +1801,15 @@ static char *schtasks_task_name(const char *frequency)\n \treturn strbuf_detach(&label, NULL);\n }\n \n-static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd)\n+static int schtasks_remove_task(enum schedule_priority schedule)\n {\n+\tconst char *cmd = \"schtasks\";\n \tint result;\n \tstruct strvec args = STRVEC_INIT;\n \tconst char *frequency = get_frequency(schedule);\n \tchar *name = schtasks_task_name(frequency);\n \n+\tget_schedule_cmd(&cmd, NULL);\n \tstrvec_split(&args, cmd);\n \tstrvec_pushl(&args, \"/delete\", \"/tn\", name, \"/f\", NULL);\n \n@@ -1719,15 +1820,16 @@ static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd\n \treturn result;\n }\n \n-static int schtasks_remove_tasks(const char *cmd)\n+static int schtasks_remove_tasks(void)\n {\n-\treturn schtasks_remove_task(SCHEDULE_HOURLY, cmd) ||\n-\t\tschtasks_remove_task(SCHEDULE_DAILY, cmd) ||\n-\t\tschtasks_remove_task(SCHEDULE_WEEKLY, cmd);\n+\treturn schtasks_remove_task(SCHEDULE_HOURLY) ||\n+\t       schtasks_remove_task(SCHEDULE_DAILY) ||\n+\t       schtasks_remove_task(SCHEDULE_WEEKLY);\n }\n \n-static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule, const char *cmd)\n+static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule)\n {\n+\tconst char *cmd = \"schtasks\";\n \tint result;\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tconst char *xml;\n@@ -1736,6 +1838,8 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority\n \tchar *name = schtasks_task_name(frequency);\n \tstruct strbuf tfilename = STRBUF_INIT;\n \n+\tget_schedule_cmd(&cmd, NULL);\n+\n \tstrbuf_addf(&tfilename, \"%s/schedule_%s_XXXXXX\",\n \t\t    get_git_common_dir(), frequency);\n \ttfile = xmks_tempfile(tfilename.buf);\n@@ -1840,28 +1944,52 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority\n \treturn result;\n }\n \n-static int schtasks_schedule_tasks(const char *cmd)\n+static int schtasks_schedule_tasks(void)\n {\n \tconst char *exec_path = git_exec_path();\n \n-\treturn schtasks_schedule_task(exec_path, SCHEDULE_HOURLY, cmd) ||\n-\t\tschtasks_schedule_task(exec_path, SCHEDULE_DAILY, cmd) ||\n-\t\tschtasks_schedule_task(exec_path, SCHEDULE_WEEKLY, cmd);\n+\treturn schtasks_schedule_task(exec_path, SCHEDULE_HOURLY) ||\n+\t       schtasks_schedule_task(exec_path, SCHEDULE_DAILY) ||\n+\t       schtasks_schedule_task(exec_path, SCHEDULE_WEEKLY);\n }\n \n-static int schtasks_update_schedule(int run_maintenance, int fd, const char *cmd)\n+static int schtasks_update_schedule(int run_maintenance, int fd)\n {\n \tif (run_maintenance)\n-\t\treturn schtasks_schedule_tasks(cmd);\n+\t\treturn schtasks_schedule_tasks();\n \telse\n-\t\treturn schtasks_remove_tasks(cmd);\n+\t\treturn schtasks_remove_tasks();\n+}\n+\n+static int is_crontab_available(void)\n+{\n+\tconst char *cmd = \"crontab\";\n+\tint is_available;\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\n+\tif (get_schedule_cmd(&cmd, &is_available))\n+\t\treturn is_available;\n+\n+\tstrvec_split(&child.args, cmd);\n+\tstrvec_push(&child.args, \"-l\");\n+\tchild.no_stdin = 1;\n+\tchild.no_stdout = 1;\n+\tchild.no_stderr = 1;\n+\tchild.silent_exec_failure = 1;\n+\n+\tif (start_command(&child))\n+\t\treturn 0;\n+\t/* Ignore exit code, as an empty crontab will return error. */\n+\tfinish_command(&child);\n+\treturn 1;\n }\n \n #define BEGIN_LINE \"# BEGIN GIT MAINTENANCE SCHEDULE\"\n #define END_LINE \"# END GIT MAINTENANCE SCHEDULE\"\n \n-static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n+static int crontab_update_schedule(int run_maintenance, int fd)\n {\n+\tconst char *cmd = \"crontab\";\n \tint result = 0;\n \tint in_old_region = 0;\n \tstruct child_process crontab_list = CHILD_PROCESS_INIT;\n@@ -1869,6 +1997,7 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n \tFILE *cron_list, *cron_in;\n \tstruct strbuf line = STRBUF_INIT;\n \n+\tget_schedule_cmd(&cmd, NULL);\n \tstrvec_split(&crontab_list.args, cmd);\n \tstrvec_push(&crontab_list.args, \"-l\");\n \tcrontab_list.in = -1;\n@@ -1945,66 +2074,160 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)\n \treturn result;\n }\n \n+enum scheduler {\n+\tSCHEDULER_INVALID = -1,\n+\tSCHEDULER_AUTO,\n+\tSCHEDULER_CRON,\n+\tSCHEDULER_LAUNCHCTL,\n+\tSCHEDULER_SCHTASKS,\n+};\n+\n+static const struct {\n+\tconst char *name;\n+\tint (*is_available)(void);\n+\tint (*update_schedule)(int run_maintenance, int fd);\n+} scheduler_fn[] = {\n+\t[SCHEDULER_CRON] = {\n+\t\t.name = \"crontab\",\n+\t\t.is_available = is_crontab_available,\n+\t\t.update_schedule = crontab_update_schedule,\n+\t},\n+\t[SCHEDULER_LAUNCHCTL] = {\n+\t\t.name = \"launchctl\",\n+\t\t.is_available = is_launchctl_available,\n+\t\t.update_schedule = launchctl_update_schedule,\n+\t},\n+\t[SCHEDULER_SCHTASKS] = {\n+\t\t.name = \"schtasks\",\n+\t\t.is_available = is_schtasks_available,\n+\t\t.update_schedule = schtasks_update_schedule,\n+\t},\n+};\n+\n+static enum scheduler parse_scheduler(const char *value)\n+{\n+\tif (!value)\n+\t\treturn SCHEDULER_INVALID;\n+\telse if (!strcasecmp(value, \"auto\"))\n+\t\treturn SCHEDULER_AUTO;\n+\telse if (!strcasecmp(value, \"cron\") || !strcasecmp(value, \"crontab\"))\n+\t\treturn SCHEDULER_CRON;\n+\telse if (!strcasecmp(value, \"launchctl\"))\n+\t\treturn SCHEDULER_LAUNCHCTL;\n+\telse if (!strcasecmp(value, \"schtasks\"))\n+\t\treturn SCHEDULER_SCHTASKS;\n+\telse\n+\t\treturn SCHEDULER_INVALID;\n+}\n+\n+static int maintenance_opt_scheduler(const struct option *opt, const char *arg,\n+\t\t\t\t     int unset)\n+{\n+\tenum scheduler *scheduler = opt->value;\n+\n+\tBUG_ON_OPT_NEG(unset);\n+\n+\t*scheduler = parse_scheduler(arg);\n+\tif (*scheduler == SCHEDULER_INVALID)\n+\t\treturn error(_(\"unrecognized --scheduler argument '%s'\"), arg);\n+\treturn 0;\n+}\n+\n+struct maintenance_start_opts {\n+\tenum scheduler scheduler;\n+};\n+\n+static enum scheduler resolve_scheduler(enum scheduler scheduler)\n+{\n+\tif (scheduler != SCHEDULER_AUTO)\n+\t\treturn scheduler;\n+\n #if defined(__APPLE__)\n-static const char platform_scheduler[] = \"launchctl\";\n+\treturn SCHEDULER_LAUNCHCTL;\n+\n #elif defined(GIT_WINDOWS_NATIVE)\n-static const char platform_scheduler[] = \"schtasks\";\n+\treturn SCHEDULER_SCHTASKS;\n+\n #else\n-static const char platform_scheduler[] = \"crontab\";\n+\treturn SCHEDULER_CRON;\n #endif\n+}\n \n-static int update_background_schedule(int enable)\n+static void validate_scheduler(enum scheduler scheduler)\n {\n-\tint result;\n-\tconst char *scheduler = platform_scheduler;\n-\tconst char *cmd = scheduler;\n-\tchar *testing;\n+\tif (scheduler == SCHEDULER_INVALID)\n+\t\tBUG(\"invalid scheduler\");\n+\tif (scheduler == SCHEDULER_AUTO)\n+\t\tBUG(\"resolve_scheduler should have been called before\");\n+\n+\tif (!scheduler_fn[scheduler].is_available())\n+\t\tdie(_(\"%s scheduler is not available\"),\n+\t\t    scheduler_fn[scheduler].name);\n+}\n+\n+static int update_background_schedule(const struct maintenance_start_opts *opts,\n+\t\t\t\t      int enable)\n+{\n+\tunsigned int i;\n+\tint result = 0;\n \tstruct lock_file lk;\n \tchar *lock_path = xstrfmt(\"%s/schedule\", the_repository->objects->odb->path);\n \n-\ttesting = xstrdup_or_null(getenv(\"GIT_TEST_MAINT_SCHEDULER\"));\n-\tif (testing) {\n-\t\tchar *sep = strchr(testing, ':');\n-\t\tif (!sep)\n-\t\t\tdie(\"GIT_TEST_MAINT_SCHEDULER unparseable: %s\", testing);\n-\t\t*sep = '\\0';\n-\t\tscheduler = testing;\n-\t\tcmd = sep + 1;\n+\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {\n+\t\tfree(lock_path);\n+\t\treturn error(_(\"another process is scheduling background maintenance\"));\n \t}\n \n-\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {\n-\t\tresult = error(_(\"another process is scheduling background maintenance\"));\n-\t\tgoto cleanup;\n+\tfor (i = 1; i < ARRAY_SIZE(scheduler_fn); i++) {\n+\t\tif (enable && opts->scheduler == i)\n+\t\t\tcontinue;\n+\t\tif (!scheduler_fn[i].is_available())\n+\t\t\tcontinue;\n+\t\tscheduler_fn[i].update_schedule(0, get_lock_file_fd(&lk));\n \t}\n \n-\tif (!strcmp(scheduler, \"launchctl\"))\n-\t\tresult = launchctl_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse if (!strcmp(scheduler, \"schtasks\"))\n-\t\tresult = schtasks_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse if (!strcmp(scheduler, \"crontab\"))\n-\t\tresult = crontab_update_schedule(enable, get_lock_file_fd(&lk), cmd);\n-\telse\n-\t\tdie(\"unknown background scheduler: %s\", scheduler);\n+\tif (enable)\n+\t\tresult = scheduler_fn[opts->scheduler].update_schedule(\n+\t\t\t1, get_lock_file_fd(&lk));\n \n \trollback_lock_file(&lk);\n \n-cleanup:\n \tfree(lock_path);\n-\tfree(testing);\n \treturn result;\n }\n \n-static int maintenance_start(void)\n+static const char *const builtin_maintenance_start_usage[] = {\n+\tN_(\"git maintenance start [--scheduler=<scheduler>]\"),\n+\tNULL\n+};\n+\n+static int maintenance_start(int argc, const char **argv, const char *prefix)\n {\n+\tstruct maintenance_start_opts opts = { 0 };\n+\tstruct option options[] = {\n+\t\tOPT_CALLBACK_F(\n+\t\t\t0, \"scheduler\", &opts.scheduler, N_(\"scheduler\"),\n+\t\t\tN_(\"scheduler to trigger git maintenance run\"),\n+\t\t\tPARSE_OPT_NONEG, maintenance_opt_scheduler),\n+\t\tOPT_END()\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     builtin_maintenance_start_usage, 0);\n+\tif (argc)\n+\t\tusage_with_options(builtin_maintenance_start_usage, options);\n+\n+\topts.scheduler = resolve_scheduler(opts.scheduler);\n+\tvalidate_scheduler(opts.scheduler);\n+\n \tif (maintenance_register())\n \t\twarning(_(\"failed to add repo to global config\"));\n-\n-\treturn update_background_schedule(1);\n+\treturn update_background_schedule(&opts, 1);\n }\n \n static int maintenance_stop(void)\n {\n-\treturn update_background_schedule(0);\n+\treturn update_background_schedule(NULL, 0);\n }\n \n static const char builtin_maintenance_usage[] =\tN_(\"git maintenance <subcommand> [<options>]\");\n@@ -2018,7 +2241,7 @@ int cmd_maintenance(int argc, const char **argv, const char *prefix)\n \tif (!strcmp(argv[1], \"run\"))\n \t\treturn maintenance_run(argc - 1, argv + 1, prefix);\n \tif (!strcmp(argv[1], \"start\"))\n-\t\treturn maintenance_start();\n+\t\treturn maintenance_start(argc - 1, argv + 1, prefix);\n \tif (!strcmp(argv[1], \"stop\"))\n \t\treturn maintenance_stop();\n \tif (!strcmp(argv[1], \"register\"))\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 58f46c77e6..27bce7992c 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -492,8 +492,21 @@ test_expect_success !MINGW 'register and unregister with regex metacharacters' '\n \t\tmaintenance.repo \"$(pwd)/$META\"\n '\n \n+test_expect_success 'start --scheduler=<scheduler>' '\n+\ttest_expect_code 129 git maintenance start --scheduler=foo 2>err &&\n+\ttest_i18ngrep \"unrecognized --scheduler argument\" err &&\n+\n+\ttest_expect_code 129 git maintenance start --no-scheduler 2>err &&\n+\ttest_i18ngrep \"unknown option\" err &&\n+\n+\ttest_expect_code 128 \\\n+\t\tenv GIT_TEST_MAINT_SCHEDULER=\"launchctl:true,schtasks:true\" \\\n+\t\tgit maintenance start --scheduler=crontab 2>err &&\n+\ttest_i18ngrep \"fatal: crontab scheduler is not available\" err\n+'\n+\n test_expect_success 'start from empty cron table' '\n-\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start --scheduler=crontab &&\n \n \t# start registers the repo\n \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n@@ -516,7 +529,7 @@ test_expect_success 'stop from existing schedule' '\n \n test_expect_success 'start preserves existing schedule' '\n \techo \"Important information!\" >cron.txt &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt\" git maintenance start --scheduler=crontab &&\n \tgrep \"Important information!\" cron.txt\n '\n \n@@ -545,7 +558,7 @@ test_expect_success 'start and stop macOS maintenance' '\n \tEOF\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start --scheduler=launchctl &&\n \n \t# start registers the repo\n \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n@@ -596,7 +609,7 @@ test_expect_success 'start and stop Windows maintenance' '\n \tEOF\n \n \trm -f args &&\n-\tGIT_TEST_MAINT_SCHEDULER=\"schtasks:./print-args\" git maintenance start &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"schtasks:./print-args\" git maintenance start --scheduler=schtasks &&\n \n \t# start registers the repo\n \tgit config --get --global --fixed-value maintenance.repo \"$(pwd)\" &&\n@@ -619,6 +632,40 @@ test_expect_success 'start and stop Windows maintenance' '\n \ttest_cmp expect args\n '\n \n+test_expect_success 'start and stop when several schedulers are available' '\n+\twrite_script print-args <<-\\EOF &&\n+\tprintf \"%s\\n\" \"$*\" | sed \"s:gui/[0-9][0-9]*:gui/[UID]:; s:\\(schtasks /create .* /xml\\).*:\\1:;\" >>args\n+\tEOF\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=launchctl &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >expect &&\n+\tfor frequency in hourly daily weekly\n+\tdo\n+\t\tPLIST=\"$pfx/Library/LaunchAgents/org.git-scm.git.$frequency.plist\" &&\n+\t\techo \"launchctl bootout gui/[UID] $PLIST\" >>expect &&\n+\t\techo \"launchctl bootstrap gui/[UID] $PLIST\" >>expect || return 1\n+\tdone &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance start --scheduler=schtasks &&\n+\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n+\t\thourly daily weekly >expect &&\n+\tprintf \"schtasks /create /tn Git Maintenance (%s) /f /xml\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\ttest_cmp expect args &&\n+\n+\trm -f args &&\n+\tGIT_TEST_MAINT_SCHEDULER=\"launchctl:./print-args launchctl,schtasks:./print-args schtasks\" git maintenance stop &&\n+\tprintf \"launchctl bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\\n\" \\\n+\t\thourly daily weekly >expect &&\n+\tprintf \"schtasks /delete /tn Git Maintenance (%s) /f\\n\" \\\n+\t\thourly daily weekly >>expect &&\n+\ttest_cmp expect args\n+'\n+\n test_expect_success 'register preserves existing strategy' '\n \tgit config maintenance.strategy none &&\n \tgit maintenance register &&\n-- \n2.33.0\n\n"},{"id":"434896","messageId":"28e0f22a-e9be-bb18-df0f-4d95cb6d5145@gmail.com","threadId":"55603","inReplyTo":"20210904205500.13074-1-lenaic@lhuard.fr","subject":"Re: [PATCH v10 0/3] maintenance: add support for systemd timers on Linux","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-09-07T16:48:36Z","receivedAt":"2021-09-07T16:48:41Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 9/4/2021 4:54 PM, Lénaïc Huard wrote:\n> Hello,\n> \n> Please find hereafter my updated patchset to add support for systemd\n> timers on Linux for the `git maintenance start` command.\n> \n> The only changes compared to the previous version are fixes for the\n> two typos in a comment that Ramsay Jones pointed out [1]\n> \n> [1] https://lore.kernel.org/git/51246c10-fe0b-b8e5-cdc3-54bdc6c8054e@ramsayjones.plus.com/\n\nThe changes in the most recent two versions look good to me.\n\nThank you for this contribution!\n-Stolee\n"},{"id":"435078","messageId":"4c0c1fd4-f3f4-5e3a-abd2-0350c18b18b7@gmail.com","threadId":"55603","inReplyTo":"28e0f22a-e9be-bb18-df0f-4d95cb6d5145@gmail.com","subject":"Re: [PATCH v10 0/3] maintenance: add support for systemd timers on Linux","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-09-08T11:44:26Z","receivedAt":"2021-09-08T11:44:34Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 9/7/2021 12:48 PM, Derrick Stolee wrote:\n> On 9/4/2021 4:54 PM, Lénaïc Huard wrote:\n>> Hello,\n>>\n>> Please find hereafter my updated patchset to add support for systemd\n>> timers on Linux for the `git maintenance start` command.\n>>\n>> The only changes compared to the previous version are fixes for the\n>> two typos in a comment that Ramsay Jones pointed out [1]\n>>\n>> [1] https://lore.kernel.org/git/51246c10-fe0b-b8e5-cdc3-54bdc6c8054e@ramsayjones.plus.com/\n> \n> The changes in the most recent two versions look good to me.\n\nI recently tested the 'seen' branch for an unrelated reason, but found\nthat the t7900-maintenance.sh test failed for me. It was during test 34,\n'start and stop Linux/systemd maintenance' with the following issue:\n\n  + systemd-analyze verify systemd/user/git-maintenance@.service\n  Failed to create /user.slice/user-1000.slice/session-44.scope/init.scope control group: Permission denied\n  Failed to initialize manager: Permission denied\n\nNow, this test has the prereq SYSTEMD_ANALYZE, but for some reason this\nlater command fails for permission issues. I'm running Ubuntu, if that\nhelps.\n\nThanks,\n-Stolee\n"},{"id":"435213","messageId":"1749747.rkkkC1iLqI@coruscant.lhuard.fr","threadId":"55603","inReplyTo":"4c0c1fd4-f3f4-5e3a-abd2-0350c18b18b7@gmail.com","subject":"Re: [PATCH v10 0/3] maintenance: add support for systemd timers on Linux","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-09-09T05:52:56Z","receivedAt":"2021-09-09T05:53:14Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Le mercredi 8 septembre 2021, 13:44:26 CEST Derrick Stolee a écrit :\n> On 9/7/2021 12:48 PM, Derrick Stolee wrote:\n> > On 9/4/2021 4:54 PM, Lénaïc Huard wrote:\n> >> Hello,\n> >> \n> >> Please find hereafter my updated patchset to add support for systemd\n> >> timers on Linux for the `git maintenance start` command.\n> >> \n> >> The only changes compared to the previous version are fixes for the\n> >> two typos in a comment that Ramsay Jones pointed out [1]\n> >> \n> >> [1]\n> >> https://lore.kernel.org/git/51246c10-fe0b-b8e5-cdc3-54bdc6c8054e@ramsayj\n> >> ones.plus.com/> \n> > The changes in the most recent two versions look good to me.\n> \n> I recently tested the 'seen' branch for an unrelated reason, but found\n> that the t7900-maintenance.sh test failed for me. It was during test 34,\n> 'start and stop Linux/systemd maintenance' with the following issue:\n> \n>   + systemd-analyze verify systemd/user/git-maintenance@.service\n>   Failed to create /user.slice/user-1000.slice/session-44.scope/init.scope\n> control group: Permission denied Failed to initialize manager: Permission\n> denied\n> \n> Now, this test has the prereq SYSTEMD_ANALYZE, but for some reason this\n> later command fails for permission issues. I'm running Ubuntu, if that\n> helps.\n\nThank you for the feedback.\n\nCould you please share which version of Ubuntu and which version of systemd \nyou are using ?\n\nI’ve just tried to start an Ubuntu Impish 21.10 which uses systemd \n248.3-1ubuntu3 and to test the `seen` git branch.\n\nAll tests of `t/t7900-maintenance.sh` passed including the one which is \nfailing for you.\n\nAs `systemd-analyse verify` should only check a unit file validity [1], I \nwouldn’t expect it to fail on a cgroup manipulation.\n\n[1] https://www.freedesktop.org/software/systemd/man/systemd-analyze.html#systemd-analyze%20verify%20FILE...\n\nI tried to run \nsystemd-analyze verify /etc/systemd/system/sshd.service\nand it didn’t produce the error you mentioned but if I `strace` it, I can find:\n\nmkdir(\"/sys/fs/cgroup/unified/user.slice/user-1000.slice/session-3.scope/\ninit.scope\", 0755) = -1 EACCES (Permission denied)\n\nThis makes me think your version of systemd is wrongly considering this cgroup \ndirectory failure as fatal.\nI’d like to know more precisely which versions are affected.\n\nKind regards,\nLénaïc.\n\n\n"},{"id":"435324","messageId":"0d1a8684-65c9-5229-0cd5-2e3f4a0914f3@gmail.com","threadId":"55603","inReplyTo":"1749747.rkkkC1iLqI@coruscant.lhuard.fr","subject":"Re: [PATCH v10 0/3] maintenance: add support for systemd timers on Linux","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-09-09T19:55:50Z","receivedAt":"2021-09-09T19:55:57Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 9/9/2021 1:52 AM, Lénaïc Huard wrote:\n> Le mercredi 8 septembre 2021, 13:44:26 CEST Derrick Stolee a écrit :\n>> On 9/7/2021 12:48 PM, Derrick Stolee wrote:\n>>> On 9/4/2021 4:54 PM, Lénaïc Huard wrote:\n>>>> Hello,\n>>>>\n>>>> Please find hereafter my updated patchset to add support for systemd\n>>>> timers on Linux for the `git maintenance start` command.\n>>>>\n>>>> The only changes compared to the previous version are fixes for the\n>>>> two typos in a comment that Ramsay Jones pointed out [1]\n>>>>\n>>>> [1]\n>>>> https://lore.kernel.org/git/51246c10-fe0b-b8e5-cdc3-54bdc6c8054e@ramsayj\n>>>> ones.plus.com/> \n>>> The changes in the most recent two versions look good to me.\n>>\n>> I recently tested the 'seen' branch for an unrelated reason, but found\n>> that the t7900-maintenance.sh test failed for me. It was during test 34,\n>> 'start and stop Linux/systemd maintenance' with the following issue:\n>>\n>>   + systemd-analyze verify systemd/user/git-maintenance@.service\n>>   Failed to create /user.slice/user-1000.slice/session-44.scope/init.scope\n>> control group: Permission denied Failed to initialize manager: Permission\n>> denied\n>>\n>> Now, this test has the prereq SYSTEMD_ANALYZE, but for some reason this\n>> later command fails for permission issues. I'm running Ubuntu, if that\n>> helps.\n> \n> Thank you for the feedback.\n> \n> Could you please share which version of Ubuntu and which version of systemd \n> you are using ?\n> \n> I’ve just tried to start an Ubuntu Impish 21.10 which uses systemd \n> 248.3-1ubuntu3 and to test the `seen` git branch.\n> \n> All tests of `t/t7900-maintenance.sh` passed including the one which is \n> failing for you.\n> \n> As `systemd-analyse verify` should only check a unit file validity [1], I \n> wouldn’t expect it to fail on a cgroup manipulation.\n> \n> [1] https://www.freedesktop.org/software/systemd/man/systemd-analyze.html#systemd-analyze%20verify%20FILE...\n> \n> I tried to run \n> systemd-analyze verify /etc/systemd/system/sshd.service\n> and it didn’t produce the error you mentioned but if I `strace` it, I can find:\n> \n> mkdir(\"/sys/fs/cgroup/unified/user.slice/user-1000.slice/session-3.scope/\n> init.scope\", 0755) = -1 EACCES (Permission denied)\n> \n> This makes me think your version of systemd is wrongly considering this cgroup \n> directory failure as fatal.\n> I’d like to know more precisely which versions are affected.\n I am on Ubuntu 18.04.\n\n$ systemd --version\nsystemd 237\n+PAM +AUDIT +SELINUX +IMA +APPARMOR +SMACK +SYSVINIT +UTMP +LIBCRYPTSETUP +GCRYPT +GNUTLS +ACL +XZ +LZ4 +SECCOMP +BLKID +ELFUTILS +KMOD -IDN2 +IDN -PCRE2 default-hierarchy=hybrid\n\nI tried upgrading with apt-get, but that did not get me a new\nversion.\n\nThanks,\n-Stolee\n"},{"id":"437133","messageId":"874ka618n4.fsf@evledraar.gmail.com","threadId":"55603","inReplyTo":"0d1a8684-65c9-5229-0cd5-2e3f4a0914f3@gmail.com","subject":"Re: [PATCH v10 0/3] maintenance: add support for systemd timers on Linux","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-27T12:50:28Z","receivedAt":"2021-09-27T12:54:27Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Sep 09 2021, Derrick Stolee wrote:\n\n> On 9/9/2021 1:52 AM, Lénaïc Huard wrote:\n>> Le mercredi 8 septembre 2021, 13:44:26 CEST Derrick Stolee a écrit :\n>>> On 9/7/2021 12:48 PM, Derrick Stolee wrote:\n>>>> On 9/4/2021 4:54 PM, Lénaïc Huard wrote:\n>>>>> Hello,\n>>>>>\n>>>>> Please find hereafter my updated patchset to add support for systemd\n>>>>> timers on Linux for the `git maintenance start` command.\n>>>>>\n>>>>> The only changes compared to the previous version are fixes for the\n>>>>> two typos in a comment that Ramsay Jones pointed out [1]\n>>>>>\n>>>>> [1]\n>>>>> https://lore.kernel.org/git/51246c10-fe0b-b8e5-cdc3-54bdc6c8054e@ramsayj\n>>>>> ones.plus.com/> \n>>>> The changes in the most recent two versions look good to me.\n>>>\n>>> I recently tested the 'seen' branch for an unrelated reason, but found\n>>> that the t7900-maintenance.sh test failed for me. It was during test 34,\n>>> 'start and stop Linux/systemd maintenance' with the following issue:\n>>>\n>>>   + systemd-analyze verify systemd/user/git-maintenance@.service\n>>>   Failed to create /user.slice/user-1000.slice/session-44.scope/init.scope\n>>> control group: Permission denied Failed to initialize manager: Permission\n>>> denied\n>>>\n>>> Now, this test has the prereq SYSTEMD_ANALYZE, but for some reason this\n>>> later command fails for permission issues. I'm running Ubuntu, if that\n>>> helps.\n>> \n>> Thank you for the feedback.\n>> \n>> Could you please share which version of Ubuntu and which version of systemd \n>> you are using ?\n>> \n>> I’ve just tried to start an Ubuntu Impish 21.10 which uses systemd \n>> 248.3-1ubuntu3 and to test the `seen` git branch.\n>> \n>> All tests of `t/t7900-maintenance.sh` passed including the one which is \n>> failing for you.\n>> \n>> As `systemd-analyse verify` should only check a unit file validity [1], I \n>> wouldn’t expect it to fail on a cgroup manipulation.\n>> \n>> [1] https://www.freedesktop.org/software/systemd/man/systemd-analyze.html#systemd-analyze%20verify%20FILE...\n>> \n>> I tried to run \n>> systemd-analyze verify /etc/systemd/system/sshd.service\n>> and it didn’t produce the error you mentioned but if I `strace` it, I can find:\n>> \n>> mkdir(\"/sys/fs/cgroup/unified/user.slice/user-1000.slice/session-3.scope/\n>> init.scope\", 0755) = -1 EACCES (Permission denied)\n>> \n>> This makes me think your version of systemd is wrongly considering this cgroup \n>> directory failure as fatal.\n>> I’d like to know more precisely which versions are affected.\n>  I am on Ubuntu 18.04.\n>\n> $ systemd --version\n> systemd 237\n> +PAM +AUDIT +SELINUX +IMA +APPARMOR +SMACK +SYSVINIT +UTMP\n> +LIBCRYPTSETUP +GCRYPT +GNUTLS +ACL +XZ +LZ4 +SECCOMP +BLKID +ELFUTILS\n> +KMOD -IDN2 +IDN -PCRE2 default-hierarchy=hybrid\n>\n> I tried upgrading with apt-get, but that did not get me a new\n> version.\n\nIt seems this discussion has gone stale, but this is still broken on\nsome systems. This is gcc135 on the GCC Farm, which passes the prereq\nthis commit adds:\n\n    $ systemd-analyze verify systemd/user/git-maintenance@.service\n    Failed to open /dev/tty0: Permission denied\n    Failed to load systemd/user/git-maintenance@.service: Invalid argument\n\nI don't know the systemd specifics involved, but this seems like a\nrather straightforward problem of assuming permissions that aren't\nuniversal. I.e. let's try to do that in the prereq instead?\n\nOS details, if they matter:\n    \n    [avar@gcc135 t]$ systemd-analyze --version\n    systemd 219\n    +PAM +AUDIT +SELINUX +IMA -APPARMOR +SMACK +SYSVINIT +UTMP +LIBCRYPTSETUP +GCRYPT +GNUTLS +ACL +XZ +LZ4 -SECCOMP +BLKID +ELFUTILS +KMOD +IDN\n    [avar@gcc135 t]$ cat /etc/centos-release\n    CentOS Linux release 7.9.2009 (AltArch)\n    \n"},{"id":"437198","messageId":"7292569.mAQTRl3RYs@coruscant.lhuard.fr","threadId":"55603","inReplyTo":"874ka618n4.fsf@evledraar.gmail.com","subject":"Re: [PATCH v10 0/3] maintenance: add support for systemd timers on Linux","fromName":"Lénaïc Huard","fromEmail":"lenaic@lhuard.fr","sentAt":"2021-09-27T21:44:04Z","receivedAt":"2021-09-27T21:44:49Z","isPatch":true,"sender":{"key":"lenaic@lhuard.fr","avatar":"https://avatars.githubusercontent.com/u/1437785?v=4"},"body":"Le lundi 27 septembre 2021, 14:50:28 CEST Ævar Arnfjörð Bjarmason a écrit :\n> On Thu, Sep 09 2021, Derrick Stolee wrote:\n> > On 9/9/2021 1:52 AM, Lénaïc Huard wrote:\n> >> Le mercredi 8 septembre 2021, 13:44:26 CEST Derrick Stolee a écrit :\n> >>> On 9/7/2021 12:48 PM, Derrick Stolee wrote:\n> >>>> On 9/4/2021 4:54 PM, Lénaïc Huard wrote:\n> >>>>> Hello,\n> >>>>> \n> >>>>> Please find hereafter my updated patchset to add support for systemd\n> >>>>> timers on Linux for the `git maintenance start` command.\n> >>>>> \n> >>>>> The only changes compared to the previous version are fixes for the\n> >>>>> two typos in a comment that Ramsay Jones pointed out [1]\n> >>>>> \n> >>>>> [1]\n> >>>>> https://lore.kernel.org/git/51246c10-fe0b-b8e5-cdc3-54bdc6c8054e@ramsa\n> >>>>> yj\n> >>>>> ones.plus.com/>\n> >>>> \n> >>>> The changes in the most recent two versions look good to me.\n> >>> \n> >>> I recently tested the 'seen' branch for an unrelated reason, but found\n> >>> that the t7900-maintenance.sh test failed for me. It was during test 34,\n> >>> \n> >>> 'start and stop Linux/systemd maintenance' with the following issue:\n> >>>   + systemd-analyze verify systemd/user/git-maintenance@.service\n> >>>   Failed to create\n> >>>   /user.slice/user-1000.slice/session-44.scope/init.scope\n> >>> \n> >>> control group: Permission denied Failed to initialize manager:\n> >>> Permission\n> >>> denied\n> >>> \n> >>> Now, this test has the prereq SYSTEMD_ANALYZE, but for some reason this\n> >>> later command fails for permission issues. I'm running Ubuntu, if that\n> >>> helps.\n> >> \n> >> Thank you for the feedback.\n> >> \n> >> Could you please share which version of Ubuntu and which version of\n> >> systemd\n> >> you are using ?\n> >> \n> >> I’ve just tried to start an Ubuntu Impish 21.10 which uses systemd\n> >> 248.3-1ubuntu3 and to test the `seen` git branch.\n> >> \n> >> All tests of `t/t7900-maintenance.sh` passed including the one which is\n> >> failing for you.\n> >> \n> >> As `systemd-analyse verify` should only check a unit file validity [1], I\n> >> wouldn’t expect it to fail on a cgroup manipulation.\n> >> \n> >> [1]\n> >> https://www.freedesktop.org/software/systemd/man/systemd-analyze.html#sy\n> >> stemd-analyze%20verify%20FILE...\n> >> \n> >> I tried to run\n> >> systemd-analyze verify /etc/systemd/system/sshd.service\n> >> and it didn’t produce the error you mentioned but if I `strace` it, I can\n> >> find:\n> >> \n> >> mkdir(\"/sys/fs/cgroup/unified/user.slice/user-1000.slice/session-3.scope/\n> >> init.scope\", 0755) = -1 EACCES (Permission denied)\n> >> \n> >> This makes me think your version of systemd is wrongly considering this\n> >> cgroup directory failure as fatal.\n> >> I’d like to know more precisely which versions are affected.\n> >> \n> >  I am on Ubuntu 18.04.\n> > \n> > $ systemd --version\n> > systemd 237\n> > +PAM +AUDIT +SELINUX +IMA +APPARMOR +SMACK +SYSVINIT +UTMP\n> > +LIBCRYPTSETUP +GCRYPT +GNUTLS +ACL +XZ +LZ4 +SECCOMP +BLKID +ELFUTILS\n> > +KMOD -IDN2 +IDN -PCRE2 default-hierarchy=hybrid\n> > \n> > I tried upgrading with apt-get, but that did not get me a new\n> > version.\n> \n> It seems this discussion has gone stale, but this is still broken on\n> some systems. This is gcc135 on the GCC Farm, which passes the prereq\n> this commit adds:\n> \n>     $ systemd-analyze verify systemd/user/git-maintenance@.service\n>     Failed to open /dev/tty0: Permission denied\n>     Failed to load systemd/user/git-maintenance@.service: Invalid argument\n> \n> I don't know the systemd specifics involved, but this seems like a\n> rather straightforward problem of assuming permissions that aren't\n> universal. I.e. let's try to do that in the prereq instead?\n> \n> OS details, if they matter:\n> \n>     [avar@gcc135 t]$ systemd-analyze --version\n>     systemd 219\n>     +PAM +AUDIT +SELINUX +IMA -APPARMOR +SMACK +SYSVINIT +UTMP\n> +LIBCRYPTSETUP +GCRYPT +GNUTLS +ACL +XZ +LZ4 -SECCOMP +BLKID +ELFUTILS\n> +KMOD +IDN [avar@gcc135 t]$ cat /etc/centos-release\n>     CentOS Linux release 7.9.2009 (AltArch)\n\nHello,\n\nI’ve just submitted a new patch to change the way the prerequisite is \nimplemented:\n\nhttps://lore.kernel.org/git/20210927213016.21714-2-lenaic@lhuard.fr/\n\nI’m not sure what was going wrong with old versions of systemd but with this \nnew patch, the permission errors raised by `systemd-analyze` should now make \nthe prerequisite fail instead of making the test fail.\n\n\n\n"}]}