Skip to content

Commit 3c7ba30

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 2265418 commit 3c7ba30

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
@@ -1512,6 +1512,114 @@ static int maintenance_unregister(void)
15121512
return run_command(&config_unset);
15131513
}
15141514

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

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
@@ -316,4 +316,48 @@ test_expect_success 'register and unregister' '
316316
test_cmp before actual
317317
'
318318

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