mirror of
https://github.com/git/git.git
synced 2024-12-12 03:14:11 +08:00
Merge branch 'ds/maintenance-part-4'
Follow-up on the "maintenance part-3" which introduced scheduled maintenance tasks to support platforms whose native scheduling methods are not 'cron'. * ds/maintenance-part-4: maintenance: use Windows scheduled tasks maintenance: use launchctl on macOS maintenance: include 'cron' details in docs maintenance: extract platform-specific scheduling
This commit is contained in:
commit
b2ace18759
@ -218,6 +218,122 @@ Further, the `git gc` command should not be combined with
|
||||
but does not take the lock in the same way as `git maintenance run`. If
|
||||
possible, use `git maintenance run --task=gc` instead of `git gc`.
|
||||
|
||||
The following sections describe the mechanisms put in place to run
|
||||
background maintenance by `git maintenance start` and how to customize
|
||||
them.
|
||||
|
||||
BACKGROUND MAINTENANCE ON POSIX SYSTEMS
|
||||
---------------------------------------
|
||||
|
||||
The standard mechanism for scheduling background tasks on POSIX systems
|
||||
is cron(8). This tool executes commands based on a given schedule. The
|
||||
current list of user-scheduled tasks can be found by running `crontab -l`.
|
||||
The schedule written by `git maintenance start` is similar to this:
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
# BEGIN GIT MAINTENANCE SCHEDULE
|
||||
# The following schedule was created by Git
|
||||
# Any edits made in this region might be
|
||||
# replaced in the future by a Git command.
|
||||
|
||||
0 1-23 * * * "/<path>/git" --exec-path="/<path>" for-each-repo --config=maintenance.repo maintenance run --schedule=hourly
|
||||
0 0 * * 1-6 "/<path>/git" --exec-path="/<path>" for-each-repo --config=maintenance.repo maintenance run --schedule=daily
|
||||
0 0 * * 0 "/<path>/git" --exec-path="/<path>" for-each-repo --config=maintenance.repo maintenance run --schedule=weekly
|
||||
|
||||
# END GIT MAINTENANCE SCHEDULE
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
The comments are used as a region to mark the schedule as written by Git.
|
||||
Any modifications within this region will be completely deleted by
|
||||
`git maintenance stop` or overwritten by `git maintenance start`.
|
||||
|
||||
The `crontab` entry specifies the full path of the `git` executable to
|
||||
ensure that the executed `git` command is the same one with which
|
||||
`git maintenance start` was issued independent of `PATH`. If the same user
|
||||
runs `git maintenance start` with multiple Git executables, then only the
|
||||
latest executable is used.
|
||||
|
||||
These commands use `git for-each-repo --config=maintenance.repo` to run
|
||||
`git maintenance run --schedule=<frequency>` on each repository listed in
|
||||
the multi-valued `maintenance.repo` config option. These are typically
|
||||
loaded from the user-specific global config. The `git maintenance` process
|
||||
then determines which maintenance tasks are configured to run on each
|
||||
repository with each `<frequency>` using the `maintenance.<task>.schedule`
|
||||
config options. These values are loaded from the global or repository
|
||||
config values.
|
||||
|
||||
If the config values are insufficient to achieve your desired background
|
||||
maintenance schedule, then you can create your own schedule. If you run
|
||||
`crontab -e`, then an editor will load with your user-specific `cron`
|
||||
schedule. In that editor, you can add your own schedule lines. You could
|
||||
start by adapting the default schedule listed earlier, or you could read
|
||||
the crontab(5) documentation for advanced scheduling techniques. Please
|
||||
do use the full path and `--exec-path` techniques from the default
|
||||
schedule to ensure you are executing the correct binaries in your
|
||||
schedule.
|
||||
|
||||
|
||||
BACKGROUND MAINTENANCE ON MACOS SYSTEMS
|
||||
---------------------------------------
|
||||
|
||||
While macOS technically supports `cron`, using `crontab -e` requires
|
||||
elevated privileges and the executed process does not have a full user
|
||||
context. Without a full user context, Git and its credential helpers
|
||||
cannot access stored credentials, so some maintenance tasks are not
|
||||
functional.
|
||||
|
||||
Instead, `git maintenance start` interacts with the `launchctl` tool,
|
||||
which is the recommended way to schedule timed jobs in macOS. Scheduling
|
||||
maintenance through `git maintenance (start|stop)` requires some
|
||||
`launchctl` features available only in macOS 10.11 or later.
|
||||
|
||||
Your user-specific scheduled tasks are stored as XML-formatted `.plist`
|
||||
files in `~/Library/LaunchAgents/`. You can see the currently-registered
|
||||
tasks using the following command:
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
$ ls ~/Library/LaunchAgents/org.git-scm.git*
|
||||
org.git-scm.git.daily.plist
|
||||
org.git-scm.git.hourly.plist
|
||||
org.git-scm.git.weekly.plist
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
One task is registered for each `--schedule=<frequency>` option. To
|
||||
inspect how the XML format describes each schedule, open one of these
|
||||
`.plist` files in an editor and inspect the `<array>` element following
|
||||
the `<key>StartCalendarInterval</key>` element.
|
||||
|
||||
`git maintenance start` will overwrite these files and register the
|
||||
tasks again with `launchctl`, so any customizations should be done by
|
||||
creating your own `.plist` files with distinct names. Similarly, the
|
||||
`git maintenance stop` command will unregister the tasks with `launchctl`
|
||||
and delete the `.plist` files.
|
||||
|
||||
To create more advanced customizations to your background tasks, see
|
||||
launchctl.plist(5) 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
|
||||
---
|
||||
|
424
builtin/gc.c
424
builtin/gc.c
@ -1493,38 +1493,368 @@ static int maintenance_unregister(void)
|
||||
return run_command(&config_unset);
|
||||
}
|
||||
|
||||
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 *launchctl_service_name(const char *frequency)
|
||||
{
|
||||
struct strbuf label = STRBUF_INIT;
|
||||
strbuf_addf(&label, "org.git-scm.git.%s", frequency);
|
||||
return strbuf_detach(&label, NULL);
|
||||
}
|
||||
|
||||
static char *launchctl_service_filename(const char *name)
|
||||
{
|
||||
char *expanded;
|
||||
struct strbuf filename = STRBUF_INIT;
|
||||
strbuf_addf(&filename, "~/Library/LaunchAgents/%s.plist", name);
|
||||
|
||||
expanded = expand_user_path(filename.buf, 1);
|
||||
if (!expanded)
|
||||
die(_("failed to expand path '%s'"), filename.buf);
|
||||
|
||||
strbuf_release(&filename);
|
||||
return expanded;
|
||||
}
|
||||
|
||||
static char *launchctl_get_uid(void)
|
||||
{
|
||||
return xstrfmt("gui/%d", getuid());
|
||||
}
|
||||
|
||||
static int launchctl_boot_plist(int enable, const char *filename, const char *cmd)
|
||||
{
|
||||
int result;
|
||||
struct child_process child = CHILD_PROCESS_INIT;
|
||||
char *uid = launchctl_get_uid();
|
||||
|
||||
strvec_split(&child.args, cmd);
|
||||
if (enable)
|
||||
strvec_push(&child.args, "bootstrap");
|
||||
else
|
||||
strvec_push(&child.args, "bootout");
|
||||
strvec_push(&child.args, uid);
|
||||
strvec_push(&child.args, filename);
|
||||
|
||||
child.no_stderr = 1;
|
||||
child.no_stdout = 1;
|
||||
|
||||
if (start_command(&child))
|
||||
die(_("failed to start launchctl"));
|
||||
|
||||
result = finish_command(&child);
|
||||
|
||||
free(uid);
|
||||
return result;
|
||||
}
|
||||
|
||||
static int launchctl_remove_plist(enum schedule_priority schedule, const char *cmd)
|
||||
{
|
||||
const char *frequency = get_frequency(schedule);
|
||||
char *name = launchctl_service_name(frequency);
|
||||
char *filename = launchctl_service_filename(name);
|
||||
int result = launchctl_boot_plist(0, filename, cmd);
|
||||
unlink(filename);
|
||||
free(filename);
|
||||
free(name);
|
||||
return result;
|
||||
}
|
||||
|
||||
static int launchctl_remove_plists(const char *cmd)
|
||||
{
|
||||
return launchctl_remove_plist(SCHEDULE_HOURLY, cmd) ||
|
||||
launchctl_remove_plist(SCHEDULE_DAILY, cmd) ||
|
||||
launchctl_remove_plist(SCHEDULE_WEEKLY, cmd);
|
||||
}
|
||||
|
||||
static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule, const char *cmd)
|
||||
{
|
||||
FILE *plist;
|
||||
int i;
|
||||
const char *preamble, *repeat;
|
||||
const char *frequency = get_frequency(schedule);
|
||||
char *name = launchctl_service_name(frequency);
|
||||
char *filename = launchctl_service_filename(name);
|
||||
|
||||
if (safe_create_leading_directories(filename))
|
||||
die(_("failed to create directories for '%s'"), filename);
|
||||
plist = xfopen(filename, "w");
|
||||
|
||||
preamble = "<?xml version=\"1.0\"?>\n"
|
||||
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
|
||||
"<plist version=\"1.0\">"
|
||||
"<dict>\n"
|
||||
"<key>Label</key><string>%s</string>\n"
|
||||
"<key>ProgramArguments</key>\n"
|
||||
"<array>\n"
|
||||
"<string>%s/git</string>\n"
|
||||
"<string>--exec-path=%s</string>\n"
|
||||
"<string>for-each-repo</string>\n"
|
||||
"<string>--config=maintenance.repo</string>\n"
|
||||
"<string>maintenance</string>\n"
|
||||
"<string>run</string>\n"
|
||||
"<string>--schedule=%s</string>\n"
|
||||
"</array>\n"
|
||||
"<key>StartCalendarInterval</key>\n"
|
||||
"<array>\n";
|
||||
fprintf(plist, preamble, name, exec_path, exec_path, frequency);
|
||||
|
||||
switch (schedule) {
|
||||
case SCHEDULE_HOURLY:
|
||||
repeat = "<dict>\n"
|
||||
"<key>Hour</key><integer>%d</integer>\n"
|
||||
"<key>Minute</key><integer>0</integer>\n"
|
||||
"</dict>\n";
|
||||
for (i = 1; i <= 23; i++)
|
||||
fprintf(plist, repeat, i);
|
||||
break;
|
||||
|
||||
case SCHEDULE_DAILY:
|
||||
repeat = "<dict>\n"
|
||||
"<key>Day</key><integer>%d</integer>\n"
|
||||
"<key>Hour</key><integer>0</integer>\n"
|
||||
"<key>Minute</key><integer>0</integer>\n"
|
||||
"</dict>\n";
|
||||
for (i = 1; i <= 6; i++)
|
||||
fprintf(plist, repeat, i);
|
||||
break;
|
||||
|
||||
case SCHEDULE_WEEKLY:
|
||||
fprintf(plist,
|
||||
"<dict>\n"
|
||||
"<key>Day</key><integer>0</integer>\n"
|
||||
"<key>Hour</key><integer>0</integer>\n"
|
||||
"<key>Minute</key><integer>0</integer>\n"
|
||||
"</dict>\n");
|
||||
break;
|
||||
|
||||
default:
|
||||
/* unreachable */
|
||||
break;
|
||||
}
|
||||
fprintf(plist, "</array>\n</dict>\n</plist>\n");
|
||||
fclose(plist);
|
||||
|
||||
/* bootout might fail if not already running, so ignore */
|
||||
launchctl_boot_plist(0, filename, cmd);
|
||||
if (launchctl_boot_plist(1, filename, cmd))
|
||||
die(_("failed to bootstrap service %s"), filename);
|
||||
|
||||
free(filename);
|
||||
free(name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int launchctl_add_plists(const char *cmd)
|
||||
{
|
||||
const char *exec_path = git_exec_path();
|
||||
|
||||
return launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY, cmd) ||
|
||||
launchctl_schedule_plist(exec_path, SCHEDULE_DAILY, cmd) ||
|
||||
launchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY, cmd);
|
||||
}
|
||||
|
||||
static int launchctl_update_schedule(int run_maintenance, int fd, const char *cmd)
|
||||
{
|
||||
if (run_maintenance)
|
||||
return launchctl_add_plists(cmd);
|
||||
else
|
||||
return launchctl_remove_plists(cmd);
|
||||
}
|
||||
|
||||
static char *schtasks_task_name(const char *frequency)
|
||||
{
|
||||
struct strbuf label = STRBUF_INIT;
|
||||
strbuf_addf(&label, "Git Maintenance (%s)", frequency);
|
||||
return strbuf_detach(&label, NULL);
|
||||
}
|
||||
|
||||
static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd)
|
||||
{
|
||||
int result;
|
||||
struct strvec args = STRVEC_INIT;
|
||||
const char *frequency = get_frequency(schedule);
|
||||
char *name = schtasks_task_name(frequency);
|
||||
|
||||
strvec_split(&args, cmd);
|
||||
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 schtasks_remove_tasks(const char *cmd)
|
||||
{
|
||||
return schtasks_remove_task(SCHEDULE_HOURLY, cmd) ||
|
||||
schtasks_remove_task(SCHEDULE_DAILY, cmd) ||
|
||||
schtasks_remove_task(SCHEDULE_WEEKLY, cmd);
|
||||
}
|
||||
|
||||
static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule, const char *cmd)
|
||||
{
|
||||
int result;
|
||||
struct child_process child = CHILD_PROCESS_INIT;
|
||||
const char *xml;
|
||||
struct tempfile *tfile;
|
||||
const char *frequency = get_frequency(schedule);
|
||||
char *name = schtasks_task_name(frequency);
|
||||
struct strbuf tfilename = STRBUF_INIT;
|
||||
|
||||
strbuf_addf(&tfilename, "%s/schedule_%s_XXXXXX",
|
||||
get_git_common_dir(), frequency);
|
||||
tfile = xmks_tempfile(tfilename.buf);
|
||||
strbuf_release(&tfilename);
|
||||
|
||||
if (!fdopen_tempfile(tfile, "w"))
|
||||
die(_("failed to create temp xml file"));
|
||||
|
||||
xml = "<?xml version=\"1.0\" ?>\n"
|
||||
"<Task version=\"1.4\" xmlns=\"http://schemas.microsoft.com/windows/2004/02/mit/task\">\n"
|
||||
"<Triggers>\n"
|
||||
"<CalendarTrigger>\n";
|
||||
fputs(xml, tfile->fp);
|
||||
|
||||
switch (schedule) {
|
||||
case SCHEDULE_HOURLY:
|
||||
fprintf(tfile->fp,
|
||||
"<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(tfile->fp,
|
||||
"<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(tfile->fp,
|
||||
"<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(tfile->fp, xml, exec_path, exec_path, frequency);
|
||||
strvec_split(&child.args, cmd);
|
||||
strvec_pushl(&child.args, "/create", "/tn", name, "/f", "/xml",
|
||||
get_tempfile_path(tfile), NULL);
|
||||
close_tempfile_gently(tfile);
|
||||
|
||||
child.no_stdout = 1;
|
||||
child.no_stderr = 1;
|
||||
|
||||
if (start_command(&child))
|
||||
die(_("failed to start schtasks"));
|
||||
result = finish_command(&child);
|
||||
|
||||
delete_tempfile(&tfile);
|
||||
free(name);
|
||||
return result;
|
||||
}
|
||||
|
||||
static int schtasks_schedule_tasks(const char *cmd)
|
||||
{
|
||||
const char *exec_path = git_exec_path();
|
||||
|
||||
return schtasks_schedule_task(exec_path, SCHEDULE_HOURLY, cmd) ||
|
||||
schtasks_schedule_task(exec_path, SCHEDULE_DAILY, cmd) ||
|
||||
schtasks_schedule_task(exec_path, SCHEDULE_WEEKLY, cmd);
|
||||
}
|
||||
|
||||
static int schtasks_update_schedule(int run_maintenance, int fd, const char *cmd)
|
||||
{
|
||||
if (run_maintenance)
|
||||
return schtasks_schedule_tasks(cmd);
|
||||
else
|
||||
return schtasks_remove_tasks(cmd);
|
||||
}
|
||||
|
||||
#define BEGIN_LINE "# BEGIN GIT MAINTENANCE SCHEDULE"
|
||||
#define END_LINE "# END GIT MAINTENANCE SCHEDULE"
|
||||
|
||||
static int update_background_schedule(int run_maintenance)
|
||||
static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)
|
||||
{
|
||||
int result = 0;
|
||||
int in_old_region = 0;
|
||||
struct child_process crontab_list = CHILD_PROCESS_INIT;
|
||||
struct child_process crontab_edit = CHILD_PROCESS_INIT;
|
||||
FILE *cron_list, *cron_in;
|
||||
const char *crontab_name;
|
||||
struct strbuf line = STRBUF_INIT;
|
||||
struct lock_file lk;
|
||||
char *lock_path = xstrfmt("%s/schedule", the_repository->objects->odb->path);
|
||||
|
||||
if (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0)
|
||||
return error(_("another process is scheduling background maintenance"));
|
||||
|
||||
crontab_name = getenv("GIT_TEST_CRONTAB");
|
||||
if (!crontab_name)
|
||||
crontab_name = "crontab";
|
||||
|
||||
strvec_split(&crontab_list.args, crontab_name);
|
||||
strvec_split(&crontab_list.args, cmd);
|
||||
strvec_push(&crontab_list.args, "-l");
|
||||
crontab_list.in = -1;
|
||||
crontab_list.out = dup(lk.tempfile->fd);
|
||||
crontab_list.out = dup(fd);
|
||||
crontab_list.git_cmd = 0;
|
||||
|
||||
if (start_command(&crontab_list)) {
|
||||
result = error(_("failed to run 'crontab -l'; your system might not support 'cron'"));
|
||||
goto cleanup;
|
||||
}
|
||||
if (start_command(&crontab_list))
|
||||
return error(_("failed to run 'crontab -l'; your system might not support 'cron'"));
|
||||
|
||||
/* Ignore exit code, as an empty crontab will return error. */
|
||||
finish_command(&crontab_list);
|
||||
@ -1533,17 +1863,15 @@ static int update_background_schedule(int run_maintenance)
|
||||
* Read from the .lock file, filtering out the old
|
||||
* schedule while appending the new schedule.
|
||||
*/
|
||||
cron_list = fdopen(lk.tempfile->fd, "r");
|
||||
cron_list = fdopen(fd, "r");
|
||||
rewind(cron_list);
|
||||
|
||||
strvec_split(&crontab_edit.args, crontab_name);
|
||||
strvec_split(&crontab_edit.args, cmd);
|
||||
crontab_edit.in = -1;
|
||||
crontab_edit.git_cmd = 0;
|
||||
|
||||
if (start_command(&crontab_edit)) {
|
||||
result = error(_("failed to run 'crontab'; your system might not support 'cron'"));
|
||||
goto cleanup;
|
||||
}
|
||||
if (start_command(&crontab_edit))
|
||||
return error(_("failed to run 'crontab'; your system might not support 'cron'"));
|
||||
|
||||
cron_in = fdopen(crontab_edit.in, "w");
|
||||
if (!cron_in) {
|
||||
@ -1587,14 +1915,54 @@ static int update_background_schedule(int run_maintenance)
|
||||
close(crontab_edit.in);
|
||||
|
||||
done_editing:
|
||||
if (finish_command(&crontab_edit)) {
|
||||
if (finish_command(&crontab_edit))
|
||||
result = error(_("'crontab' died"));
|
||||
goto cleanup;
|
||||
}
|
||||
fclose(cron_list);
|
||||
else
|
||||
fclose(cron_list);
|
||||
return result;
|
||||
}
|
||||
|
||||
#if defined(__APPLE__)
|
||||
static const char platform_scheduler[] = "launchctl";
|
||||
#elif defined(GIT_WINDOWS_NATIVE)
|
||||
static const char platform_scheduler[] = "schtasks";
|
||||
#else
|
||||
static const char platform_scheduler[] = "crontab";
|
||||
#endif
|
||||
|
||||
static int update_background_schedule(int enable)
|
||||
{
|
||||
int result;
|
||||
const char *scheduler = platform_scheduler;
|
||||
const char *cmd = scheduler;
|
||||
char *testing;
|
||||
struct lock_file lk;
|
||||
char *lock_path = xstrfmt("%s/schedule", the_repository->objects->odb->path);
|
||||
|
||||
testing = xstrdup_or_null(getenv("GIT_TEST_MAINT_SCHEDULER"));
|
||||
if (testing) {
|
||||
char *sep = strchr(testing, ':');
|
||||
if (!sep)
|
||||
die("GIT_TEST_MAINT_SCHEDULER unparseable: %s", testing);
|
||||
*sep = '\0';
|
||||
scheduler = testing;
|
||||
cmd = sep + 1;
|
||||
}
|
||||
|
||||
if (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0)
|
||||
return error(_("another process is scheduling background maintenance"));
|
||||
|
||||
if (!strcmp(scheduler, "launchctl"))
|
||||
result = launchctl_update_schedule(enable, lk.tempfile->fd, cmd);
|
||||
else if (!strcmp(scheduler, "schtasks"))
|
||||
result = schtasks_update_schedule(enable, lk.tempfile->fd, cmd);
|
||||
else if (!strcmp(scheduler, "crontab"))
|
||||
result = crontab_update_schedule(enable, lk.tempfile->fd, cmd);
|
||||
else
|
||||
die("unknown background scheduler: %s", scheduler);
|
||||
|
||||
cleanup:
|
||||
rollback_lock_file(&lk);
|
||||
free(testing);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,19 @@ test_description='git maintenance builtin'
|
||||
GIT_TEST_COMMIT_GRAPH=0
|
||||
GIT_TEST_MULTI_PACK_INDEX=0
|
||||
|
||||
test_lazy_prereq XMLLINT '
|
||||
xmllint --version
|
||||
'
|
||||
|
||||
test_xmllint () {
|
||||
if test_have_prereq XMLLINT
|
||||
then
|
||||
xmllint --noout "$@"
|
||||
else
|
||||
true
|
||||
fi
|
||||
}
|
||||
|
||||
test_expect_success 'help text' '
|
||||
test_expect_code 129 git maintenance -h 2>err &&
|
||||
test_i18ngrep "usage: git maintenance <subcommand>" err &&
|
||||
@ -419,7 +432,7 @@ test_expect_success !MINGW 'register and unregister with regex metacharacters' '
|
||||
'
|
||||
|
||||
test_expect_success 'start from empty cron table' '
|
||||
GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance start &&
|
||||
GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab cron.txt" git maintenance start &&
|
||||
|
||||
# start registers the repo
|
||||
git config --get --global --fixed-value maintenance.repo "$(pwd)" &&
|
||||
@ -430,19 +443,19 @@ test_expect_success 'start from empty cron table' '
|
||||
'
|
||||
|
||||
test_expect_success 'stop from existing schedule' '
|
||||
GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance stop &&
|
||||
GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab cron.txt" git maintenance stop &&
|
||||
|
||||
# stop does not unregister the repo
|
||||
git config --get --global --fixed-value maintenance.repo "$(pwd)" &&
|
||||
|
||||
# Operation is idempotent
|
||||
GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance stop &&
|
||||
GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab cron.txt" git maintenance stop &&
|
||||
test_must_be_empty cron.txt
|
||||
'
|
||||
|
||||
test_expect_success 'start preserves existing schedule' '
|
||||
echo "Important information!" >cron.txt &&
|
||||
GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance start &&
|
||||
GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab cron.txt" git maintenance start &&
|
||||
grep "Important information!" cron.txt
|
||||
'
|
||||
|
||||
@ -457,11 +470,94 @@ test_expect_success 'magic markers are correct' '
|
||||
|
||||
test_expect_success 'stop preserves surrounding schedule' '
|
||||
echo "Crucial information!" >>cron.txt &&
|
||||
GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance stop &&
|
||||
GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab cron.txt" git maintenance stop &&
|
||||
grep "Important information!" cron.txt &&
|
||||
grep "Crucial information!" cron.txt
|
||||
'
|
||||
|
||||
test_expect_success 'start and stop macOS maintenance' '
|
||||
# ensure $HOME can be compared against hook arguments on all platforms
|
||||
pfx=$(cd "$HOME" && pwd) &&
|
||||
|
||||
write_script print-args <<-\EOF &&
|
||||
echo $* | sed "s:gui/[0-9][0-9]*:gui/[UID]:" >>args
|
||||
EOF
|
||||
|
||||
rm -f args &&
|
||||
GIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start &&
|
||||
|
||||
# start registers the repo
|
||||
git config --get --global --fixed-value maintenance.repo "$(pwd)" &&
|
||||
|
||||
ls "$HOME/Library/LaunchAgents" >actual &&
|
||||
cat >expect <<-\EOF &&
|
||||
org.git-scm.git.daily.plist
|
||||
org.git-scm.git.hourly.plist
|
||||
org.git-scm.git.weekly.plist
|
||||
EOF
|
||||
test_cmp expect actual &&
|
||||
|
||||
rm -f expect &&
|
||||
for frequency in hourly daily weekly
|
||||
do
|
||||
PLIST="$pfx/Library/LaunchAgents/org.git-scm.git.$frequency.plist" &&
|
||||
test_xmllint "$PLIST" &&
|
||||
grep schedule=$frequency "$PLIST" &&
|
||||
echo "bootout gui/[UID] $PLIST" >>expect &&
|
||||
echo "bootstrap gui/[UID] $PLIST" >>expect || return 1
|
||||
done &&
|
||||
test_cmp expect args &&
|
||||
|
||||
rm -f args &&
|
||||
GIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance stop &&
|
||||
|
||||
# stop does not unregister the repo
|
||||
git config --get --global --fixed-value maintenance.repo "$(pwd)" &&
|
||||
|
||||
printf "bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\n" \
|
||||
hourly daily weekly >expect &&
|
||||
test_cmp expect args &&
|
||||
ls "$HOME/Library/LaunchAgents" >actual &&
|
||||
test_line_count = 0 actual
|
||||
'
|
||||
|
||||
test_expect_success 'start and stop Windows maintenance' '
|
||||
write_script print-args <<-\EOF &&
|
||||
echo $* >>args
|
||||
while test $# -gt 0
|
||||
do
|
||||
case "$1" in
|
||||
/xml) shift; xmlfile=$1; break ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
test -z "$xmlfile" || cp "$xmlfile" "$xmlfile.xml"
|
||||
EOF
|
||||
|
||||
rm -f args &&
|
||||
GIT_TEST_MAINT_SCHEDULER="schtasks:./print-args" git maintenance start &&
|
||||
|
||||
# start registers the repo
|
||||
git config --get --global --fixed-value maintenance.repo "$(pwd)" &&
|
||||
|
||||
for frequency in hourly daily weekly
|
||||
do
|
||||
grep "/create /tn Git Maintenance ($frequency) /f /xml" args &&
|
||||
file=$(ls .git/schedule_${frequency}*.xml) &&
|
||||
test_xmllint "$file" || return 1
|
||||
done &&
|
||||
|
||||
rm -f args &&
|
||||
GIT_TEST_MAINT_SCHEDULER="schtasks:./print-args" git maintenance stop &&
|
||||
|
||||
# stop does not unregister the repo
|
||||
git config --get --global --fixed-value maintenance.repo "$(pwd)" &&
|
||||
|
||||
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 &&
|
||||
|
@ -1713,7 +1713,8 @@ test_lazy_prereq REBASE_P '
|
||||
'
|
||||
|
||||
# Ensure that no test accidentally triggers a Git command
|
||||
# that runs 'crontab', affecting a user's cron schedule.
|
||||
# Tests that verify the cron integration must set this locally
|
||||
# that runs the actual maintenance scheduler, affecting a user's
|
||||
# system permanently.
|
||||
# Tests that verify the scheduler integration must set this locally
|
||||
# to avoid errors.
|
||||
GIT_TEST_CRONTAB="exit 1"
|
||||
GIT_TEST_MAINT_SCHEDULER="none:exit 1"
|
||||
|
Loading…
Reference in New Issue
Block a user