@@ -316,6 +316,28 @@ https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSy
for more information.
+BACKGROUND MAINTENANCE ON WINDOWS SYSTEMS
+-----------------------------------------
+
+Windows does not support `cron` and instead has its own system for
+scheduling background tasks. The `git maintenance start` command uses
+the `schtasks` command to submit tasks to this system. You can inspect
+all background tasks using the Task Scheduler application. The tasks
+added by Git have names of the form `Git Maintenance (<frequency>)`.
+The Task Scheduler GUI has ways to inspect these tasks, but you can also
+export the tasks to XML files and view the details there.
+
+Note that since Git is a console application, these background tasks
+create a console window visible to the current user. This can be changed
+manually by selecting the "Run whether user is logged in or not" option
+in Task Scheduler. This change requires a password input, which is why
+`git maintenance start` does not select it by default.
+
+If you want to customize the background tasks, please rename the tasks
+so future calls to `git maintenance (start|stop)` do not overwrite your
+custom tasks.
+
+
GIT
---
Part of the linkgit:git[1] suite
@@ -1698,6 +1698,187 @@ static int platform_update_schedule(int run_maintenance, int fd)
else
return remove_plists();
}
+
+#elif defined(GIT_WINDOWS_NATIVE)
+
+static const char *get_frequency(enum schedule_priority schedule)
+{
+ switch (schedule) {
+ case SCHEDULE_HOURLY:
+ return "hourly";
+ case SCHEDULE_DAILY:
+ return "daily";
+ case SCHEDULE_WEEKLY:
+ return "weekly";
+ default:
+ BUG("invalid schedule %d", schedule);
+ }
+}
+
+static char *get_task_name(const char *frequency)
+{
+ struct strbuf label = STRBUF_INIT;
+ strbuf_addf(&label, "Git Maintenance (%s)", frequency);
+ return strbuf_detach(&label, NULL);
+}
+
+static int remove_task(enum schedule_priority schedule)
+{
+ int result;
+ struct strvec args = STRVEC_INIT;
+ const char *frequency = get_frequency(schedule);
+ char *name = get_task_name(frequency);
+ const char *schtasks = getenv("GIT_TEST_CRONTAB");
+ if (!schtasks)
+ schtasks = "schtasks";
+
+ strvec_split(&args, schtasks);
+ strvec_pushl(&args, "/delete", "/tn", name, "/f", NULL);
+
+ result = run_command_v_opt(args.v, 0);
+
+ strvec_clear(&args);
+ free(name);
+ return result;
+}
+
+static int remove_scheduled_tasks(void)
+{
+ return remove_task(SCHEDULE_HOURLY) ||
+ remove_task(SCHEDULE_DAILY) ||
+ remove_task(SCHEDULE_WEEKLY);
+}
+
+static int schedule_task(const char *exec_path, enum schedule_priority schedule)
+{
+ int result;
+ struct strvec args = STRVEC_INIT;
+ const char *xml, *schtasks;
+ char *xmlpath;
+ FILE *xmlfp;
+ const char *frequency = get_frequency(schedule);
+ char *name = get_task_name(frequency);
+
+ xmlpath = xstrfmt("%s/schedule-%s.xml",
+ the_repository->objects->odb->path,
+ frequency);
+ xmlfp = fopen(xmlpath, "w");
+ if (!xmlfp)
+ die(_("failed to open '%s'"), xmlpath);
+
+ xml = "<?xml version=\"1.0\" encoding=\"UTF-16\"?>\n"
+ "<Task version=\"1.4\" xmlns=\"http://schemas.microsoft.com/windows/2004/02/mit/task\">\n"
+ "<Triggers>\n"
+ "<CalendarTrigger>\n";
+ fprintf(xmlfp, xml);
+
+ switch (schedule) {
+ case SCHEDULE_HOURLY:
+ fprintf(xmlfp,
+ "<StartBoundary>2020-01-01T01:00:00</StartBoundary>\n"
+ "<Enabled>true</Enabled>\n"
+ "<ScheduleByDay>\n"
+ "<DaysInterval>1</DaysInterval>\n"
+ "</ScheduleByDay>\n"
+ "<Repetition>\n"
+ "<Interval>PT1H</Interval>\n"
+ "<Duration>PT23H</Duration>\n"
+ "<StopAtDurationEnd>false</StopAtDurationEnd>\n"
+ "</Repetition>\n");
+ break;
+
+ case SCHEDULE_DAILY:
+ fprintf(xmlfp,
+ "<StartBoundary>2020-01-01T00:00:00</StartBoundary>\n"
+ "<Enabled>true</Enabled>\n"
+ "<ScheduleByWeek>\n"
+ "<DaysOfWeek>\n"
+ "<Monday />\n"
+ "<Tuesday />\n"
+ "<Wednesday />\n"
+ "<Thursday />\n"
+ "<Friday />\n"
+ "<Saturday />\n"
+ "</DaysOfWeek>\n"
+ "<WeeksInterval>1</WeeksInterval>\n"
+ "</ScheduleByWeek>\n");
+ break;
+
+ case SCHEDULE_WEEKLY:
+ fprintf(xmlfp,
+ "<StartBoundary>2020-01-01T00:00:00</StartBoundary>\n"
+ "<Enabled>true</Enabled>\n"
+ "<ScheduleByWeek>\n"
+ "<DaysOfWeek>\n"
+ "<Sunday />\n"
+ "</DaysOfWeek>\n"
+ "<WeeksInterval>1</WeeksInterval>\n"
+ "</ScheduleByWeek>\n");
+ break;
+
+ default:
+ break;
+ }
+
+ xml= "</CalendarTrigger>\n"
+ "</Triggers>\n"
+ "<Principals>\n"
+ "<Principal id=\"Author\">\n"
+ "<LogonType>InteractiveToken</LogonType>\n"
+ "<RunLevel>LeastPrivilege</RunLevel>\n"
+ "</Principal>\n"
+ "</Principals>\n"
+ "<Settings>\n"
+ "<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>\n"
+ "<Enabled>true</Enabled>\n"
+ "<Hidden>true</Hidden>\n"
+ "<UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>\n"
+ "<WakeToRun>false</WakeToRun>\n"
+ "<ExecutionTimeLimit>PT72H</ExecutionTimeLimit>\n"
+ "<Priority>7</Priority>\n"
+ "</Settings>\n"
+ "<Actions Context=\"Author\">\n"
+ "<Exec>\n"
+ "<Command>\"%s\\git.exe\"</Command>\n"
+ "<Arguments>--exec-path=\"%s\" for-each-repo --config=maintenance.repo maintenance run --schedule=%s</Arguments>\n"
+ "</Exec>\n"
+ "</Actions>\n"
+ "</Task>\n";
+ fprintf(xmlfp, xml, exec_path, exec_path, frequency);
+ fclose(xmlfp);
+
+ schtasks = getenv("GIT_TEST_CRONTAB");
+ if (!schtasks)
+ schtasks = "schtasks";
+ strvec_split(&args, schtasks);
+ strvec_pushl(&args, "/create", "/tn", name, "/f", "/xml", xmlpath, NULL);
+
+ result = run_command_v_opt(args.v, 0);
+
+ strvec_clear(&args);
+ unlink(xmlpath);
+ free(xmlpath);
+ free(name);
+ return result;
+}
+
+static int add_scheduled_tasks(void)
+{
+ const char *exec_path = git_exec_path();
+
+ return schedule_task(exec_path, SCHEDULE_HOURLY) ||
+ schedule_task(exec_path, SCHEDULE_DAILY) ||
+ schedule_task(exec_path, SCHEDULE_WEEKLY);
+}
+
+static int platform_update_schedule(int run_maintenance, int fd)
+{
+ if (run_maintenance)
+ return add_scheduled_tasks();
+ else
+ return remove_scheduled_tasks();
+}
+
#else
#define BEGIN_LINE "# BEGIN GIT MAINTENANCE SCHEDULE"
#define END_LINE "# END GIT MAINTENANCE SCHEDULE"
@@ -367,7 +367,7 @@ test_expect_success 'register and unregister' '
test_cmp before actual
'
-test_expect_success !MACOS_MAINTENANCE 'start from empty cron table' '
+test_expect_success !MACOS_MAINTENANCE,!MINGW 'start from empty cron table' '
GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance start &&
# start registers the repo
@@ -378,7 +378,7 @@ test_expect_success !MACOS_MAINTENANCE 'start from empty cron table' '
grep "for-each-repo --config=maintenance.repo maintenance run --schedule=weekly" cron.txt
'
-test_expect_success !MACOS_MAINTENANCE 'stop from existing schedule' '
+test_expect_success !MACOS_MAINTENANCE,!MINGW 'stop from existing schedule' '
GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance stop &&
# stop does not unregister the repo
@@ -389,7 +389,7 @@ test_expect_success !MACOS_MAINTENANCE 'stop from existing schedule' '
test_must_be_empty cron.txt
'
-test_expect_success !MACOS_MAINTENANCE 'start preserves existing schedule' '
+test_expect_success !MACOS_MAINTENANCE,!MINGW 'start preserves existing schedule' '
echo "Important information!" >cron.txt &&
GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance start &&
grep "Important information!" cron.txt
@@ -442,6 +442,36 @@ test_expect_success MACOS_MAINTENANCE 'start and stop macOS maintenance' '
test_cmp expect args
'
+test_expect_success MINGW 'start and stop Windows maintenance' '
+ write_script print-args <<-\EOF &&
+ echo $* >>args
+ EOF
+
+ rm -f args &&
+ GIT_TEST_CRONTAB="/bin/sh print-args" git maintenance start &&
+
+ # start registers the repo
+ git config --get --global maintenance.repo "$(pwd)" &&
+
+ for frequency in hourly daily weekly
+ do
+ printf "/create /tn Git Maintenance (%s) /f /xml .git/objects/schedule-%s.xml\n" \
+ $frequency $frequency
+ done >expect &&
+ test_cmp expect args &&
+
+ rm -f args &&
+ GIT_TEST_CRONTAB="/bin/sh print-args" git maintenance stop &&
+
+ # stop does not unregister the repo
+ git config --get --global maintenance.repo "$(pwd)" &&
+
+ rm expect &&
+ printf "/delete /tn Git Maintenance (%s) /f\n" \
+ hourly daily weekly >expect &&
+ test_cmp expect args
+'
+
test_expect_success 'register preserves existing strategy' '
git config maintenance.strategy none &&
git maintenance register &&