Skip to content

Commit 70194b8

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 d80cf87 commit 70194b8

8 files changed

Lines changed: 237 additions & 0 deletions

File tree

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: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1509,6 +1509,140 @@ static int maintenance_unregister(void)
15091509
return run_command(&config_unset);
15101510
}
15111511

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

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
@@ -270,4 +270,48 @@ test_expect_success 'register and unregister' '
270270
test_cmp before actual
271271
'
272272

273+
test_expect_success 'start from empty cron table' '
274+
GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance start &&
275+
276+
# start registers the repo
277+
git config --get --global maintenance.repo "$TRASH_DIRECTORY" &&
278+
279+
cat >scheduled <<-\EOF &&
280+
281+
# BEGIN GIT MAINTENANCE SCHEDULE
282+
# The following schedule was created by Git
283+
# Any edits made in this region might be
284+
# replaced in the future by a Git command.
285+
286+
0 * * * * git for-each-repo --config=maintenance.repo maintenance run --scheduled
287+
288+
# END GIT MAINTENANCE SCHEDULE
289+
EOF
290+
test_cmp scheduled cron.txt
291+
'
292+
293+
test_expect_success 'stop from existing schedule' '
294+
GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance stop &&
295+
296+
# stop does not unregister the repo
297+
git config --get --global maintenance.repo "$TRASH_DIRECTORY" &&
298+
299+
# The newline is preserved
300+
echo >empty &&
301+
test_cmp empty cron.txt &&
302+
303+
# Operation is idempotent
304+
GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance stop &&
305+
test_cmp empty cron.txt
306+
'
307+
308+
test_expect_success 'start preserves existing schedule' '
309+
echo "Important information!" >warning &&
310+
cat warning >cron.txt &&
311+
GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance start &&
312+
cat warning >kept-warning &&
313+
cat scheduled >>kept-warning &&
314+
test_cmp kept-warning cron.txt
315+
'
316+
273317
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)