Merge branch 'ds/maintenance-part-3'

Parts of "git maintenance" to ease writing crontab entries (and
other scheduling system configuration) for it.

* ds/maintenance-part-3:
  maintenance: add troubleshooting guide to docs
  maintenance: use 'incremental' strategy by default
  maintenance: create maintenance.strategy config
  maintenance: add start/stop subcommands
  maintenance: add [un]register subcommands
  for-each-repo: run subcommands on configured repos
  maintenance: add --schedule option and config
  maintenance: optionally skip --auto process
This commit is contained in:
Junio C Hamano 2020-11-18 13:32:53 -08:00
commit 7660da1618
17 changed files with 759 additions and 8 deletions

1
.gitignore vendored
View File

@ -67,6 +67,7 @@
/git-filter-branch /git-filter-branch
/git-fmt-merge-msg /git-fmt-merge-msg
/git-for-each-ref /git-for-each-ref
/git-for-each-repo
/git-format-patch /git-format-patch
/git-fsck /git-fsck
/git-fsck-objects /git-fsck-objects

View File

@ -1,3 +1,23 @@
maintenance.auto::
This boolean config option controls whether some commands run
`git maintenance run --auto` after doing their normal work. Defaults
to true.
maintenance.strategy::
This string config option provides a way to specify one of a few
recommended schedules for background maintenance. This only affects
which tasks are run during `git maintenance run --schedule=X`
commands, provided no `--task=<task>` arguments are provided.
Further, if a `maintenance.<task>.schedule` config value is set,
then that value is used instead of the one provided by
`maintenance.strategy`. The possible strategy strings are:
+
* `none`: This default setting implies no task are run at any schedule.
* `incremental`: This setting optimizes for performing small maintenance
activities that do not delete any data. This does not schedule the `gc`
task, but runs the `prefetch` and `commit-graph` tasks hourly and the
`loose-objects` and `incremental-repack` tasks daily.
maintenance.<task>.enabled:: maintenance.<task>.enabled::
This boolean config option controls whether the maintenance task This boolean config option controls whether the maintenance task
with name `<task>` is run when no `--task` option is specified to with name `<task>` is run when no `--task` option is specified to
@ -5,6 +25,11 @@ maintenance.<task>.enabled::
`--task` option exists. By default, only `maintenance.gc.enabled` `--task` option exists. By default, only `maintenance.gc.enabled`
is true. is true.
maintenance.<task>.schedule::
This config option controls whether or not the given `<task>` runs
during a `git maintenance run --schedule=<frequency>` command. The
value must be one of "hourly", "daily", or "weekly".
maintenance.commit-graph.auto:: maintenance.commit-graph.auto::
This integer config option controls how often the `commit-graph` task This integer config option controls how often the `commit-graph` task
should be run as part of `git maintenance run --auto`. If zero, then should be run as part of `git maintenance run --auto`. If zero, then

View File

@ -0,0 +1,59 @@
git-for-each-repo(1)
====================
NAME
----
git-for-each-repo - Run a Git command on a list of repositories
SYNOPSIS
--------
[verse]
'git for-each-repo' --config=<config> [--] <arguments>
DESCRIPTION
-----------
Run a Git command on a list of repositories. The arguments after the
known options or `--` indicator are used as the arguments for the Git
subprocess.
THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
For example, we could run maintenance on each of a list of repositories
stored in a `maintenance.repo` config variable using
-------------
git for-each-repo --config=maintenance.repo maintenance run
-------------
This will run `git -C <repo> maintenance run` for each value `<repo>`
in the multi-valued config variable `maintenance.repo`.
OPTIONS
-------
--config=<config>::
Use the given config variable as a multi-valued list storing
absolute path names. Iterate on that list of paths to run
the given arguments.
+
These config values are loaded from system, global, and local Git config,
as available. If `git for-each-repo` is run in a directory that is not a
Git repository, then only the system and global config is used.
SUBPROCESS BEHAVIOR
-------------------
If any `git -C <repo> <arguments>` subprocess returns a non-zero exit code,
then the `git for-each-repo` process returns that exit code without running
more subprocesses.
Each `git -C <repo> <arguments>` subprocess inherits the standard file
descriptors `stdin`, `stdout`, and `stderr`.
GIT
---
Part of the linkgit:git[1] suite

View File

