Skip to content

Commit 2fec604

Browse files
derrickstoleegitster
authored andcommitted
maintenance: add start/stop subcommands
Add new subcommands to 'git maintenance' that start or stop background maintenance using 'cron', when available. This integration is as simple as I could make it, barring some implementation complications. The schedule is laid out as follows: 0 1-23 * * * $cmd maintenance run --schedule=hourly 0 0 * * 1-6 $cmd maintenance run --schedule=daily 0 0 * * 0 $cmd maintenance run --schedule=weekly where $cmd is a properly-qualified 'git for-each-repo' execution: $cmd=$path/git --exec-path=$path for-each-repo --config=maintenance.repo where $path points to the location of the Git executable running 'git maintenance start'. This is critical for systems with multiple versions of Git. Specifically, macOS has a system version at '/usr/bin/git' while the version that users can install resides at '/usr/local/bin/git' (symlinked to '/usr/local/libexec/git-core/git'). This will also use your locally-built version if you build and run this in your development environment without installing first. This conditional schedule avoids having cron launch multiple 'git for-each-repo' commands in parallel. Such parallel commands would likely lead to the 'hourly' and 'daily' tasks competing over the object database lock. This could lead to to some tasks never being run! Since the --schedule=<frequency> argument will run all tasks with _at least_ the given frequency, the daily runs will also run the hourly tasks. Similarly, the weekly runs will also run the daily and hourly tasks. The GIT_TEST_CRONTAB environment variable is not intended for users to edit, but instead as a way to mock the 'crontab [-l]' command. This variable is set in test-lib.sh to avoid a future test from accidentally running anything with the cron integration from modifying the user's schedule. We use GIT_TEST_CRONTAB='test-tool crontab <file>' in our tests to check how the schedule is modified in 'git maintenance (start|stop)' commands. Signed-off-by: Derrick Stolee <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 0c18b70 commit 2fec604

File tree

8 files changed

+207
-0
lines changed

8 files changed

+207
-0
lines changed

Documentation/git-maintenance.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,17 @@ run::
4545
config options are true. By default, only `maintenance.gc.enabled`
4646
is true.
4747

48+
start::
49+
Start running maintenance on the current repository. This performs
50+
the same config updates as the `register` subcommand, then updates
51+
the background scheduler to run `git maintenance run --scheduled`
52+
on an hourly basis.
53+
54+
stop::
55+
Halt the background maintenance schedule. The current repository
56+
is not removed from the list of maintained repositories, in case
57+
the background maintenance is restarted later.
58+
4859
unregister::
4960
Remove the current repository from background maintenance. This
5061
only removes the repository from the configured list. It does not

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,7 @@ TEST_BUILTINS_OBJS += test-advise.o
690690
TEST_BUILTINS_OBJS += test-bloom.o
691691
TEST_BUILTINS_OBJS += test-chmtime.o
692692
TEST_BUILTINS_OBJS += test-config.o
693+
TEST_BUILTINS_OBJS += test-crontab.o
693694
TEST_BUILTINS_OBJS += test-ctype.o
694695
TEST_BUILTINS_OBJS += test-date.o
695696
TEST_BUILTINS_OBJS += test-delta.o

builtin/gc.c

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "refs.h"
3232
#include "remote.h"
3333
#include "object-store.h"
34+
#include "exec-cmd.h"
3435

3536
#define FAILED_RUN "failed to run %s"
3637

@@ -1456,6 +1457,125 @@ static int maintenance_unregister(void)
14561457
return run_command(&config_unset);
14571458
}
14581459

