]> git.ipfire.org Git - thirdparty/git.git/commitdiff
maintenance: add start/stop subcommands
authorDerrick Stolee <dstolee@microsoft.com>
Fri, 11 Sep 2020 17:49:18 +0000 (17:49 +0000)
committerJunio C Hamano <gitster@pobox.com>
Fri, 25 Sep 2020 17:59:44 +0000 (10:59 -0700)
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>
Documentation/git-maintenance.txt
Makefile
builtin/gc.c
t/helper/test-crontab.c [new file with mode: 0644]
t/helper/test-tool.c
t/helper/test-tool.h
t/t7900-maintenance.sh
t/test-lib.sh

index 1c59fd0cb55fafcfa2be3e6e0ecac32fe7106b39..7628a6d15722575af73a4bbe67352c94fd0c53c5 100644 (file)
@@ -45,6 +45,17 @@ run::
        config options are true. By default, only `maintenance.gc.enabled`
        is true.
 
+start::
+       Start running maintenance on the current repository. This performs
+       the same config updates as the `register` subcommand, then updates
+       the background scheduler to run `git maintenance run --scheduled`
+       on an hourly basis.
+
+stop::
+       Halt the background maintenance schedule. The current repository
+       is not removed from the list of maintained repositories, in case
+       the background maintenance is restarted later.
+
 unregister::
        Remove the current repository from background maintenance. This
        only removes the repository from the configured list. It does not
index 7c588ff036a20e4665e11dbedce252c58e0f071d..c39b39bd7d5ec4f8b99dba54c0a2a8bdc5c877d8 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -690,6 +690,7 @@ TEST_BUILTINS_OBJS += test-advise.o
 TEST_BUILTINS_OBJS += test-bloom.o
 TEST_BUILTINS_OBJS += test-chmtime.o
 TEST_BUILTINS_OBJS += test-config.o
+TEST_BUILTINS_OBJS += test-crontab.o
 TEST_BUILTINS_OBJS += test-ctype.o
 TEST_BUILTINS_OBJS += test-date.o
 TEST_BUILTINS_OBJS += test-delta.o
index edf1d35ce5f38cba2a74ce64f23d241c2786ac62..a387f465857edd683eb32d56edbb364bf4b9668c 100644 (file)
@@ -31,6 +31,7 @@
 #include "refs.h"
 #include "remote.h"
 #include "object-store.h"
+#include "exec-cmd.h"
 
 #define FAILED_RUN "failed to run %s"
 
@@ -1456,6 +1457,125 @@ static int maintenance_unregister(void)
        return run_command(&config_unset);
 }
 
+#define BEGIN_LINE "# BEGIN GIT MAINTENANCE SCHEDULE"
+#define END_LINE "# END GIT MAINTENANCE SCHEDULE"
+
+static int update_background_schedule(int run_maintenance)
+{
+       int result = 0;
+       int in_old_region = 0;
+       struct child_process crontab_list = CHILD_PROCESS_INIT;
+       struct child_process crontab_edit = CHILD_PROCESS_INIT;
+       FILE *cron_list, *cron_in;
+       const char *crontab_name;
+       struct strbuf line = STRBUF_INIT;
+       struct lock_file lk;
+       char *lock_path = xstrfmt("%s/schedule", the_repository->objects->odb->path);
+
+       if (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0)
+               return error(_("another process is scheduling background maintenance"));
+
+       crontab_name = getenv("GIT_TEST_CRONTAB");
+       if (!crontab_name)
+               crontab_name = "crontab";
+
+       strvec_split(&crontab_list.args, crontab_name);
+       strvec_push(&crontab_list.args, "-l");
+       crontab_list.in = -1;
+       crontab_list.out = dup(lk.tempfile->fd);
+       crontab_list.git_cmd = 0;
+
+       if (start_command(&crontab_list)) {
+               result = error(_("failed to run 'crontab -l'; your system might not support 'cron'"));
+               goto cleanup;
+       }
+
+       /* Ignore exit code, as an empty crontab will return error. */
+       finish_command(&crontab_list);
+
+       /*
+        * Read from the .lock file, filtering out the old
+        * schedule while appending the new schedule.
+        */
+       cron_list = fdopen(lk.tempfile->fd, "r");
+       rewind(cron_list);
+
+       strvec_split(&crontab_edit.args, crontab_name);
+       crontab_edit.in = -1;
+       crontab_edit.git_cmd = 0;
+
+       if (start_command(&crontab_edit)) {
+               result = error(_("failed to run 'crontab'; your system might not support 'cron'"));
+               goto cleanup;
+       }
+
+       cron_in = fdopen(crontab_edit.in, "w");
+       if (!cron_in) {
+               result = error(_("failed to open stdin of 'crontab'"));
+               goto done_editing;
+       }
+
+       while (!strbuf_getline_lf(&line, cron_list)) {
+               if (!in_old_region && !strcmp(line.buf, BEGIN_LINE))
+                       in_old_region = 1;
+               if (in_old_region)
+                       continue;
+               fprintf(cron_in, "%s\n", line.buf);
+               if (in_old_region && !strcmp(line.buf, END_LINE))
+                       in_old_region = 0;
+       }
+
+       if (run_maintenance) {
+               struct strbuf line_format = STRBUF_INIT;
+               const char *exec_path = git_exec_path();
+
+               fprintf(cron_in, "%s\n", BEGIN_LINE);
+               fprintf(cron_in,
+                       "# The following schedule was created by Git\n");
+               fprintf(cron_in, "# Any edits made in this region might be\n");
+               fprintf(cron_in,
+                       "# replaced in the future by a Git command.\n\n");
+
+               strbuf_addf(&line_format,
+                           "%%s %%s * * %%s \"%s/git\" --exec-path=\"%s\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%s\n",
+                           exec_path, exec_path);
+               fprintf(cron_in, line_format.buf, "0", "1-23", "*", "hourly");
+               fprintf(cron_in, line_format.buf, "0", "0", "1-6", "daily");
+               fprintf(cron_in, line_format.buf, "0", "0", "0", "weekly");
+               strbuf_release(&line_format);
+
+               fprintf(cron_in, "\n%s\n", END_LINE);
+       }
+
+       fflush(cron_in);
+       fclose(cron_in);
+       close(crontab_edit.in);
+
+done_editing:
+       if (finish_command(&crontab_edit)) {
+               result = error(_("'crontab' died"));
+               goto cleanup;
+       }
+       fclose(cron_list);
+
+cleanup:
+       rollback_lock_file(&lk);
+       return result;
+}
+
+static int maintenance_start(void)
+{
+       if (maintenance_register())
+               warning(_("failed to add repo to global config"));
+
+       return update_background_schedule(1);
+}
+
+static int maintenance_stop(void)
+{
+       return update_background_schedule(0);
+}
+
 static const char builtin_maintenance_usage[] =        N_("git maintenance <subcommand> [<options>]");
 
 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)
 
        if (!strcmp(argv[1], "run"))
                return maintenance_run(argc - 1, argv + 1, prefix);
