Skip to content

Commit be78607

Browse files
committed
maintenance: add start/stop subcommands
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 <dstolee@microsoft.com>
1 parent 4f5f3c1 commit be78607

File tree

8 files changed

+211
-0
lines changed

8 files changed

+211
-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: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,6 +1511,114 @@ static int maintenance_unregister(void)
15111511
return run_command(&config_unset);
15121512
}
15131513

1514+
#define BEGIN_LINE "# BEGIN GIT MAINTENANCE SCHEDULE"
1515+
#define END_LINE "# END GIT MAINTENANCE SCHEDULE"
1516+
1517+
static int update_background_schedule(int run_maintenance)
1518+
{
1519+
int result = 0;
1520+
int in_old_region = 0;
1521+
struct child_process crontab_list = CHILD_PROCESS_INIT;
1522+
struct child_process crontab_edit = CHILD_PROCESS_INIT;
1523+
FILE *cron_list, *cron_in;
1524+
const char *crontab_name;
1525+
struct strbuf line = STRBUF_INIT;
1526+
struct lock_file lk;
1527+
char *lock_path = xstrfmt("%s/schedule", the_repository->objects->odb->path);
1528+
1529+
if (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0)
1530+
return error(_("another process is scheduling background maintenance"));
1531+
1532+
crontab_name = getenv("GIT_TEST_CRONTAB");
1533+
if (!crontab_name)
1534+
crontab_name = "crontab";
1535+
1536+
strvec_split(&crontab_list.args, crontab_name);
1537+
strvec_push(&crontab_list.args, "-l");
1538+
crontab_list.in = -1;
1539+
crontab_list.out = dup(lk.tempfile->fd);
1540+
crontab_list.git_cmd = 0;
1541+
1542+
if (start_command(&crontab_list)) {
1543+
result = error(_("failed to run 'crontab -l'; your system might not support 'cron'"));
1544+
goto cleanup;
1545+
}
1546+
1547+
/* Ignore exit code, as an empty crontab will return error. */
1548+
finish_command(&crontab_list);
1549+
1550+
/*
1551+
* Read from the .lock file, filtering out the old
1552+
* schedule while appending the new schedule.
1553+
*/
1554+
cron_list = fdopen(lk.tempfile->fd, "r");
1555+
rewind(cron_list);
1556+
1557+
strvec_split(&crontab_edit.args, crontab_name);
1558+
crontab_edit.in = -1;
1559+
crontab_edit.git_cmd = 0;
1560+
1561+
if (start_command(&crontab_edit)) {
1562+
result = error(_("failed to run 'crontab'; your system might not support 'cron'"));
1563+
goto cleanup;
1564+
}
1565+
1566+
cron_in = fdopen(crontab_edit.in, "w");
1567+
if (!cron_in) {
1568+
result = error(_("failed to open stdin of 'crontab'"));
1569+
goto done_editing;
1570+
}
1571+
1572+
while (!strbuf_getline_lf(&line, cron_list)) {
1573+
if (!in_old_region && !strcmp(line.buf, BEGIN_LINE))
1574+
in_old_region = 1;
1575+
if (in_old_region)
1576+
continue;
1577+
fprintf(cron_in, "%s\n", line.buf);
1578+
if (in_old_region && !strcmp(line.buf, END_LINE))
1579+
in_old_region = 0;
1580+
}
1581+
1582+
if (run_maintenance) {
1583+
fprintf(cron_in, "\n%s\n", BEGIN_LINE);
1584+
fprintf(cron_in, "# The following schedule was created by Git\n");
1585+
fprintf(cron_in, "# Any edits made in this region might be\n");
1586+
fprintf(cron_in, "# replaced in the future by a Git command.\n\n");
1587+
1588+
fprintf(cron_in, "0 * * * * git for-each-repo --config=maintenance.repo maintenance run --scheduled\n");
1589+
1590+
fprintf(cron_in, "\n%s\n", END_LINE);
1591+
}
1592+
1593+
fflush(cron_in);
1594+
fclose(cron_in);
1595+
close(crontab_edit.in);
1596+
1597+
done_editing:
1598+
if (finish_command(&crontab_edit)) {
1599+
result = error(_("'crontab' died"));
1600+
goto cleanup;
1601+
}
1602+
fclose(cron_list);
1603+
1604+
cleanup:
1605+
rollback_lock_file(&lk);
1606+
return result;
1607+
}
1608+
1609+
static int maintenance_start(void)
1610+
{
1611+
if (maintenance_register())
1612+
warning(_("failed to add repo to global config"));
1613+
1614+
return update_background_schedule(1);
1615+
}
1616+
1617+
static int maintenance_stop(void)
1618+
{
1619+
return update_background_schedule(0);
1620+
}
1621+
15141622
int cmd_maintenance(int argc, const char **argv, const char *prefix)
15151623
{
15161624
static struct maintenance_opts opts;
@@ -1552,6 +1660,10 @@ int cmd_maintenance(int argc, const char **argv, const char *prefix)
15521660
return maintenance_register();
15531661
if (!strcmp(argv[0], "run"))
15541662
return maintenance_run(&opts);
1663+
if (!strcmp(argv[0], "start"))
1664+
return maintenance_start();
1665+
if (!strcmp(argv[0], "stop"))
1666+
return maintenance_stop();
15551667
if (!strcmp(argv[0], "unregister"))
15561668
return maintenance_unregister();
15571669

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 stdou.
8+
* Otherwise, write from stdin into <file>.
9+
*/
10+
int cmd__crontab(int argc, const char **argv)
11+
{
12+
char 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: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,4 +319,48 @@ test_expect_success 'register and unregister' '
319319
test_cmp before actual
320320
'
321321

322+
test_expect_success 'start from empty cron table' '
323+
GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance start &&
324+
325+
# start registers the repo
326+
git config --get --global maintenance.repo "$TRASH_DIRECTORY" &&
327+
328+
cat >scheduled <<-\EOF &&
329+
330+
# BEGIN GIT MAINTENANCE SCHEDULE
331+
# The following schedule was created by Git
332+
# Any edits made in this region might be
333+
# replaced in the future by a Git command.
334+
335+
0 * * * * git for-each-repo --config=maintenance.repo maintenance run --scheduled
336+
337+
# END GIT MAINTENANCE SCHEDULE
338+
EOF
339+
test_cmp scheduled cron.txt
340+
'
341+
342+
test_expect_success 'stop from existing schedule' '
343+
GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance stop &&
344+
345+
# stop does not unregister the repo
346+
git config --get --global maintenance.repo "$TRASH_DIRECTORY" &&
347+
348+
# The newline is preserved
349+
echo >empty &&
350+
test_cmp empty cron.txt &&
351+
352+
# Operation is idempotent
353+
GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance stop &&
354+
test_cmp empty cron.txt
355+
'
356+
357+
test_expect_success 'start preserves existing schedule' '
358+
echo "Important information!" >warning &&
359+
cat warning >cron.txt &&
360+
GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance start &&
361+
cat warning >kept-warning &&
362+
cat scheduled >>kept-warning &&
363+
test_cmp kept-warning cron.txt
364+
'
365+
322366
test_done

t/test-lib.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1692,3 +1692,9 @@ test_lazy_prereq SHA1 '
16921692
test_lazy_prereq REBASE_P '
16931693
test -z "$GIT_TEST_SKIP_REBASE_P"
16941694
'
1695+
1696+
# Ensure that no test accidentally triggers a Git command
1697+
# that runs 'crontab', affecting a user's cron schedule.
1698+
# Tests that verify the cron integration must set this locally
1699+
# to avoid errors.
1700+
GIT_TEST_CRONTAB="exit 1"

0 commit comments

Comments
 (0)