1460+
#define BEGIN_LINE "# BEGIN GIT MAINTENANCE SCHEDULE"
1461+
#define END_LINE "# END GIT MAINTENANCE SCHEDULE"
1462+
1463+
static int update_background_schedule(int run_maintenance)
1464+
{
1465+
int result = 0;
1466+
int in_old_region = 0;
1467+
struct child_process crontab_list = CHILD_PROCESS_INIT;
1468+
struct child_process crontab_edit = CHILD_PROCESS_INIT;
1469+
FILE *cron_list, *cron_in;
1470+
const char *crontab_name;
1471+
struct strbuf line = STRBUF_INIT;
1472+
struct lock_file lk;
1473+
char *lock_path = xstrfmt("%s/schedule", the_repository->objects->odb->path);
1474+
1475+
if (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0)
1476+
return error(_("another process is scheduling background maintenance"));
1477+
1478+
crontab_name = getenv("GIT_TEST_CRONTAB");
1479+
if (!crontab_name)
1480+
crontab_name = "crontab";
1481+
1482+
strvec_split(&crontab_list.args, crontab_name);
1483+
strvec_push(&crontab_list.args, "-l");
1484+
crontab_list.in = -1;
1485+
crontab_list.out = dup(lk.tempfile->fd);
1486+
crontab_list.git_cmd = 0;
1487+
1488+
if (start_command(&crontab_list)) {
1489+
result = error(_("failed to run 'crontab -l'; your system might not support 'cron'"));
1490+
goto cleanup;
1491+
}
1492+
1493+
/* Ignore exit code, as an empty crontab will return error. */
1494+
finish_command(&crontab_list);
1495+
1496+
/*
1497+
* Read from the .lock file, filtering out the old
1498+
* schedule while appending the new schedule.
1499+
*/
1500+
cron_list = fdopen(lk.tempfile->fd, "r");
1501+
rewind(cron_list);
1502+
1503+
strvec_split(&crontab_edit.args, crontab_name);
1504+
crontab_edit.in = -1;
1505+
crontab_edit.git_cmd = 0;
1506+
1507+
if (start_command(&crontab_edit)) {
1508+
result = error(_("failed to run 'crontab'; your system might not support 'cron'"));
1509+
goto cleanup;
1510+
}
1511+
1512+
cron_in = fdopen(crontab_edit.in, "w");
1513+
if (!cron_in) {
1514+
result = error(_("failed to open stdin of 'crontab'"));
1515+
goto done_editing;
1516+
}
1517+
1518+
while (!strbuf_getline_lf(&line, cron_list)) {
1519+
if (!in_old_region && !strcmp(line.buf, BEGIN_LINE))
1520+
in_old_region = 1;
1521+
if (in_old_region)
1522+
continue;
1523+
fprintf(cron_in, "%s\n", line.buf);
1524+
if (in_old_region && !strcmp(line.buf, END_LINE))
1525+
in_old_region = 0;
1526+
}
1527+
1528+
if (run_maintenance) {
1529+
struct strbuf line_format = STRBUF_INIT;
1530+
const char *exec_path = git_exec_path();
1531+
1532+
fprintf(cron_in, "%s\n", BEGIN_LINE);
1533+
fprintf(cron_in,
1534+
"# The following schedule was created by Git\n");
1535+
fprintf(cron_in, "# Any edits made in this region might be\n");
1536+
fprintf(cron_in,
1537+
"# replaced in the future by a Git command.\n\n");
1538+
1539+
strbuf_addf(&line_format,
1540+
"%%s %%s * * %%s \"%s/git\" --exec-path=\"%s\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%s\n",
1541+
exec_path, exec_path);
1542+
fprintf(cron_in, line_format.buf, "0", "1-23", "*", "hourly");
1543+
fprintf(cron_in, line_format.buf, "0", "0", "1-6", "daily");
1544+
fprintf(cron_in, line_format.buf, "0", "0", "0", "weekly");
1545+
strbuf_release(&line_format);
1546+
1547+
fprintf(cron_in, "\n%s\n", END_LINE);
1548+
}
1549+
1550+
fflush(cron_in);
1551+
fclose(cron_in);
1552+
close(crontab_edit.in);
1553+
1554+
done_editing:
1555+
if (finish_command(&crontab_edit)) {
1556+
result = error(_("'crontab' died"));
1557+
goto cleanup;
1558+
}
1559+
fclose(cron_list);
1560+
1561+
cleanup:
1562+
rollback_lock_file(&lk);
1563+
return result;
1564+
}
1565+
1566+
static int maintenance_start(void)
1567+
{
1568+
if (maintenance_register())
1569+
warning(_("failed to add repo to global config"));
1570+
1571+
return update_background_schedule(1);
1572+
}
1573+
1574+
static int maintenance_stop(void)
1575+
{
1576+
return update_background_schedule(0);
1577+
}
1578+
14591579
static const char builtin_maintenance_usage[] = N_("git maintenance <subcommand> [<options>]");
14601580

