@matheusbsbs / matheus-bagliano / commits / 2fec604f8d
Fork 0

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 <dstolee@microsoft.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Derrick Stolee committed Sep 11, 2020 at 17:49 UTC 2fec604f8df51fcf47551723e68a3447915b9eb9
8 files changed +207
Documentation/git-maintenance.txt
+11
@@ -45,6 +45,17 @@ run::
45 config options are true. By default, only `maintenance.gc.enabled`
46 is true.
47
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 +
59 unregister::
60 Remove the current repository from background maintenance. This
61 only removes the repository from the configured list. It does not
Makefile
+1
@@ -690,6 +690,7 @@ TEST_BUILTINS_OBJS += test-advise.o
690 TEST_BUILTINS_OBJS += test-bloom.o
691 TEST_BUILTINS_OBJS += test-chmtime.o
692 TEST_BUILTINS_OBJS += test-config.o
693 +TEST_BUILTINS_OBJS += test-crontab.o
694 TEST_BUILTINS_OBJS += test-ctype.o
695 TEST_BUILTINS_OBJS += test-date.o
696 TEST_BUILTINS_OBJS += test-delta.o
builtin/gc.c
+124
@@ -31,6 +31,7 @@
31 #include "refs.h"
32 #include "remote.h"
33 #include "object-store.h"
34 +#include "exec-cmd.h"
35
36 #define FAILED_RUN "failed to run %s"
37
@@ -1456,6 +1457,125 @@ static int maintenance_unregister(void)
1457 return run_command(&config_unset);
1458 }
1459
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 +
1579 static const char builtin_maintenance_usage[] = N_("git maintenance <subcommand> [<options>]");
1580
1581 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)
1586
1587 if (!strcmp(argv[1], "run"))
1588 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();
1593 if (!strcmp(argv[1], "register"))
1594 return maintenance_register();
1595 if (!strcmp(argv[1], "unregister"))
t/helper/test-crontab.c new
+35
@@ -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
+1
@@ -18,6 +18,7 @@ static struct test_cmd cmds[] = {
18 { "bloom", cmd__bloom },
19 { "chmtime", cmd__chmtime },
20 { "config", cmd__config },
21 + { "crontab", cmd__crontab },
22 { "ctype", cmd__ctype },
23 { "date", cmd__date },
24 { "delta", cmd__delta },
t/helper/test-tool.h
+1
@@ -8,6 +8,7 @@ int cmd__advise_if_enabled(int argc, const char **argv);
8 int cmd__bloom(int argc, const char **argv);
9 int cmd__chmtime(int argc, const char **argv);
10 int cmd__config(int argc, const char **argv);
11 +int cmd__crontab(int argc, const char **argv);
12 int cmd__ctype(int argc, const char **argv);
13 int cmd__date(int argc, const char **argv);
14 int cmd__delta(int argc, const char **argv);
t/t7900-maintenance.sh
+28
@@ -315,4 +315,32 @@ test_expect_success 'register and unregister' '
315 test_cmp before actual
316 '
317
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 +
346 test_done
t/test-lib.sh
+6
@@ -1702,3 +1702,9 @@ test_lazy_prereq SHA1 '
1702 test_lazy_prereq REBASE_P '
1703 test -z "$GIT_TEST_SKIP_REBASE_P"
1704 '
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"