@ -29,6 +29,32 @@ Git repository.
SUBCOMMANDS SUBCOMMANDS
----------- -----------
register::
Initialize Git config values so any scheduled maintenance will
start running on this repository. This adds the repository to the
`maintenance.repo` config variable in the current user's global
config and enables some recommended configuration values for
`maintenance.<task>.schedule`. The tasks that are enabled are safe
for running in the background without disrupting foreground
processes.
+
The `register` subcomand will also set the `maintenance.strategy` config
value to `incremental`, if this value is not previously set. The
`incremental` strategy uses the following schedule for each maintenance
task:
+
--
* `gc`: disabled.
* `commit-graph`: hourly.
* `prefetch`: hourly.
* `loose-objects`: daily.
* `incremental-repack`: daily.
--
+
`git maintenance register` will also disable foreground maintenance by
setting `maintenance.auto = false` in the current repository. This config
setting will remain after a `git maintenance unregister` command.
run:: run::
Run one or more maintenance tasks. If one or more `--task` options Run one or more maintenance tasks. If one or more `--task` options
are specified, then those tasks are run in that order. Otherwise, are specified, then those tasks are run in that order. Otherwise,
@ -36,6 +62,22 @@ run::
config options are true. By default, only `maintenance.gc.enabled` config options are true. By default, only `maintenance.gc.enabled`
is true. 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
stop the background maintenance processes from running.
TASKS TASKS
----- -----
@ -110,7 +152,18 @@ OPTIONS
only if certain thresholds are met. For example, the `gc` task only if certain thresholds are met. For example, the `gc` task
runs when the number of loose objects exceeds the number stored runs when the number of loose objects exceeds the number stored
in the `gc.auto` config setting, or when the number of pack-files in the `gc.auto` config setting, or when the number of pack-files
exceeds the `gc.autoPackLimit` config setting. exceeds the `gc.autoPackLimit` config setting. Not compatible with
the `--schedule` option.
--schedule::
When combined with the `run` subcommand, run maintenance tasks
only if certain time conditions are met, as specified by the
`maintenance.<task>.schedule` config value for each `<task>`.
This config value specifies a number of seconds since the last
time that task ran, according to the `maintenance.<task>.lastRun`
config value. The tasks that are tested are those provided by
the `--task=<task>` option(s) or those with
`maintenance.<task>.enabled` set to true.
--quiet:: --quiet::
Do not report progress or other information over `stderr`. Do not report progress or other information over `stderr`.
@ -122,6 +175,50 @@ OPTIONS
`maintenance.<task>.enabled` configured as `true` are considered. `maintenance.<task>.enabled` configured as `true` are considered.
See the 'TASKS' section for the list of accepted `<task>` values. See the 'TASKS' section for the list of accepted `<task>` values.
TROUBLESHOOTING
---------------
The `git maintenance` command is designed to simplify the repository
maintenance patterns while minimizing user wait time during Git commands.
A variety of configuration options are available to allow customizing this
process. The default maintenance options focus on operations that complete
quickly, even on large repositories.
Users may find some cases where scheduled maintenance tasks do not run as
frequently as intended. Each `git maintenance run` command takes a lock on
the repository's object database, and this prevents other concurrent
`git maintenance run` commands from running on the same repository. Without
this safeguard, competing processes could leave the repository in an
unpredictable state.
The background maintenance schedule runs `git maintenance run` processes
on an hourly basis. Each run executes the "hourly" tasks. At midnight,
that process also executes the "daily" tasks. At midnight on the first day
of the week, that process also executes the "weekly" tasks. A single
process iterates over each registered repository, performing the scheduled
tasks for that frequency. Depending on the number of registered
repositories and their sizes, this process may take longer than an hour.
In this case, multiple `git maintenance run` commands may run on the same
repository at the same time, colliding on the object database lock. This
results in one of the two tasks not running.
If you find that some maintenance windows are taking longer than one hour
to complete, then consider reducing the complexity of your maintenance
tasks. For example, the `gc` task is much slower than the
`incremental-repack` task. However, this comes at a cost of a slightly
larger object database. Consider moving more expensive tasks to be run
less frequently.
Expert users may consider scheduling their own maintenance tasks using a
different schedule than is available through `git maintenance start` and
Git configuration options. These users should be aware of the object
database lock and how concurrent `git maintenance run` commands behave.
Further, the `git gc` command should not be combined with
`git maintenance run` commands. `git gc` modifies the object database
but does not take the lock in the same way as `git maintenance run`. If
possible, use `git maintenance run --task=gc` instead of `git gc`.
GIT GIT
--- ---
Part of the linkgit:git[1] suite Part of the linkgit:git[1] suite