+       if (!strcmp(argv[1], "start"))
+               return maintenance_start();
+       if (!strcmp(argv[1], "stop"))
+               return maintenance_stop();
        if (!strcmp(argv[1], "register"))
                return maintenance_register();
        if (!strcmp(argv[1], "unregister"))
diff --git a/t/helper/test-crontab.c b/t/helper/test-crontab.c
new file mode 100644 (file)
index 0000000..e7c0137
--- /dev/null
@@ -0,0 +1,35 @@
+#include "test-tool.h"
+#include "cache.h"
+
+/*
+ * Usage: test-tool cron <file> [-l]
+ *
+ * If -l is specified, then write the contents of <file> to stdout.
+ * Otherwise, write from stdin into <file>.
+ */
+int cmd__crontab(int argc, const char **argv)
+{
+       int a;
+       FILE *from, *to;
+
+       if (argc == 3 && !strcmp(argv[2], "-l")) {
+               from = fopen(argv[1], "r");
+               if (!from)
+                       return 0;
+               to = stdout;
+       } else if (argc == 2) {
+               from = stdin;
+               to = fopen(argv[1], "w");
+       } else
+               return error("unknown arguments");
+
+       while ((a = fgetc(from)) != EOF)
+               fputc(a, to);
+
+       if (argc == 3)
+               fclose(from);
+       else
+               fclose(to);
+
+       return 0;
+}
index 590b2efca705c57a6f9df1df42163588429a918a..432b49d94819b76a2243cc9e8451a2f7549ca348 100644 (file)
@@ -18,6 +18,7 @@ static struct test_cmd cmds[] = {
        { "bloom", cmd__bloom },
        { "chmtime", cmd__chmtime },
        { "config", cmd__config },
+       { "crontab", cmd__crontab },
        { "ctype", cmd__ctype },
        { "date", cmd__date },
        { "delta", cmd__delta },
index ddc8e990e918232093b4958e1c94f44436142053..7c3281e071730dce08e4d6cd831d7f4db6a5c6fc 100644 (file)
@@ -8,6 +8,7 @@ int cmd__advise_if_enabled(int argc, const char **argv);
 int cmd__bloom(int argc, const char **argv);
 int cmd__chmtime(int argc, const char **argv);
 int cmd__config(int argc, const char **argv);
+int cmd__crontab(int argc, const char **argv);
 int cmd__ctype(int argc, const char **argv);
 int cmd__date(int argc, const char **argv);
 int cmd__delta(int argc, const char **argv);
index 8f383d01d9c53e154ee9a08b09510d045478f0b0..7715e403913828a9747eac39fdafa43534a76ebb 100755 (executable)
@@ -315,4 +315,32 @@ test_expect_success 'register and unregister' '
        test_cmp before actual
 '
 
+test_expect_success 'start from empty cron table' '
+       GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance start &&
+
+       # start registers the repo
+       git config --get --global maintenance.repo "$(pwd)" &&
+
+       grep "for-each-repo --config=maintenance.repo maintenance run --schedule=daily" cron.txt &&
+       grep "for-each-repo --config=maintenance.repo maintenance run --schedule=hourly" cron.txt &&
+       grep "for-each-repo --config=maintenance.repo maintenance run --schedule=weekly" cron.txt
+'
+
+test_expect_success 'stop from existing schedule' '
+       GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance stop &&
+
+       # stop does not unregister the repo
+       git config --get --global maintenance.repo "$(pwd)" &&
+
+       # Operation is idempotent
+       GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance stop &&
+       test_must_be_empty cron.txt
+'
+
+test_expect_success 'start preserves existing schedule' '
+       echo "Important information!" >cron.txt &&
+       GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance start &&
+       grep "Important information!" cron.txt
+'
+
 test_done
index ef31f400374755015ccec300bdb450021aeccc17..4a60d1ed76632ecc1d24c5f6b52c3c5962449499 100644 (file)
@@ -1702,3 +1702,9 @@ test_lazy_prereq SHA1 '
 test_lazy_prereq REBASE_P '
        test -z "$GIT_TEST_SKIP_REBASE_P"
 '
+
+# Ensure that no test accidentally triggers a Git command
+# that runs 'crontab', affecting a user's cron schedule.
+# Tests that verify the cron integration must set this locally
+# to avoid errors.
+GIT_TEST_CRONTAB="exit 1"