14611581
int cmd_maintenance(int argc, const char **argv, const char *prefix)
@@ -1466,6 +1586,10 @@ int cmd_maintenance(int argc, const char **argv, const char *prefix)
14661586

14671587
if (!strcmp(argv[1], "run"))
14681588
return maintenance_run(argc - 1, argv + 1, prefix);
1589+
if (!strcmp(argv[1], "start"))
1590+
return maintenance_start();
1591+
if (!strcmp(argv[1], "stop"))
1592+
return maintenance_stop();
14691593
if (!strcmp(argv[1], "register"))
14701594
return maintenance_register();
14711595
if (!strcmp(argv[1], "unregister"))

t/helper/test-crontab.c

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#include "test-tool.h"
2+
#include "cache.h"
3+
4+
/*
5+
* Usage: test-tool cron <file> [-l]
6+
*
7+
* If -l is specified, then write the contents of <file> to stdout.
8+
* Otherwise, write from stdin into <file>.
9+
*/
10+
int cmd__crontab(int argc, const char **argv)
11+
{
12+
int a;
13+
FILE *from, *to;
14+
15+
if (argc == 3 && !strcmp(argv[2], "-l")) {
16+
from = fopen(argv[1], "r");
17+
if (!from)
18+
return 0;
19+
to = stdout;
20+
} else if (argc == 2) {
21+
from = stdin;
22+
to = fopen(argv[1], "w");
23+
} else
24+
return error("unknown arguments");
25+
26+
while ((a = fgetc(from)) != EOF)
27+
fputc(a, to);
28+
29+
if (argc == 3)
30+
fclose(from);
31+
else
32+
fclose(to);
33+
34+
return 0;
35+
}

t/helper/test-tool.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ static struct test_cmd cmds[] = {
1818
{ "bloom", cmd__bloom },
1919
{ "chmtime", cmd__chmtime },
2020
{ "config", cmd__config },
21+
{ "crontab", cmd__crontab },
2122
{ "ctype", cmd__ctype },
2223
{ "date", cmd__date },
2324
{ "delta", cmd__delta },

t/helper/test-tool.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ int cmd__advise_if_enabled(int argc, const char **argv);
88
int cmd__bloom(int argc, const char **argv);
99
int cmd__chmtime(int argc, const char **argv);
1010
int cmd__config(int argc, const char **argv);
11+
int cmd__crontab(int argc, const char **argv);
1112
int cmd__ctype(int argc, const char **argv);
1213
int cmd__date(int argc, const char **argv);
1314
int cmd__delta(int argc, const char **argv);

t/t7900-maintenance.sh

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,4 +315,32 @@ test_expect_success 'register and unregister' '
315315
test_cmp before actual
316316
'
317317

318+
test_expect_success 'start from empty cron table' '
319+
GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance start &&
320+
321+
# start registers the repo
322+
git config --get --global maintenance.repo "$(pwd)" &&
323+
324+
grep "for-each-repo --config=maintenance.repo maintenance run --schedule=daily" cron.txt &&
325+
grep "for-each-repo --config=maintenance.repo maintenance run --schedule=hourly" cron.txt &&
326+
grep "for-each-repo --config=maintenance.repo maintenance run --schedule=weekly" cron.txt
327+
'
328+
329+
test_expect_success 'stop from existing schedule' '
330+
GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance stop &&
331+
332+
# stop does not unregister the repo
333+
git config --get --global maintenance.repo "$(pwd)" &&
334+
335+
# Operation is idempotent
336+
GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance stop &&
337+
test_must_be_empty cron.txt
338+
'
339+
340+
test_expect_success 'start preserves existing schedule' '
341+
echo "Important information!" >cron.txt &&
342+
GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance start &&
343+
grep "Important information!" cron.txt
344+
'
345+
318346
test_done

t/test-lib.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1702,3 +1702,9 @@ test_lazy_prereq SHA1 '
17021702
test_lazy_prereq REBASE_P '
17031703
test -z "$GIT_TEST_SKIP_REBASE_P"
17041704
'
1705+
1706+
# Ensure that no test accidentally triggers a Git command
1707+
# that runs 'crontab', affecting a user's cron schedule.
1708+
# Tests that verify the cron integration must set this locally
1709+
# to avoid errors.
1710+
GIT_TEST_CRONTAB="exit 1"

0 commit comments

Comments
 (0)