View File

@ -694,6 +694,7 @@ TEST_BUILTINS_OBJS += test-advise.o
TEST_BUILTINS_OBJS += test-bloom.o TEST_BUILTINS_OBJS += test-bloom.o
TEST_BUILTINS_OBJS += test-chmtime.o TEST_BUILTINS_OBJS += test-chmtime.o
TEST_BUILTINS_OBJS += test-config.o TEST_BUILTINS_OBJS += test-config.o
TEST_BUILTINS_OBJS += test-crontab.o
TEST_BUILTINS_OBJS += test-ctype.o TEST_BUILTINS_OBJS += test-ctype.o
TEST_BUILTINS_OBJS += test-date.o TEST_BUILTINS_OBJS += test-date.o
TEST_BUILTINS_OBJS += test-delta.o TEST_BUILTINS_OBJS += test-delta.o
@ -1089,6 +1090,7 @@ BUILTIN_OBJS += builtin/fetch-pack.o
BUILTIN_OBJS += builtin/fetch.o BUILTIN_OBJS += builtin/fetch.o
BUILTIN_OBJS += builtin/fmt-merge-msg.o BUILTIN_OBJS += builtin/fmt-merge-msg.o
BUILTIN_OBJS += builtin/for-each-ref.o BUILTIN_OBJS += builtin/for-each-ref.o
BUILTIN_OBJS += builtin/for-each-repo.o
BUILTIN_OBJS += builtin/fsck.o BUILTIN_OBJS += builtin/fsck.o
BUILTIN_OBJS += builtin/gc.o BUILTIN_OBJS += builtin/gc.o
BUILTIN_OBJS += builtin/get-tar-commit-id.o BUILTIN_OBJS += builtin/get-tar-commit-id.o

View File

@ -155,6 +155,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix);
int cmd_fetch_pack(int argc, const char **argv, const char *prefix); int cmd_fetch_pack(int argc, const char **argv, const char *prefix);
int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix); int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix);
int cmd_for_each_ref(int argc, const char **argv, const char *prefix); int cmd_for_each_ref(int argc, const char **argv, const char *prefix);
int cmd_for_each_repo(int argc, const char **argv, const char *prefix);
int cmd_format_patch(int argc, const char **argv, const char *prefix); int cmd_format_patch(int argc, const char **argv, const char *prefix);
int cmd_fsck(int argc, const char **argv, const char *prefix); int cmd_fsck(int argc, const char **argv, const char *prefix);
int cmd_gc(int argc, const char **argv, const char *prefix); int cmd_gc(int argc, const char **argv, const char *prefix);

58
builtin/for-each-repo.c Normal file
View File

@ -0,0 +1,58 @@
#include "cache.h"
#include "config.h"
#include "builtin.h"
#include "parse-options.h"
#include "run-command.h"
#include "string-list.h"
static const char * const for_each_repo_usage[] = {
N_("git for-each-repo --config=<config> <command-args>"),
NULL
};
static int run_command_on_repo(const char *path,
void *cbdata)
{
int i;
struct child_process child = CHILD_PROCESS_INIT;
struct strvec *args = (struct strvec *)cbdata;
child.git_cmd = 1;
strvec_pushl(&child.args, "-C", path, NULL);
for (i = 0; i < args->nr; i++)
strvec_push(&child.args, args->v[i]);
return run_command(&child);
}
int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
{
static const char *config_key = NULL;
int i, result = 0;
const struct string_list *values;
struct strvec args = STRVEC_INIT;
const struct option options[] = {
OPT_STRING(0, "config", &config_key, N_("config"),
N_("config key storing a list of repository paths")),
OPT_END()
};
argc = parse_options(argc, argv, prefix, options, for_each_repo_usage,
PARSE_OPT_STOP_AT_NON_OPTION);
if (!config_key)
die(_("missing --config=<config>"));
for (i = 0; i < argc; i++)
strvec_push(&args, argv[i]);
values = repo_config_get_value_multi(the_repository,
config_key);
for (i = 0; !result && i < values->nr; i++)
result = run_command_on_repo(values->items[i].string, &args);
return result;
}

View File

