diff options
Diffstat (limited to 'builtin/gc.c')
-rw-r--r-- | builtin/gc.c | 986 |
1 files changed, 666 insertions, 320 deletions
diff --git a/builtin/gc.c b/builtin/gc.c index 719cae9a88..34848626e4 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -9,13 +9,12 @@ * * Copyright (c) 2006 Shawn O. Pearce */ - +#define USE_THE_REPOSITORY_VARIABLE #include "builtin.h" #include "abspath.h" #include "date.h" #include "environment.h" #include "hex.h" -#include "repository.h" #include "config.h" #include "tempfile.h" #include "lockfile.h" @@ -49,20 +48,7 @@ static const char * const builtin_gc_usage[] = { NULL }; -static int pack_refs = 1; -static int prune_reflogs = 1; -static int cruft_packs = 1; -static int aggressive_depth = 50; -static int aggressive_window = 250; -static int gc_auto_threshold = 6700; -static int gc_auto_pack_limit = 50; -static int detach_auto = 1; static timestamp_t gc_log_expire_time; -static const char *gc_log_expire = "1.day.ago"; -static const char *prune_expire = "2.weeks.ago"; -static const char *prune_worktrees_expire = "3.months.ago"; -static unsigned long big_pack_threshold; -static unsigned long max_delta_cache_size = DEFAULT_DELTA_CACHE_SIZE; static struct strvec reflog = STRVEC_INIT; static struct strvec repack = STRVEC_INIT; @@ -122,13 +108,6 @@ static void process_log_file_at_exit(void) process_log_file(); } -static void process_log_file_on_signal(int signo) -{ - process_log_file(); - sigchain_pop(signo); - raise(signo); -} - static int gc_config_is_timestamp_never(const char *var) { const char *value; @@ -142,48 +121,158 @@ static int gc_config_is_timestamp_never(const char *var) return 0; } -static void gc_config(void) +struct gc_config { + int pack_refs; + int prune_reflogs; + int cruft_packs; + unsigned long max_cruft_size; + int aggressive_depth; + int aggressive_window; + int gc_auto_threshold; + int gc_auto_pack_limit; + int detach_auto; + char *gc_log_expire; + char *prune_expire; + char *prune_worktrees_expire; + char *repack_filter; + char *repack_filter_to; + unsigned long big_pack_threshold; + unsigned long max_delta_cache_size; +}; + +#define GC_CONFIG_INIT { \ + .pack_refs = 1, \ + .prune_reflogs = 1, \ + .cruft_packs = 1, \ + .aggressive_depth = 50, \ + .aggressive_window = 250, \ + .gc_auto_threshold = 6700, \ + .gc_auto_pack_limit = 50, \ + .detach_auto = 1, \ + .gc_log_expire = xstrdup("1.day.ago"), \ + .prune_expire = xstrdup("2.weeks.ago"), \ + .prune_worktrees_expire = xstrdup("3.months.ago"), \ + .max_delta_cache_size = DEFAULT_DELTA_CACHE_SIZE, \ +} + +static void gc_config_release(struct gc_config *cfg) +{ + free(cfg->gc_log_expire); + free(cfg->prune_expire); + free(cfg->prune_worktrees_expire); + free(cfg->repack_filter); + free(cfg->repack_filter_to); +} + +static void gc_config(struct gc_config *cfg) { const char *value; + char *owned = NULL; if (!git_config_get_value("gc.packrefs", &value)) { if (value && !strcmp(value, "notbare")) - pack_refs = -1; + cfg->pack_refs = -1; else - pack_refs = git_config_bool("gc.packrefs", value); + cfg->pack_refs = git_config_bool("gc.packrefs", value); } if (gc_config_is_timestamp_never("gc.reflogexpire") && gc_config_is_timestamp_never("gc.reflogexpireunreachable")) - prune_reflogs = 0; + cfg->prune_reflogs = 0; + + git_config_get_int("gc.aggressivewindow", &cfg->aggressive_window); + git_config_get_int("gc.aggressivedepth", &cfg->aggressive_depth); + git_config_get_int("gc.auto", &cfg->gc_auto_threshold); + git_config_get_int("gc.autopacklimit", &cfg->gc_auto_pack_limit); + git_config_get_bool("gc.autodetach", &cfg->detach_auto); + git_config_get_bool("gc.cruftpacks", &cfg->cruft_packs); + git_config_get_ulong("gc.maxcruftsize", &cfg->max_cruft_size); + + if (!repo_config_get_expiry(the_repository, "gc.pruneexpire", &owned)) { + free(cfg->prune_expire); + cfg->prune_expire = owned; + } + + if (!repo_config_get_expiry(the_repository, "gc.worktreepruneexpire", &owned)) { + free(cfg->prune_worktrees_expire); + cfg->prune_worktrees_expire = owned; + } + + if (!repo_config_get_expiry(the_repository, "gc.logexpiry", &owned)) { + free(cfg->gc_log_expire); + cfg->gc_log_expire = owned; + } + + git_config_get_ulong("gc.bigpackthreshold", &cfg->big_pack_threshold); + git_config_get_ulong("pack.deltacachesize", &cfg->max_delta_cache_size); - git_config_get_int("gc.aggressivewindow", &aggressive_window); - git_config_get_int("gc.aggressivedepth", &aggressive_depth); - git_config_get_int("gc.auto", &gc_auto_threshold); - git_config_get_int("gc.autopacklimit", &gc_auto_pack_limit); - git_config_get_bool("gc.autodetach", &detach_auto); - git_config_get_bool("gc.cruftpacks", &cruft_packs); - git_config_get_expiry("gc.pruneexpire", &prune_expire); - git_config_get_expiry("gc.worktreepruneexpire", &prune_worktrees_expire); - git_config_get_expiry("gc.logexpiry", &gc_log_expire); + if (!git_config_get_string("gc.repackfilter", &owned)) { + free(cfg->repack_filter); + cfg->repack_filter = owned; + } - git_config_get_ulong("gc.bigpackthreshold", &big_pack_threshold); - git_config_get_ulong("pack.deltacachesize", &max_delta_cache_size); + if (!git_config_get_string("gc.repackfilterto", &owned)) { + free(cfg->repack_filter_to); + cfg->repack_filter_to = owned; + } git_config(git_default_config, NULL); } -struct maintenance_run_opts; -static int maintenance_task_pack_refs(MAYBE_UNUSED struct maintenance_run_opts *opts) +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; +} + +struct maintenance_run_opts { + int auto_flag; + int detach; + int quiet; + enum schedule_priority schedule; +}; +#define MAINTENANCE_RUN_OPTS_INIT { \ + .detach = -1, \ +} + +static int pack_refs_condition(UNUSED struct gc_config *cfg) +{ + /* + * The auto-repacking logic for refs is handled by the ref backends and + * exposed via `git pack-refs --auto`. We thus always return truish + * here and let the backend decide for us. + */ + return 1; +} + +static int maintenance_task_pack_refs(struct maintenance_run_opts *opts, + UNUSED struct gc_config *cfg) { struct child_process cmd = CHILD_PROCESS_INIT; cmd.git_cmd = 1; strvec_pushl(&cmd.args, "pack-refs", "--all", "--prune", NULL); + if (opts->auto_flag) + strvec_push(&cmd.args, "--auto"); + return run_command(&cmd); } -static int too_many_loose_objects(void) +static int too_many_loose_objects(struct gc_config *cfg) { /* * Quickly check if a "gc" is needed, by estimating how @@ -202,7 +291,7 @@ static int too_many_loose_objects(void) if (!dir) return 0; - auto_threshold = DIV_ROUND_UP(gc_auto_threshold, 256); + auto_threshold = DIV_ROUND_UP(cfg->gc_auto_threshold, 256); while ((ent = readdir(dir)) != NULL) { if (strspn(ent->d_name, "0123456789abcdef") != hexsz_loose || ent->d_name[hexsz_loose] != '\0') @@ -238,12 +327,12 @@ static struct packed_git *find_base_packs(struct string_list *packs, return base; } -static int too_many_packs(void) +static int too_many_packs(struct gc_config *cfg) { struct packed_git *p; int cnt; - if (gc_auto_pack_limit <= 0) + if (cfg->gc_auto_pack_limit <= 0) return 0; for (cnt = 0, p = get_all_packs(the_repository); p; p = p->next) { @@ -257,7 +346,7 @@ static int too_many_packs(void) */ cnt++; } - return gc_auto_pack_limit < cnt; + return cfg->gc_auto_pack_limit < cnt; } static uint64_t total_ram(void) @@ -291,7 +380,8 @@ static uint64_t total_ram(void) return 0; } -static uint64_t estimate_repack_memory(struct packed_git *pack) +static uint64_t estimate_repack_memory(struct gc_config *cfg, + struct packed_git *pack) { unsigned long nr_objects = repo_approximate_object_count(the_repository); size_t os_cache, heap; @@ -328,7 +418,7 @@ static uint64_t estimate_repack_memory(struct packed_git *pack) */ heap += delta_base_cache_limit; /* and of course pack-objects has its own delta cache */ - heap += max_delta_cache_size; + heap += cfg->max_delta_cache_size; return os_cache + heap; } @@ -339,22 +429,31 @@ static int keep_one_pack(struct string_list_item *item, void *data UNUSED) return 0; } -static void add_repack_all_option(struct string_list *keep_pack) +static void add_repack_all_option(struct gc_config *cfg, + struct string_list *keep_pack) { - if (prune_expire && !strcmp(prune_expire, "now")) + if (cfg->prune_expire && !strcmp(cfg->prune_expire, "now")) strvec_push(&repack, "-a"); - else if (cruft_packs) { + else if (cfg->cruft_packs) { strvec_push(&repack, "--cruft"); - if (prune_expire) - strvec_pushf(&repack, "--cruft-expiration=%s", prune_expire); + if (cfg->prune_expire) + strvec_pushf(&repack, "--cruft-expiration=%s", cfg->prune_expire); + if (cfg->max_cruft_size) + strvec_pushf(&repack, "--max-cruft-size=%lu", + cfg->max_cruft_size); } else { strvec_push(&repack, "-A"); - if (prune_expire) - strvec_pushf(&repack, "--unpack-unreachable=%s", prune_expire); + if (cfg->prune_expire) + strvec_pushf(&repack, "--unpack-unreachable=%s", cfg->prune_expire); } if (keep_pack) for_each_string_list(keep_pack, keep_one_pack, NULL); + + if (cfg->repack_filter && *cfg->repack_filter) + strvec_pushf(&repack, "--filter=%s", cfg->repack_filter); + if (cfg->repack_filter_to && *cfg->repack_filter_to) + strvec_pushf(&repack, "--filter-to=%s", cfg->repack_filter_to); } static void add_repack_incremental_option(void) @@ -362,13 +461,13 @@ static void add_repack_incremental_option(void) strvec_push(&repack, "--no-write-bitmap-index"); } -static int need_to_gc(void) +static int need_to_gc(struct gc_config *cfg) { /* * Setting gc.auto to 0 or negative can disable the * automatic gc. */ - if (gc_auto_threshold <= 0) + if (cfg->gc_auto_threshold <= 0) return 0; /* @@ -377,13 +476,13 @@ static int need_to_gc(void) * we run "repack -A -d -l". Otherwise we tell the caller * there is no need. */ - if (too_many_packs()) { + if (too_many_packs(cfg)) { struct string_list keep_pack = STRING_LIST_INIT_NODUP; - if (big_pack_threshold) { - find_base_packs(&keep_pack, big_pack_threshold); - if (keep_pack.nr >= gc_auto_pack_limit) { - big_pack_threshold = 0; + if (cfg->big_pack_threshold) { + find_base_packs(&keep_pack, cfg->big_pack_threshold); + if (keep_pack.nr >= cfg->gc_auto_pack_limit) { + cfg->big_pack_threshold = 0; string_list_clear(&keep_pack, 0); find_base_packs(&keep_pack, 0); } @@ -392,7 +491,7 @@ static int need_to_gc(void) uint64_t mem_have, mem_want; mem_have = total_ram(); - mem_want = estimate_repack_memory(p); + mem_want = estimate_repack_memory(cfg, p); /* * Only allow 1/2 of memory for pack-objects, leave @@ -403,14 +502,14 @@ static int need_to_gc(void) string_list_clear(&keep_pack, 0); } - add_repack_all_option(&keep_pack); + add_repack_all_option(cfg, &keep_pack); string_list_clear(&keep_pack, 0); - } else if (too_many_loose_objects()) + } else if (too_many_loose_objects(cfg)) add_repack_incremental_option(); else return 0; - if (run_hooks("pre-auto-gc")) + if (run_hooks(the_repository, "pre-auto-gc")) return 0; return 1; } @@ -532,7 +631,8 @@ done: return ret; } -static void gc_before_repack(void) +static void gc_before_repack(struct maintenance_run_opts *opts, + struct gc_config *cfg) { /* * We may be called twice, as both the pre- and @@ -543,10 +643,10 @@ static void gc_before_repack(void) if (done++) return; - if (pack_refs && maintenance_task_pack_refs(NULL)) + if (cfg->pack_refs && maintenance_task_pack_refs(opts, cfg)) die(FAILED_RUN, "pack-refs"); - if (prune_reflogs) { + if (cfg->prune_reflogs) { struct child_process cmd = CHILD_PROCESS_INIT; cmd.git_cmd = 1; @@ -556,10 +656,12 @@ static void gc_before_repack(void) } } -int cmd_gc(int argc, const char **argv, const char *prefix) +int cmd_gc(int argc, +const char **argv, +const char *prefix, +struct repository *repo UNUSED) { int aggressive = 0; - int auto_gc = 0; int quiet = 0; int force = 0; const char *name; @@ -568,16 +670,25 @@ int cmd_gc(int argc, const char **argv, const char *prefix) int keep_largest_pack = -1; timestamp_t dummy; struct child_process rerere_cmd = CHILD_PROCESS_INIT; + struct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT; + struct gc_config cfg = GC_CONFIG_INIT; + const char *prune_expire_sentinel = "sentinel"; + const char *prune_expire_arg = prune_expire_sentinel; + int ret; struct option builtin_gc_options[] = { OPT__QUIET(&quiet, N_("suppress progress reporting")), - { OPTION_STRING, 0, "prune", &prune_expire, N_("date"), + { OPTION_STRING, 0, "prune", &prune_expire_arg, N_("date"), N_("prune unreferenced objects"), - PARSE_OPT_OPTARG, NULL, (intptr_t)prune_expire }, - OPT_BOOL(0, "cruft", &cruft_packs, N_("pack unreferenced objects separately")), + PARSE_OPT_OPTARG, NULL, (intptr_t)prune_expire_arg }, + OPT_BOOL(0, "cruft", &cfg.cruft_packs, N_("pack unreferenced objects separately")), + OPT_MAGNITUDE(0, "max-cruft-size", &cfg.max_cruft_size, + N_("with --cruft, limit the size of new cruft packs")), OPT_BOOL(0, "aggressive", &aggressive, N_("be more thorough (increased runtime)")), - OPT_BOOL_F(0, "auto", &auto_gc, N_("enable auto-gc mode"), + OPT_BOOL_F(0, "auto", &opts.auto_flag, N_("enable auto-gc mode"), PARSE_OPT_NOCOMPLETE), + OPT_BOOL(0, "detach", &opts.detach, + N_("perform garbage collection in the background")), OPT_BOOL_F(0, "force", &force, N_("force running gc even if there may be another gc running"), PARSE_OPT_NOCOMPLETE), @@ -595,84 +706,103 @@ int cmd_gc(int argc, const char **argv, const char *prefix) strvec_pushl(&prune_worktrees, "worktree", "prune", "--expire", NULL); strvec_pushl(&rerere, "rerere", "gc", NULL); - /* default expiry time, overwritten in gc_config */ - gc_config(); - if (parse_expiry_date(gc_log_expire, &gc_log_expire_time)) - die(_("failed to parse gc.logExpiry value %s"), gc_log_expire); + gc_config(&cfg); + + if (parse_expiry_date(cfg.gc_log_expire, &gc_log_expire_time)) + die(_("failed to parse gc.logExpiry value %s"), cfg.gc_log_expire); - if (pack_refs < 0) - pack_refs = !is_bare_repository(); + if (cfg.pack_refs < 0) + cfg.pack_refs = !is_bare_repository(); argc = parse_options(argc, argv, prefix, builtin_gc_options, builtin_gc_usage, 0); if (argc > 0) usage_with_options(builtin_gc_usage, builtin_gc_options); - if (prune_expire && parse_expiry_date(prune_expire, &dummy)) - die(_("failed to parse prune expiry value %s"), prune_expire); + if (prune_expire_arg != prune_expire_sentinel) { + free(cfg.prune_expire); + cfg.prune_expire = xstrdup_or_null(prune_expire_arg); + } + if (cfg.prune_expire && parse_expiry_date(cfg.prune_expire, &dummy)) + die(_("failed to parse prune expiry value %s"), cfg.prune_expire); if (aggressive) { strvec_push(&repack, "-f"); - if (aggressive_depth > 0) - strvec_pushf(&repack, "--depth=%d", aggressive_depth); - if (aggressive_window > 0) - strvec_pushf(&repack, "--window=%d", aggressive_window); + if (cfg.aggressive_depth > 0) + strvec_pushf(&repack, "--depth=%d", cfg.aggressive_depth); + if (cfg.aggressive_window > 0) + strvec_pushf(&repack, "--window=%d", cfg.aggressive_window); } if (quiet) strvec_push(&repack, "-q"); - if (auto_gc) { + if (opts.auto_flag) { + if (cfg.detach_auto && opts.detach < 0) + opts.detach = 1; + /* * Auto-gc should be least intrusive as possible. */ - if (!need_to_gc()) - return 0; + if (!need_to_gc(&cfg)) { + ret = 0; + goto out; + } + if (!quiet) { - if (detach_auto) + if (opts.detach > 0) fprintf(stderr, _("Auto packing the repository in background for optimum performance.\n")); else fprintf(stderr, _("Auto packing the repository for optimum performance.\n")); fprintf(stderr, _("See \"git help gc\" for manual housekeeping.\n")); } - if (detach_auto) { - int ret = report_last_gc_error(); - - if (ret == 1) - /* Last gc --auto failed. Skip this one. */ - return 0; - else if (ret) - /* an I/O error occurred, already reported */ - return ret; - - if (lock_repo_for_gc(force, &pid)) - return 0; - gc_before_repack(); /* dies on failure */ - delete_tempfile(&pidfile); - - /* - * failure to daemonize is ok, we'll continue - * in foreground - */ - daemonized = !daemonize(); - } } else { struct string_list keep_pack = STRING_LIST_INIT_NODUP; if (keep_largest_pack != -1) { if (keep_largest_pack) find_base_packs(&keep_pack, 0); - } else if (big_pack_threshold) { - find_base_packs(&keep_pack, big_pack_threshold); + } else if (cfg.big_pack_threshold) { + find_base_packs(&keep_pack, cfg.big_pack_threshold); } - add_repack_all_option(&keep_pack); + add_repack_all_option(&cfg, &keep_pack); string_list_clear(&keep_pack, 0); } + if (opts.detach > 0) { + ret = report_last_gc_error(); + if (ret == 1) { + /* Last gc --auto failed. Skip this one. */ + ret = 0; + goto out; + + } else if (ret) { + /* an I/O error occurred, already reported */ + goto out; + } + + if (lock_repo_for_gc(force, &pid)) { + ret = 0; + goto out; + } + + gc_before_repack(&opts, &cfg); /* dies on failure */ + delete_tempfile(&pidfile); + + /* + * failure to daemonize is ok, we'll continue + * in foreground + */ + daemonized = !daemonize(); + } + name = lock_repo_for_gc(force, &pid); if (name) { - if (auto_gc) - return 0; /* be quiet on --auto */ + if (opts.auto_flag) { + ret = 0; + goto out; /* be quiet on --auto */ + } + die(_("gc is already running on machine '%s' pid %"PRIuMAX" (use --force if not)"), name, (uintmax_t)pid); } @@ -682,11 +812,10 @@ int cmd_gc(int argc, const char **argv, const char *prefix) git_path("gc.log"), LOCK_DIE_ON_ERROR); dup2(get_lock_file_fd(&log_lock), 2); - sigchain_push_common(process_log_file_on_signal); atexit(process_log_file_at_exit); } - gc_before_repack(); + gc_before_repack(&opts, &cfg); if (!repository_format_precious_objects) { struct child_process repack_cmd = CHILD_PROCESS_INIT; @@ -697,11 +826,11 @@ int cmd_gc(int argc, const char **argv, const char *prefix) if (run_command(&repack_cmd)) die(FAILED_RUN, repack.v[0]); - if (prune_expire) { + if (cfg.prune_expire) { struct child_process prune_cmd = CHILD_PROCESS_INIT; /* run `git prune` even if using cruft packs */ - strvec_push(&prune, prune_expire); + strvec_push(&prune, cfg.prune_expire); if (quiet) strvec_push(&prune, "--no-progress"); if (repo_has_promisor_remote(the_repository)) @@ -714,10 +843,10 @@ int cmd_gc(int argc, const char **argv, const char *prefix) } } - if (prune_worktrees_expire) { + if (cfg.prune_worktrees_expire) { struct child_process prune_worktrees_cmd = CHILD_PROCESS_INIT; - strvec_push(&prune_worktrees, prune_worktrees_expire); + strvec_push(&prune_worktrees, cfg.prune_worktrees_expire); prune_worktrees_cmd.git_cmd = 1; strvec_pushv(&prune_worktrees_cmd.args, prune_worktrees.v); if (run_command(&prune_worktrees_cmd)) @@ -741,13 +870,15 @@ int cmd_gc(int argc, const char **argv, const char *prefix) !quiet && !daemonized ? COMMIT_GRAPH_WRITE_PROGRESS : 0, NULL); - if (auto_gc && too_many_loose_objects()) + if (opts.auto_flag && too_many_loose_objects(&cfg)) warning(_("There are too many unreachable loose objects; " "run 'git prune' to remove them.")); if (!daemonized) unlink(git_path("gc.log")); +out: + gc_config_release(&cfg); return 0; } @@ -756,26 +887,6 @@ static const char *const builtin_maintenance_run_usage[] = { 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) { @@ -792,12 +903,6 @@ static int maintenance_opt_schedule(const struct option *opt, const char *arg, return 0; } -struct maintenance_run_opts { - int auto_flag; - int quiet; - enum schedule_priority schedule; -}; - /* Remember to update object flag allocation in object.h */ #define SEEN (1u<<0) @@ -807,6 +912,7 @@ struct cg_auto_data { }; static int dfs_on_ref(const char *refname UNUSED, + const char *referent UNUSED, const struct object_id *oid, int flags UNUSED, void *cb_data) @@ -817,7 +923,7 @@ static int dfs_on_ref(const char *refname UNUSED, struct commit_list *stack = NULL; struct commit *commit; - if (!peel_iterated_oid(oid, &peeled)) + if (!peel_iterated_oid(the_repository, oid, &peeled)) oid = &peeled; if (oid_object_info(the_repository, oid, NULL) != OBJ_COMMIT) return 0; @@ -863,7 +969,7 @@ static int dfs_on_ref(const char *refname UNUSED, return result; } -static int should_write_commit_graph(void) +static int should_write_commit_graph(struct gc_config *cfg UNUSED) { int result; struct cg_auto_data data; @@ -878,7 +984,8 @@ static int should_write_commit_graph(void) if (data.limit < 0) return 1; - result = for_each_ref(dfs_on_ref, &data); + result = refs_for_each_ref(get_main_ref_store(the_repository), + dfs_on_ref, &data); repo_clear_commit_marks(the_repository, SEEN); @@ -899,7 +1006,8 @@ static int run_write_commit_graph(struct maintenance_run_opts *opts) return !!run_command(&child); } -static int maintenance_task_commit_graph(struct maintenance_run_opts *opts) +static int maintenance_task_commit_graph(struct maintenance_run_opts *opts, + struct gc_config *cfg UNUSED) { prepare_repo_settings(the_repository); if (!the_repository->settings.core_commit_graph) @@ -933,7 +1041,8 @@ static int fetch_remote(struct remote *remote, void *cbdata) return !!run_command(&child); } -static int maintenance_task_prefetch(struct maintenance_run_opts *opts) +static int maintenance_task_prefetch(struct maintenance_run_opts *opts, + struct gc_config *cfg UNUSED) { if (for_each_remote(fetch_remote, opts)) { error(_("failed to prefetch remotes")); @@ -943,7 +1052,8 @@ static int maintenance_task_prefetch(struct maintenance_run_opts *opts) return 0; } -static int maintenance_task_gc(struct maintenance_run_opts *opts) +static int maintenance_task_gc(struct maintenance_run_opts *opts, + struct gc_config *cfg UNUSED) { struct child_process child = CHILD_PROCESS_INIT; @@ -956,6 +1066,7 @@ static int maintenance_task_gc(struct maintenance_run_opts *opts) strvec_push(&child.args, "--quiet"); else strvec_push(&child.args, "--no-quiet"); + strvec_push(&child.args, "--no-detach"); return run_command(&child); } @@ -991,7 +1102,7 @@ static int loose_object_count(const struct object_id *oid UNUSED, return 0; } -static int loose_object_auto_condition(void) +static int loose_object_auto_condition(struct gc_config *cfg UNUSED) { int count = 0; @@ -1051,6 +1162,12 @@ static int pack_loose(struct maintenance_run_opts *opts) pack_proc.in = -1; + /* + * git-pack-objects(1) ends up writing the pack hash to stdout, which + * we do not care for. + */ + pack_proc.out = -1; + if (start_command(&pack_proc)) { error(_("failed to start 'git pack-objects' process")); return 1; @@ -1076,12 +1193,13 @@ static int pack_loose(struct maintenance_run_opts *opts) return result; } -static int maintenance_task_loose_objects(struct maintenance_run_opts *opts) +static int maintenance_task_loose_objects(struct maintenance_run_opts *opts, + struct gc_config *cfg UNUSED) { return prune_packed(opts) || pack_loose(opts); } -static int incremental_repack_auto_condition(void) +static int incremental_repack_auto_condition(struct gc_config *cfg UNUSED) { struct packed_git *p; int incremental_repack_auto_limit = 10; @@ -1200,7 +1318,8 @@ static int multi_pack_index_repack(struct maintenance_run_opts *opts) return 0; } -static int maintenance_task_incremental_repack(struct maintenance_run_opts *opts) +static int maintenance_task_incremental_repack(struct maintenance_run_opts *opts, + struct gc_config *cfg UNUSED) { prepare_repo_settings(the_repository); if (!the_repository->settings.core_multi_pack_index) { @@ -1217,14 +1336,15 @@ static int maintenance_task_incremental_repack(struct maintenance_run_opts *opts return 0; } -typedef int maintenance_task_fn(struct maintenance_run_opts *opts); +typedef int maintenance_task_fn(struct maintenance_run_opts *opts, + struct gc_config *cfg); /* * An auto condition function returns 1 if the task should run * and 0 if the task should NOT run. See needs_to_gc() for an * example. */ -typedef int maintenance_auto_fn(void); +typedef int maintenance_auto_fn(struct gc_config *cfg); struct maintenance_task { const char *name; @@ -1279,7 +1399,7 @@ static struct maintenance_task tasks[] = { [TASK_PACK_REFS] = { "pack-refs", maintenance_task_pack_refs, - NULL, + pack_refs_condition, }, }; @@ -1291,7 +1411,8 @@ static int compare_tasks_by_selection(const void *a_, const void *b_) return b->selected_order - a->selected_order; } -static int maintenance_run_tasks(struct maintenance_run_opts *opts) +static int maintenance_run_tasks(struct maintenance_run_opts *opts, + struct gc_config *cfg) { int i, found_selected = 0; int result = 0; @@ -1315,6 +1436,13 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts) } free(lock_path); + /* Failure to daemonize is ok, we'll continue in foreground. */ + if (opts->detach > 0) { + trace2_region_enter("maintenance", "detach", the_repository); + daemonize(); + trace2_region_leave("maintenance", "detach", the_repository); + } + for (i = 0; !found_selected && i < TASK__COUNT; i++) found_selected = tasks[i].selected_order >= 0; @@ -1330,14 +1458,14 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts) if (opts->auto_flag && (!tasks[i].auto_condition || - !tasks[i].auto_condition())) + !tasks[i].auto_condition(cfg))) continue; if (opts->schedule && tasks[i].schedule < opts->schedule) continue; trace2_region_enter("maintenance", tasks[i].name, r); - if (tasks[i].fn(opts)) { + if (tasks[i].fn(opts, cfg)) { error(_("task '%s' failed"), tasks[i].name); result = 1; } @@ -1350,9 +1478,9 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts) static void initialize_maintenance_strategy(void) { - char *config_str; + const char *config_str; - if (git_config_get_string("maintenance.strategy", &config_str)) + if (git_config_get_string_tmp("maintenance.strategy", &config_str)) return; if (!strcasecmp(config_str, "incremental")) { @@ -1374,7 +1502,6 @@ static void initialize_task_config(int schedule) { int i; struct strbuf config_name = STRBUF_INIT; - gc_config(); if (schedule) initialize_maintenance_strategy(); @@ -1403,7 +1530,7 @@ static void initialize_task_config(int schedule) strbuf_release(&config_name); } -static int task_option_parse(const struct option *opt, +static int task_option_parse(const struct option *opt UNUSED, const char *arg, int unset) { int i, num_selected = 0; @@ -1437,10 +1564,13 @@ static int task_option_parse(const struct option *opt, static int maintenance_run(int argc, const char **argv, const char *prefix) { int i; - struct maintenance_run_opts opts; + struct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT; + struct gc_config cfg = GC_CONFIG_INIT; struct option builtin_maintenance_run_options[] = { OPT_BOOL(0, "auto", &opts.auto_flag, N_("run tasks based on the state of the repository")), + OPT_BOOL(0, "detach", &opts.detach, + N_("perform maintenance in the background")), OPT_CALLBACK(0, "schedule", &opts.schedule, N_("frequency"), N_("run tasks based on frequency"), maintenance_opt_schedule), @@ -1451,7 +1581,7 @@ static int maintenance_run(int argc, const char **argv, const char *prefix) PARSE_OPT_NONEG, task_option_parse), OPT_END() }; - memset(&opts, 0, sizeof(opts)); + int ret; opts.quiet = !isatty(2); @@ -1466,12 +1596,16 @@ static int maintenance_run(int argc, const char **argv, const char *prefix) if (opts.auto_flag && opts.schedule) die(_("use at most one of --auto and --schedule=<frequency>")); + gc_config(&cfg); initialize_task_config(opts.schedule); if (argc != 0) usage_with_options(builtin_maintenance_run_usage, builtin_maintenance_run_options); - return maintenance_run_tasks(&opts); + + ret = maintenance_run_tasks(&opts, &cfg); + gc_config_release(&cfg); + return ret; } static char *get_maintpath(void) @@ -1526,19 +1660,18 @@ static int maintenance_register(int argc, const char **argv, const char *prefix) if (!found) { int rc; - char *user_config = NULL, *xdg_config = NULL; + char *global_config_file = NULL; if (!config_file) { - git_global_config(&user_config, &xdg_config); - config_file = user_config; - if (!user_config) - die(_("$HOME not set")); + global_config_file = git_global_config(); + config_file = global_config_file; } + if (!config_file) + die(_("$HOME not set")); rc = git_config_set_multivar_in_file_gently( config_file, "maintenance.repo", maintpath, - CONFIG_REGEX_NONE, 0); - free(user_config); - free(xdg_config); + CONFIG_REGEX_NONE, NULL, 0); + free(global_config_file); if (rc) die(_("unable to add '%s' value of '%s'"), @@ -1595,18 +1728,18 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi if (found) { int rc; - char *user_config = NULL, *xdg_config = NULL; + char *global_config_file = NULL; + if (!config_file) { - git_global_config(&user_config, &xdg_config); - config_file = user_config; - if (!user_config) - die(_("$HOME not set")); + global_config_file = git_global_config(); + config_file = global_config_file; } + if (!config_file) + die(_("$HOME not set")); rc = git_config_set_multivar_in_file_gently( - config_file, key, NULL, maintpath, + config_file, key, NULL, maintpath, NULL, CONFIG_FLAGS_MULTI_REPLACE | CONFIG_FLAGS_FIXED_VALUE); - free(user_config); - free(xdg_config); + free(global_config_file); if (rc && (!force || rc == CONFIG_NOTHING_SET)) @@ -1635,6 +1768,42 @@ static const char *get_frequency(enum schedule_priority schedule) } } +static const char *extraconfig[] = { + "credential.interactive=false", + "core.askPass=true", /* 'true' returns success, but no output. */ + NULL +}; + +static const char *get_extra_config_parameters(void) { + static const char *result = NULL; + struct strbuf builder = STRBUF_INIT; + + if (result) + return result; + + for (const char **s = extraconfig; s && *s; s++) + strbuf_addf(&builder, "-c %s ", *s); + + result = strbuf_detach(&builder, NULL); + return result; +} + +static const char *get_extra_launchctl_strings(void) { + static const char *result = NULL; + struct strbuf builder = STRBUF_INIT; + + if (result) + return result; + + for (const char **s = extraconfig; s && *s; s++) { + strbuf_addstr(&builder, "<string>-c</string>\n"); + strbuf_addf(&builder, "<string>%s</string>\n", *s); + } + + result = strbuf_detach(&builder, NULL); + return result; +} + /* * get_schedule_cmd` reads the GIT_TEST_MAINT_SCHEDULER environment variable * to mock the schedulers that `git maintenance start` rely on. @@ -1649,39 +1818,43 @@ static const char *get_frequency(enum schedule_priority schedule) * * If $GIT_TEST_MAINT_SCHEDULER is set, return true. * In this case, the *cmd value is read as input. * - * * if the input value *cmd is the key of one of the comma-separated list - * item, then *is_available is set to true and *cmd is modified and becomes + * * if the input value cmd is the key of one of the comma-separated list + * item, then *is_available is set to true and *out is set to * the mock command. * * * if the input value *cmd isn’t the key of any of the comma-separated list - * item, then *is_available is set to false. + * item, then *is_available is set to false and *out is set to the original + * command. * * Ex.: * GIT_TEST_MAINT_SCHEDULER not set * +-------+-------------------------------------------------+ * | Input | Output | - * | *cmd | return code | *cmd | *is_available | + * | *cmd | return code | *out | *is_available | * +-------+-------------+-------------------+---------------+ - * | "foo" | false | "foo" (unchanged) | (unchanged) | + * | "foo" | false | "foo" (allocated) | (unchanged) | * +-------+-------------+-------------------+---------------+ * * GIT_TEST_MAINT_SCHEDULER set to “foo:./mock_foo.sh,bar:./mock_bar.sh” * +-------+-------------------------------------------------+ * | Input | Output | - * | *cmd | return code | *cmd | *is_available | + * | *cmd | return code | *out | *is_available | * +-------+-------------+-------------------+---------------+ * | "foo" | true | "./mock.foo.sh" | true | - * | "qux" | true | "qux" (unchanged) | false | + * | "qux" | true | "qux" (allocated) | false | * +-------+-------------+-------------------+---------------+ */ -static int get_schedule_cmd(const char **cmd, int *is_available) +static int get_schedule_cmd(const char *cmd, int *is_available, char **out) { char *testing = xstrdup_or_null(getenv("GIT_TEST_MAINT_SCHEDULER")); struct string_list_item *item; struct string_list list = STRING_LIST_INIT_NODUP; - if (!testing) + if (!testing) { + if (out) + *out = xstrdup(cmd); return 0; + } if (is_available) *is_available = 0; @@ -1693,26 +1866,40 @@ static int get_schedule_cmd(const char **cmd, int *is_available) if (string_list_split_in_place(&pair, item->string, ":", 2) != 2) continue; - if (!strcmp(*cmd, pair.items[0].string)) { - *cmd = pair.items[1].string; + if (!strcmp(cmd, pair.items[0].string)) { + if (out) + *out = xstrdup(pair.items[1].string); if (is_available) *is_available = 1; - string_list_clear(&list, 0); - UNLEAK(testing); - return 1; + string_list_clear(&pair, 0); + goto out; } + + string_list_clear(&pair, 0); } + if (out) + *out = xstrdup(cmd); + +out: string_list_clear(&list, 0); free(testing); return 1; } +static int get_random_minute(void) +{ + /* Use a static value when under tests. */ + if (getenv("GIT_TEST_MAINT_SCHEDULER")) + return 13; + + return git_rand() % 60; +} + static int is_launchctl_available(void) { - const char *cmd = "launchctl"; int is_available; - if (get_schedule_cmd(&cmd, &is_available)) + if (get_schedule_cmd("launchctl", &is_available, NULL)) return is_available; #ifdef __APPLE__ @@ -1750,12 +1937,12 @@ static char *launchctl_get_uid(void) static int launchctl_boot_plist(int enable, const char *filename) { - const char *cmd = "launchctl"; + char *cmd; int result; struct child_process child = CHILD_PROCESS_INIT; char *uid = launchctl_get_uid(); - get_schedule_cmd(&cmd, NULL); + get_schedule_cmd("launchctl", NULL, &cmd); strvec_split(&child.args, cmd); strvec_pushl(&child.args, enable ? "bootstrap" : "bootout", uid, filename, NULL); @@ -1768,6 +1955,7 @@ static int launchctl_boot_plist(int enable, const char *filename) result = finish_command(&child); + free(cmd); free(uid); return result; } @@ -1819,9 +2007,10 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit static unsigned long lock_file_timeout_ms = ULONG_MAX; struct strbuf plist = STRBUF_INIT, plist2 = STRBUF_INIT; struct stat st; - const char *cmd = "launchctl"; + char *cmd; + int minute = get_random_minute(); - get_schedule_cmd(&cmd, NULL); + get_schedule_cmd("launchctl", NULL, &cmd); preamble = "<?xml version=\"1.0\"?>\n" "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n" "<plist version=\"1.0\">" @@ -1831,7 +2020,9 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit "<array>\n" "<string>%s/git</string>\n" "<string>--exec-path=%s</string>\n" + "%s" /* For extra config parameters. */ "<string>for-each-repo</string>\n" + "<string>--keep-going</string>\n" "<string>--config=maintenance.repo</string>\n" "<string>maintenance</string>\n" "<string>run</string>\n" @@ -1839,35 +2030,37 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit "</array>\n" "<key>StartCalendarInterval</key>\n" "<array>\n"; - strbuf_addf(&plist, preamble, name, exec_path, exec_path, frequency); + strbuf_addf(&plist, preamble, name, exec_path, exec_path, + get_extra_launchctl_strings(), frequency); switch (schedule) { case SCHEDULE_HOURLY: repeat = "<dict>\n" "<key>Hour</key><integer>%d</integer>\n" - "<key>Minute</key><integer>0</integer>\n" + "<key>Minute</key><integer>%d</integer>\n" "</dict>\n"; for (i = 1; i <= 23; i++) - strbuf_addf(&plist, repeat, i); + strbuf_addf(&plist, repeat, i, minute); break; case SCHEDULE_DAILY: repeat = "<dict>\n" "<key>Day</key><integer>%d</integer>\n" "<key>Hour</key><integer>0</integer>\n" - "<key>Minute</key><integer>0</integer>\n" + "<key>Minute</key><integer>%d</integer>\n" "</dict>\n"; for (i = 1; i <= 6; i++) - strbuf_addf(&plist, repeat, i); + strbuf_addf(&plist, repeat, i, minute); break; case SCHEDULE_WEEKLY: - strbuf_addstr(&plist, - "<dict>\n" - "<key>Day</key><integer>0</integer>\n" - "<key>Hour</key><integer>0</integer>\n" - "<key>Minute</key><integer>0</integer>\n" - "</dict>\n"); + strbuf_addf(&plist, + "<dict>\n" + "<key>Day</key><integer>0</integer>\n" + "<key>Hour</key><integer>0</integer>\n" + "<key>Minute</key><integer>%d</integer>\n" + "</dict>\n", + minute); break; default: @@ -1909,6 +2102,7 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit free(filename); free(name); + free(cmd); strbuf_release(&plist); strbuf_release(&plist2); return 0; @@ -1923,7 +2117,7 @@ static int launchctl_add_plists(void) launchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY); } -static int launchctl_update_schedule(int run_maintenance, int fd) +static int launchctl_update_schedule(int run_maintenance, int fd UNUSED) { if (run_maintenance) return launchctl_add_plists(); @@ -1933,9 +2127,8 @@ static int launchctl_update_schedule(int run_maintenance, int fd) static int is_schtasks_available(void) { - const char *cmd = "schtasks"; int is_available; - if (get_schedule_cmd(&cmd, &is_available)) + if (get_schedule_cmd("schtasks", &is_available, NULL)) return is_available; #ifdef GIT_WINDOWS_NATIVE @@ -1954,15 +2147,16 @@ static char *schtasks_task_name(const char *frequency) static int schtasks_remove_task(enum schedule_priority schedule) { - const char *cmd = "schtasks"; + char *cmd; struct child_process child = CHILD_PROCESS_INIT; const char *frequency = get_frequency(schedule); char *name = schtasks_task_name(frequency); - get_schedule_cmd(&cmd, NULL); + get_schedule_cmd("schtasks", NULL, &cmd); strvec_split(&child.args, cmd); strvec_pushl(&child.args, "/delete", "/tn", name, "/f", NULL); free(name); + free(cmd); return run_command(&child); } @@ -1976,7 +2170,7 @@ static int schtasks_remove_tasks(void) static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule) { - const char *cmd = "schtasks"; + char *cmd; int result; struct child_process child = CHILD_PROCESS_INIT; const char *xml; @@ -1984,11 +2178,12 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority const char *frequency = get_frequency(schedule); char *name = schtasks_task_name(frequency); struct strbuf tfilename = STRBUF_INIT; + int minute = get_random_minute(); - get_schedule_cmd(&cmd, NULL); + get_schedule_cmd("schtasks", NULL, &cmd); strbuf_addf(&tfilename, "%s/schedule_%s_XXXXXX", - get_git_common_dir(), frequency); + repo_get_common_dir(the_repository), frequency); tfile = xmks_tempfile(tfilename.buf); strbuf_release(&tfilename); @@ -2004,7 +2199,7 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority switch (schedule) { case SCHEDULE_HOURLY: fprintf(tfile->fp, - "<StartBoundary>2020-01-01T01:00:00</StartBoundary>\n" + "<StartBoundary>2020-01-01T01:%02d:00</StartBoundary>\n" "<Enabled>true</Enabled>\n" "<ScheduleByDay>\n" "<DaysInterval>1</DaysInterval>\n" @@ -2013,12 +2208,13 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority "<Interval>PT1H</Interval>\n" "<Duration>PT23H</Duration>\n" "<StopAtDurationEnd>false</StopAtDurationEnd>\n" - "</Repetition>\n"); + "</Repetition>\n", + minute); break; case SCHEDULE_DAILY: fprintf(tfile->fp, - "<StartBoundary>2020-01-01T00:00:00</StartBoundary>\n" + "<StartBoundary>2020-01-01T00:%02d:00</StartBoundary>\n" "<Enabled>true</Enabled>\n" "<ScheduleByWeek>\n" "<DaysOfWeek>\n" @@ -2030,19 +2226,21 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority "<Saturday />\n" "</DaysOfWeek>\n" "<WeeksInterval>1</WeeksInterval>\n" - "</ScheduleByWeek>\n"); + "</ScheduleByWeek>\n", + minute); break; case SCHEDULE_WEEKLY: fprintf(tfile->fp, - "<StartBoundary>2020-01-01T00:00:00</StartBoundary>\n" + "<StartBoundary>2020-01-01T00:%02d:00</StartBoundary>\n" "<Enabled>true</Enabled>\n" "<ScheduleByWeek>\n" "<DaysOfWeek>\n" "<Sunday />\n" "</DaysOfWeek>\n" "<WeeksInterval>1</WeeksInterval>\n" - "</ScheduleByWeek>\n"); + "</ScheduleByWeek>\n", + minute); break; default: @@ -2069,11 +2267,12 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority "<Actions Context=\"Author\">\n" "<Exec>\n" "<Command>\"%s\\headless-git.exe\"</Command>\n" - "<Arguments>--exec-path=\"%s\" for-each-repo --config=maintenance.repo maintenance run --schedule=%s</Arguments>\n" + "<Arguments>--exec-path=\"%s\" %s for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%s</Arguments>\n" "</Exec>\n" "</Actions>\n" "</Task>\n"; - fprintf(tfile->fp, xml, exec_path, exec_path, frequency); + fprintf(tfile->fp, xml, exec_path, exec_path, + get_extra_config_parameters(), frequency); strvec_split(&child.args, cmd); strvec_pushl(&child.args, "/create", "/tn", name, "/f", "/xml", get_tempfile_path(tfile), NULL); @@ -2088,6 +2287,7 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority delete_tempfile(&tfile); free(name); + free(cmd); return result; } @@ -2100,7 +2300,7 @@ static int schtasks_schedule_tasks(void) schtasks_schedule_task(exec_path, SCHEDULE_WEEKLY); } -static int schtasks_update_schedule(int run_maintenance, int fd) +static int schtasks_update_schedule(int run_maintenance, int fd UNUSED) { if (run_maintenance) return schtasks_schedule_tasks(); @@ -2129,21 +2329,28 @@ static int check_crontab_process(const char *cmd) static int is_crontab_available(void) { - const char *cmd = "crontab"; + char *cmd; int is_available; + int ret; - if (get_schedule_cmd(&cmd, &is_available)) - return is_available; + if (get_schedule_cmd("crontab", &is_available, &cmd)) { + ret = is_available; + goto out; + } #ifdef __APPLE__ /* * macOS has cron, but it requires special permissions and will * create a UI alert when attempting to run this command. */ - return 0; + ret = 0; #else - return check_crontab_process(cmd); + ret = check_crontab_process(cmd); #endif + +out: + free(cmd); + return ret; } #define BEGIN_LINE "# BEGIN GIT MAINTENANCE SCHEDULE" @@ -2151,7 +2358,7 @@ static int is_crontab_available(void) static int crontab_update_schedule(int run_maintenance, int fd) { - const char *cmd = "crontab"; + char *cmd; int result = 0; int in_old_region = 0; struct child_process crontab_list = CHILD_PROCESS_INIT; @@ -2159,16 +2366,19 @@ static int crontab_update_schedule(int run_maintenance, int fd) FILE *cron_list, *cron_in; struct strbuf line = STRBUF_INIT; struct tempfile *tmpedit = NULL; + int minute = get_random_minute(); - get_schedule_cmd(&cmd, NULL); + get_schedule_cmd("crontab", NULL, &cmd); strvec_split(&crontab_list.args, cmd); strvec_push(&crontab_list.args, "-l"); crontab_list.in = -1; crontab_list.out = dup(fd); crontab_list.git_cmd = 0; - if (start_command(&crontab_list)) - return error(_("failed to run 'crontab -l'; your system might not support 'cron'")); + if (start_command(&crontab_list)) { + result = error(_("failed to run 'crontab -l'; your system might not support 'cron'")); + goto out; + } /* Ignore exit code, as an empty crontab will return error. */ finish_command(&crontab_list); @@ -2213,11 +2423,11 @@ static int crontab_update_schedule(int run_maintenance, int fd) "# 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"); + "%%d %%s * * %%s \"%s/git\" --exec-path=\"%s\" %s for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%%s\n", + exec_path, exec_path, get_extra_config_parameters()); + fprintf(cron_in, line_format.buf, minute, "1-23", "*", "hourly"); + fprintf(cron_in, line_format.buf, minute, "0", "1-6", "daily"); + fprintf(cron_in, line_format.buf, minute, "0", "0", "weekly"); strbuf_release(&line_format); fprintf(cron_in, "\n%s\n", END_LINE); @@ -2238,8 +2448,10 @@ static int crontab_update_schedule(int run_maintenance, int fd) result = error(_("'crontab' died")); else fclose(cron_list); + out: delete_tempfile(&tmpedit); + free(cmd); return result; } @@ -2262,10 +2474,9 @@ static int real_is_systemd_timer_available(void) static int is_systemd_timer_available(void) { - const char *cmd = "systemctl"; int is_available; - if (get_schedule_cmd(&cmd, &is_available)) + if (get_schedule_cmd("systemctl", &is_available, NULL)) return is_available; return real_is_systemd_timer_available(); @@ -2276,77 +2487,54 @@ static char *xdg_config_home_systemd(const char *filename) return xdg_config_home_for("systemd/user", filename); } -static int systemd_timer_enable_unit(int enable, - enum schedule_priority schedule) -{ - const char *cmd = "systemctl"; - struct child_process child = CHILD_PROCESS_INIT; - const char *frequency = get_frequency(schedule); - - /* - * Disabling the systemd unit while it is already disabled makes - * systemctl print an error. - * Let's ignore it since it means we already are in the expected state: - * the unit is disabled. - * - * On the other hand, enabling a systemd unit which is already enabled - * produces no error. - */ - if (!enable) - child.no_stderr = 1; - - get_schedule_cmd(&cmd, NULL); - strvec_split(&child.args, cmd); - strvec_pushl(&child.args, "--user", enable ? "enable" : "disable", - "--now", NULL); - strvec_pushf(&child.args, "git-maintenance@%s.timer", frequency); - - if (start_command(&child)) - return error(_("failed to start systemctl")); - if (finish_command(&child)) - /* - * Disabling an already disabled systemd unit makes - * systemctl fail. - * Let's ignore this failure. - * - * Enabling an enabled systemd unit doesn't fail. - */ - if (enable) - return error(_("failed to run systemctl")); - return 0; -} +#define SYSTEMD_UNIT_FORMAT "git-maintenance@%s.%s" -static int systemd_timer_delete_unit_templates(void) +static int systemd_timer_delete_timer_file(enum schedule_priority priority) { int ret = 0; - char *filename = xdg_config_home_systemd("git-maintenance@.timer"); - if (unlink(filename) && !is_missing_file_error(errno)) - ret = error_errno(_("failed to delete '%s'"), filename); - FREE_AND_NULL(filename); + const char *frequency = get_frequency(priority); + char *local_timer_name = xstrfmt(SYSTEMD_UNIT_FORMAT, frequency, "timer"); + char *filename = xdg_config_home_systemd(local_timer_name); - filename = xdg_config_home_systemd("git-maintenance@.service"); if (unlink(filename) && !is_missing_file_error(errno)) ret = error_errno(_("failed to delete '%s'"), filename); free(filename); + free(local_timer_name); return ret; } -static int systemd_timer_delete_units(void) +static int systemd_timer_delete_service_template(void) { - return systemd_timer_enable_unit(0, SCHEDULE_HOURLY) || - systemd_timer_enable_unit(0, SCHEDULE_DAILY) || - systemd_timer_enable_unit(0, SCHEDULE_WEEKLY) || - systemd_timer_delete_unit_templates(); + int ret = 0; + char *local_service_name = xstrfmt(SYSTEMD_UNIT_FORMAT, "", "service"); + char *filename = xdg_config_home_systemd(local_service_name); + if (unlink(filename) && !is_missing_file_error(errno)) + ret = error_errno(_("failed to delete '%s'"), filename); + + free(filename); + free(local_service_name); + return ret; } -static int systemd_timer_write_unit_templates(const char *exec_path) +/* + * Write the schedule information into a git-maintenance@<schedule>.timer + * file using a custom minute. This timer file cannot use the templating + * system, so we generate a specific file for each. + */ +static int systemd_timer_write_timer_file(enum schedule_priority schedule, + int minute) { + int res = -1; char *filename; FILE *file; const char *unit; + char *schedule_pattern = NULL; + const char *frequency = get_frequency(schedule); + char *local_timer_name = xstrfmt(SYSTEMD_UNIT_FORMAT, frequency, "timer"); + + filename = xdg_config_home_systemd(local_timer_name); - filename = xdg_config_home_systemd("git-maintenance@.timer"); if (safe_create_leading_directories(filename)) { error(_("failed to create directories for '%s'"), filename); goto error; @@ -2355,6 +2543,23 @@ static int systemd_timer_write_unit_templates(const char *exec_path) if (!file) goto error; + switch (schedule) { + case SCHEDULE_HOURLY: + schedule_pattern = xstrfmt("*-*-* 1..23:%02d:00", minute); + break; + + case SCHEDULE_DAILY: + schedule_pattern = xstrfmt("Tue..Sun *-*-* 0:%02d:00", minute); + break; + + case SCHEDULE_WEEKLY: + schedule_pattern = xstrfmt("Mon 0:%02d:00", minute); + break; + + default: + BUG("Unhandled schedule_priority"); + } + unit = "# This file was created and is maintained by Git.\n" "# Any edits made in this file might be replaced in the future\n" "# by a Git command.\n" @@ -2363,12 +2568,12 @@ static int systemd_timer_write_unit_templates(const char *exec_path) "Description=Optimize Git repositories data\n" "\n" "[Timer]\n" - "OnCalendar=%i\n" + "OnCalendar=%s\n" "Persistent=true\n" "\n" "[Install]\n" "WantedBy=timers.target\n"; - if (fputs(unit, file) == EOF) { + if (fprintf(file, unit, schedule_pattern) < 0) { error(_("failed to write to '%s'"), filename); fclose(file); goto error; @@ -2377,9 +2582,36 @@ static int systemd_timer_write_unit_templates(const char *exec_path) error_errno(_("failed to flush '%s'"), filename); goto error; } + + res = 0; + +error: + free(schedule_pattern); + free(local_timer_name); free(filename); + return res; +} - filename = xdg_config_home_systemd("git-maintenance@.service"); +/* + * No matter the schedule, we use the same service and can make use of the + * templating system. When installing git-maintenance@<schedule>.timer, + * systemd will notice that git-maintenance@.service exists as a template + * and will use this file and insert the <schedule> into the template at + * the position of "%i". + */ +static int systemd_timer_write_service_template(const char *exec_path) +{ + int res = -1; + char *filename; + FILE *file; + const char *unit; + char *local_service_name = xstrfmt(SYSTEMD_UNIT_FORMAT, "", "service"); + + filename = xdg_config_home_systemd(local_service_name); + if (safe_create_leading_directories(filename)) { + error(_("failed to create directories for '%s'"), filename); + goto error; + } file = fopen_or_warn(filename, "w"); if (!file) goto error; @@ -2393,7 +2625,7 @@ static int systemd_timer_write_unit_templates(const char *exec_path) "\n" "[Service]\n" "Type=oneshot\n" - "ExecStart=\"%s/git\" --exec-path=\"%s\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\n" + "ExecStart=\"%s/git\" --exec-path=\"%s\" %s for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%%i\n" "LockPersonality=yes\n" "MemoryDenyWriteExecute=yes\n" "NoNewPrivileges=yes\n" @@ -2403,7 +2635,7 @@ static int systemd_timer_write_unit_templates(const char *exec_path) "RestrictSUIDSGID=yes\n" "SystemCallArchitectures=native\n" "SystemCallFilter=@system-service\n"; - if (fprintf(file, unit, exec_path, exec_path) < 0) { + if (fprintf(file, unit, exec_path, exec_path, get_extra_config_parameters()) < 0) { error(_("failed to write to '%s'"), filename); fclose(file); goto error; @@ -2412,29 +2644,128 @@ static int systemd_timer_write_unit_templates(const char *exec_path) error_errno(_("failed to flush '%s'"), filename); goto error; } - free(filename); - return 0; + + res = 0; error: + free(local_service_name); free(filename); - systemd_timer_delete_unit_templates(); - return -1; + return res; +} + +static int systemd_timer_enable_unit(int enable, + enum schedule_priority schedule, + int minute) +{ + char *cmd = NULL; + struct child_process child = CHILD_PROCESS_INIT; + const char *frequency = get_frequency(schedule); + int ret; + + /* + * Disabling the systemd unit while it is already disabled makes + * systemctl print an error. + * Let's ignore it since it means we already are in the expected state: + * the unit is disabled. + * + * On the other hand, enabling a systemd unit which is already enabled + * produces no error. + */ + if (!enable) { + child.no_stderr = 1; + } else if (systemd_timer_write_timer_file(schedule, minute)) { + ret = -1; + goto out; + } + + get_schedule_cmd("systemctl", NULL, &cmd); + strvec_split(&child.args, cmd); + strvec_pushl(&child.args, "--user", enable ? "enable" : "disable", + "--now", NULL); + strvec_pushf(&child.args, SYSTEMD_UNIT_FORMAT, frequency, "timer"); + + if (start_command(&child)) { + ret = error(_("failed to start systemctl")); + goto out; + } + + if (finish_command(&child)) { + /* + * Disabling an already disabled systemd unit makes + * systemctl fail. + * Let's ignore this failure. + * + * Enabling an enabled systemd unit doesn't fail. + */ + if (enable) { + ret = error(_("failed to run systemctl")); + goto out; + } + } + + ret = 0; + +out: + free(cmd); + return ret; +} + +/* + * A previous version of Git wrote the timer units as template files. + * Clean these up, if they exist. + */ +static void systemd_timer_delete_stale_timer_templates(void) +{ + char *timer_template_name = xstrfmt(SYSTEMD_UNIT_FORMAT, "", "timer"); + char *filename = xdg_config_home_systemd(timer_template_name); + + if (unlink(filename) && !is_missing_file_error(errno)) + warning(_("failed to delete '%s'"), filename); + + free(filename); + free(timer_template_name); +} + +static int systemd_timer_delete_unit_files(void) +{ + systemd_timer_delete_stale_timer_templates(); + + /* Purposefully not short-circuited to make sure all are called. */ + return systemd_timer_delete_timer_file(SCHEDULE_HOURLY) | + systemd_timer_delete_timer_file(SCHEDULE_DAILY) | + systemd_timer_delete_timer_file(SCHEDULE_WEEKLY) | + systemd_timer_delete_service_template(); +} + +static int systemd_timer_delete_units(void) +{ + int minute = get_random_minute(); + /* Purposefully not short-circuited to make sure all are called. */ + return systemd_timer_enable_unit(0, SCHEDULE_HOURLY, minute) | + systemd_timer_enable_unit(0, SCHEDULE_DAILY, minute) | + systemd_timer_enable_unit(0, SCHEDULE_WEEKLY, minute) | + systemd_timer_delete_unit_files(); } static int systemd_timer_setup_units(void) { + int minute = get_random_minute(); const char *exec_path = git_exec_path(); - int ret = systemd_timer_write_unit_templates(exec_path) || - systemd_timer_enable_unit(1, SCHEDULE_HOURLY) || - systemd_timer_enable_unit(1, SCHEDULE_DAILY) || - systemd_timer_enable_unit(1, SCHEDULE_WEEKLY); + int ret = systemd_timer_write_service_template(exec_path) || + systemd_timer_enable_unit(1, SCHEDULE_HOURLY, minute) || + systemd_timer_enable_unit(1, SCHEDULE_DAILY, minute) || + systemd_timer_enable_unit(1, SCHEDULE_WEEKLY, minute); + if (ret) systemd_timer_delete_units(); + else + systemd_timer_delete_stale_timer_templates(); + return ret; } -static int systemd_timer_update_schedule(int run_maintenance, int fd) +static int systemd_timer_update_schedule(int run_maintenance, int fd UNUSED) { if (run_maintenance) return systemd_timer_setup_units(); @@ -2559,8 +2890,17 @@ static int update_background_schedule(const struct maintenance_start_opts *opts, char *lock_path = xstrfmt("%s/schedule", the_repository->objects->odb->path); if (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) { + if (errno == EEXIST) + error(_("unable to create '%s.lock': %s.\n\n" + "Another scheduled git-maintenance(1) process seems to be running in this\n" + "repository. Please make sure no other maintenance processes are running and\n" + "then try again. If it still fails, a git-maintenance(1) process may have\n" + "crashed in this repository earlier: remove the file manually to continue."), + absolute_path(lock_path), strerror(errno)); + else + error_errno(_("cannot acquire lock for scheduled background maintenance")); free(lock_path); - return error(_("another process is scheduling background maintenance")); + return -1; } for (i = 1; i < ARRAY_SIZE(scheduler_fn); i++) { @@ -2606,9 +2946,12 @@ static int maintenance_start(int argc, const char **argv, const char *prefix) opts.scheduler = resolve_scheduler(opts.scheduler); validate_scheduler(opts.scheduler); + if (update_background_schedule(&opts, 1)) + die(_("failed to set up maintenance schedule")); + if (maintenance_register(ARRAY_SIZE(register_args)-1, register_args, NULL)) warning(_("failed to add repo to global config")); - return update_background_schedule(&opts, 1); + return 0; } static const char *const builtin_maintenance_stop_usage[] = { @@ -2633,7 +2976,10 @@ static const char * const builtin_maintenance_usage[] = { NULL, }; -int cmd_maintenance(int argc, const char **argv, const char *prefix) +int cmd_maintenance(int argc, + const char **argv, + const char *prefix, + struct repository *repo UNUSED) { parse_opt_subcommand_fn *fn = NULL; struct option builtin_maintenance_options[] = { |