summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJunio C Hamano <gitster@pobox.com>2025-11-03 06:49:55 -0800
committerJunio C Hamano <gitster@pobox.com>2025-11-03 06:49:55 -0800
commit3cf3369e8114c79fe2e54714cbf6dcae8b7fad9a (patch)
tree7f9a852e2f11efdc1e42359a0e6bb74727394c66
parent52364670907b84f91bcd42035ddd30ceac0f2771 (diff)
parenta4265572bb8488205b53a4a1af0c8d877f11dbe6 (diff)
Merge branch 'ps/maintenance-geometric'
"git maintenance" command learns the "geometric" strategy where it avoids doing maintenance tasks that rebuilds everything from scratch. * ps/maintenance-geometric: t7900: fix a flaky test due to git-repack always regenerating MIDX builtin/maintenance: introduce "geometric" strategy builtin/maintenance: make "gc" strategy accessible builtin/maintenance: extend "maintenance.strategy" to manual maintenance builtin/maintenance: run maintenance tasks depending on type builtin/maintenance: improve readability of strategies builtin/maintenance: don't silently ignore invalid strategy builtin/maintenance: make the geometric factor configurable builtin/maintenance: introduce "geometric-repack" task builtin/gc: make `too_many_loose_objects()` reusable without GC config builtin/gc: remove global `repack` variable
-rw-r--r--Documentation/config/maintenance.adoc49
-rw-r--r--builtin/gc.c313
-rwxr-xr-xt/t7900-maintenance.sh245
3 files changed, 544 insertions, 63 deletions
diff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc
index 2f71934218..d0c38f03fa 100644
--- a/Documentation/config/maintenance.adoc
+++ b/Documentation/config/maintenance.adoc
@@ -16,19 +16,36 @@ detach.
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:
+ recommended strategies for repository maintenance. This affects
+ which tasks are run during `git maintenance run`, provided no
+ `--task=<task>` arguments are provided. This setting impacts manual
+ maintenance, auto-maintenance as well as scheduled maintenance. The
+ tasks that run may be different depending on the maintenance type.
+
-* `none`: This default setting implies no tasks are run at any schedule.
+The maintenance strategy can be further tweaked by setting
+`maintenance.<task>.enabled` and `maintenance.<task>.schedule`. If set, these
+values are used instead of the defaults provided by `maintenance.strategy`.
++
+The possible strategies are:
++
+* `none`: This strategy implies no tasks are run at all. This is the default
+ strategy for scheduled maintenance.
+* `gc`: This strategy runs the `gc` task. This is the default strategy for
+ manual maintenance.
+* `geometric`: This strategy performs geometric repacking of packfiles and
+ keeps auxiliary data structures up-to-date. The strategy expires data in the
+ reflog and removes worktrees that cannot be located anymore. When the
+ geometric repacking strategy would decide to do an all-into-one repack, then
+ the strategy generates a cruft pack for all unreachable objects. Objects that
+ are already part of a cruft pack will be expired.
++
+This repacking strategy is a full replacement for the `gc` strategy and is
+recommended for large repositories.
* `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, the
`loose-objects` and `incremental-repack` tasks daily, and the `pack-refs`
- task weekly.
+ task weekly. Manual repository maintenance uses the `gc` task.
maintenance.<task>.enabled::
This boolean config option controls whether the maintenance task
@@ -75,6 +92,22 @@ maintenance.incremental-repack.auto::
number of pack-files not in the multi-pack-index is at least the value
of `maintenance.incremental-repack.auto`. The default value is 10.
+maintenance.geometric-repack.auto::
+ This integer config option controls how often the `geometric-repack`
+ task should be run as part of `git maintenance run --auto`. If zero,
+ then the `geometric-repack` task will not run with the `--auto`
+ option. A negative value will force the task to run every time.
+ Otherwise, a positive value implies the command should run either when
+ there are packfiles that need to be merged together to retain the
+ geometric progression, or when there are at least this many loose
+ objects that would be written into a new packfile. The default value is
+ 100.
+
+maintenance.geometric-repack.splitFactor::
+ This integer config option controls the factor used for the geometric
+ sequence. See the `--geometric=` option in linkgit:git-repack[1] for
+ more details. Defaults to `2`.
+
maintenance.reflog-expire.auto::
This integer config option controls how often the `reflog-expire` task
should be run as part of `git maintenance run --auto`. If zero, then
diff --git a/builtin/gc.c b/builtin/gc.c
index 541d7471f1..d212cbb9b8 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -34,6 +34,7 @@
#include "pack-objects.h"
#include "path.h"
#include "reflog.h"
+#include "repack.h"
#include "rerere.h"
#include "blob.h"
#include "tree.h"
@@ -55,7 +56,6 @@ static const char * const builtin_gc_usage[] = {
};
static timestamp_t gc_log_expire_time;
-static struct strvec repack = STRVEC_INIT;
static struct tempfile *pidfile;
static struct lock_file log_lock;
static struct string_list pack_garbage = STRING_LIST_INIT_DUP;
@@ -255,6 +255,7 @@ enum maintenance_task_label {
TASK_PREFETCH,
TASK_LOOSE_OBJECTS,
TASK_INCREMENTAL_REPACK,
+ TASK_GEOMETRIC_REPACK,
TASK_GC,
TASK_COMMIT_GRAPH,
TASK_PACK_REFS,
@@ -448,7 +449,7 @@ out:
return should_gc;
}
-static int too_many_loose_objects(struct gc_config *cfg)
+static int too_many_loose_objects(int limit)
{
/*
* Quickly check if a "gc" is needed, by estimating how
@@ -470,7 +471,7 @@ static int too_many_loose_objects(struct gc_config *cfg)
if (!dir)
return 0;
- auto_threshold = DIV_ROUND_UP(cfg->gc_auto_threshold, 256);
+ auto_threshold = DIV_ROUND_UP(limit, 256);
while ((ent = readdir(dir)) != NULL) {
if (strspn(ent->d_name, "0123456789abcdef") != hexsz_loose ||
ent->d_name[hexsz_loose] != '\0')
@@ -616,48 +617,50 @@ static uint64_t estimate_repack_memory(struct gc_config *cfg,
return os_cache + heap;
}
-static int keep_one_pack(struct string_list_item *item, void *data UNUSED)
+static int keep_one_pack(struct string_list_item *item, void *data)
{
- strvec_pushf(&repack, "--keep-pack=%s", basename(item->string));
+ struct strvec *args = data;
+ strvec_pushf(args, "--keep-pack=%s", basename(item->string));
return 0;
}
static void add_repack_all_option(struct gc_config *cfg,
- struct string_list *keep_pack)
+ struct string_list *keep_pack,
+ struct strvec *args)
{
if (cfg->prune_expire && !strcmp(cfg->prune_expire, "now")
&& !(cfg->cruft_packs && cfg->repack_expire_to))
- strvec_push(&repack, "-a");
+ strvec_push(args, "-a");
else if (cfg->cruft_packs) {
- strvec_push(&repack, "--cruft");
+ strvec_push(args, "--cruft");
if (cfg->prune_expire)
- strvec_pushf(&repack, "--cruft-expiration=%s", cfg->prune_expire);
+ strvec_pushf(args, "--cruft-expiration=%s", cfg->prune_expire);
if (cfg->max_cruft_size)
- strvec_pushf(&repack, "--max-cruft-size=%lu",
+ strvec_pushf(args, "--max-cruft-size=%lu",
cfg->max_cruft_size);
if (cfg->repack_expire_to)
- strvec_pushf(&repack, "--expire-to=%s", cfg->repack_expire_to);
+ strvec_pushf(args, "--expire-to=%s", cfg->repack_expire_to);
} else {
- strvec_push(&repack, "-A");
+ strvec_push(args, "-A");
if (cfg->prune_expire)
- strvec_pushf(&repack, "--unpack-unreachable=%s", cfg->prune_expire);
+ strvec_pushf(args, "--unpack-unreachable=%s", cfg->prune_expire);
}
if (keep_pack)
- for_each_string_list(keep_pack, keep_one_pack, NULL);
+ for_each_string_list(keep_pack, keep_one_pack, args);
if (cfg->repack_filter && *cfg->repack_filter)
- strvec_pushf(&repack, "--filter=%s", cfg->repack_filter);
+ strvec_pushf(args, "--filter=%s", cfg->repack_filter);
if (cfg->repack_filter_to && *cfg->repack_filter_to)
- strvec_pushf(&repack, "--filter-to=%s", cfg->repack_filter_to);
+ strvec_pushf(args, "--filter-to=%s", cfg->repack_filter_to);
}
-static void add_repack_incremental_option(void)
+static void add_repack_incremental_option(struct strvec *args)
{
- strvec_push(&repack, "--no-write-bitmap-index");
+ strvec_push(args, "--no-write-bitmap-index");
}
-static int need_to_gc(struct gc_config *cfg)
+static int need_to_gc(struct gc_config *cfg, struct strvec *repack_args)
{
/*
* Setting gc.auto to 0 or negative can disable the
@@ -698,10 +701,10 @@ static int need_to_gc(struct gc_config *cfg)
string_list_clear(&keep_pack, 0);
}
- add_repack_all_option(cfg, &keep_pack);
+ add_repack_all_option(cfg, &keep_pack, repack_args);
string_list_clear(&keep_pack, 0);
- } else if (too_many_loose_objects(cfg))
- add_repack_incremental_option();
+ } else if (too_many_loose_objects(cfg->gc_auto_threshold))
+ add_repack_incremental_option(repack_args);
else
return 0;
@@ -850,6 +853,7 @@ int cmd_gc(int argc,
int keep_largest_pack = -1;
int skip_foreground_tasks = 0;
timestamp_t dummy;
+ struct strvec repack_args = STRVEC_INIT;
struct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT;
struct gc_config cfg = GC_CONFIG_INIT;
const char *prune_expire_sentinel = "sentinel";
@@ -889,7 +893,7 @@ int cmd_gc(int argc,
show_usage_with_options_if_asked(argc, argv,
builtin_gc_usage, builtin_gc_options);
- strvec_pushl(&repack, "repack", "-d", "-l", NULL);
+ strvec_pushl(&repack_args, "repack", "-d", "-l", NULL);
gc_config(&cfg);
@@ -912,14 +916,14 @@ int cmd_gc(int argc,
die(_("failed to parse prune expiry value %s"), cfg.prune_expire);
if (aggressive) {
- strvec_push(&repack, "-f");
+ strvec_push(&repack_args, "-f");
if (cfg.aggressive_depth > 0)
- strvec_pushf(&repack, "--depth=%d", cfg.aggressive_depth);
+ strvec_pushf(&repack_args, "--depth=%d", cfg.aggressive_depth);
if (cfg.aggressive_window > 0)
- strvec_pushf(&repack, "--window=%d", cfg.aggressive_window);
+ strvec_pushf(&repack_args, "--window=%d", cfg.aggressive_window);
}
if (opts.quiet)
- strvec_push(&repack, "-q");
+ strvec_push(&repack_args, "-q");
if (opts.auto_flag) {
if (cfg.detach_auto && opts.detach < 0)
@@ -928,7 +932,7 @@ int cmd_gc(int argc,
/*
* Auto-gc should be least intrusive as possible.
*/
- if (!need_to_gc(&cfg)) {
+ if (!need_to_gc(&cfg, &repack_args)) {
ret = 0;
goto out;
}
@@ -950,7 +954,7 @@ int cmd_gc(int argc,
find_base_packs(&keep_pack, cfg.big_pack_threshold);
}
- add_repack_all_option(&cfg, &keep_pack);
+ add_repack_all_option(&cfg, &keep_pack, &repack_args);
string_list_clear(&keep_pack, 0);
}
@@ -1012,9 +1016,9 @@ int cmd_gc(int argc,
repack_cmd.git_cmd = 1;
repack_cmd.close_object_store = 1;
- strvec_pushv(&repack_cmd.args, repack.v);
+ strvec_pushv(&repack_cmd.args, repack_args.v);
if (run_command(&repack_cmd))
- die(FAILED_RUN, repack.v[0]);
+ die(FAILED_RUN, repack_args.v[0]);
if (cfg.prune_expire) {
struct child_process prune_cmd = CHILD_PROCESS_INIT;
@@ -1053,7 +1057,7 @@ int cmd_gc(int argc,
!opts.quiet && !daemonized ? COMMIT_GRAPH_WRITE_PROGRESS : 0,
NULL);
- if (opts.auto_flag && too_many_loose_objects(&cfg))
+ if (opts.auto_flag && too_many_loose_objects(cfg.gc_auto_threshold))
warning(_("There are too many unreachable loose objects; "
"run 'git prune' to remove them."));
@@ -1065,6 +1069,7 @@ int cmd_gc(int argc,
out:
maintenance_run_opts_release(&opts);
+ strvec_clear(&repack_args);
gc_config_release(&cfg);
return 0;
}
@@ -1267,6 +1272,19 @@ static int maintenance_task_gc_background(struct maintenance_run_opts *opts,
return run_command(&child);
}
+static int gc_condition(struct gc_config *cfg)
+{
+ /*
+ * Note that it's fine to drop the repack arguments here, as we execute
+ * git-gc(1) as a separate child process anyway. So it knows to compute
+ * these arguments again.
+ */
+ struct strvec repack_args = STRVEC_INIT;
+ int ret = need_to_gc(cfg, &repack_args);
+ strvec_clear(&repack_args);
+ return ret;
+}
+
static int prune_packed(struct maintenance_run_opts *opts)
{
struct child_process child = CHILD_PROCESS_INIT;
@@ -1548,6 +1566,108 @@ static int maintenance_task_incremental_repack(struct maintenance_run_opts *opts
return 0;
}
+static int maintenance_task_geometric_repack(struct maintenance_run_opts *opts,
+ struct gc_config *cfg)
+{
+ struct pack_geometry geometry = {
+ .split_factor = 2,
+ };
+ struct pack_objects_args po_args = {
+ .local = 1,
+ };
+ struct existing_packs existing_packs = EXISTING_PACKS_INIT;
+ struct string_list kept_packs = STRING_LIST_INIT_DUP;
+ struct child_process child = CHILD_PROCESS_INIT;
+ int ret;
+
+ repo_config_get_int(the_repository, "maintenance.geometric-repack.splitFactor",
+ &geometry.split_factor);
+
+ existing_packs.repo = the_repository;
+ existing_packs_collect(&existing_packs, &kept_packs);
+ pack_geometry_init(&geometry, &existing_packs, &po_args);
+ pack_geometry_split(&geometry);
+
+ child.git_cmd = 1;
+
+ strvec_pushl(&child.args, "repack", "-d", "-l", NULL);
+ if (geometry.split < geometry.pack_nr)
+ strvec_pushf(&child.args, "--geometric=%d",
+ geometry.split_factor);
+ else
+ add_repack_all_option(cfg, NULL, &child.args);
+ if (opts->quiet)
+ strvec_push(&child.args, "--quiet");
+ if (the_repository->settings.core_multi_pack_index)
+ strvec_push(&child.args, "--write-midx");
+
+ if (run_command(&child)) {
+ ret = error(_("failed to perform geometric repack"));
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ existing_packs_release(&existing_packs);
+ pack_geometry_release(&geometry);
+ return ret;
+}
+
+static int geometric_repack_auto_condition(struct gc_config *cfg UNUSED)
+{
+ struct pack_geometry geometry = {
+ .split_factor = 2,
+ };
+ struct pack_objects_args po_args = {
+ .local = 1,
+ };
+ struct existing_packs existing_packs = EXISTING_PACKS_INIT;
+ struct string_list kept_packs = STRING_LIST_INIT_DUP;
+ int auto_value = 100;
+ int ret;
+
+ repo_config_get_int(the_repository, "maintenance.geometric-repack.auto",
+ &auto_value);
+ if (!auto_value)
+ return 0;
+ if (auto_value < 0)
+ return 1;
+
+ repo_config_get_int(the_repository, "maintenance.geometric-repack.splitFactor",
+ &geometry.split_factor);
+
+ existing_packs.repo = the_repository;
+ existing_packs_collect(&existing_packs, &kept_packs);
+ pack_geometry_init(&geometry, &existing_packs, &po_args);
+ pack_geometry_split(&geometry);
+
+ /*
+ * When we'd merge at least two packs with one another we always
+ * perform the repack.
+ */
+ if (geometry.split) {
+ ret = 1;
+ goto out;
+ }
+
+ /*
+ * Otherwise, we estimate the number of loose objects to determine
+ * whether we want to create a new packfile or not.
+ */
+ if (too_many_loose_objects(auto_value)) {
+ ret = 1;
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ existing_packs_release(&existing_packs);
+ pack_geometry_release(&geometry);
+ return ret;
+}
+
typedef int (*maintenance_task_fn)(struct maintenance_run_opts *opts,
struct gc_config *cfg);
typedef int (*maintenance_auto_fn)(struct gc_config *cfg);
@@ -1590,11 +1710,16 @@ static const struct maintenance_task tasks[] = {
.background = maintenance_task_incremental_repack,
.auto_condition = incremental_repack_auto_condition,
},
+ [TASK_GEOMETRIC_REPACK] = {
+ .name = "geometric-repack",
+ .background = maintenance_task_geometric_repack,
+ .auto_condition = geometric_repack_auto_condition,
+ },
[TASK_GC] = {
.name = "gc",
.foreground = maintenance_task_gc_foreground,
.background = maintenance_task_gc_background,
- .auto_condition = need_to_gc,
+ .auto_condition = gc_condition,
},
[TASK_COMMIT_GRAPH] = {
.name = "commit-graph",
@@ -1700,39 +1825,116 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,
return result;
}
+enum maintenance_type {
+ /* As invoked via `git maintenance run --schedule=`. */
+ MAINTENANCE_TYPE_SCHEDULED = (1 << 0),
+ /* As invoked via `git maintenance run` and with `--auto`. */
+ MAINTENANCE_TYPE_MANUAL = (1 << 1),
+};
+
struct maintenance_strategy {
struct {
- int enabled;
+ unsigned type;
enum schedule_priority schedule;
} tasks[TASK__COUNT];
};
static const struct maintenance_strategy none_strategy = { 0 };
-static const struct maintenance_strategy default_strategy = {
+
+static const struct maintenance_strategy gc_strategy = {
.tasks = {
- [TASK_GC].enabled = 1,
+ [TASK_GC] = {
+ .type = MAINTENANCE_TYPE_MANUAL | MAINTENANCE_TYPE_SCHEDULED,
+ .schedule = SCHEDULE_DAILY,
+ },
},
};
+
static const struct maintenance_strategy incremental_strategy = {
.tasks = {
- [TASK_COMMIT_GRAPH].enabled = 1,
- [TASK_COMMIT_GRAPH].schedule = SCHEDULE_HOURLY,
- [TASK_PREFETCH].enabled = 1,
- [TASK_PREFETCH].schedule = SCHEDULE_HOURLY,
- [TASK_INCREMENTAL_REPACK].enabled = 1,
- [TASK_INCREMENTAL_REPACK].schedule = SCHEDULE_DAILY,
- [TASK_LOOSE_OBJECTS].enabled = 1,
- [TASK_LOOSE_OBJECTS].schedule = SCHEDULE_DAILY,
- [TASK_PACK_REFS].enabled = 1,
- [TASK_PACK_REFS].schedule = SCHEDULE_WEEKLY,
+ [TASK_COMMIT_GRAPH] = {
+ .type = MAINTENANCE_TYPE_SCHEDULED,
+ .schedule = SCHEDULE_HOURLY,
+ },
+ [TASK_PREFETCH] = {
+ .type = MAINTENANCE_TYPE_SCHEDULED,
+ .schedule = SCHEDULE_HOURLY,
+ },
+ [TASK_INCREMENTAL_REPACK] = {
+ .type = MAINTENANCE_TYPE_SCHEDULED,
+ .schedule = SCHEDULE_DAILY,
+ },
+ [TASK_LOOSE_OBJECTS] = {
+ .type = MAINTENANCE_TYPE_SCHEDULED,
+ .schedule = SCHEDULE_DAILY,
+ },
+ [TASK_PACK_REFS] = {
+ .type = MAINTENANCE_TYPE_SCHEDULED,
+ .schedule = SCHEDULE_WEEKLY,
+ },
+ /*
+ * Historically, the "incremental" strategy was only available
+ * in the context of scheduled maintenance when set up via
+ * "maintenance.strategy". We have later expanded that config
+ * to also cover manual maintenance.
+ *
+ * To retain backwards compatibility with the previous status
+ * quo we thus run git-gc(1) in case manual maintenance was
+ * requested. This is the same as the default strategy, which
+ * would have been in use beforehand.
+ */
+ [TASK_GC] = {
+ .type = MAINTENANCE_TYPE_MANUAL,
+ },
+ },
+};
+
+static const struct maintenance_strategy geometric_strategy = {
+ .tasks = {
+ [TASK_COMMIT_GRAPH] = {
+ .type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,
+ .schedule = SCHEDULE_HOURLY,
+ },
+ [TASK_GEOMETRIC_REPACK] = {
+ .type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,
+ .schedule = SCHEDULE_DAILY,
+ },
+ [TASK_PACK_REFS] = {
+ .type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,
+ .schedule = SCHEDULE_DAILY,
+ },
+ [TASK_RERERE_GC] = {
+ .type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,
+ .schedule = SCHEDULE_WEEKLY,
+ },
+ [TASK_REFLOG_EXPIRE] = {
+ .type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,
+ .schedule = SCHEDULE_WEEKLY,
+ },
+ [TASK_WORKTREE_PRUNE] = {
+ .type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,
+ .schedule = SCHEDULE_WEEKLY,
+ },
},
};
+static struct maintenance_strategy parse_maintenance_strategy(const char *name)
+{
+ if (!strcasecmp(name, "incremental"))
+ return incremental_strategy;
+ if (!strcasecmp(name, "gc"))
+ return gc_strategy;
+ if (!strcasecmp(name, "geometric"))
+ return geometric_strategy;
+ die(_("unknown maintenance strategy: '%s'"), name);
+}
+
static void initialize_task_config(struct maintenance_run_opts *opts,
const struct string_list *selected_tasks)
{
struct strbuf config_name = STRBUF_INIT;
struct maintenance_strategy strategy;
+ enum maintenance_type type;
const char *config_str;
/*
@@ -1760,19 +1962,20 @@ static void initialize_task_config(struct maintenance_run_opts *opts,
* - Unscheduled maintenance uses our default strategy.
*
* Both of these are affected by the gitconfig though, which may
- * override specific aspects of our strategy.
+ * override specific aspects of our strategy. Furthermore, both
+ * strategies can be overridden by setting "maintenance.strategy".
*/
if (opts->schedule) {
strategy = none_strategy;
-
- if (!repo_config_get_string_tmp(the_repository, "maintenance.strategy", &config_str)) {
- if (!strcasecmp(config_str, "incremental"))
- strategy = incremental_strategy;
- }
+ type = MAINTENANCE_TYPE_SCHEDULED;
} else {
- strategy = default_strategy;
+ strategy = gc_strategy;
+ type = MAINTENANCE_TYPE_MANUAL;
}
+ if (!repo_config_get_string_tmp(the_repository, "maintenance.strategy", &config_str))
+ strategy = parse_maintenance_strategy(config_str);
+
for (size_t i = 0; i < TASK__COUNT; i++) {
int config_value;
@@ -1780,8 +1983,8 @@ static void initialize_task_config(struct maintenance_run_opts *opts,
strbuf_addf(&config_name, "maintenance.%s.enabled",
tasks[i].name);
if (!repo_config_get_bool(the_repository, config_name.buf, &config_value))
- strategy.tasks[i].enabled = config_value;
- if (!strategy.tasks[i].enabled)
+ strategy.tasks[i].type = config_value ? type : 0;
+ if (!(strategy.tasks[i].type & type))
continue;
if (opts->schedule) {
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index ddd273d8dc..614184a097 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -465,6 +465,176 @@ test_expect_success 'maintenance.incremental-repack.auto (when config is unset)'
)
'
+run_and_verify_geometric_pack () {
+ EXPECTED_PACKS="$1" &&
+
+ # Verify that we perform a geometric repack.
+ rm -f "trace2.txt" &&
+ GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \
+ git maintenance run --task=geometric-repack 2>/dev/null &&
+ test_subcommand git repack -d -l --geometric=2 \
+ --quiet --write-midx <trace2.txt &&
+
+ # Verify that the number of packfiles matches our expectation.
+ ls -l .git/objects/pack/*.pack >packfiles &&
+ test_line_count = "$EXPECTED_PACKS" packfiles &&
+
+ # And verify that there are no loose objects anymore.
+ git count-objects -v >count &&
+ test_grep '^count: 0$' count
+}
+
+test_expect_success 'geometric repacking task' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ git config set maintenance.auto false &&
+ test_commit initial &&
+
+ # The initial repack causes an all-into-one repack.
+ GIT_TRACE2_EVENT="$(pwd)/initial-repack.txt" \
+ git maintenance run --task=geometric-repack 2>/dev/null &&
+ test_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago \
+ --quiet --write-midx <initial-repack.txt &&
+
+ # Repacking should now cause a no-op geometric repack because
+ # no packfiles need to be combined.
+ ls -l .git/objects/pack/*.pack >before &&
+ run_and_verify_geometric_pack 1 &&
+ ls -l .git/objects/pack/*.pack >after &&
+ test_cmp before after &&
+
+ # This incremental change creates a new packfile that only
+ # soaks up loose objects. The packfiles are not getting merged
+ # at this point.
+ test_commit loose &&
+ run_and_verify_geometric_pack 2 &&
+
+ # Both packfiles have 3 objects, so the next run would cause us
+ # to merge all packfiles together. This should be turned into
+ # an all-into-one-repack.
+ GIT_TRACE2_EVENT="$(pwd)/all-into-one-repack.txt" \
+ git maintenance run --task=geometric-repack 2>/dev/null &&
+ test_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago \
+ --quiet --write-midx <all-into-one-repack.txt &&
+
+ # The geometric repack soaks up unreachable objects.
+ echo blob-1 | git hash-object -w --stdin -t blob &&
+ run_and_verify_geometric_pack 2 &&
+
+ # A second unreachable object should be written into another packfile.
+ echo blob-2 | git hash-object -w --stdin -t blob &&
+ run_and_verify_geometric_pack 3 &&
+
+ # And these two small packs should now be merged via the
+ # geometric repack. The large packfile should remain intact.
+ run_and_verify_geometric_pack 2 &&
+
+ # If we now add two more objects and repack twice we should
+ # then see another all-into-one repack. This time around
+ # though, as we have unreachable objects, we should also see a
+ # cruft pack.
+ echo blob-3 | git hash-object -w --stdin -t blob &&
+ echo blob-4 | git hash-object -w --stdin -t blob &&
+ run_and_verify_geometric_pack 3 &&
+ GIT_TRACE2_EVENT="$(pwd)/cruft-repack.txt" \
+ git maintenance run --task=geometric-repack 2>/dev/null &&
+ test_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago \
+ --quiet --write-midx <cruft-repack.txt &&
+ ls .git/objects/pack/*.pack >packs &&
+ test_line_count = 2 packs &&
+ ls .git/objects/pack/*.mtimes >cruft &&
+ test_line_count = 1 cruft
+ )
+'
+
+test_geometric_repack_needed () {
+ NEEDED="$1"
+ GEOMETRIC_CONFIG="$2" &&
+ rm -f trace2.txt &&
+ GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \
+ git ${GEOMETRIC_CONFIG:+-c maintenance.geometric-repack.$GEOMETRIC_CONFIG} \
+ maintenance run --auto --task=geometric-repack 2>/dev/null &&
+ case "$NEEDED" in
+ true)
+ test_grep "\[\"git\",\"repack\"," trace2.txt;;
+ false)
+ ! test_grep "\[\"git\",\"repack\"," trace2.txt;;
+ *)
+ BUG "invalid parameter: $NEEDED";;
+ esac
+}
+
+test_expect_success 'geometric repacking with --auto' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+
+ # An empty repository does not need repacking, except when
+ # explicitly told to do it.
+ test_geometric_repack_needed false &&
+ test_geometric_repack_needed false auto=0 &&
+ test_geometric_repack_needed false auto=1 &&
+ test_geometric_repack_needed true auto=-1 &&
+
+ test_oid_init &&
+
+ # Loose objects cause a repack when crossing the limit. Note
+ # that the number of objects gets extrapolated by having a look
+ # at the "objects/17/" shard.
+ test_commit "$(test_oid blob17_1)" &&
+ test_geometric_repack_needed false &&
+ test_commit "$(test_oid blob17_2)" &&
+ test_geometric_repack_needed false auto=257 &&
+ test_geometric_repack_needed true auto=256 &&
+
+ # Force another repack.
+ test_commit first &&
+ test_commit second &&
+ test_geometric_repack_needed true auto=-1 &&
+
+ # We now have two packfiles that would be merged together. As
+ # such, the repack should always happen unless the user has
+ # disabled the auto task.
+ test_geometric_repack_needed false auto=0 &&
+ test_geometric_repack_needed true auto=9000
+ )
+'
+
+test_expect_success 'geometric repacking honors configured split factor' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ git config set maintenance.auto false &&
+
+ # Create three different packs with 9, 2 and 1 object, respectively.
+ # This is done so that only a subset of packs would be merged
+ # together so that we can verify that `git repack` receives the
+ # correct geometric factor.
+ for i in $(test_seq 9)
+ do
+ echo first-$i | git hash-object -w --stdin -t blob || return 1
+ done &&
+ git repack --geometric=2 -d &&
+
+ for i in $(test_seq 2)
+ do
+ echo second-$i | git hash-object -w --stdin -t blob || return 1
+ done &&
+ git repack --geometric=2 -d &&
+
+ echo third | git hash-object -w --stdin -t blob &&
+ git repack --geometric=2 -d &&
+
+ test_geometric_repack_needed false splitFactor=2 &&
+ test_geometric_repack_needed true splitFactor=3 &&
+ test_subcommand git repack -d -l --geometric=3 --quiet --write-midx <trace2.txt
+ )
+'
+
test_expect_success 'pack-refs task' '
for n in $(test_seq 1 5)
do
@@ -716,6 +886,76 @@ test_expect_success 'maintenance.strategy inheritance' '
<modified-daily.txt
'
+test_strategy () {
+ STRATEGY="$1"
+ shift
+
+ cat >expect &&
+ rm -f trace2.txt &&
+ GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \
+ git -c maintenance.strategy=$STRATEGY maintenance run --quiet "$@" &&
+ sed -n 's/{"event":"child_start","sid":"[^/"]*",.*,"argv":\["\(.*\)\"]}/\1/p' <trace2.txt |
+ sed 's/","/ /g' >actual
+ test_cmp expect actual
+}
+
+test_expect_success 'maintenance.strategy is respected' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+
+ test_must_fail git -c maintenance.strategy=unknown maintenance run 2>err &&
+ test_grep "unknown maintenance strategy: .unknown." err &&
+
+ test_strategy incremental <<-\EOF &&
+ git pack-refs --all --prune
+ git reflog expire --all
+ git gc --quiet --no-detach --skip-foreground-tasks
+ EOF
+
+ test_strategy incremental --schedule=weekly <<-\EOF &&
+ git pack-refs --all --prune
+ git prune-packed --quiet
+ git multi-pack-index write --no-progress
+ git multi-pack-index expire --no-progress
+ git multi-pack-index repack --no-progress --batch-size=1
+ git commit-graph write --split --reachable --no-progress
+ EOF
+
+ test_strategy gc <<-\EOF &&
+ git pack-refs --all --prune
+ git reflog expire --all
+ git gc --quiet --no-detach --skip-foreground-tasks
+ EOF
+
+ test_strategy gc --schedule=weekly <<-\EOF &&
+ git pack-refs --all --prune
+ git reflog expire --all
+ git gc --quiet --no-detach --skip-foreground-tasks
+ EOF
+
+ test_strategy geometric <<-\EOF &&
+ git pack-refs --all --prune
+ git reflog expire --all
+ git repack -d -l --geometric=2 --quiet --write-midx
+ git commit-graph write --split --reachable --no-progress
+ git worktree prune --expire 3.months.ago
+ git rerere gc
+ EOF
+
+ test_strategy geometric --schedule=weekly <<-\EOF
+ git pack-refs --all --prune
+ git reflog expire --all
+ git repack -d -l --geometric=2 --quiet --write-midx
+ git commit-graph write --split --reachable --no-progress
+ git worktree prune --expire 3.months.ago
+ git rerere gc
+ EOF
+ )
+'
+
test_expect_success 'register and unregister' '
test_when_finished git config --global --unset-all maintenance.repo &&
@@ -1093,6 +1333,11 @@ test_expect_success 'fails when running outside of a repository' '
nongit test_must_fail git maintenance unregister
'
+test_expect_success 'fails when configured to use an invalid strategy' '
+ test_must_fail git -c maintenance.strategy=invalid maintenance run --schedule=hourly 2>err &&
+ test_grep "unknown maintenance strategy: .invalid." err
+'
+
test_expect_success 'register and unregister bare repo' '
test_when_finished "git config --global --unset-all maintenance.repo || :" &&
test_might_fail git config --global --unset-all maintenance.repo &&