@ -31,6 +31,7 @@
#include "refs.h" #include "refs.h"
#include "remote.h" #include "remote.h"
#include "object-store.h" #include "object-store.h"
#include "exec-cmd.h"
#define FAILED_RUN "failed to run %s" #define FAILED_RUN "failed to run %s"
@ -703,14 +704,51 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
return 0; return 0;
} }
static const char * const builtin_maintenance_run_usage[] = { static const char *const builtin_maintenance_run_usage[] = {
N_("git maintenance run [--auto] [--[no-]quiet] [--task=<task>]"), N_("git maintenance run [--auto] [--[no-]quiet] [--task=<task>] [--schedule]"),
NULL NULL
}; };
enum schedule_priority {
SCHEDULE_NONE = 0,
SCHEDULE_WEEKLY = 1,
SCHEDULE_DAILY = 2,
SCHEDULE_HOURLY = 3,
};
static enum schedule_priority parse_schedule(const char *value)
{
if (!value)
return SCHEDULE_NONE;
if (!strcasecmp(value, "hourly"))
return SCHEDULE_HOURLY;
if (!strcasecmp(value, "daily"))
return SCHEDULE_DAILY;
if (!strcasecmp(value, "weekly"))
return SCHEDULE_WEEKLY;
return SCHEDULE_NONE;
}
static int maintenance_opt_schedule(const struct option *opt, const char *arg,
int unset)
{
enum schedule_priority *priority = opt->value;
if (unset)
die(_("--no-schedule is not allowed"));
*priority = parse_schedule(arg);
if (!*priority)
die(_("unrecognized --schedule argument '%s'"), arg);
return 0;
}
struct maintenance_run_opts { struct maintenance_run_opts {
int auto_flag; int auto_flag;
int quiet; int quiet;
enum schedule_priority schedule;
}; };
/* Remember to update object flag allocation in object.h */ /* Remember to update object flag allocation in object.h */
@ -1168,6 +1206,8 @@ struct maintenance_task {
maintenance_auto_fn *auto_condition; maintenance_auto_fn *auto_condition;
unsigned enabled:1; unsigned enabled:1;
enum schedule_priority schedule;
/* -1 if not selected. */ /* -1 if not selected. */
int selected_order; int selected_order;
}; };
@ -1263,6 +1303,9 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts)
!tasks[i].auto_condition())) !tasks[i].auto_condition()))
continue; continue;
if (opts->schedule && tasks[i].schedule < opts->schedule)
continue;
trace2_region_enter("maintenance", tasks[i].name, r); trace2_region_enter("maintenance", tasks[i].name, r);
if (tasks[i].fn(opts)) { if (tasks[i].fn(opts)) {
error(_("task '%s' failed"), tasks[i].name); error(_("task '%s' failed"), tasks[i].name);
@ -1275,21 +1318,54 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts)
return result; return result;
} }
static void initialize_task_config(void) static void initialize_maintenance_strategy(void)
{
char *config_str;
if (git_config_get_string("maintenance.strategy", &config_str))
return;
if (!strcasecmp(config_str, "incremental")) {
tasks[TASK_GC].schedule = SCHEDULE_NONE;
tasks[TASK_COMMIT_GRAPH].enabled = 1;
tasks[TASK_COMMIT_GRAPH].schedule = SCHEDULE_HOURLY;
tasks[TASK_PREFETCH].enabled = 1;
tasks[TASK_PREFETCH].schedule = SCHEDULE_HOURLY;
tasks[TASK_INCREMENTAL_REPACK].enabled = 1;
tasks[TASK_INCREMENTAL_REPACK].schedule = SCHEDULE_DAILY;
tasks[TASK_LOOSE_OBJECTS].enabled = 1;
tasks[TASK_LOOSE_OBJECTS].schedule = SCHEDULE_DAILY;
}
}
static void initialize_task_config(int schedule)
{ {
int i; int i;
struct strbuf config_name = STRBUF_INIT; struct strbuf config_name = STRBUF_INIT;
gc_config(); gc_config();
if (schedule)
initialize_maintenance_strategy();
for (i = 0; i < TASK__COUNT; i++) { for (i = 0; i < TASK__COUNT; i++) {
int config_value; int config_value;
char *config_str;
strbuf_setlen(&config_name, 0); strbuf_reset(&config_name);
strbuf_addf(&config_name, "maintenance.%s.enabled", strbuf_addf(&config_name, "maintenance.%s.enabled",
tasks[i].name); tasks[i].name);
if (!git_config_get_bool(config_name.buf, &config_value)) if (!git_config_get_bool(config_name.buf, &config_value))
tasks[i].enabled = config_value; tasks[i].enabled = config_value;
strbuf_reset(&config_name);
strbuf_addf(&config_name, "maintenance.%s.schedule",
tasks[i].name);
if (!git_config_get_string(config_name.buf, &config_str)) {
tasks[i].schedule = parse_schedule(config_str);
free(config_str);
}
} }
strbuf_release(&config_name); strbuf_release(&config_name);
@ -1333,6 +1409,9 @@ static int maintenance_run(int argc, const char **argv, const char *prefix)
struct option builtin_maintenance_run_options[] = { struct option builtin_maintenance_run_options[] = {
OPT_BOOL(0, "auto", &opts.auto_flag, OPT_BOOL(0, "auto", &opts.auto_flag,
N_("run tasks based on the state of the repository")), N_("run tasks based on the state of the repository")),
OPT_CALLBACK(0, "schedule", &opts.schedule, N_("frequency"),
N_("run tasks based on frequency"),
maintenance_opt_schedule),
OPT_BOOL(0, "quiet", &opts.quiet, OPT_BOOL(0, "quiet", &opts.quiet,
N_("do not report progress or other information over stderr")), N_("do not report progress or other information over stderr")),
OPT_CALLBACK_F(0, "task", NULL, N_("task"), OPT_CALLBACK_F(0, "task", NULL, N_("task"),
@ -1343,7 +1422,6 @@ static int maintenance_run(int argc, const char **argv, const char *prefix)
memset(&opts, 0, sizeof(opts)); memset(&opts, 0, sizeof(opts));
opts.quiet = !isatty(2); opts.quiet = !isatty(2);
initialize_task_config();
for (i = 0; i < TASK__COUNT; i++) for (i = 0; i < TASK__COUNT; i++)
tasks[i].selected_order = -1; tasks[i].selected_order = -1;
@ -1353,13 +1431,196 @@ static int maintenance_run(int argc, const char **argv, const char *prefix)
builtin_maintenance_run_usage, builtin_maintenance_run_usage,
PARSE_OPT_STOP_AT_NON_OPTION); PARSE_OPT_STOP_AT_NON_OPTION);
if (opts.auto_flag && opts.schedule)
die(_("use at most one of --auto and --schedule=<frequency>"));
initialize_task_config(opts.schedule);
if (argc != 0) if (argc != 0)
usage_with_options(builtin_maintenance_run_usage, usage_with_options(builtin_maintenance_run_usage,
builtin_maintenance_run_options); builtin_maintenance_run_options);
return maintenance_run_tasks(&opts); return maintenance_run_tasks(&opts);
} }
static const char builtin_maintenance_usage[] = N_("git maintenance run [<options>]"); static int maintenance_register(void)
{
char *config_value;
struct child_process config_set = CHILD_PROCESS_INIT;
struct child_process config_get = CHILD_PROCESS_INIT;
/* There is no current repository, so skip registering it */
if (!the_repository || !the_repository->gitdir)
return 0;
/* Disable foreground maintenance */
git_config_set("maintenance.auto", "false");
/* Set maintenance strategy, if unset */
if (!git_config_get_string("maintenance.strategy", &config_value))
free(config_value);
else
git_config_set("maintenance.strategy", "incremental");
config_get.git_cmd = 1;
strvec_pushl(&config_get.args, "config", "--global", "--get", "maintenance.repo",
the_repository->worktree ? the_repository->worktree
: the_repository->gitdir,
NULL);
config_get.out = -1;
if (start_command(&config_get))
return error(_("failed to run 'git config'"));
/* We already have this value in our config! */
if (!finish_command(&config_get))
return 0;
config_set.git_cmd = 1;
strvec_pushl(&config_set.args, "config", "--add", "--global", "maintenance.repo",
the_repository->worktree ? the_repository->worktree
: the_repository->gitdir,
NULL);
return run_command(&config_set);
}
static int maintenance_unregister(void)
{
struct child_process config_unset = CHILD_PROCESS_INIT;
if (!the_repository || !the_repository->gitdir)
return error(_("no current repository to unregister"));
config_unset.git_cmd = 1;
strvec_pushl(&config_unset.args, "config", "--global", "--unset",
"maintenance.repo",
the_repository->worktree ? the_repository->worktree
: the_repository->gitdir,
NULL);
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) int cmd_maintenance(int argc, const char **argv, const char *prefix)
{ {
@ -1369,6 +1630,14 @@ int cmd_maintenance(int argc, const char **argv, const char *prefix)
if (!strcmp(argv[1], "run")) if (!strcmp(argv[1], "run"))
return maintenance_run(argc - 1, argv + 1, prefix); 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"))
return maintenance_unregister();
die(_("invalid subcommand: %s"), argv[1]); die(_("invalid subcommand: %s"), argv[1]);
} }

View File

@ -94,6 +94,7 @@ git-fetch-pack synchingrepositories
git-filter-branch ancillarymanipulators git-filter-branch ancillarymanipulators
git-fmt-merge-msg purehelpers git-fmt-merge-msg purehelpers
git-for-each-ref plumbinginterrogators git-for-each-ref plumbinginterrogators
git-for-each-repo plumbinginterrogators
git-format-patch mainporcelain git-format-patch mainporcelain
git-fsck ancillaryinterrogators complete git-fsck ancillaryinterrogators complete
git-gc mainporcelain git-gc mainporcelain

1
git.c
View File

@ -516,6 +516,7 @@ static struct cmd_struct commands[] = {
{ "fetch-pack", cmd_fetch_pack, RUN_SETUP | NO_PARSEOPT }, { "fetch-pack", cmd_fetch_pack, RUN_SETUP | NO_PARSEOPT },
{ "fmt-merge-msg", cmd_fmt_merge_msg, RUN_SETUP }, { "fmt-merge-msg", cmd_fmt_merge_msg, RUN_SETUP },
{ "for-each-ref", cmd_for_each_ref, RUN_SETUP }, { "for-each-ref", cmd_for_each_ref, RUN_SETUP },
{ "for-each-repo", cmd_for_each_repo, RUN_SETUP_GENTLY },
{ "format-patch", cmd_format_patch, RUN_SETUP }, { "format-patch", cmd_format_patch, RUN_SETUP },
{ "fsck", cmd_fsck, RUN_SETUP }, { "fsck", cmd_fsck, RUN_SETUP },
{ "fsck-objects", cmd_fsck, RUN_SETUP }, { "fsck-objects", cmd_fsck, RUN_SETUP },

View File

@ -7,6 +7,7 @@
#include "strbuf.h" #include "strbuf.h"
#include "string-list.h" #include "string-list.h"
#include "quote.h" #include "quote.h"
#include "config.h"
void child_process_init(struct child_process *child) void child_process_init(struct child_process *child)
{ {
@ -1868,8 +1869,13 @@ int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,
int run_auto_maintenance(int quiet) int run_auto_maintenance(int quiet)
{ {
int enabled;
struct child_process maint = CHILD_PROCESS_INIT; struct child_process maint = CHILD_PROCESS_INIT;
if (!git_config_get_bool("maintenance.auto", &enabled) &&
!enabled)
return 0;
maint.git_cmd = 1; maint.git_cmd = 1;
strvec_pushl(&maint.args, "maintenance", "run", "--auto", NULL); strvec_pushl(&maint.args, "maintenance", "run", "--auto", NULL);
strvec_push(&maint.args, quiet ? "--quiet" : "--no-quiet"); strvec_push(&maint.args, quiet ? "--quiet" : "--no-quiet");

35
t/helper/test-crontab.c Normal file
View File

@ -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;
}

View File

@ -18,6 +18,7 @@ static struct test_cmd cmds[] = {
{ "bloom", cmd__bloom }, { "bloom", cmd__bloom },
{ "chmtime", cmd__chmtime }, { "chmtime", cmd__chmtime },
{ "config", cmd__config }, { "config", cmd__config },
{ "crontab", cmd__crontab },
{ "ctype", cmd__ctype }, { "ctype", cmd__ctype },
{ "date", cmd__date }, { "date", cmd__date },
{ "delta", cmd__delta }, { "delta", cmd__delta },

View 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__bloom(int argc, const char **argv);
int cmd__chmtime(int argc, const char **argv); int cmd__chmtime(int argc, const char **argv);
int cmd__config(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__ctype(int argc, const char **argv);
int cmd__date(int argc, const char **argv); int cmd__date(int argc, const char **argv);
int cmd__delta(int argc, const char **argv); int cmd__delta(int argc, const char **argv);

30
t/t0068-for-each-repo.sh Executable file
View File

@ -0,0 +1,30 @@
#!/bin/sh
test_description='git for-each-repo builtin'
. ./test-lib.sh
test_expect_success 'run based on configured value' '
git init one &&
git init two &&
git init three &&
git -C two commit --allow-empty -m "DID NOT RUN" &&
git config run.key "$TRASH_DIRECTORY/one" &&
git config --add run.key "$TRASH_DIRECTORY/three" &&
git for-each-repo --config=run.key commit --allow-empty -m "ran" &&
git -C one log -1 --pretty=format:%s >message &&
grep ran message &&
git -C two log -1 --pretty=format:%s >message &&
! grep ran message &&
git -C three log -1 --pretty=format:%s >message &&
grep ran message &&
git for-each-repo --config=run.key -- commit --allow-empty -m "ran again" &&
git -C one log -1 --pretty=format:%s >message &&
grep again message &&
git -C two log -1 --pretty=format:%s >message &&
! grep again message &&
git -C three log -1 --pretty=format:%s >message &&
grep again message
'
test_done

View File

@ -9,7 +9,7 @@ GIT_TEST_MULTI_PACK_INDEX=0
test_expect_success 'help text' ' test_expect_success 'help text' '
test_expect_code 129 git maintenance -h 2>err && test_expect_code 129 git maintenance -h 2>err &&
test_i18ngrep "usage: git maintenance run" err && test_i18ngrep "usage: git maintenance <subcommand>" err &&
test_expect_code 128 git maintenance barf 2>err && test_expect_code 128 git maintenance barf 2>err &&
test_i18ngrep "invalid subcommand: barf" err && test_i18ngrep "invalid subcommand: barf" err &&
test_expect_code 129 git maintenance 2>err && test_expect_code 129 git maintenance 2>err &&
@ -28,6 +28,19 @@ test_expect_success 'run [--auto|--quiet]' '
test_subcommand git gc --no-quiet <run-no-quiet.txt test_subcommand git gc --no-quiet <run-no-quiet.txt
' '
test_expect_success 'maintenance.auto config option' '
GIT_TRACE2_EVENT="$(pwd)/default" git commit --quiet --allow-empty -m 1 &&
test_subcommand git maintenance run --auto --quiet <default &&
GIT_TRACE2_EVENT="$(pwd)/true" \
git -c maintenance.auto=true \
commit --quiet --allow-empty -m 2 &&
test_subcommand git maintenance run --auto --quiet <true &&
GIT_TRACE2_EVENT="$(pwd)/false" \
git -c maintenance.auto=false \
commit --quiet --allow-empty -m 3 &&
test_subcommand ! git maintenance run --auto --quiet <false
'
test_expect_success 'maintenance.<task>.enabled' ' test_expect_success 'maintenance.<task>.enabled' '
git config maintenance.gc.enabled false && git config maintenance.gc.enabled false &&
git config maintenance.commit-graph.enabled true && git config maintenance.commit-graph.enabled true &&
@ -284,4 +297,148 @@ test_expect_success 'maintenance.incremental-repack.auto' '
test_subcommand git multi-pack-index write --no-progress <trace-B test_subcommand git multi-pack-index write --no-progress <trace-B
' '
test_expect_success '--auto and --schedule incompatible' '
test_must_fail git maintenance run --auto --schedule=daily 2>err &&
test_i18ngrep "at most one" err
'
test_expect_success 'invalid --schedule value' '
test_must_fail git maintenance run --schedule=annually 2>err &&
test_i18ngrep "unrecognized --schedule" err
'
test_expect_success '--schedule inheritance weekly -> daily -> hourly' '
git config maintenance.loose-objects.enabled true &&
git config maintenance.loose-objects.schedule hourly &&
git config maintenance.commit-graph.enabled true &&
git config maintenance.commit-graph.schedule daily &&
git config maintenance.incremental-repack.enabled true &&
git config maintenance.incremental-repack.schedule weekly &&
GIT_TRACE2_EVENT="$(pwd)/hourly.txt" \
git maintenance run --schedule=hourly 2>/dev/null &&
test_subcommand git prune-packed --quiet <hourly.txt &&
test_subcommand ! git commit-graph write --split --reachable \
--no-progress <hourly.txt &&
test_subcommand ! git multi-pack-index write --no-progress <hourly.txt &&
GIT_TRACE2_EVENT="$(pwd)/daily.txt" \
git maintenance run --schedule=daily 2>/dev/null &&
test_subcommand git prune-packed --quiet <daily.txt &&
test_subcommand git commit-graph write --split --reachable \
--no-progress <daily.txt &&
test_subcommand ! git multi-pack-index write --no-progress <daily.txt &&
GIT_TRACE2_EVENT="$(pwd)/weekly.txt" \
git maintenance run --schedule=weekly 2>/dev/null &&
test_subcommand git prune-packed --quiet <weekly.txt &&
test_subcommand git commit-graph write --split --reachable \
--no-progress <weekly.txt &&
test_subcommand git multi-pack-index write --no-progress <weekly.txt
'
test_expect_success 'maintenance.strategy inheritance' '
for task in commit-graph loose-objects incremental-repack
do
git config --unset maintenance.$task.schedule || return 1
done &&
test_when_finished git config --unset maintenance.strategy &&
git config maintenance.strategy incremental &&
GIT_TRACE2_EVENT="$(pwd)/incremental-hourly.txt" \
git maintenance run --schedule=hourly --quiet &&
GIT_TRACE2_EVENT="$(pwd)/incremental-daily.txt" \
git maintenance run --schedule=daily --quiet &&
test_subcommand git commit-graph write --split --reachable \
--no-progress <incremental-hourly.txt &&
test_subcommand ! git prune-packed --quiet <incremental-hourly.txt &&
test_subcommand ! git multi-pack-index write --no-progress \
<incremental-hourly.txt &&
test_subcommand git commit-graph write --split --reachable \
--no-progress <incremental-daily.txt &&
test_subcommand git prune-packed --quiet <incremental-daily.txt &&
test_subcommand git multi-pack-index write --no-progress \
<incremental-daily.txt &&
# Modify defaults
git config maintenance.commit-graph.schedule daily &&
git config maintenance.loose-objects.schedule hourly &&
git config maintenance.incremental-repack.enabled false &&
GIT_TRACE2_EVENT="$(pwd)/modified-hourly.txt" \
git maintenance run --schedule=hourly --quiet &&
GIT_TRACE2_EVENT="$(pwd)/modified-daily.txt" \
git maintenance run --schedule=daily --quiet &&
test_subcommand ! git commit-graph write --split --reachable \
--no-progress <modified-hourly.txt &&
test_subcommand git prune-packed --quiet <modified-hourly.txt &&
test_subcommand ! git multi-pack-index write --no-progress \
<modified-hourly.txt &&
test_subcommand git commit-graph write --split --reachable \
--no-progress <modified-daily.txt &&
test_subcommand git prune-packed --quiet <modified-daily.txt &&
test_subcommand ! git multi-pack-index write --no-progress \
<modified-daily.txt
'
test_expect_success 'register and unregister' '
test_when_finished git config --global --unset-all maintenance.repo &&
git config --global --add maintenance.repo /existing1 &&
git config --global --add maintenance.repo /existing2 &&
git config --global --get-all maintenance.repo >before &&
git maintenance register &&
test_cmp_config false maintenance.auto &&
git config --global --get-all maintenance.repo >between &&
cp before expect &&
pwd >>expect &&
test_cmp expect between &&
git maintenance unregister &&
git config --global --get-all maintenance.repo >actual &&
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_expect_success 'register preserves existing strategy' '
git config maintenance.strategy none &&
git maintenance register &&
test_config maintenance.strategy none &&
git config --unset maintenance.strategy &&
git maintenance register &&
test_config maintenance.strategy incremental
'
test_done test_done

View File

@ -1711,6 +1711,7 @@ test_lazy_prereq SHA1 '
test_lazy_prereq REBASE_P ' test_lazy_prereq REBASE_P '
test -z "$GIT_TEST_SKIP_REBASE_P" test -z "$GIT_TEST_SKIP_REBASE_P"
' '
# Special-purpose prereq for transitioning to a new default branch name: # Special-purpose prereq for transitioning to a new default branch name:
# Some tests need more than just a mindless (case-preserving) s/master/main/g # Some tests need more than just a mindless (case-preserving) s/master/main/g
# replacement. The non-trivial adjustments are guarded behind this # replacement. The non-trivial adjustments are guarded behind this
@ -1718,3 +1719,9 @@ test_lazy_prereq REBASE_P '
test_lazy_prereq PREPARE_FOR_MAIN_BRANCH ' test_lazy_prereq PREPARE_FOR_MAIN_BRANCH '
test "$GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME" = main test "$GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME" = main
' '
# 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"