{"thread":{"id":"64330","subject":"[PATCH 0/8] builtin/maintenance: introduce \"geometric\" strategy","startedAt":"2025-10-16T07:26:48Z","lastAt":"2025-10-27T20:58:31Z","messageCount":69,"participants":["Patrick Steinhardt","Justin Tobler","Junio C Hamano","Taylor Blau","Jeff King"],"isPatch":true,"patchVersion":1,"patchTotal":8},"messages":[{"id":"528933","messageId":"20251016-pks-maintenance-geometric-strategy-v1-0-18943d474203@pks.im","threadId":"64330","inReplyTo":null,"subject":"[PATCH 0/8] builtin/maintenance: introduce \"geometric\" strategy","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-16T07:26:31Z","receivedAt":"2025-10-16T07:26:48Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nby default, git-maintenance(1) uses git-gc(1) to perform repository\nhousekeeping. This tool has a couple of shortcomings, most importantly\nthat it regularly does all-into-one repacks. This doesn't really work\nall that well in the context of monorepos, where you really want to\navoid repacking all objects regularly.\n\nAn alternative maintenance strategy is the \"incremental\" strategy, but\nthis strategy has two downsides:\n\n  - Strategies in general only apply to scheduled maintenance. So if you\n    run git-maintenance(1), you still end up with git-gc(1).\n\n  - The strategy is designed to not ever delete any data, but a full\n    replacment for git-gc(1) needs to also prune reflogs, rereree caches\n    and vanished worktrees.\n\nThis patch series aims to fix both of these issues.\n\nFirst, the series introduces a new \"geometric\" maintenance task, which\nmakes use of geometric repacking as exposed by git-repack(1) in the\ngeneral case. In the case where a geometric repack ends up merging all\npackfiles into one we instead do an all-into-one repack with cruft packs\nso that we can still phase out objects over time.\n\nSecond, the series extends maintenance strategies to also cover normal\nmaintenance. If the user has configured the \"geometric\" strategy, we'll\nthus use it for both manual and scheduled maintenance. For backwards\ncompatibility, the \"incremental\" strategy is changed so that it uses\ngit-gc(1) for manual maintenance and the other tasks for scheduled\nmaintenance.\n\nThe series is built on top of b660e2dcb9 (Sync with 'maint', 2025-10-14)\nwith tb/incremental-midx-part-3.1 at c886af90f8 (SQUASH??? play well\nwith other topics by preemptively including \"repository.h\", 2025-09-29)\nmerged into it.\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (8):\n      builtin/gc: remove global `repack` variable\n      builtin/gc: make `too_many_loose_objects()` reusable without GC config\n      builtin/maintenance: introduce \"geometric-repack\" task\n      builtin/maintenance: don't silently ignore invalid strategy\n      builtin/maintenance: run maintenance tasks depending on type\n      builtin/maintenance: extend \"maintenance.strategy\" to manual maintenance\n      builtin/maintenance: make \"gc\" strategy accessible\n      builtin/maintenance: introduce \"geometric\" strategy\n\n Documentation/config/maintenance.adoc |  44 +++++-\n builtin/gc.c                          | 271 +++++++++++++++++++++++++++-------\n t/t7900-maintenance.sh                | 212 ++++++++++++++++++++++++++\n 3 files changed, 469 insertions(+), 58 deletions(-)\n\n\n---\nbase-commit: 0bb2c786c2349dd6700727153c13d81cbfb41710\nchange-id: 20251015-pks-maintenance-geometric-strategy-580c58581b01\n\n"},{"id":"528934","messageId":"20251016-pks-maintenance-geometric-strategy-v1-1-18943d474203@pks.im","threadId":"64330","inReplyTo":"20251016-pks-maintenance-geometric-strategy-v1-0-18943d474203@pks.im","subject":"[PATCH 1/8] builtin/gc: remove global `repack` variable","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-16T07:26:32Z","receivedAt":"2025-10-16T07:26:50Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The global `repack` variable is used to store all command line arguments\nthat we eventually want to pass to git-repack(1). It is being appended\nto from multiple different functions, which makes it hard to follow the\nlogic. Besides being hard to follow, it also makes it unnecessarily hard\nto reuse this infrastructure in new code.\n\nRefactor the code so that we store this variable on the stack and pass\na pointer to it around as needed. This is done so that we can reuse\n`add_repack_all_options()` in a subsequent commit.\n\nThe refactoring itself is straight-forward. One function that deserves\nattention though is `need_to_gc()`: this function determines whether or\nnot we need to execute garbage collection for `git gc --auto`, but also\nfor `git maintenance run --auto`. But besides figuring out whether we\nhave to perform GC, the function also sets up the `repack` arguments.\n\nFor `git gc --auto` it's trivial to adapt, as we already have the\non-stack variable at our fingertips. But for the maintenance condition\nit's less obvious what to do.\n\nAs it turns out, we can just use another temporary variable there that\nwe then immediately discard. If we need to perform GC we execute a child\ngit-gc(1) process to repack objects for us, and that process will have\nto recompute the arguments anyway.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 74 ++++++++++++++++++++++++++++++++++++------------------------\n 1 file changed, 45 insertions(+), 29 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e19e13d9788..e9772eb3a30 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -55,7 +55,6 @@ static const char * const builtin_gc_usage[] = {\n };\n \n static timestamp_t gc_log_expire_time;\n-static struct strvec repack = STRVEC_INIT;\n static struct tempfile *pidfile;\n static struct lock_file log_lock;\n static struct string_list pack_garbage = STRING_LIST_INIT_DUP;\n@@ -618,48 +617,50 @@ static uint64_t estimate_repack_memory(struct gc_config *cfg,\n \treturn os_cache + heap;\n }\n \n-static int keep_one_pack(struct string_list_item *item, void *data UNUSED)\n+static int keep_one_pack(struct string_list_item *item, void *data)\n {\n-\tstrvec_pushf(&repack, \"--keep-pack=%s\", basename(item->string));\n+\tstruct strvec *args = data;\n+\tstrvec_pushf(args, \"--keep-pack=%s\", basename(item->string));\n \treturn 0;\n }\n \n static void add_repack_all_option(struct gc_config *cfg,\n-\t\t\t\t  struct string_list *keep_pack)\n+\t\t\t\t  struct string_list *keep_pack,\n+\t\t\t\t  struct strvec *args)\n {\n \tif (cfg->prune_expire && !strcmp(cfg->prune_expire, \"now\")\n \t\t&& !(cfg->cruft_packs && cfg->repack_expire_to))\n-\t\tstrvec_push(&repack, \"-a\");\n+\t\tstrvec_push(args, \"-a\");\n \telse if (cfg->cruft_packs) {\n-\t\tstrvec_push(&repack, \"--cruft\");\n+\t\tstrvec_push(args, \"--cruft\");\n \t\tif (cfg->prune_expire)\n-\t\t\tstrvec_pushf(&repack, \"--cruft-expiration=%s\", cfg->prune_expire);\n+\t\t\tstrvec_pushf(args, \"--cruft-expiration=%s\", cfg->prune_expire);\n \t\tif (cfg->max_cruft_size)\n-\t\t\tstrvec_pushf(&repack, \"--max-cruft-size=%lu\",\n+\t\t\tstrvec_pushf(args, \"--max-cruft-size=%lu\",\n \t\t\t\t     cfg->max_cruft_size);\n \t\tif (cfg->repack_expire_to)\n-\t\t\tstrvec_pushf(&repack, \"--expire-to=%s\", cfg->repack_expire_to);\n+\t\t\tstrvec_pushf(args, \"--expire-to=%s\", cfg->repack_expire_to);\n \t} else {\n-\t\tstrvec_push(&repack, \"-A\");\n+\t\tstrvec_push(args, \"-A\");\n \t\tif (cfg->prune_expire)\n-\t\t\tstrvec_pushf(&repack, \"--unpack-unreachable=%s\", cfg->prune_expire);\n+\t\t\tstrvec_pushf(args, \"--unpack-unreachable=%s\", cfg->prune_expire);\n \t}\n \n \tif (keep_pack)\n-\t\tfor_each_string_list(keep_pack, keep_one_pack, NULL);\n+\t\tfor_each_string_list(keep_pack, keep_one_pack, args);\n \n \tif (cfg->repack_filter && *cfg->repack_filter)\n-\t\tstrvec_pushf(&repack, \"--filter=%s\", cfg->repack_filter);\n+\t\tstrvec_pushf(args, \"--filter=%s\", cfg->repack_filter);\n \tif (cfg->repack_filter_to && *cfg->repack_filter_to)\n-\t\tstrvec_pushf(&repack, \"--filter-to=%s\", cfg->repack_filter_to);\n+\t\tstrvec_pushf(args, \"--filter-to=%s\", cfg->repack_filter_to);\n }\n \n-static void add_repack_incremental_option(void)\n+static void add_repack_incremental_option(struct strvec *args)\n {\n-\tstrvec_push(&repack, \"--no-write-bitmap-index\");\n+\tstrvec_push(args, \"--no-write-bitmap-index\");\n }\n \n-static int need_to_gc(struct gc_config *cfg)\n+static int need_to_gc(struct gc_config *cfg, struct strvec *repack_args)\n {\n \t/*\n \t * Setting gc.auto to 0 or negative can disable the\n@@ -700,10 +701,10 @@ static int need_to_gc(struct gc_config *cfg)\n \t\t\t\tstring_list_clear(&keep_pack, 0);\n \t\t}\n \n-\t\tadd_repack_all_option(cfg, &keep_pack);\n+\t\tadd_repack_all_option(cfg, &keep_pack, repack_args);\n \t\tstring_list_clear(&keep_pack, 0);\n \t} else if (too_many_loose_objects(cfg))\n-\t\tadd_repack_incremental_option();\n+\t\tadd_repack_incremental_option(repack_args);\n \telse\n \t\treturn 0;\n \n@@ -852,6 +853,7 @@ int cmd_gc(int argc,\n \tint keep_largest_pack = -1;\n \tint skip_foreground_tasks = 0;\n \ttimestamp_t dummy;\n+\tstruct strvec repack_args = STRVEC_INIT;\n \tstruct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT;\n \tstruct gc_config cfg = GC_CONFIG_INIT;\n \tconst char *prune_expire_sentinel = \"sentinel\";\n@@ -891,7 +893,7 @@ int cmd_gc(int argc,\n \tshow_usage_with_options_if_asked(argc, argv,\n \t\t\t\t\t builtin_gc_usage, builtin_gc_options);\n \n-\tstrvec_pushl(&repack, \"repack\", \"-d\", \"-l\", NULL);\n+\tstrvec_pushl(&repack_args, \"repack\", \"-d\", \"-l\", NULL);\n \n \tgc_config(&cfg);\n \n@@ -914,14 +916,14 @@ int cmd_gc(int argc,\n \t\tdie(_(\"failed to parse prune expiry value %s\"), cfg.prune_expire);\n \n \tif (aggressive) {\n-\t\tstrvec_push(&repack, \"-f\");\n+\t\tstrvec_push(&repack_args, \"-f\");\n \t\tif (cfg.aggressive_depth > 0)\n-\t\t\tstrvec_pushf(&repack, \"--depth=%d\", cfg.aggressive_depth);\n+\t\t\tstrvec_pushf(&repack_args, \"--depth=%d\", cfg.aggressive_depth);\n \t\tif (cfg.aggressive_window > 0)\n-\t\t\tstrvec_pushf(&repack, \"--window=%d\", cfg.aggressive_window);\n+\t\t\tstrvec_pushf(&repack_args, \"--window=%d\", cfg.aggressive_window);\n \t}\n \tif (opts.quiet)\n-\t\tstrvec_push(&repack, \"-q\");\n+\t\tstrvec_push(&repack_args, \"-q\");\n \n \tif (opts.auto_flag) {\n \t\tif (cfg.detach_auto && opts.detach < 0)\n@@ -930,7 +932,7 @@ int cmd_gc(int argc,\n \t\t/*\n \t\t * Auto-gc should be least intrusive as possible.\n \t\t */\n-\t\tif (!need_to_gc(&cfg)) {\n+\t\tif (!need_to_gc(&cfg, &repack_args)) {\n \t\t\tret = 0;\n \t\t\tgoto out;\n \t\t}\n@@ -952,7 +954,7 @@ int cmd_gc(int argc,\n \t\t\tfind_base_packs(&keep_pack, cfg.big_pack_threshold);\n \t\t}\n \n-\t\tadd_repack_all_option(&cfg, &keep_pack);\n+\t\tadd_repack_all_option(&cfg, &keep_pack, &repack_args);\n \t\tstring_list_clear(&keep_pack, 0);\n \t}\n \n@@ -1014,9 +1016,9 @@ int cmd_gc(int argc,\n \n \t\trepack_cmd.git_cmd = 1;\n \t\trepack_cmd.close_object_store = 1;\n-\t\tstrvec_pushv(&repack_cmd.args, repack.v);\n+\t\tstrvec_pushv(&repack_cmd.args, repack_args.v);\n \t\tif (run_command(&repack_cmd))\n-\t\t\tdie(FAILED_RUN, repack.v[0]);\n+\t\t\tdie(FAILED_RUN, repack_args.v[0]);\n \n \t\tif (cfg.prune_expire) {\n \t\t\tstruct child_process prune_cmd = CHILD_PROCESS_INIT;\n@@ -1067,6 +1069,7 @@ int cmd_gc(int argc,\n \n out:\n \tmaintenance_run_opts_release(&opts);\n+\tstrvec_clear(&repack_args);\n \tgc_config_release(&cfg);\n \treturn 0;\n }\n@@ -1269,6 +1272,19 @@ static int maintenance_task_gc_background(struct maintenance_run_opts *opts,\n \treturn run_command(&child);\n }\n \n+static int gc_condition(struct gc_config *cfg)\n+{\n+\t/*\n+\t * Note that it's fine to drop the repack arguments here, as we execute\n+\t * git-gc(1) as a separate child process anyway. So it knows to compute\n+\t * these arguments again.\n+\t */\n+\tstruct strvec repack_args = STRVEC_INIT;\n+\tint ret = need_to_gc(cfg, &repack_args);\n+\tstrvec_clear(&repack_args);\n+\treturn ret;\n+}\n+\n static int prune_packed(struct maintenance_run_opts *opts)\n {\n \tstruct child_process child = CHILD_PROCESS_INIT;\n@@ -1596,7 +1612,7 @@ static const struct maintenance_task tasks[] = {\n \t\t.name = \"gc\",\n \t\t.foreground = maintenance_task_gc_foreground,\n \t\t.background = maintenance_task_gc_background,\n-\t\t.auto_condition = need_to_gc,\n+\t\t.auto_condition = gc_condition,\n \t},\n \t[TASK_COMMIT_GRAPH] = {\n \t\t.name = \"commit-graph\",\n\n-- \n2.51.0.869.ge66316f041.dirty\n\n"},{"id":"528935","messageId":"20251016-pks-maintenance-geometric-strategy-v1-2-18943d474203@pks.im","threadId":"64330","inReplyTo":"20251016-pks-maintenance-geometric-strategy-v1-0-18943d474203@pks.im","subject":"[PATCH 2/8] builtin/gc: make `too_many_loose_objects()` reusable without GC config","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-16T07:26:33Z","receivedAt":"2025-10-16T07:26:52Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"To decide whether or not a repository needs to be repacked we estimate\nthe number of loose objects. If the number exceeds a certain threshold\nwe perform the repack, otherwise we don't.\n\nThis is done via `too_many_loose_objects()`, which takes as parameter\nthe `struct gc_config`. This configuration is only used to determine the\nthreshold. In a subsequent commit we'll add another caller of this\nfunction that wants to pass a different limit than the one stored in\nthat structure.\n\nRefactor the function accordingly so that we only take the limit as\nparameter instead of the whole structure.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 8 ++++----\n 1 file changed, 4 insertions(+), 4 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e9772eb3a30..026d3a1d714 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -447,7 +447,7 @@ static int rerere_gc_condition(struct gc_config *cfg UNUSED)\n \treturn should_gc;\n }\n \n-static int too_many_loose_objects(struct gc_config *cfg)\n+static int too_many_loose_objects(int limit)\n {\n \t/*\n \t * Quickly check if a \"gc\" is needed, by estimating how\n@@ -469,7 +469,7 @@ static int too_many_loose_objects(struct gc_config *cfg)\n \tif (!dir)\n \t\treturn 0;\n \n-\tauto_threshold = DIV_ROUND_UP(cfg->gc_auto_threshold, 256);\n+\tauto_threshold = DIV_ROUND_UP(limit, 256);\n \twhile ((ent = readdir(dir)) != NULL) {\n \t\tif (strspn(ent->d_name, \"0123456789abcdef\") != hexsz_loose ||\n \t\t    ent->d_name[hexsz_loose] != '\\0')\n@@ -703,7 +703,7 @@ static int need_to_gc(struct gc_config *cfg, struct strvec *repack_args)\n \n \t\tadd_repack_all_option(cfg, &keep_pack, repack_args);\n \t\tstring_list_clear(&keep_pack, 0);\n-\t} else if (too_many_loose_objects(cfg))\n+\t} else if (too_many_loose_objects(cfg->gc_auto_threshold))\n \t\tadd_repack_incremental_option(repack_args);\n \telse\n \t\treturn 0;\n@@ -1057,7 +1057,7 @@ int cmd_gc(int argc,\n \t\t\t\t\t     !opts.quiet && !daemonized ? COMMIT_GRAPH_WRITE_PROGRESS : 0,\n \t\t\t\t\t     NULL);\n \n-\tif (opts.auto_flag && too_many_loose_objects(&cfg))\n+\tif (opts.auto_flag && too_many_loose_objects(cfg.gc_auto_threshold))\n \t\twarning(_(\"There are too many unreachable loose objects; \"\n \t\t\t\"run 'git prune' to remove them.\"));\n \n\n-- \n2.51.0.869.ge66316f041.dirty\n\n"},{"id":"528936","messageId":"20251016-pks-maintenance-geometric-strategy-v1-3-18943d474203@pks.im","threadId":"64330","inReplyTo":"20251016-pks-maintenance-geometric-strategy-v1-0-18943d474203@pks.im","subject":"[PATCH 3/8] builtin/maintenance: introduce \"geometric-repack\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-16T07:26:34Z","receivedAt":"2025-10-16T07:26:55Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"geometric-repack\" task. This task uses our geometric\nrepack infrastructure as provided by git-repack(1) itself, which is a\nstrategy that especially hosting providers tend to use to amortize the\ncosts of repacking objects.\n\nThere is one issue though with geometric repacks, namely that they\nunconditionally pack all loose objects, regardless of whether or not\nthey are reachable. This is done because it means that we can completely\nskip the reachability step, which significantly speeds up the operation.\nBut it has the big downside that we are unable to expire objects over\ntime.\n\nTo address this issue we thus use a split strategy in this new task:\nwhenever a geometric repack would merge together all packs, we instead\ndo an all-into-one repack. By default, these all-into-one repacks have\ncruft packs enabled, so unreachable objects would now be written into\ntheir own pack. Consequently, they won't be soaked up during geometric\nrepacking anymore and can be expired with the next full repack, assuming\nthat their expiry date has surpassed.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/maintenance.adoc |  11 +++\n builtin/gc.c                          | 102 +++++++++++++++++++++++++\n t/t7900-maintenance.sh                | 137 ++++++++++++++++++++++++++++++++++\n 3 files changed, 250 insertions(+)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex 2f719342183..26dc5de423f 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -75,6 +75,17 @@ maintenance.incremental-repack.auto::\n \tnumber of pack-files not in the multi-pack-index is at least the value\n \tof `maintenance.incremental-repack.auto`. The default value is 10.\n \n+maintenance.geometric-repack.auto::\n+\tThis integer config option controls how often the `geometric-repack`\n+\ttask should be run as part of `git maintenance run --auto`. If zero,\n+\tthen the `geometric-repack` task will not run with the `--auto`\n+\toption. A negative value will force the task to run every time.\n+\tOtherwise, a positive value implies the command should run either when\n+\tthere are packfiles that need to be merged together to retain the\n+\tgeometric progression, or when there are at least this many loose\n+\tobjects that would be written into a new packfile. The default value is\n+\t100.\n+\n maintenance.reflog-expire.auto::\n \tThis integer config option controls how often the `reflog-expire` task\n \tshould be run as part of `git maintenance run --auto`. If zero, then\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 026d3a1d714..2c9ecd464d2 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -34,6 +34,7 @@\n #include \"pack-objects.h\"\n #include \"path.h\"\n #include \"reflog.h\"\n+#include \"repack.h\"\n #include \"rerere.h\"\n #include \"blob.h\"\n #include \"tree.h\"\n@@ -254,6 +255,7 @@ enum maintenance_task_label {\n \tTASK_PREFETCH,\n \tTASK_LOOSE_OBJECTS,\n \tTASK_INCREMENTAL_REPACK,\n+\tTASK_GEOMETRIC_REPACK,\n \tTASK_GC,\n \tTASK_COMMIT_GRAPH,\n \tTASK_PACK_REFS,\n@@ -1566,6 +1568,101 @@ static int maintenance_task_incremental_repack(struct maintenance_run_opts *opts\n \treturn 0;\n }\n \n+static int maintenance_task_geometric_repack(struct maintenance_run_opts *opts,\n+\t\t\t\t\t     struct gc_config *cfg)\n+{\n+\tstruct pack_geometry geometry = {\n+\t\t.split_factor = 2,\n+\t};\n+\tstruct pack_objects_args po_args = {\n+\t\t.local = 1,\n+\t};\n+\tstruct existing_packs existing_packs = EXISTING_PACKS_INIT;\n+\tstruct string_list kept_packs = STRING_LIST_INIT_DUP;\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\tint ret;\n+\n+\texisting_packs.repo = the_repository;\n+\texisting_packs_collect(&existing_packs, &kept_packs);\n+\tpack_geometry_init(&geometry, &existing_packs, &po_args);\n+\tpack_geometry_split(&geometry);\n+\n+\tchild.git_cmd = 1;\n+\n+\tstrvec_pushl(&child.args, \"repack\", \"-d\", \"-l\", NULL);\n+\tif (geometry.split < geometry.pack_nr)\n+\t\tstrvec_push(&child.args, \"--geometric=2\");\n+\telse\n+\t\tadd_repack_all_option(cfg, NULL, &child.args);\n+\tif (opts->quiet)\n+\t\tstrvec_push(&child.args, \"--quiet\");\n+\tif (the_repository->settings.core_multi_pack_index)\n+\t\tstrvec_push(&child.args, \"--write-midx\");\n+\n+\tif (run_command(&child)) {\n+\t\tret = error(_(\"failed to perform geometric repack\"));\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\texisting_packs_release(&existing_packs);\n+\tpack_geometry_release(&geometry);\n+\treturn ret;\n+}\n+\n+static int geometric_repack_auto_condition(struct gc_config *cfg UNUSED)\n+{\n+\tstruct pack_geometry geometry = {\n+\t\t.split_factor = 2,\n+\t};\n+\tstruct pack_objects_args po_args = {\n+\t\t.local = 1,\n+\t};\n+\tstruct existing_packs existing_packs = EXISTING_PACKS_INIT;\n+\tstruct string_list kept_packs = STRING_LIST_INIT_DUP;\n+\tint auto_value = 100;\n+\tint ret;\n+\n+\trepo_config_get_int(the_repository, \"maintenance.geometric-repack.auto\",\n+\t\t\t    &auto_value);\n+\tif (!auto_value)\n+\t\treturn 0;\n+\tif (auto_value < 0)\n+\t\treturn 1;\n+\n+\texisting_packs.repo = the_repository;\n+\texisting_packs_collect(&existing_packs, &kept_packs);\n+\tpack_geometry_init(&geometry, &existing_packs, &po_args);\n+\tpack_geometry_split(&geometry);\n+\n+\t/*\n+\t * When we'd merge at least two packs with one another we always\n+\t * perform the repack.\n+\t */\n+\tif (geometry.split) {\n+\t\tret = 1;\n+\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t * Otherwise, we estimate the number of loose objects to determine\n+\t * whether we want to create a new packfile or not.\n+\t */\n+\tif (too_many_loose_objects(auto_value)) {\n+\t\tret = 1;\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\texisting_packs_release(&existing_packs);\n+\tpack_geometry_release(&geometry);\n+\treturn ret;\n+}\n+\n typedef int (*maintenance_task_fn)(struct maintenance_run_opts *opts,\n \t\t\t\t   struct gc_config *cfg);\n typedef int (*maintenance_auto_fn)(struct gc_config *cfg);\n@@ -1608,6 +1705,11 @@ static const struct maintenance_task tasks[] = {\n \t\t.background = maintenance_task_incremental_repack,\n \t\t.auto_condition = incremental_repack_auto_condition,\n \t},\n+\t[TASK_GEOMETRIC_REPACK] = {\n+\t\t.name = \"geometric-repack\",\n+\t\t.background = maintenance_task_geometric_repack,\n+\t\t.auto_condition = geometric_repack_auto_condition,\n+\t},\n \t[TASK_GC] = {\n \t\t.name = \"gc\",\n \t\t.foreground = maintenance_task_gc_foreground,\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex ddd273d8dc2..83a373fe94b 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -465,6 +465,143 @@ test_expect_success 'maintenance.incremental-repack.auto (when config is unset)'\n \t)\n '\n \n+run_and_verify_geometric_pack () {\n+\tEXPECTED_PACKS=\"$1\" &&\n+\n+\t# Verify that we perform a geometric repack.\n+\trm -f \"trace2.txt\" &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/trace2.txt\" \\\n+\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n+\ttest_subcommand git repack -d -l --geometric=2 --quiet --write-midx <trace2.txt &&\n+\n+\t# Verify that the number of packfiles matches our expectation.\n+\tls -l .git/objects/pack/*.pack >packfiles &&\n+\ttest_line_count = \"$EXPECTED_PACKS\" packfiles &&\n+\n+\t# And verify that there are no loose objects anymore.\n+\tcat >expect <<-\\EOF &&\n+\tinfo\n+\tpack\n+\tEOF\n+\tls .git/objects >actual &&\n+\ttest_cmp expect actual\n+}\n+\n+test_expect_success 'geometric repacking task' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tgit config set maintenance.auto false &&\n+\t\ttest_commit initial &&\n+\n+\t\t# The initial repack causes an all-into-one repack.\n+\t\tGIT_TRACE2_EVENT=\"$(pwd)/initial-repack.txt\" \\\n+\t\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n+\t\ttest_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago --quiet --write-midx <initial-repack.txt &&\n+\n+\t\t# Repacking should now cause a no-op geometric repack because\n+\t\t# no packfiles need to be combined.\n+\t\tls -l .git/objects/pack >before &&\n+\t\trun_and_verify_geometric_pack 1 &&\n+\t\tls -l .git/objects/pack >after &&\n+\t\ttest_cmp before after &&\n+\n+\t\t# This incremental change creates a new packfile that only\n+\t\t# soaks up loose objects. The packfiles are not getting merged\n+\t\t# at this point.\n+\t\ttest_commit loose &&\n+\t\trun_and_verify_geometric_pack 2 &&\n+\n+\t\t# Both packfiles have 3 objects, so the next run would cause us\n+\t\t# to merge both packfiles together. This should be turned into\n+\t\t# an all-into-one-repack.\n+\t\tGIT_TRACE2_EVENT=\"$(pwd)/all-into-one-repack.txt\" \\\n+\t\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n+\t\ttest_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago --quiet --write-midx <all-into-one-repack.txt &&\n+\n+\t\t# The geometric repack soaks up unreachable objects.\n+\t\techo blob-1 | git hash-object -w --stdin -t blob &&\n+\t\trun_and_verify_geometric_pack 2 &&\n+\n+\t\t# A second unreachable object should be written into another packfile.\n+\t\techo blob-2 | git hash-object -w --stdin -t blob &&\n+\t\trun_and_verify_geometric_pack 3 &&\n+\n+\t\t# And these two small packs should now be merged via the\n+\t\t# geometric repack. The large packfile should remain intact.\n+\t\trun_and_verify_geometric_pack 2 &&\n+\n+\t\t# If we now add two more objects and repack twice we should\n+\t\t# then see another all-into-one repack. This time around\n+\t\t# though, as we have unreachable objects, we should also see a\n+\t\t# cruft pack.\n+\t\techo blob-3 | git hash-object -w --stdin -t blob &&\n+\t\techo blob-4 | git hash-object -w --stdin -t blob &&\n+\t\trun_and_verify_geometric_pack 3 &&\n+\t\tGIT_TRACE2_EVENT=\"$(pwd)/cruft-repack.txt\" \\\n+\t\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n+\t\ttest_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago --quiet --write-midx <cruft-repack.txt &&\n+\t\tls .git/objects/pack/*.pack >packs &&\n+\t\ttest_line_count = 2 packs &&\n+\t\tls .git/objects/pack/*.mtimes >cruft &&\n+\t\ttest_line_count = 1 cruft\n+\t)\n+'\n+\n+test_geometric_repack_needed () {\n+\tNEEDED=\"$1\"\n+\tAUTO_LIMIT=\"$2\" &&\n+\trm -f trace2.txt &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/trace2.txt\" \\\n+\t\tgit ${AUTO_LIMIT:+-c maintenance.geometric-repack.auto=$AUTO_LIMIT} maintenance run --auto --task=geometric-repack &&\n+\tcase \"$NEEDED\" in\n+\ttrue)\n+\t\ttest_grep \"\\[\\\"git\\\",\\\"repack\\\",\" trace2.txt;;\n+\tfalse)\n+\t\t! test_grep \"\\[\\\"git\\\",\\\"repack\\\",\" trace2.txt;;\n+\t*)\n+\t\tBUG \"invalid parameter: $NEEDED\";;\n+\tesac\n+}\n+\n+test_expect_success 'geometric repacking with --auto' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\n+\t\t# An empty repository does not need repacking, except when\n+\t\t# explicitly told to do it.\n+\t\ttest_geometric_repack_needed false &&\n+\t\ttest_geometric_repack_needed false 0 &&\n+\t\ttest_geometric_repack_needed false 1 &&\n+\t\ttest_geometric_repack_needed true -1 &&\n+\n+\t\ttest_oid_init &&\n+\n+\t\t# Loose objects cause a repack when crossing the limit. Note\n+\t\t# that the number of objects gets extrapolated by having a look\n+\t\t# at the \"objects/17/\" shard.\n+\t\ttest_commit \"$(test_oid blob17_1)\" &&\n+\t\ttest_geometric_repack_needed false &&\n+\t\ttest_commit \"$(test_oid blob17_2)\" &&\n+\t\ttest_geometric_repack_needed false 257 &&\n+\t\ttest_geometric_repack_needed true 256 &&\n+\n+\t\t# Force another repack.\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_geometric_repack_needed true -1 &&\n+\n+\t\t# We now have two packfiles that would be merged together. As\n+\t\t# such, the repack should always happen unless the user has\n+\t\t# disabled the auto task.\n+\t\ttest_geometric_repack_needed false 0 &&\n+\t\ttest_geometric_repack_needed true 9000\n+\t)\n+'\n+\n test_expect_success 'pack-refs task' '\n \tfor n in $(test_seq 1 5)\n \tdo\n\n-- \n2.51.0.869.ge66316f041.dirty\n\n"},{"id":"528937","messageId":"20251016-pks-maintenance-geometric-strategy-v1-4-18943d474203@pks.im","threadId":"64330","inReplyTo":"20251016-pks-maintenance-geometric-strategy-v1-0-18943d474203@pks.im","subject":"[PATCH 4/8] builtin/maintenance: don't silently ignore invalid strategy","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-16T07:26:35Z","receivedAt":"2025-10-16T07:26:58Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When parsing maintenance strategies we completely ignore the\nuser-configured value in case it is unknown to us. This makes it\nbasically undiscoverable to the user that scheduled maintenance is\ndevolving into a no-op.\n\nChange this to instead die when seeing an unknown maintenance strategy.\nWhile at it, pull out the parsing logic into a separate function so that\nwe can reuse it in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c           | 17 +++++++++++------\n t/t7900-maintenance.sh |  5 +++++\n 2 files changed, 16 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 2c9ecd464d2..e358e8d13b4 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1848,6 +1848,13 @@ static const struct maintenance_strategy incremental_strategy = {\n \t},\n };\n \n+static struct maintenance_strategy parse_maintenance_strategy(const char *name)\n+{\n+\tif (!strcasecmp(name, \"incremental\"))\n+\t\treturn incremental_strategy;\n+\tdie(_(\"unknown maintenance strategy: '%s'\"), name);\n+}\n+\n static void initialize_task_config(struct maintenance_run_opts *opts,\n \t\t\t\t   const struct string_list *selected_tasks)\n {\n@@ -1883,12 +1890,10 @@ static void initialize_task_config(struct maintenance_run_opts *opts,\n \t * override specific aspects of our strategy.\n \t */\n \tif (opts->schedule) {\n-\t\tstrategy = none_strategy;\n-\n-\t\tif (!repo_config_get_string_tmp(the_repository, \"maintenance.strategy\", &config_str)) {\n-\t\t\tif (!strcasecmp(config_str, \"incremental\"))\n-\t\t\t\tstrategy = incremental_strategy;\n-\t\t}\n+\t\tif (!repo_config_get_string_tmp(the_repository, \"maintenance.strategy\", &config_str))\n+\t\t\tstrategy = parse_maintenance_strategy(config_str);\n+\t\telse\n+\t\t\tstrategy = none_strategy;\n \t} else {\n \t\tstrategy = default_strategy;\n \t}\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 83a373fe94b..45334f7ad3a 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -1230,6 +1230,11 @@ test_expect_success 'fails when running outside of a repository' '\n \tnongit test_must_fail git maintenance unregister\n '\n \n+test_expect_success 'fails when configured to use an invalid strategy' '\n+\ttest_must_fail git -c maintenance.strategy=invalid maintenance run --schedule=hourly 2>err &&\n+\ttest_grep \"unknown maintenance strategy: .invalid.\" err\n+'\n+\n test_expect_success 'register and unregister bare repo' '\n \ttest_when_finished \"git config --global --unset-all maintenance.repo || :\" &&\n \ttest_might_fail git config --global --unset-all maintenance.repo &&\n\n-- \n2.51.0.869.ge66316f041.dirty\n\n"},{"id":"528938","messageId":"20251016-pks-maintenance-geometric-strategy-v1-5-18943d474203@pks.im","threadId":"64330","inReplyTo":"20251016-pks-maintenance-geometric-strategy-v1-0-18943d474203@pks.im","subject":"[PATCH 5/8] builtin/maintenance: run maintenance tasks depending on type","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-16T07:26:36Z","receivedAt":"2025-10-16T07:27:01Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We basically have three different ways to execute repository\nmaintenance:\n\n  1. Manual maintenance via `git maintenance run`.\n\n  2. Automatic maintenance via `git maintenance run --auto`.\n\n  3. Scheduled maintenance via `git maintenance run --schedule=`.\n\nAt the moment, maintenance strategies only have an effect for the last\ntype of maintenance. This is about to change in subsequent commits, but\nto do so we need to be able to skip some tasks depending on how exactly\nmaintenance was invoked.\n\nIntroduce a new maintenance type that discern between manual (1 & 2) and\nscheduled (3) maintenance. Convert the `enabled` field into a bitset so\nthat it becomes possible to specifiy which tasks exactly should run in a\nspecific context.\n\nThe types picked for existing strategies match the status quo:\n\n  - The default strategy is only ever executed as part of a manual\n    maintenance run. It is not possible to use it for scheduled\n    maintenance.\n\n  - The incremental strategy is only ever executed as part of a\n    scheduled maintenance run. It is not possible to use it for manual\n    maintenance.\n\nThe strategies will be tweaked in subsequent commits to make use of this\nnew infrastructure.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 30 +++++++++++++++++++++---------\n 1 file changed, 21 insertions(+), 9 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e358e8d13b4..4f70650e7ac 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1820,30 +1820,39 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \treturn result;\n }\n \n+enum maintenance_type {\n+\t/* As invoked via `git maintenance run --schedule=`. */\n+\tMAINTENANCE_TYPE_SCHEDULED = (1 << 0),\n+\t/* As invoked via `git maintenance run` and with `--auto`. */\n+\tMAINTENANCE_TYPE_MANUAL    = (1 << 1),\n+};\n+\n struct maintenance_strategy {\n \tstruct {\n-\t\tint enabled;\n+\t\tunsigned type;\n \t\tenum schedule_priority schedule;\n \t} tasks[TASK__COUNT];\n };\n \n static const struct maintenance_strategy none_strategy = { 0 };\n+\n static const struct maintenance_strategy default_strategy = {\n \t.tasks = {\n-\t\t[TASK_GC].enabled = 1,\n+\t\t[TASK_GC].type = MAINTENANCE_TYPE_MANUAL,\n \t},\n };\n+\n static const struct maintenance_strategy incremental_strategy = {\n \t.tasks = {\n-\t\t[TASK_COMMIT_GRAPH].enabled = 1,\n+\t\t[TASK_COMMIT_GRAPH].type = MAINTENANCE_TYPE_SCHEDULED,\n \t\t[TASK_COMMIT_GRAPH].schedule = SCHEDULE_HOURLY,\n-\t\t[TASK_PREFETCH].enabled = 1,\n+\t\t[TASK_PREFETCH].type = MAINTENANCE_TYPE_SCHEDULED,\n \t\t[TASK_PREFETCH].schedule = SCHEDULE_HOURLY,\n-\t\t[TASK_INCREMENTAL_REPACK].enabled = 1,\n+\t\t[TASK_INCREMENTAL_REPACK].type = MAINTENANCE_TYPE_SCHEDULED,\n \t\t[TASK_INCREMENTAL_REPACK].schedule = SCHEDULE_DAILY,\n-\t\t[TASK_LOOSE_OBJECTS].enabled = 1,\n+\t\t[TASK_LOOSE_OBJECTS].type = MAINTENANCE_TYPE_SCHEDULED,\n \t\t[TASK_LOOSE_OBJECTS].schedule = SCHEDULE_DAILY,\n-\t\t[TASK_PACK_REFS].enabled = 1,\n+\t\t[TASK_PACK_REFS].type = MAINTENANCE_TYPE_SCHEDULED,\n \t\t[TASK_PACK_REFS].schedule = SCHEDULE_WEEKLY,\n \t},\n };\n@@ -1860,6 +1869,7 @@ static void initialize_task_config(struct maintenance_run_opts *opts,\n {\n \tstruct strbuf config_name = STRBUF_INIT;\n \tstruct maintenance_strategy strategy;\n+\tenum maintenance_type type;\n \tconst char *config_str;\n \n \t/*\n@@ -1894,8 +1904,10 @@ static void initialize_task_config(struct maintenance_run_opts *opts,\n \t\t\tstrategy = parse_maintenance_strategy(config_str);\n \t\telse\n \t\t\tstrategy = none_strategy;\n+\t\ttype = MAINTENANCE_TYPE_SCHEDULED;\n \t} else {\n \t\tstrategy = default_strategy;\n+\t\ttype = MAINTENANCE_TYPE_MANUAL;\n \t}\n \n \tfor (size_t i = 0; i < TASK__COUNT; i++) {\n@@ -1905,8 +1917,8 @@ static void initialize_task_config(struct maintenance_run_opts *opts,\n \t\tstrbuf_addf(&config_name, \"maintenance.%s.enabled\",\n \t\t\t    tasks[i].name);\n \t\tif (!repo_config_get_bool(the_repository, config_name.buf, &config_value))\n-\t\t\tstrategy.tasks[i].enabled = config_value;\n-\t\tif (!strategy.tasks[i].enabled)\n+\t\t\tstrategy.tasks[i].type = config_value ? type : 0;\n+\t\tif (!(strategy.tasks[i].type & type))\n \t\t\tcontinue;\n \n \t\tif (opts->schedule) {\n\n-- \n2.51.0.869.ge66316f041.dirty\n\n"},{"id":"528939","messageId":"20251016-pks-maintenance-geometric-strategy-v1-6-18943d474203@pks.im","threadId":"64330","inReplyTo":"20251016-pks-maintenance-geometric-strategy-v1-0-18943d474203@pks.im","subject":"[PATCH 6/8] builtin/maintenance: extend \"maintenance.strategy\" to manual maintenance","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-16T07:26:37Z","receivedAt":"2025-10-16T07:27:04Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"maintenance.strategy\" configuration allows users to configure how\nGit is supposed to perform repository maintenance. The idea is that we\nprovide a set of high-level strategies that may be useful in different\ncontexts, like for example when handling a large monorepo. Furthermore,\nthe strategy can be tweaked by the user by overriding specific tasks.\n\nIn its current form though, the strategy only applies to scheduled\nmaintenance. This creates something of a gap, as scheduled and manual\nmaintenance will now use _different_ strategies as the latter would\ncontinue to use git-gc(1) by default. This makes the strategies way less\nuseful than they could be on the one hand. But even more importantly,\nthe two different strategies might clash with one another, where one of\nthe strategies performs maintenance in such a way that it discards\nbenefits from the other strategy.\n\nSo ideally, it should be possible to pick one strategy that then applies\nglobally to all the different ways that we perform maintenance. This\ndoesn't necessarily mean that the strategy always does the _same_ thing\nfor every maintenance type. But it means that the strategy can configure\nthe different types to work in tandem with each other.\n\nChange the meaning of \"maintenance.strategy\" accordingly so that the\nstrategy is applied to both types, manual and scheduled. As preceding\ncommits have introduced logic to run maintenance tasks depending on this\ntype we can tweak strategies so that they perform those tasks depending\non the context.\n\nNote that this raises the question of backwards compatibility: when the\nuser has configured the \"incremental\" strategy we would have ignored\nthat strategy beforehand. Instead, repository maintenance would have\ncontinued to use git-gc(1) by default.\n\nBut luckily, we can match that behaviour by:\n\n  - Keeping all current tasks of the incremental strategy as\n    `MAINTENANCE_TYPE_SCHEDULED`. This ensures that those tasks will not\n    run during manual maintenance.\n\n  - Configuring the \"gc\" task so that it is invoked during manual\n    maintenance.\n\nLike this, the user shouldn't observe any difference in behaviour.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/maintenance.adoc | 22 ++++++++++++-------\n builtin/gc.c                          | 24 ++++++++++++++++-----\n t/t7900-maintenance.sh                | 40 +++++++++++++++++++++++++++++++++++\n 3 files changed, 73 insertions(+), 13 deletions(-)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex 26dc5de423f..dc6fd9b7fda 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -16,19 +16,25 @@ detach.\n \n maintenance.strategy::\n \tThis string config option provides a way to specify one of a few\n-\trecommended schedules for background maintenance. This only affects\n-\twhich tasks are run during `git maintenance run --schedule=X`\n-\tcommands, provided no `--task=<task>` arguments are provided.\n-\tFurther, if a `maintenance.<task>.schedule` config value is set,\n-\tthen that value is used instead of the one provided by\n-\t`maintenance.strategy`. The possible strategy strings are:\n+\trecommended strategies for repository maintenance. This affects\n+\twhich tasks are run during `git maintenance run`, provided no\n+\t`--task=<task>` arguments are provided. This setting impacts manual\n+\tmaintenance, auto-maintenance as well as scheduled maintenance. The\n+\ttasks that run may be different depending on the maintenance type.\n +\n-* `none`: This default setting implies no tasks are run at any schedule.\n+The maintenance strategy can be further tweaked by setting\n+`maintenance.<task>.enabled` and `maintenance.<task>.schedule`. If set, these\n+values are used instead of the defaults provided by `maintenance.strategy`.\n++\n+The possible strategies are:\n++\n+* `none`: This strategy implies no tasks are run at all. This is the default\n+  strategy for scheduled maintenance.\n * `incremental`: This setting optimizes for performing small maintenance\n   activities that do not delete any data. This does not schedule the `gc`\n   task, but runs the `prefetch` and `commit-graph` tasks hourly, the\n   `loose-objects` and `incremental-repack` tasks daily, and the `pack-refs`\n-  task weekly.\n+  task weekly. Manual repository maintenance uses the `gc` task.\n \n maintenance.<task>.enabled::\n \tThis boolean config option controls whether the maintenance task\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 4f70650e7ac..971d557d370 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1854,6 +1854,19 @@ static const struct maintenance_strategy incremental_strategy = {\n \t\t[TASK_LOOSE_OBJECTS].schedule = SCHEDULE_DAILY,\n \t\t[TASK_PACK_REFS].type = MAINTENANCE_TYPE_SCHEDULED,\n \t\t[TASK_PACK_REFS].schedule = SCHEDULE_WEEKLY,\n+\n+\t\t/*\n+\t\t * Historically, the \"incremental\" strategy was only available\n+\t\t * in the context of scheduled maintenance when set up via\n+\t\t * \"maintenance.strategy\". We have later expanded that config\n+\t\t * to also cover manual maintenance.\n+\t\t *\n+\t\t * To retain backwards compatibility with the previous status\n+\t\t * quo we thus run git-gc(1) in case manual maintenance was\n+\t\t * requested. This is the same as the default strategy, which\n+\t\t * would have been in use beforehand.\n+\t\t */\n+\t\t[TASK_GC].type = MAINTENANCE_TYPE_MANUAL,\n \t},\n };\n \n@@ -1897,19 +1910,20 @@ static void initialize_task_config(struct maintenance_run_opts *opts,\n \t *   - Unscheduled maintenance uses our default strategy.\n \t *\n \t * Both of these are affected by the gitconfig though, which may\n-\t * override specific aspects of our strategy.\n+\t * override specific aspects of our strategy. Furthermore, both\n+\t * strategies can be overridden by setting \"maintenance.strategy\".\n \t */\n \tif (opts->schedule) {\n-\t\tif (!repo_config_get_string_tmp(the_repository, \"maintenance.strategy\", &config_str))\n-\t\t\tstrategy = parse_maintenance_strategy(config_str);\n-\t\telse\n-\t\t\tstrategy = none_strategy;\n+\t\tstrategy = none_strategy;\n \t\ttype = MAINTENANCE_TYPE_SCHEDULED;\n \t} else {\n \t\tstrategy = default_strategy;\n \t\ttype = MAINTENANCE_TYPE_MANUAL;\n \t}\n \n+\tif (!repo_config_get_string_tmp(the_repository, \"maintenance.strategy\", &config_str))\n+\t\tstrategy = parse_maintenance_strategy(config_str);\n+\n \tfor (size_t i = 0; i < TASK__COUNT; i++) {\n \t\tint config_value;\n \ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 45334f7ad3a..439f1bfba0c 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -853,6 +853,46 @@ test_expect_success 'maintenance.strategy inheritance' '\n \t\t<modified-daily.txt\n '\n \n+test_strategy () {\n+\tSTRATEGY=\"$1\"\n+\tshift\n+\n+\tcat >expect &&\n+\trm -f trace2.txt &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/trace2.txt\" \\\n+\t\tgit -c maintenance.strategy=$STRATEGY maintenance run --quiet \"$@\" &&\n+\tsed -n 's/{\"event\":\"child_start\",\"sid\":\"[^/\"]*\",.*,\"argv\":\\[\"\\(.*\\)\\\"]}/\\1/p' <trace2.txt |\n+\t\tsed 's/\",\"/ /g'  >actual\n+\ttest_cmp expect actual\n+}\n+\n+test_expect_success 'maintenance.strategy is respected' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\n+\t\ttest_must_fail git -c maintenance.strategy=unknown maintenance run 2>err &&\n+\t\ttest_grep \"unknown maintenance strategy: .unknown.\" err &&\n+\n+\t\ttest_strategy incremental <<-\\EOF &&\n+\t\tgit pack-refs --all --prune\n+\t\tgit reflog expire --all\n+\t\tgit gc --quiet --no-detach --skip-foreground-tasks\n+\t\tEOF\n+\n+\t\ttest_strategy incremental --schedule=weekly <<-\\EOF\n+\t\tgit pack-refs --all --prune\n+\t\tgit prune-packed --quiet\n+\t\tgit multi-pack-index write --no-progress\n+\t\tgit multi-pack-index expire --no-progress\n+\t\tgit multi-pack-index repack --no-progress --batch-size=1\n+\t\tgit commit-graph write --split --reachable --no-progress\n+\t\tEOF\n+\t)\n+'\n+\n test_expect_success 'register and unregister' '\n \ttest_when_finished git config --global --unset-all maintenance.repo &&\n \n\n-- \n2.51.0.869.ge66316f041.dirty\n\n"},{"id":"528940","messageId":"20251016-pks-maintenance-geometric-strategy-v1-7-18943d474203@pks.im","threadId":"64330","inReplyTo":"20251016-pks-maintenance-geometric-strategy-v1-0-18943d474203@pks.im","subject":"[PATCH 7/8] builtin/maintenance: make \"gc\" strategy accessible","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-16T07:26:38Z","receivedAt":"2025-10-16T07:27:06Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While the user can pick the \"incremental\" maintenance strategy, it is\nnot possible to explicitly use the \"gc\" strategy. This has two\ndownsides:\n\n  - It is impossible to use the default \"gc\" strategy for a specific\n    repository when the strategy was globally set to a different strategy.\n\n  - It is not possible to use git-gc(1) for scheduled maintenance.\n\nAddress these issues by making making the \"gc\" strategy configurable.\nFurthermore, extend the strategy so that git-gc(1) runs for both manual\nand scheduled maintenance.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/maintenance.adoc |  2 ++\n builtin/gc.c                          |  9 ++++++---\n t/t7900-maintenance.sh                | 14 +++++++++++++-\n 3 files changed, 21 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex dc6fd9b7fda..648b6db47c6 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -30,6 +30,8 @@ The possible strategies are:\n +\n * `none`: This strategy implies no tasks are run at all. This is the default\n   strategy for scheduled maintenance.\n+* `gc`: This strategy runs the `gc` task. This is the default strategy for\n+  manual maintenance.\n * `incremental`: This setting optimizes for performing small maintenance\n   activities that do not delete any data. This does not schedule the `gc`\n   task, but runs the `prefetch` and `commit-graph` tasks hourly, the\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 971d557d370..3673f3db630 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1836,9 +1836,10 @@ struct maintenance_strategy {\n \n static const struct maintenance_strategy none_strategy = { 0 };\n \n-static const struct maintenance_strategy default_strategy = {\n+static const struct maintenance_strategy gc_strategy = {\n \t.tasks = {\n-\t\t[TASK_GC].type = MAINTENANCE_TYPE_MANUAL,\n+\t\t[TASK_GC].type = MAINTENANCE_TYPE_MANUAL | MAINTENANCE_TYPE_SCHEDULED,\n+\t\t[TASK_GC].schedule = SCHEDULE_DAILY,\n \t},\n };\n \n@@ -1874,6 +1875,8 @@ static struct maintenance_strategy parse_maintenance_strategy(const char *name)\n {\n \tif (!strcasecmp(name, \"incremental\"))\n \t\treturn incremental_strategy;\n+\tif (!strcasecmp(name, \"gc\"))\n+\t\treturn gc_strategy;\n \tdie(_(\"unknown maintenance strategy: '%s'\"), name);\n }\n \n@@ -1917,7 +1920,7 @@ static void initialize_task_config(struct maintenance_run_opts *opts,\n \t\tstrategy = none_strategy;\n \t\ttype = MAINTENANCE_TYPE_SCHEDULED;\n \t} else {\n-\t\tstrategy = default_strategy;\n+\t\tstrategy = gc_strategy;\n \t\ttype = MAINTENANCE_TYPE_MANUAL;\n \t}\n \ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 439f1bfba0c..1acd701830e 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -882,7 +882,7 @@ test_expect_success 'maintenance.strategy is respected' '\n \t\tgit gc --quiet --no-detach --skip-foreground-tasks\n \t\tEOF\n \n-\t\ttest_strategy incremental --schedule=weekly <<-\\EOF\n+\t\ttest_strategy incremental --schedule=weekly <<-\\EOF &&\n \t\tgit pack-refs --all --prune\n \t\tgit prune-packed --quiet\n \t\tgit multi-pack-index write --no-progress\n@@ -890,6 +890,18 @@ test_expect_success 'maintenance.strategy is respected' '\n \t\tgit multi-pack-index repack --no-progress --batch-size=1\n \t\tgit commit-graph write --split --reachable --no-progress\n \t\tEOF\n+\n+\t\ttest_strategy gc <<-\\EOF &&\n+\t\tgit pack-refs --all --prune\n+\t\tgit reflog expire --all\n+\t\tgit gc --quiet --no-detach --skip-foreground-tasks\n+\t\tEOF\n+\n+\t\ttest_strategy gc --schedule=weekly <<-\\EOF\n+\t\tgit pack-refs --all --prune\n+\t\tgit reflog expire --all\n+\t\tgit gc --quiet --no-detach --skip-foreground-tasks\n+\t\tEOF\n \t)\n '\n \n\n-- \n2.51.0.869.ge66316f041.dirty\n\n"},{"id":"528941","messageId":"20251016-pks-maintenance-geometric-strategy-v1-8-18943d474203@pks.im","threadId":"64330","inReplyTo":"20251016-pks-maintenance-geometric-strategy-v1-0-18943d474203@pks.im","subject":"[PATCH 8/8] builtin/maintenance: introduce \"geometric\" strategy","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-16T07:26:39Z","receivedAt":"2025-10-16T07:27:09Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We have two different repacking strategies in Git:\n\n  - The \"gc\" strategy uses git-gc(1).\n\n  - The \"incremental\" strategy uses multi-pack indices and `git\n    multi-pack-index repack` to merge together smaller packfiles as\n    determined by a specific batch size.\n\nThe former strategy is our old and trusted default, whereas the latter\nhas historically been used for our scheduled maintenance. But both\nstrategies have their shortcomings:\n\n  - The \"gc\" strategy performs regular all-into-one repacks. Furthermore\n    it is rather inflexible, as it is not easily possible for a user to\n    enable or disable specific subtasks.\n\n  - The \"incremental\" strategy is not a full replacement for the \"gc\"\n    strategy as it doesn't know to prune stale data.\n\nSo today, we don't have a strategy that is well-suited for large repos\nwhile being a full replacement for the \"gc\" strategy.\n\nIntroduce a new \"geometric\" strategy that aims to fill this gap. This\nstrategy invokes all the usual cleanup tasks that git-gc(1) does like\npruning reflogs and rerere caches as well as stale worktrees. But where\nit differs from both the \"gc\" and \"incremental\" strategy is that it uses\nour geometric repacking infrastructure exposed by git-repack(1) to\nrepack packfiles. The advantage of geometric repacking is that we only\nneed to perform an all-into-one repack when the object count in a repo\nhas grown significantly.\n\nOne downside of this strategy is that pruning of unreferenced objects is\nnot going to happen regularly anymore. Every geometric repack knows to\nsoak up all loose objects regardless of their reachability, and merging\ntwo or more packs doesn't consider reachability, either. Consequently,\nthe number of unreachable objects will grow over time.\n\nThis is remedied by doing an all-into-one repack instead of a geometric\nrepack whenever we determine that the geometric repack would end up\nmerging all packfiles anyway. This all-into-one repack then performs our\nusual reachability checks and writes unreachable objects into a cruft\npack. As cruft packs won't ever be merged during geometric repacks we\ncan thus phase out these objects over time.\n\nOf course, this still means that we retain unreachable objects for far\nlonger than with the \"gc\" strategy. But the maintenance strategy is\nintended especially for large repositories, where the basic assumption\nis that the set of unreachable objects will be significantly dwarfed by\nthe number of reachable objects.\n\nIf this assumption is ever proven to be too disadvantageous we could for\nexample introduce a time-based strategy: if the largest packfile has not\nbeen touched for longer than $T, we perform an all-into-one repack. But\nfor now, such a mechanism is deferred into the future as it is not clear\nyet whether it is needed in the first place.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/maintenance.adoc |  9 +++++++++\n builtin/gc.c                          | 19 +++++++++++++++++++\n t/t7900-maintenance.sh                | 20 +++++++++++++++++++-\n 3 files changed, 47 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex 648b6db47c6..5ab88c2b328 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -32,6 +32,15 @@ The possible strategies are:\n   strategy for scheduled maintenance.\n * `gc`: This strategy runs the `gc` task. This is the default strategy for\n   manual maintenance.\n+* `geometric`: This strategy performs geometric repacking of packfiles and\n+  keeps auxiliary data structures up-to-date. The strategy expires data in the\n+  reflog and removes worktrees that cannot be located anymore. When the\n+  geometric repacking strategy would decide to do an all-into-one repack, then\n+  the strategy generates a cruft pack for all unreachable objects. Objects that\n+  are already part of a cruft pack will be expired.\n++\n+This repacking strategy is a full replacement for the `gc` strategy and is\n+recommended for large repositories.\n * `incremental`: This setting optimizes for performing small maintenance\n   activities that do not delete any data. This does not schedule the `gc`\n   task, but runs the `prefetch` and `commit-graph` tasks hourly, the\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 3673f3db630..bf603de8a2f 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1871,12 +1871,31 @@ static const struct maintenance_strategy incremental_strategy = {\n \t},\n };\n \n+static const struct maintenance_strategy geometric_strategy = {\n+\t.tasks = {\n+\t\t[TASK_COMMIT_GRAPH].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t[TASK_COMMIT_GRAPH].schedule = SCHEDULE_HOURLY,\n+\t\t[TASK_GEOMETRIC_REPACK].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t[TASK_GEOMETRIC_REPACK].schedule = SCHEDULE_DAILY,\n+\t\t[TASK_PACK_REFS].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t[TASK_PACK_REFS].schedule = SCHEDULE_DAILY,\n+\t\t[TASK_RERERE_GC].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t[TASK_RERERE_GC].schedule = SCHEDULE_WEEKLY,\n+\t\t[TASK_REFLOG_EXPIRE].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t[TASK_REFLOG_EXPIRE].schedule = SCHEDULE_WEEKLY,\n+\t\t[TASK_WORKTREE_PRUNE].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t[TASK_WORKTREE_PRUNE].schedule = SCHEDULE_WEEKLY,\n+\t},\n+};\n+\n static struct maintenance_strategy parse_maintenance_strategy(const char *name)\n {\n \tif (!strcasecmp(name, \"incremental\"))\n \t\treturn incremental_strategy;\n \tif (!strcasecmp(name, \"gc\"))\n \t\treturn gc_strategy;\n+\tif (!strcasecmp(name, \"geometric\"))\n+\t\treturn geometric_strategy;\n \tdie(_(\"unknown maintenance strategy: '%s'\"), name);\n }\n \ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 1acd701830e..bb61b4d7f44 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -897,11 +897,29 @@ test_expect_success 'maintenance.strategy is respected' '\n \t\tgit gc --quiet --no-detach --skip-foreground-tasks\n \t\tEOF\n \n-\t\ttest_strategy gc --schedule=weekly <<-\\EOF\n+\t\ttest_strategy gc --schedule=weekly <<-\\EOF &&\n \t\tgit pack-refs --all --prune\n \t\tgit reflog expire --all\n \t\tgit gc --quiet --no-detach --skip-foreground-tasks\n \t\tEOF\n+\n+\t\ttest_strategy geometric <<-\\EOF &&\n+\t\tgit pack-refs --all --prune\n+\t\tgit reflog expire --all\n+\t\tgit repack -d -l --geometric=2 --quiet --write-midx\n+\t\tgit commit-graph write --split --reachable --no-progress\n+\t\tgit worktree prune --expire 3.months.ago\n+\t\tgit rerere gc\n+\t\tEOF\n+\n+\t\ttest_strategy geometric --schedule=weekly <<-\\EOF\n+\t\tgit pack-refs --all --prune\n+\t\tgit reflog expire --all\n+\t\tgit repack -d -l --geometric=2 --quiet --write-midx\n+\t\tgit commit-graph write --split --reachable --no-progress\n+\t\tgit worktree prune --expire 3.months.ago\n+\t\tgit rerere gc\n+\t\tEOF\n \t)\n '\n \n\n-- \n2.51.0.869.ge66316f041.dirty\n\n"},{"id":"529002","messageId":"uaw3lnxa2yi7mgwmkm2l3lpr7hkfwludzjai53an5nhjh2woif@2kgtzxpnp2ma","threadId":"64330","inReplyTo":"20251016-pks-maintenance-geometric-strategy-v1-1-18943d474203@pks.im","subject":"Re: [PATCH 1/8] builtin/gc: remove global `repack` variable","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-10-16T20:07:55Z","receivedAt":"2025-10-16T20:08:00Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/10/16 09:26AM, Patrick Steinhardt wrote:\n> The global `repack` variable is used to store all command line arguments\n> that we eventually want to pass to git-repack(1). It is being appended\n> to from multiple different functions, which makes it hard to follow the\n> logic. Besides being hard to follow, it also makes it unnecessarily hard\n> to reuse this infrastructure in new code.\n> \n> Refactor the code so that we store this variable on the stack and pass\n> a pointer to it around as needed. This is done so that we can reuse\n> `add_repack_all_options()` in a subsequent commit.\n> \n> The refactoring itself is straight-forward. One function that deserves\n> attention though is `need_to_gc()`: this function determines whether or\n> not we need to execute garbage collection for `git gc --auto`, but also\n> for `git maintenance run --auto`. But besides figuring out whether we\n> have to perform GC, the function also sets up the `repack` arguments.\n> \n> For `git gc --auto` it's trivial to adapt, as we already have the\n> on-stack variable at our fingertips. But for the maintenance condition\n> it's less obvious what to do.\n> \n> As it turns out, we can just use another temporary variable there that\n> we then immediately discard. If we need to perform GC we execute a child\n> git-gc(1) process to repack objects for us, and that process will have\n> to recompute the arguments anyway.\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n[snip]\n> @@ -1269,6 +1272,19 @@ static int maintenance_task_gc_background(struct maintenance_run_opts *opts,\n>  \treturn run_command(&child);\n>  }\n>  \n> +static int gc_condition(struct gc_config *cfg)\n> +{\n> +\t/*\n> +\t * Note that it's fine to drop the repack arguments here, as we execute\n> +\t * git-gc(1) as a separate child process anyway. So it knows to compute\n> +\t * these arguments again.\n> +\t */\n\nAlso we don't expect any arguments to be configured ahead of time so we\nare good.\n\n> +\tstruct strvec repack_args = STRVEC_INIT;\n> +\tint ret = need_to_gc(cfg, &repack_args);\n> +\tstrvec_clear(&repack_args);\n> +\treturn ret;\n> +}\n> +\n>  static int prune_packed(struct maintenance_run_opts *opts)\n>  {\n>  \tstruct child_process child = CHILD_PROCESS_INIT;\n> @@ -1596,7 +1612,7 @@ static const struct maintenance_task tasks[] = {\n>  \t\t.name = \"gc\",\n>  \t\t.foreground = maintenance_task_gc_foreground,\n>  \t\t.background = maintenance_task_gc_background,\n> -\t\t.auto_condition = need_to_gc,\n> +\t\t.auto_condition = gc_condition,\n\nNow that the `need_to_gc()` function signature has changed, we use a\nwrapper function that provides the repack args. In this case, only the\nargs that get set during `need_to_gc()` are required, and the args are\nnot needed afterwards, so it is safe to discard.\n\nThis patch looks good.\n\n-Justin\n"},{"id":"529012","messageId":"uos7cczvzlrwjgcyhzyfirck62qjnb4zcoy6ga2o3pbnba7cfj@ag2pnonze5tu","threadId":"64330","inReplyTo":"20251016-pks-maintenance-geometric-strategy-v1-3-18943d474203@pks.im","subject":"Re: [PATCH 3/8] builtin/maintenance: introduce \"geometric-repack\" task","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-10-16T20:51:17Z","receivedAt":"2025-10-16T20:51:21Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/10/16 09:26AM, Patrick Steinhardt wrote:\n> Introduce a new \"geometric-repack\" task. This task uses our geometric\n> repack infrastructure as provided by git-repack(1) itself, which is a\n> strategy that especially hosting providers tend to use to amortize the\n> costs of repacking objects.\n> \n> There is one issue though with geometric repacks, namely that they\n> unconditionally pack all loose objects, regardless of whether or not\n> they are reachable. This is done because it means that we can completely\n> skip the reachability step, which significantly speeds up the operation.\n> But it has the big downside that we are unable to expire objects over\n> time.\n> \n> To address this issue we thus use a split strategy in this new task:\n> whenever a geometric repack would merge together all packs, we instead\n> do an all-into-one repack. By default, these all-into-one repacks have\n> cruft packs enabled, so unreachable objects would now be written into\n> their own pack. Consequently, they won't be soaked up during geometric\n> repacking anymore and can be expired with the next full repack, assuming\n> that their expiry date has surpassed.\n\nSo normal geometric repacks don't ever check for unreachable objects,\neven if all the packs are being merged together. With this new strategy\nthough, when a geometric repack would normally merge together all packs,\nwe instead to an all-into-one repack which does check for unreachable\nobjects.\n\nDoes checking for unreachable objects in this case slow down the repack\nsignificantly?\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n[snip]\n> @@ -1566,6 +1568,101 @@ static int maintenance_task_incremental_repack(struct maintenance_run_opts *opts\n>  \treturn 0;\n>  }\n>  \n> +static int maintenance_task_geometric_repack(struct maintenance_run_opts *opts,\n> +\t\t\t\t\t     struct gc_config *cfg)\n> +{\n> +\tstruct pack_geometry geometry = {\n> +\t\t.split_factor = 2,\n> +\t};\n> +\tstruct pack_objects_args po_args = {\n> +\t\t.local = 1,\n> +\t};\n> +\tstruct existing_packs existing_packs = EXISTING_PACKS_INIT;\n> +\tstruct string_list kept_packs = STRING_LIST_INIT_DUP;\n> +\tstruct child_process child = CHILD_PROCESS_INIT;\n> +\tint ret;\n> +\n> +\texisting_packs.repo = the_repository;\n> +\texisting_packs_collect(&existing_packs, &kept_packs);\n> +\tpack_geometry_init(&geometry, &existing_packs, &po_args);\n> +\tpack_geometry_split(&geometry);\n> +\n> +\tchild.git_cmd = 1;\n> +\n> +\tstrvec_pushl(&child.args, \"repack\", \"-d\", \"-l\", NULL);\n> +\tif (geometry.split < geometry.pack_nr)\n> +\t\tstrvec_push(&child.args, \"--geometric=2\");\n> +\telse\n> +\t\tadd_repack_all_option(cfg, NULL, &child.args);\n\nHere we do the full repack when the all packs are to be merged. Makes\nsense.\n\n[snip]\n> @@ -1608,6 +1705,11 @@ static const struct maintenance_task tasks[] = {\n>  \t\t.background = maintenance_task_incremental_repack,\n>  \t\t.auto_condition = incremental_repack_auto_condition,\n>  \t},\n> +\t[TASK_GEOMETRIC_REPACK] = {\n> +\t\t.name = \"geometric-repack\",\n> +\t\t.background = maintenance_task_geometric_repack,\n> +\t\t.auto_condition = geometric_repack_auto_condition,\n> +\t},\n\nHere we configure the new maintenance task. Nice :)\n\nThe rest of this patch looks good.\n\n-Justin\n"},{"id":"529015","messageId":"xmqq347i36mu.fsf@gitster.g","threadId":"64330","inReplyTo":"20251016-pks-maintenance-geometric-strategy-v1-2-18943d474203@pks.im","subject":"Re: [PATCH 2/8] builtin/gc: make `too_many_loose_objects()` reusable without GC config","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-16T20:59:21Z","receivedAt":"2025-10-16T20:59:24Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> To decide whether or not a repository needs to be repacked we estimate\n> the number of loose objects. If the number exceeds a certain threshold\n> we perform the repack, otherwise we don't.\n>\n> This is done via `too_many_loose_objects()`, which takes as parameter\n> the `struct gc_config`. This configuration is only used to determine the\n> threshold. In a subsequent commit we'll add another caller of this\n> function that wants to pass a different limit than the one stored in\n> that structure.\n>\n> Refactor the function accordingly so that we only take the limit as\n> parameter instead of the whole structure.\n\nTrivially correct and makes perfect sense.\n"},{"id":"529039","messageId":"aPHeflRSMpoRukta@pks.im","threadId":"64330","inReplyTo":"uos7cczvzlrwjgcyhzyfirck62qjnb4zcoy6ga2o3pbnba7cfj@ag2pnonze5tu","subject":"Re: [PATCH 3/8] builtin/maintenance: introduce \"geometric-repack\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-17T06:13:18Z","receivedAt":"2025-10-17T06:13:26Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Oct 16, 2025 at 03:51:17PM -0500, Justin Tobler wrote:\n> On 25/10/16 09:26AM, Patrick Steinhardt wrote:\n> > Introduce a new \"geometric-repack\" task. This task uses our geometric\n> > repack infrastructure as provided by git-repack(1) itself, which is a\n> > strategy that especially hosting providers tend to use to amortize the\n> > costs of repacking objects.\n> > \n> > There is one issue though with geometric repacks, namely that they\n> > unconditionally pack all loose objects, regardless of whether or not\n> > they are reachable. This is done because it means that we can completely\n> > skip the reachability step, which significantly speeds up the operation.\n> > But it has the big downside that we are unable to expire objects over\n> > time.\n> > \n> > To address this issue we thus use a split strategy in this new task:\n> > whenever a geometric repack would merge together all packs, we instead\n> > do an all-into-one repack. By default, these all-into-one repacks have\n> > cruft packs enabled, so unreachable objects would now be written into\n> > their own pack. Consequently, they won't be soaked up during geometric\n> > repacking anymore and can be expired with the next full repack, assuming\n> > that their expiry date has surpassed.\n> \n> So normal geometric repacks don't ever check for unreachable objects,\n> even if all the packs are being merged together. With this new strategy\n> though, when a geometric repack would normally merge together all packs,\n> we instead to an all-into-one repack which does check for unreachable\n> objects.\n> \n> Does checking for unreachable objects in this case slow down the repack\n> significantly?\n\nIt'll certainly add some overhead, but I didn't quantify it. My gut\nfeeling is that the all-into-one repack is going to be slow by nature\nanyway, as we have to rewrite all objects. Doing the reachability check\non top is of course going to slow it down even further, but the relative\nimpact is going to be smaller.\n\nIn any case, we have to perform a reachability check at one point in\ntime, otherwise we won't ever be able to prune unreachable objects. I\nguess doing this at the point where we merge all packs into one is a\nreasonable tradeoff.\n\nI think the more interesting question is whether we should maybe do this\nall-into-one repack more often, so that we can prune more regularly.\nWith the proposed strategy you'd need to add a significant portion of\nnew objects before we'd ever prune them, because otherwise we won't do\nthe all-into-one repack.\n\nI think for an initial version this is going to be fine, but we might\nwant to iterarate on this eventually and add a time-based component to\nthe heuristics.\n\nPatrick\n"},{"id":"529101","messageId":"aPKuA37Y0Yzl4bpn@nand.local","threadId":"64330","inReplyTo":"20251016-pks-maintenance-geometric-strategy-v1-1-18943d474203@pks.im","subject":"Re: [PATCH 1/8] builtin/gc: remove global `repack` variable","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2025-10-17T20:58:43Z","receivedAt":"2025-10-17T20:58:45Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Thu, Oct 16, 2025 at 09:26:32AM +0200, Patrick Steinhardt wrote:\n> @@ -1269,6 +1272,19 @@ static int maintenance_task_gc_background(struct maintenance_run_opts *opts,\n>  \treturn run_command(&child);\n>  }\n>\n> +static int gc_condition(struct gc_config *cfg)\n> +{\n> +\t/*\n> +\t * Note that it's fine to drop the repack arguments here, as we execute\n> +\t * git-gc(1) as a separate child process anyway. So it knows to compute\n> +\t * these arguments again.\n> +\t */\n> +\tstruct strvec repack_args = STRVEC_INIT;\n> +\tint ret = need_to_gc(cfg, &repack_args);\n> +\tstrvec_clear(&repack_args);\n> +\treturn ret;\n> +}\n> +\n\nThanks for calling this one out in the patch message. I think had I not\nread that I would have been confused why we were putting contents into\nthe strvec here just to throw it away, but the explanation you wrote\nabove makes it clear :-).\n\nThe rest of the patch looks great to me.\n\nThanks,\nTaylor\n"},{"id":"529105","messageId":"aPLC9FIDR8Y3ayn+@nand.local","threadId":"64330","inReplyTo":"20251016-pks-maintenance-geometric-strategy-v1-3-18943d474203@pks.im","subject":"Re: [PATCH 3/8] builtin/maintenance: introduce \"geometric-repack\" task","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2025-10-17T22:28:04Z","receivedAt":"2025-10-17T22:28:08Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Thu, Oct 16, 2025 at 09:26:34AM +0200, Patrick Steinhardt wrote:\n> Introduce a new \"geometric-repack\" task. This task uses our geometric\n> repack infrastructure as provided by git-repack(1) itself, which is ab\n> strategy that especially hosting providers tend to use to amortize the\n> costs of repacking objects.\n>\n> There is one issue though with geometric repacks, namely that they\n> unconditionally pack all loose objects, regardless of whether or not\n> they are reachable. This is done because it means that we can completely\n> skip the reachability step, which significantly speeds up the operation.\n> But it has the big downside that we are unable to expire objects over\n> time.\n>\n> To address this issue we thus use a split strategy in this new task:\n> whenever a geometric repack would merge together all packs, we instead\n> do an all-into-one repack. By default, these all-into-one repacks have\n> cruft packs enabled, so unreachable objects would now be written into\n> their own pack. Consequently, they won't be soaked up during geometric\n> repacking anymore and can be expired with the next full repack, assuming\n> that their expiry date has surpassed.\n\nWell put. Geometric repacking today is really only what objects appear\nin the packfiles, not whether those objects are reachable or not. That's\npartially by design: geometric repack operations are meant to be as\nlightweight and quick as possible, so performing a potentially expensive\nreachability traversal defeats the purpose.\n\nThis mirrors what GitHub does today with their own repository\nmaintenance implementation. There is some number of geometric repack\noperations interspersed between full repacks which collapse the\ngeometric progression and move unreachable objects out into cruft packs.\n\nSo I think that what you did here makes a ton of sense to me. Ultimately\nI think there is a middle ground for geometric repacking (well outside\nof the scope for this series ;-), don't worry) that could make it do a\nlittle bit of both.\n\nIf 'git pack-objects --stdin-packs' (what ultimately implements the\nportion of geometric repacking that combines packs together) knew the\ncurrent state of a repository's references, it could mark the objects in\nthe packs to be combined as either reachable or unreachable. Then in a\nspecialized mode, you could exclude any objects which are unreachable\nfrom the resulting pack, and take a separate pass to write out a cruft\npack containing those objects before ultimately deleting the combined\npacks.\n\nI think that is all possible to do, and I think there is a way we could\ndo it relatively quickly without harming the performance of geometric\nrepacking. When traversing and marking objects, we can stop as soon as\nwe see an object that is not contained in the packs that that we're\ncombining.\n\nSo I don't think we have to do a whole-repository walk, which would\nindeed defeat the purpose of geometric repacking. The above procedure\nwould cause us to write out small cruft packs, but we could use the\n--combine-cruft-below-size option of 'git repack' to prevent too many\nsmall cruft packs from accumulating together.\n\nAnyway, nothing of that has anything to do with what you wrote here ;-).\nIt was mostly an excuse for me to write down some of these thoughts that\nI've had in my head and alluded to briefly a couple of weeks ago at Git\nMerge. Expect some actual patches in this direction from me in the not\ntoo distant future :-).\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  Documentation/config/maintenance.adoc |  11 +++\n>  builtin/gc.c                          | 102 +++++++++++++++++++++++++\n>  t/t7900-maintenance.sh                | 137 ++++++++++++++++++++++++++++++++++\n>  3 files changed, 250 insertions(+)\n>\n> diff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\n> index 2f719342183..26dc5de423f 100644\n> --- a/Documentation/config/maintenance.adoc\n> +++ b/Documentation/config/maintenance.adoc\n> @@ -75,6 +75,17 @@ maintenance.incremental-repack.auto::\n>  \tnumber of pack-files not in the multi-pack-index is at least the value\n>  \tof `maintenance.incremental-repack.auto`. The default value is 10.\n>\n> +maintenance.geometric-repack.auto::\n> +\tThis integer config option controls how often the `geometric-repack`\n> +\ttask should be run as part of `git maintenance run --auto`. If zero,\n> +\tthen the `geometric-repack` task will not run with the `--auto`\n> +\toption. A negative value will force the task to run every time.\n> +\tOtherwise, a positive value implies the command should run either when\n> +\tthere are packfiles that need to be merged together to retain the\n> +\tgeometric progression, or when there are at least this many loose\n> +\tobjects that would be written into a new packfile. The default value is\n> +\t100.\n> +\n\nOK. To make sure I understand: this limit is the minimum number of loose\nobjects would cause the geometric-repack task to run, unless there are\npack(s) which would be combined as a result of running a geometric\nrepack, in which case we run it regardless.\n\nIs that right?\n\n>  maintenance.reflog-expire.auto::\n>  \tThis integer config option controls how often the `reflog-expire` task\n>  \tshould be run as part of `git maintenance run --auto`. If zero, then\n> diff --git a/builtin/gc.c b/builtin/gc.c\n> index 026d3a1d714..2c9ecd464d2 100644\n> --- a/builtin/gc.c\n> +++ b/builtin/gc.c\n> @@ -34,6 +34,7 @@\n>  #include \"pack-objects.h\"\n>  #include \"path.h\"\n>  #include \"reflog.h\"\n> +#include \"repack.h\"\n\nHey, neat ;-).\n\n> @@ -1566,6 +1568,101 @@ static int maintenance_task_incremental_repack(struct maintenance_run_opts *opts\n>  \treturn 0;\n>  }\n>\n> +static int maintenance_task_geometric_repack(struct maintenance_run_opts *opts,\n> +\t\t\t\t\t     struct gc_config *cfg)\n> +{\n> +\tstruct pack_geometry geometry = {\n> +\t\t.split_factor = 2,\n\nI wonder if this should be configurable somewhere. It might not be a bad\nidea to introduce a 'repack.geometricSplitFactor' configuration\nvariable, defaulting to two, and use that here. It would also be nice to\nbe able to run 'git repack --geometric -d' and have it fallback to that\nsplit factor, since using \"2\" is so common that it's frustrating when I\nforget to type it out explicitly ;-).\n\n> +\t};\n> +\tstruct pack_objects_args po_args = {\n> +\t\t.local = 1,\n> +\t};\n> +\tstruct existing_packs existing_packs = EXISTING_PACKS_INIT;\n> +\tstruct string_list kept_packs = STRING_LIST_INIT_DUP;\n> +\tstruct child_process child = CHILD_PROCESS_INIT;\n> +\tint ret;\n> +\n> +\texisting_packs.repo = the_repository;\n> +\texisting_packs_collect(&existing_packs, &kept_packs);\n> +\tpack_geometry_init(&geometry, &existing_packs, &po_args);\n> +\tpack_geometry_split(&geometry);\n> +\n> +\tchild.git_cmd = 1;\n> +\n> +\tstrvec_pushl(&child.args, \"repack\", \"-d\", \"-l\", NULL);\n> +\tif (geometry.split < geometry.pack_nr)\n> +\t\tstrvec_push(&child.args, \"--geometric=2\");\n> +\telse\n> +\t\tadd_repack_all_option(cfg, NULL, &child.args);\n\nMakes sense; if we're not merging any packs, we do an all-into-one\nrepack, otherwise we do a geometric one. Looks like the function\ngeometric_repack_auto_condition() below controls whether or not we even\ntake this path, which makes sense relative to the documentation you\nwrote above.\n\n> +static int geometric_repack_auto_condition(struct gc_config *cfg UNUSED)\n> +{\n> +\tstruct pack_geometry geometry = {\n> +\t\t.split_factor = 2,\n> +\t};\n> +\tstruct pack_objects_args po_args = {\n> +\t\t.local = 1,\n> +\t};\n> +\tstruct existing_packs existing_packs = EXISTING_PACKS_INIT;\n> +\tstruct string_list kept_packs = STRING_LIST_INIT_DUP;\n> +\tint auto_value = 100;\n> +\tint ret;\n> +\n> +\trepo_config_get_int(the_repository, \"maintenance.geometric-repack.auto\",\n> +\t\t\t    &auto_value);\n> +\tif (!auto_value)\n> +\t\treturn 0;\n> +\tif (auto_value < 0)\n> +\t\treturn 1;\n> +\n> +\texisting_packs.repo = the_repository;\n> +\texisting_packs_collect(&existing_packs, &kept_packs);\n> +\tpack_geometry_init(&geometry, &existing_packs, &po_args);\n> +\tpack_geometry_split(&geometry);\n> +\n> +\t/*\n> +\t * When we'd merge at least two packs with one another we always\n> +\t * perform the repack.\n> +\t */\n> +\tif (geometry.split) {\n> +\t\tret = 1;\n> +\t\tgoto out;\n> +\t}\n\nHmm. I wish that we could somehow pass this information to the function\nabove so that we don't have to re-discover the fact that there are packs\nto combine. I'm not familiar enough with the maintenance code to know\nhow difficult that would be to do, but it looks like at least the\ngc_config pointer is shared between the auto condition and the task\nitself.\n\nThat's kind of gross to tack on there, but I could see a compelling\nargument for passing around an extra void pointer between the two that\nwould allow us to propagate this kind of data between the auto condition\nand the task itself. It's not super expensive to do so I don't think not\ndoing it is a show-stopper at least from a performance perspective, but\nit does seem like a good opportunity to DRY things up a bit.\n\n> diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\n> index ddd273d8dc2..83a373fe94b 100755\n> --- a/t/t7900-maintenance.sh\n> +++ b/t/t7900-maintenance.sh\n> @@ -465,6 +465,143 @@ test_expect_success 'maintenance.incremental-repack.auto (when config is unset)'\n>  \t)\n>  '\n>\n> +run_and_verify_geometric_pack () {\n> +\tEXPECTED_PACKS=\"$1\" &&\n> +\n> +\t# Verify that we perform a geometric repack.\n> +\trm -f \"trace2.txt\" &&\n> +\tGIT_TRACE2_EVENT=\"$(pwd)/trace2.txt\" \\\n> +\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n> +\ttest_subcommand git repack -d -l --geometric=2 --quiet --write-midx <trace2.txt &&\n\nMakes sense. I do think the test_subcommand thing is a little fragile\nhere, but verifying that the resulting pack structure forms a geometric\nprogression feels like overkill for this test, so I think what you wrote\nhere makes sense.\n\nAs an aside, would you mind wrapping these lines instead of putting the\ncommand-line invocation all together on a single line?\n\n> +\n> +\t# Verify that the number of packfiles matches our expectation.\n> +\tls -l .git/objects/pack/*.pack >packfiles &&\n> +\ttest_line_count = \"$EXPECTED_PACKS\" packfiles &&\n> +\n> +\t# And verify that there are no loose objects anymore.\n> +\tcat >expect <<-\\EOF &&\n> +\tinfo\n> +\tpack\n> +\tEOF\n> +\tls .git/objects >actual &&\n\nI wonder if there is an easier way to check for loose objects here that\ndoesn't require you to know that the \"info\" and \"pack\" directories\nexist. Perhaps something like:\n\ntest_stdout_line_count = 0 find .git/objects/?? -type f\n\n, or even\n\n    find .git/objects/?? -type f >loose.objs &&\n    test_must_be_empty loose.objs\n\n> +test_expect_success 'geometric repacking task' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tgit init repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\tgit config set maintenance.auto false &&\n> +\t\ttest_commit initial &&\n> +\n> +\t\t# The initial repack causes an all-into-one repack.\n> +\t\tGIT_TRACE2_EVENT=\"$(pwd)/initial-repack.txt\" \\\n> +\t\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n> +\t\ttest_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago --quiet --write-midx <initial-repack.txt &&\n> +\n> +\t\t# Repacking should now cause a no-op geometric repack because\n> +\t\t# no packfiles need to be combined.\n> +\t\tls -l .git/objects/pack >before &&\n> +\t\trun_and_verify_geometric_pack 1 &&\n> +\t\tls -l .git/objects/pack >after &&\n> +\t\ttest_cmp before after &&\n> +\n> +\t\t# This incremental change creates a new packfile that only\n> +\t\t# soaks up loose objects. The packfiles are not getting merged\n> +\t\t# at this point.\n> +\t\ttest_commit loose &&\n> +\t\trun_and_verify_geometric_pack 2 &&\n\nI wonder if you want to harden this test a little bit to ensure that the\nthere is only one new pack being created here, and we're not seeing\ne.g., the removal of the existing pack and creation of two new packs.\n\nI dunno, that may be overkill for this test, and I certainly don't feel\nstrongly about it.\n\n> +\n> +\t\t# Both packfiles have 3 objects, so the next run would cause us\n> +\t\t# to merge both packfiles together. This should be turned into\n\nPerhaps s/both/all/ ? What you wrote is not wrong, of course, but I\nthink \"all\" more clearly communicates that we are only doing an\nall-into-one because the geometric repack would have combined everything\ntogether anyway.\n\nThe rest of the changes look good to me.\n\nThanks,\nTaylor\n"},{"id":"529265","messageId":"aPeD75W4FjtTZDYr@pks.im","threadId":"64330","inReplyTo":"aPLC9FIDR8Y3ayn+@nand.local","subject":"Re: [PATCH 3/8] builtin/maintenance: introduce \"geometric-repack\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T13:00:31Z","receivedAt":"2025-10-21T13:00:42Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Oct 17, 2025 at 06:28:04PM -0400, Taylor Blau wrote:\n> On Thu, Oct 16, 2025 at 09:26:34AM +0200, Patrick Steinhardt wrote:\n> > Introduce a new \"geometric-repack\" task. This task uses our geometric\n> > repack infrastructure as provided by git-repack(1) itself, which is ab\n> > strategy that especially hosting providers tend to use to amortize the\n> > costs of repacking objects.\n> >\n> > There is one issue though with geometric repacks, namely that they\n> > unconditionally pack all loose objects, regardless of whether or not\n> > they are reachable. This is done because it means that we can completely\n> > skip the reachability step, which significantly speeds up the operation.\n> > But it has the big downside that we are unable to expire objects over\n> > time.\n> >\n> > To address this issue we thus use a split strategy in this new task:\n> > whenever a geometric repack would merge together all packs, we instead\n> > do an all-into-one repack. By default, these all-into-one repacks have\n> > cruft packs enabled, so unreachable objects would now be written into\n> > their own pack. Consequently, they won't be soaked up during geometric\n> > repacking anymore and can be expired with the next full repack, assuming\n> > that their expiry date has surpassed.\n> \n> Well put. Geometric repacking today is really only what objects appear\n> in the packfiles, not whether those objects are reachable or not. That's\n> partially by design: geometric repack operations are meant to be as\n> lightweight and quick as possible, so performing a potentially expensive\n> reachability traversal defeats the purpose.\n> \n> This mirrors what GitHub does today with their own repository\n> maintenance implementation. There is some number of geometric repack\n> operations interspersed between full repacks which collapse the\n> geometric progression and move unreachable objects out into cruft packs.\n\nYeah, we also do the same at GitLab.\n\n> So I think that what you did here makes a ton of sense to me. Ultimately\n> I think there is a middle ground for geometric repacking (well outside\n> of the scope for this series ;-), don't worry) that could make it do a\n> little bit of both.\n> \n> If 'git pack-objects --stdin-packs' (what ultimately implements the\n> portion of geometric repacking that combines packs together) knew the\n> current state of a repository's references, it could mark the objects in\n> the packs to be combined as either reachable or unreachable. Then in a\n> specialized mode, you could exclude any objects which are unreachable\n> from the resulting pack, and take a separate pass to write out a cruft\n> pack containing those objects before ultimately deleting the combined\n> packs.\n> \n> I think that is all possible to do, and I think there is a way we could\n> do it relatively quickly without harming the performance of geometric\n> repacking. When traversing and marking objects, we can stop as soon as\n> we see an object that is not contained in the packs that that we're\n> combining.\n> \n> So I don't think we have to do a whole-repository walk, which would\n> indeed defeat the purpose of geometric repacking. The above procedure\n> would cause us to write out small cruft packs, but we could use the\n> --combine-cruft-below-size option of 'git repack' to prevent too many\n> small cruft packs from accumulating together.\n> \n> Anyway, nothing of that has anything to do with what you wrote here ;-).\n> It was mostly an excuse for me to write down some of these thoughts that\n> I've had in my head and alluded to briefly a couple of weeks ago at Git\n> Merge. Expect some actual patches in this direction from me in the not\n> too distant future :-).\n\nLooking forward to them :)\n\n> > Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> > ---\n> >  Documentation/config/maintenance.adoc |  11 +++\n> >  builtin/gc.c                          | 102 +++++++++++++++++++++++++\n> >  t/t7900-maintenance.sh                | 137 ++++++++++++++++++++++++++++++++++\n> >  3 files changed, 250 insertions(+)\n> >\n> > diff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\n> > index 2f719342183..26dc5de423f 100644\n> > --- a/Documentation/config/maintenance.adoc\n> > +++ b/Documentation/config/maintenance.adoc\n> > @@ -75,6 +75,17 @@ maintenance.incremental-repack.auto::\n> >  \tnumber of pack-files not in the multi-pack-index is at least the value\n> >  \tof `maintenance.incremental-repack.auto`. The default value is 10.\n> >\n> > +maintenance.geometric-repack.auto::\n> > +\tThis integer config option controls how often the `geometric-repack`\n> > +\ttask should be run as part of `git maintenance run --auto`. If zero,\n> > +\tthen the `geometric-repack` task will not run with the `--auto`\n> > +\toption. A negative value will force the task to run every time.\n> > +\tOtherwise, a positive value implies the command should run either when\n> > +\tthere are packfiles that need to be merged together to retain the\n> > +\tgeometric progression, or when there are at least this many loose\n> > +\tobjects that would be written into a new packfile. The default value is\n> > +\t100.\n> > +\n> \n> OK. To make sure I understand: this limit is the minimum number of loose\n> objects would cause the geometric-repack task to run, unless there are\n> pack(s) which would be combined as a result of running a geometric\n> repack, in which case we run it regardless.\n> \n> Is that right?\n\nYeah, exactly. I was initially thinking to only frame this in the\ncontext of \"how many packs would be merged\"? But the problem with that\nis that we wouldn't ever generate _new_ packs if the repository only\never grows loose objects, and consequently we also wouldn't ever merge\nany of them.\n\n> >  maintenance.reflog-expire.auto::\n> >  \tThis integer config option controls how often the `reflog-expire` task\n> >  \tshould be run as part of `git maintenance run --auto`. If zero, then\n> > diff --git a/builtin/gc.c b/builtin/gc.c\n> > index 026d3a1d714..2c9ecd464d2 100644\n> > --- a/builtin/gc.c\n> > +++ b/builtin/gc.c\n> > @@ -34,6 +34,7 @@\n> >  #include \"pack-objects.h\"\n> >  #include \"path.h\"\n> >  #include \"reflog.h\"\n> > +#include \"repack.h\"\n> \n> Hey, neat ;-).\n\nYup, your refactorings helped a bunch :)\n\n> > @@ -1566,6 +1568,101 @@ static int maintenance_task_incremental_repack(struct maintenance_run_opts *opts\n> >  \treturn 0;\n> >  }\n> >\n> > +static int maintenance_task_geometric_repack(struct maintenance_run_opts *opts,\n> > +\t\t\t\t\t     struct gc_config *cfg)\n> > +{\n> > +\tstruct pack_geometry geometry = {\n> > +\t\t.split_factor = 2,\n> \n> I wonder if this should be configurable somewhere. It might not be a bad\n> idea to introduce a 'repack.geometricSplitFactor' configuration\n> variable, defaulting to two, and use that here. It would also be nice to\n> be able to run 'git repack --geometric -d' and have it fallback to that\n> split factor, since using \"2\" is so common that it's frustrating when I\n> forget to type it out explicitly ;-).\n\nI was also pondering over this. I think the way to do so would be to\nintroduce \"maintenance.geometric-repack.splitFactor\", as that follows\nall the other maintenance configuration we have there, as well.\n\nI decided to not do it yet as I wanted to keep the scope of this patch\nseries contained. But honestly, it's an easy-enough change to make, so\nlet me introduce another patch to do this.\n\n> > +\t};\n> > +\tstruct pack_objects_args po_args = {\n> > +\t\t.local = 1,\n> > +\t};\n> > +\tstruct existing_packs existing_packs = EXISTING_PACKS_INIT;\n> > +\tstruct string_list kept_packs = STRING_LIST_INIT_DUP;\n> > +\tstruct child_process child = CHILD_PROCESS_INIT;\n> > +\tint ret;\n> > +\n> > +\texisting_packs.repo = the_repository;\n> > +\texisting_packs_collect(&existing_packs, &kept_packs);\n> > +\tpack_geometry_init(&geometry, &existing_packs, &po_args);\n> > +\tpack_geometry_split(&geometry);\n> > +\n> > +\tchild.git_cmd = 1;\n> > +\n> > +\tstrvec_pushl(&child.args, \"repack\", \"-d\", \"-l\", NULL);\n> > +\tif (geometry.split < geometry.pack_nr)\n> > +\t\tstrvec_push(&child.args, \"--geometric=2\");\n> > +\telse\n> > +\t\tadd_repack_all_option(cfg, NULL, &child.args);\n> \n> Makes sense; if we're not merging any packs, we do an all-into-one\n> repack, otherwise we do a geometric one. Looks like the function\n> geometric_repack_auto_condition() below controls whether or not we even\n> take this path, which makes sense relative to the documentation you\n> wrote above.\n\nIt does, but only in case we do `git maintenance run --auto`. If you run\n`git maintenance run` without the flag we unconditionally execute this\ncode here. But that's fine: if the repository is already well-optimized\nwe don't end up doing anything.\n\n> > +static int geometric_repack_auto_condition(struct gc_config *cfg UNUSED)\n> > +{\n> > +\tstruct pack_geometry geometry = {\n> > +\t\t.split_factor = 2,\n> > +\t};\n> > +\tstruct pack_objects_args po_args = {\n> > +\t\t.local = 1,\n> > +\t};\n> > +\tstruct existing_packs existing_packs = EXISTING_PACKS_INIT;\n> > +\tstruct string_list kept_packs = STRING_LIST_INIT_DUP;\n> > +\tint auto_value = 100;\n> > +\tint ret;\n> > +\n> > +\trepo_config_get_int(the_repository, \"maintenance.geometric-repack.auto\",\n> > +\t\t\t    &auto_value);\n> > +\tif (!auto_value)\n> > +\t\treturn 0;\n> > +\tif (auto_value < 0)\n> > +\t\treturn 1;\n> > +\n> > +\texisting_packs.repo = the_repository;\n> > +\texisting_packs_collect(&existing_packs, &kept_packs);\n> > +\tpack_geometry_init(&geometry, &existing_packs, &po_args);\n> > +\tpack_geometry_split(&geometry);\n> > +\n> > +\t/*\n> > +\t * When we'd merge at least two packs with one another we always\n> > +\t * perform the repack.\n> > +\t */\n> > +\tif (geometry.split) {\n> > +\t\tret = 1;\n> > +\t\tgoto out;\n> > +\t}\n> \n> Hmm. I wish that we could somehow pass this information to the function\n> above so that we don't have to re-discover the fact that there are packs\n> to combine. I'm not familiar enough with the maintenance code to know\n> how difficult that would be to do, but it looks like at least the\n> gc_config pointer is shared between the auto condition and the task\n> itself.\n> \n> That's kind of gross to tack on there, but I could see a compelling\n> argument for passing around an extra void pointer between the two that\n> would allow us to propagate this kind of data between the auto condition\n> and the task itself. It's not super expensive to do so I don't think not\n> doing it is a show-stopper at least from a performance perspective, but\n> it does seem like a good opportunity to DRY things up a bit.\n\nThe problem is that the auto-condition is not evaluated when running\nwithout the \"--auto\" flag. We of course can conditionally compute the\nsplit in case we figure that the auto-condition didn't run, but it does\nget somewhat dirty.\n\nSo I'd propose to defer such a change into the future in case we notice\nthat this indeed is a problem. Is that fine with you?\n\n> > diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\n> > index ddd273d8dc2..83a373fe94b 100755\n> > --- a/t/t7900-maintenance.sh\n> > +++ b/t/t7900-maintenance.sh\n> > @@ -465,6 +465,143 @@ test_expect_success 'maintenance.incremental-repack.auto (when config is unset)'\n> >  \t)\n> >  '\n> >\n> > +run_and_verify_geometric_pack () {\n> > +\tEXPECTED_PACKS=\"$1\" &&\n> > +\n> > +\t# Verify that we perform a geometric repack.\n> > +\trm -f \"trace2.txt\" &&\n> > +\tGIT_TRACE2_EVENT=\"$(pwd)/trace2.txt\" \\\n> > +\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n> > +\ttest_subcommand git repack -d -l --geometric=2 --quiet --write-midx <trace2.txt &&\n> \n> Makes sense. I do think the test_subcommand thing is a little fragile\n> here, but verifying that the resulting pack structure forms a geometric\n> progression feels like overkill for this test, so I think what you wrote\n> here makes sense.\n\nOh, yeah, it's fragile and somewhat gross indeed. Couldn't really find a\nnicer way to do it though :/\n\n> As an aside, would you mind wrapping these lines instead of putting the\n> command-line invocation all together on a single line?\n\nSure, can do.\n\n> > +\n> > +\t# Verify that the number of packfiles matches our expectation.\n> > +\tls -l .git/objects/pack/*.pack >packfiles &&\n> > +\ttest_line_count = \"$EXPECTED_PACKS\" packfiles &&\n> > +\n> > +\t# And verify that there are no loose objects anymore.\n> > +\tcat >expect <<-\\EOF &&\n> > +\tinfo\n> > +\tpack\n> > +\tEOF\n> > +\tls .git/objects >actual &&\n> \n> I wonder if there is an easier way to check for loose objects here that\n> doesn't require you to know that the \"info\" and \"pack\" directories\n> exist. Perhaps something like:\n> \n> test_stdout_line_count = 0 find .git/objects/?? -type f\n> \n> , or even\n> \n>     find .git/objects/?? -type f >loose.objs &&\n>     test_must_be_empty loose.objs\n\nThis doesn't work though in case there is not even a single sharding\ndirectory:\n\n    find: '.git/objects/??': No such file or directory\n\nI didn't really have any other idea for now to do this.\n\n> > +test_expect_success 'geometric repacking task' '\n> > +\ttest_when_finished \"rm -rf repo\" &&\n> > +\tgit init repo &&\n> > +\t(\n> > +\t\tcd repo &&\n> > +\t\tgit config set maintenance.auto false &&\n> > +\t\ttest_commit initial &&\n> > +\n> > +\t\t# The initial repack causes an all-into-one repack.\n> > +\t\tGIT_TRACE2_EVENT=\"$(pwd)/initial-repack.txt\" \\\n> > +\t\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n> > +\t\ttest_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago --quiet --write-midx <initial-repack.txt &&\n> > +\n> > +\t\t# Repacking should now cause a no-op geometric repack because\n> > +\t\t# no packfiles need to be combined.\n> > +\t\tls -l .git/objects/pack >before &&\n> > +\t\trun_and_verify_geometric_pack 1 &&\n> > +\t\tls -l .git/objects/pack >after &&\n> > +\t\ttest_cmp before after &&\n> > +\n> > +\t\t# This incremental change creates a new packfile that only\n> > +\t\t# soaks up loose objects. The packfiles are not getting merged\n> > +\t\t# at this point.\n> > +\t\ttest_commit loose &&\n> > +\t\trun_and_verify_geometric_pack 2 &&\n> \n> I wonder if you want to harden this test a little bit to ensure that the\n> there is only one new pack being created here, and we're not seeing\n> e.g., the removal of the existing pack and creation of two new packs.\n> \n> I dunno, that may be overkill for this test, and I certainly don't feel\n> strongly about it.\n\nYeah, I had the same thought when writing this, but it quickly got ugly.\nI then decided to not do this and instead only verify that the structure\nloosely looks like we expect, and that the expected command actually\nran.\n\nIn the end we can rely on t7703 to verify the internals of how exactly\nthe geometric repack works, whereas here we only verify that the\nstrategy works as expected.\n\n> > +\n> > +\t\t# Both packfiles have 3 objects, so the next run would cause us\n> > +\t\t# to merge both packfiles together. This should be turned into\n> \n> Perhaps s/both/all/ ? What you wrote is not wrong, of course, but I\n> think \"all\" more clearly communicates that we are only doing an\n> all-into-one because the geometric repack would have combined everything\n> together anyway.\n\nSure, happy to reword.\n\nThanks for your review!\n\nPatrick\n"},{"id":"529271","messageId":"20251021-pks-maintenance-geometric-strategy-v2-0-f0d727832b80@pks.im","threadId":"64330","inReplyTo":"20251016-pks-maintenance-geometric-strategy-v1-0-18943d474203@pks.im","subject":"[PATCH v2 0/9] builtin/maintenance: introduce \"geometric\" strategy","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T14:13:22Z","receivedAt":"2025-10-21T14:13:30Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nby default, git-maintenance(1) uses git-gc(1) to perform repository\nhousekeeping. This tool has a couple of shortcomings, most importantly\nthat it regularly does all-into-one repacks. This doesn't really work\nall that well in the context of monorepos, where you really want to\navoid repacking all objects regularly.\n\nAn alternative maintenance strategy is the \"incremental\" strategy, but\nthis strategy has two downsides:\n\n  - Strategies in general only apply to scheduled maintenance. So if you\n    run git-maintenance(1), you still end up with git-gc(1).\n\n  - The strategy is designed to not ever delete any data, but a full\n    replacment for git-gc(1) needs to also prune reflogs, rereree caches\n    and vanished worktrees.\n\nThis patch series aims to fix both of these issues.\n\nFirst, the series introduces a new \"geometric\" maintenance task, which\nmakes use of geometric repacking as exposed by git-repack(1) in the\ngeneral case. In the case where a geometric repack ends up merging all\npackfiles into one we instead do an all-into-one repack with cruft packs\nso that we can still phase out objects over time.\n\nSecond, the series extends maintenance strategies to also cover normal\nmaintenance. If the user has configured the \"geometric\" strategy, we'll\nthus use it for both manual and scheduled maintenance. For backwards\ncompatibility, the \"incremental\" strategy is changed so that it uses\ngit-gc(1) for manual maintenance and the other tasks for scheduled\nmaintenance.\n\nThe series is built on top of b660e2dcb9 (Sync with 'maint', 2025-10-14)\nwith tb/incremental-midx-part-3.1 at c886af90f8 (SQUASH??? play well\nwith other topics by preemptively including \"repository.h\", 2025-09-29)\nmerged into it.\n\nChanges in v2:\n  - Make the geometric factor configurable via\n    \"maintenance.geometric-repack.splitFactor\".\n  - Wrap some overly long lines in our tests.\n  - Link to v1: https://lore.kernel.org/r/20251016-pks-maintenance-geometric-strategy-v1-0-18943d474203@pks.im\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (9):\n      builtin/gc: remove global `repack` variable\n      builtin/gc: make `too_many_loose_objects()` reusable without GC config\n      builtin/maintenance: introduce \"geometric-repack\" task\n      builtin/maintenance: make the geometric factor configurable\n      builtin/maintenance: don't silently ignore invalid strategy\n      builtin/maintenance: run maintenance tasks depending on type\n      builtin/maintenance: extend \"maintenance.strategy\" to manual maintenance\n      builtin/maintenance: make \"gc\" strategy accessible\n      builtin/maintenance: introduce \"geometric\" strategy\n\n Documentation/config/maintenance.adoc |  49 +++++-\n builtin/gc.c                          | 278 ++++++++++++++++++++++++++++------\n t/t7900-maintenance.sh                | 246 ++++++++++++++++++++++++++++++\n 3 files changed, 515 insertions(+), 58 deletions(-)\n\nRange-diff versus v1:\n\n 1:  d16d9ac4f01 =  1:  f14cf90529d builtin/gc: remove global `repack` variable\n 2:  2beb7edfdc1 =  2:  64fde2d3fb0 builtin/gc: make `too_many_loose_objects()` reusable without GC config\n 3:  e4bcc347e76 !  3:  9ba24540238 builtin/maintenance: introduce \"geometric-repack\" task\n    @@ t/t7900-maintenance.sh: test_expect_success 'maintenance.incremental-repack.auto\n     +\trm -f \"trace2.txt\" &&\n     +\tGIT_TRACE2_EVENT=\"$(pwd)/trace2.txt\" \\\n     +\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n    -+\ttest_subcommand git repack -d -l --geometric=2 --quiet --write-midx <trace2.txt &&\n    ++\ttest_subcommand git repack -d -l --geometric=2 \\\n    ++\t\t--quiet --write-midx <trace2.txt &&\n     +\n     +\t# Verify that the number of packfiles matches our expectation.\n     +\tls -l .git/objects/pack/*.pack >packfiles &&\n    @@ t/t7900-maintenance.sh: test_expect_success 'maintenance.incremental-repack.auto\n     +\t\trun_and_verify_geometric_pack 2 &&\n     +\n     +\t\t# Both packfiles have 3 objects, so the next run would cause us\n    -+\t\t# to merge both packfiles together. This should be turned into\n    ++\t\t# to merge all packfiles together. This should be turned into\n     +\t\t# an all-into-one-repack.\n     +\t\tGIT_TRACE2_EVENT=\"$(pwd)/all-into-one-repack.txt\" \\\n     +\t\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n    @@ t/t7900-maintenance.sh: test_expect_success 'maintenance.incremental-repack.auto\n     +\n     +test_geometric_repack_needed () {\n     +\tNEEDED=\"$1\"\n    -+\tAUTO_LIMIT=\"$2\" &&\n    ++\tGEOMETRIC_CONFIG=\"$2\" &&\n     +\trm -f trace2.txt &&\n     +\tGIT_TRACE2_EVENT=\"$(pwd)/trace2.txt\" \\\n    -+\t\tgit ${AUTO_LIMIT:+-c maintenance.geometric-repack.auto=$AUTO_LIMIT} maintenance run --auto --task=geometric-repack &&\n    ++\t\tgit ${GEOMETRIC_CONFIG:+-c maintenance.geometric-repack.$GEOMETRIC_CONFIG} \\\n    ++\t\tmaintenance run --auto --task=geometric-repack 2>/dev/null &&\n     +\tcase \"$NEEDED\" in\n     +\ttrue)\n     +\t\ttest_grep \"\\[\\\"git\\\",\\\"repack\\\",\" trace2.txt;;\n    @@ t/t7900-maintenance.sh: test_expect_success 'maintenance.incremental-repack.auto\n     +\t\t# An empty repository does not need repacking, except when\n     +\t\t# explicitly told to do it.\n     +\t\ttest_geometric_repack_needed false &&\n    -+\t\ttest_geometric_repack_needed false 0 &&\n    -+\t\ttest_geometric_repack_needed false 1 &&\n    -+\t\ttest_geometric_repack_needed true -1 &&\n    ++\t\ttest_geometric_repack_needed false auto=0 &&\n    ++\t\ttest_geometric_repack_needed false auto=1 &&\n    ++\t\ttest_geometric_repack_needed true auto=-1 &&\n     +\n     +\t\ttest_oid_init &&\n     +\n    @@ t/t7900-maintenance.sh: test_expect_success 'maintenance.incremental-repack.auto\n     +\t\ttest_commit \"$(test_oid blob17_1)\" &&\n     +\t\ttest_geometric_repack_needed false &&\n     +\t\ttest_commit \"$(test_oid blob17_2)\" &&\n    -+\t\ttest_geometric_repack_needed false 257 &&\n    -+\t\ttest_geometric_repack_needed true 256 &&\n    ++\t\ttest_geometric_repack_needed false auto=257 &&\n    ++\t\ttest_geometric_repack_needed true auto=256 &&\n     +\n     +\t\t# Force another repack.\n     +\t\ttest_commit first &&\n     +\t\ttest_commit second &&\n    -+\t\ttest_geometric_repack_needed true -1 &&\n    ++\t\ttest_geometric_repack_needed true auto=-1 &&\n     +\n     +\t\t# We now have two packfiles that would be merged together. As\n     +\t\t# such, the repack should always happen unless the user has\n     +\t\t# disabled the auto task.\n    -+\t\ttest_geometric_repack_needed false 0 &&\n    -+\t\ttest_geometric_repack_needed true 9000\n    ++\t\ttest_geometric_repack_needed false auto=0 &&\n    ++\t\ttest_geometric_repack_needed true auto=9000\n     +\t)\n     +'\n     +\n -:  ----------- >  4:  d1b805004b9 builtin/maintenance: make the geometric factor configurable\n 4:  c9a6e576299 =  5:  d6fa70640c2 builtin/maintenance: don't silently ignore invalid strategy\n 5:  3c82a91f152 =  6:  37f7793dab9 builtin/maintenance: run maintenance tasks depending on type\n 6:  78502ad6868 =  7:  4b15eac845c builtin/maintenance: extend \"maintenance.strategy\" to manual maintenance\n 7:  59a5450c44f =  8:  eb75881b6ae builtin/maintenance: make \"gc\" strategy accessible\n 8:  51065b109fa =  9:  5c011e7a7e2 builtin/maintenance: introduce \"geometric\" strategy\n\n---\nbase-commit: 0bb2c786c2349dd6700727153c13d81cbfb41710\nchange-id: 20251015-pks-maintenance-geometric-strategy-580c58581b01\n\n"},{"id":"529272","messageId":"20251021-pks-maintenance-geometric-strategy-v2-1-f0d727832b80@pks.im","threadId":"64330","inReplyTo":"20251021-pks-maintenance-geometric-strategy-v2-0-f0d727832b80@pks.im","subject":"[PATCH v2 1/9] builtin/gc: remove global `repack` variable","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T14:13:23Z","receivedAt":"2025-10-21T14:13:31Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The global `repack` variable is used to store all command line arguments\nthat we eventually want to pass to git-repack(1). It is being appended\nto from multiple different functions, which makes it hard to follow the\nlogic. Besides being hard to follow, it also makes it unnecessarily hard\nto reuse this infrastructure in new code.\n\nRefactor the code so that we store this variable on the stack and pass\na pointer to it around as needed. This is done so that we can reuse\n`add_repack_all_options()` in a subsequent commit.\n\nThe refactoring itself is straight-forward. One function that deserves\nattention though is `need_to_gc()`: this function determines whether or\nnot we need to execute garbage collection for `git gc --auto`, but also\nfor `git maintenance run --auto`. But besides figuring out whether we\nhave to perform GC, the function also sets up the `repack` arguments.\n\nFor `git gc --auto` it's trivial to adapt, as we already have the\non-stack variable at our fingertips. But for the maintenance condition\nit's less obvious what to do.\n\nAs it turns out, we can just use another temporary variable there that\nwe then immediately discard. If we need to perform GC we execute a child\ngit-gc(1) process to repack objects for us, and that process will have\nto recompute the arguments anyway.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 74 ++++++++++++++++++++++++++++++++++++------------------------\n 1 file changed, 45 insertions(+), 29 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e19e13d9788..e9772eb3a30 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -55,7 +55,6 @@ static const char * const builtin_gc_usage[] = {\n };\n \n static timestamp_t gc_log_expire_time;\n-static struct strvec repack = STRVEC_INIT;\n static struct tempfile *pidfile;\n static struct lock_file log_lock;\n static struct string_list pack_garbage = STRING_LIST_INIT_DUP;\n@@ -618,48 +617,50 @@ static uint64_t estimate_repack_memory(struct gc_config *cfg,\n \treturn os_cache + heap;\n }\n \n-static int keep_one_pack(struct string_list_item *item, void *data UNUSED)\n+static int keep_one_pack(struct string_list_item *item, void *data)\n {\n-\tstrvec_pushf(&repack, \"--keep-pack=%s\", basename(item->string));\n+\tstruct strvec *args = data;\n+\tstrvec_pushf(args, \"--keep-pack=%s\", basename(item->string));\n \treturn 0;\n }\n \n static void add_repack_all_option(struct gc_config *cfg,\n-\t\t\t\t  struct string_list *keep_pack)\n+\t\t\t\t  struct string_list *keep_pack,\n+\t\t\t\t  struct strvec *args)\n {\n \tif (cfg->prune_expire && !strcmp(cfg->prune_expire, \"now\")\n \t\t&& !(cfg->cruft_packs && cfg->repack_expire_to))\n-\t\tstrvec_push(&repack, \"-a\");\n+\t\tstrvec_push(args, \"-a\");\n \telse if (cfg->cruft_packs) {\n-\t\tstrvec_push(&repack, \"--cruft\");\n+\t\tstrvec_push(args, \"--cruft\");\n \t\tif (cfg->prune_expire)\n-\t\t\tstrvec_pushf(&repack, \"--cruft-expiration=%s\", cfg->prune_expire);\n+\t\t\tstrvec_pushf(args, \"--cruft-expiration=%s\", cfg->prune_expire);\n \t\tif (cfg->max_cruft_size)\n-\t\t\tstrvec_pushf(&repack, \"--max-cruft-size=%lu\",\n+\t\t\tstrvec_pushf(args, \"--max-cruft-size=%lu\",\n \t\t\t\t     cfg->max_cruft_size);\n \t\tif (cfg->repack_expire_to)\n-\t\t\tstrvec_pushf(&repack, \"--expire-to=%s\", cfg->repack_expire_to);\n+\t\t\tstrvec_pushf(args, \"--expire-to=%s\", cfg->repack_expire_to);\n \t} else {\n-\t\tstrvec_push(&repack, \"-A\");\n+\t\tstrvec_push(args, \"-A\");\n \t\tif (cfg->prune_expire)\n-\t\t\tstrvec_pushf(&repack, \"--unpack-unreachable=%s\", cfg->prune_expire);\n+\t\t\tstrvec_pushf(args, \"--unpack-unreachable=%s\", cfg->prune_expire);\n \t}\n \n \tif (keep_pack)\n-\t\tfor_each_string_list(keep_pack, keep_one_pack, NULL);\n+\t\tfor_each_string_list(keep_pack, keep_one_pack, args);\n \n \tif (cfg->repack_filter && *cfg->repack_filter)\n-\t\tstrvec_pushf(&repack, \"--filter=%s\", cfg->repack_filter);\n+\t\tstrvec_pushf(args, \"--filter=%s\", cfg->repack_filter);\n \tif (cfg->repack_filter_to && *cfg->repack_filter_to)\n-\t\tstrvec_pushf(&repack, \"--filter-to=%s\", cfg->repack_filter_to);\n+\t\tstrvec_pushf(args, \"--filter-to=%s\", cfg->repack_filter_to);\n }\n \n-static void add_repack_incremental_option(void)\n+static void add_repack_incremental_option(struct strvec *args)\n {\n-\tstrvec_push(&repack, \"--no-write-bitmap-index\");\n+\tstrvec_push(args, \"--no-write-bitmap-index\");\n }\n \n-static int need_to_gc(struct gc_config *cfg)\n+static int need_to_gc(struct gc_config *cfg, struct strvec *repack_args)\n {\n \t/*\n \t * Setting gc.auto to 0 or negative can disable the\n@@ -700,10 +701,10 @@ static int need_to_gc(struct gc_config *cfg)\n \t\t\t\tstring_list_clear(&keep_pack, 0);\n \t\t}\n \n-\t\tadd_repack_all_option(cfg, &keep_pack);\n+\t\tadd_repack_all_option(cfg, &keep_pack, repack_args);\n \t\tstring_list_clear(&keep_pack, 0);\n \t} else if (too_many_loose_objects(cfg))\n-\t\tadd_repack_incremental_option();\n+\t\tadd_repack_incremental_option(repack_args);\n \telse\n \t\treturn 0;\n \n@@ -852,6 +853,7 @@ int cmd_gc(int argc,\n \tint keep_largest_pack = -1;\n \tint skip_foreground_tasks = 0;\n \ttimestamp_t dummy;\n+\tstruct strvec repack_args = STRVEC_INIT;\n \tstruct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT;\n \tstruct gc_config cfg = GC_CONFIG_INIT;\n \tconst char *prune_expire_sentinel = \"sentinel\";\n@@ -891,7 +893,7 @@ int cmd_gc(int argc,\n \tshow_usage_with_options_if_asked(argc, argv,\n \t\t\t\t\t builtin_gc_usage, builtin_gc_options);\n \n-\tstrvec_pushl(&repack, \"repack\", \"-d\", \"-l\", NULL);\n+\tstrvec_pushl(&repack_args, \"repack\", \"-d\", \"-l\", NULL);\n \n \tgc_config(&cfg);\n \n@@ -914,14 +916,14 @@ int cmd_gc(int argc,\n \t\tdie(_(\"failed to parse prune expiry value %s\"), cfg.prune_expire);\n \n \tif (aggressive) {\n-\t\tstrvec_push(&repack, \"-f\");\n+\t\tstrvec_push(&repack_args, \"-f\");\n \t\tif (cfg.aggressive_depth > 0)\n-\t\t\tstrvec_pushf(&repack, \"--depth=%d\", cfg.aggressive_depth);\n+\t\t\tstrvec_pushf(&repack_args, \"--depth=%d\", cfg.aggressive_depth);\n \t\tif (cfg.aggressive_window > 0)\n-\t\t\tstrvec_pushf(&repack, \"--window=%d\", cfg.aggressive_window);\n+\t\t\tstrvec_pushf(&repack_args, \"--window=%d\", cfg.aggressive_window);\n \t}\n \tif (opts.quiet)\n-\t\tstrvec_push(&repack, \"-q\");\n+\t\tstrvec_push(&repack_args, \"-q\");\n \n \tif (opts.auto_flag) {\n \t\tif (cfg.detach_auto && opts.detach < 0)\n@@ -930,7 +932,7 @@ int cmd_gc(int argc,\n \t\t/*\n \t\t * Auto-gc should be least intrusive as possible.\n \t\t */\n-\t\tif (!need_to_gc(&cfg)) {\n+\t\tif (!need_to_gc(&cfg, &repack_args)) {\n \t\t\tret = 0;\n \t\t\tgoto out;\n \t\t}\n@@ -952,7 +954,7 @@ int cmd_gc(int argc,\n \t\t\tfind_base_packs(&keep_pack, cfg.big_pack_threshold);\n \t\t}\n \n-\t\tadd_repack_all_option(&cfg, &keep_pack);\n+\t\tadd_repack_all_option(&cfg, &keep_pack, &repack_args);\n \t\tstring_list_clear(&keep_pack, 0);\n \t}\n \n@@ -1014,9 +1016,9 @@ int cmd_gc(int argc,\n \n \t\trepack_cmd.git_cmd = 1;\n \t\trepack_cmd.close_object_store = 1;\n-\t\tstrvec_pushv(&repack_cmd.args, repack.v);\n+\t\tstrvec_pushv(&repack_cmd.args, repack_args.v);\n \t\tif (run_command(&repack_cmd))\n-\t\t\tdie(FAILED_RUN, repack.v[0]);\n+\t\t\tdie(FAILED_RUN, repack_args.v[0]);\n \n \t\tif (cfg.prune_expire) {\n \t\t\tstruct child_process prune_cmd = CHILD_PROCESS_INIT;\n@@ -1067,6 +1069,7 @@ int cmd_gc(int argc,\n \n out:\n \tmaintenance_run_opts_release(&opts);\n+\tstrvec_clear(&repack_args);\n \tgc_config_release(&cfg);\n \treturn 0;\n }\n@@ -1269,6 +1272,19 @@ static int maintenance_task_gc_background(struct maintenance_run_opts *opts,\n \treturn run_command(&child);\n }\n \n+static int gc_condition(struct gc_config *cfg)\n+{\n+\t/*\n+\t * Note that it's fine to drop the repack arguments here, as we execute\n+\t * git-gc(1) as a separate child process anyway. So it knows to compute\n+\t * these arguments again.\n+\t */\n+\tstruct strvec repack_args = STRVEC_INIT;\n+\tint ret = need_to_gc(cfg, &repack_args);\n+\tstrvec_clear(&repack_args);\n+\treturn ret;\n+}\n+\n static int prune_packed(struct maintenance_run_opts *opts)\n {\n \tstruct child_process child = CHILD_PROCESS_INIT;\n@@ -1596,7 +1612,7 @@ static const struct maintenance_task tasks[] = {\n \t\t.name = \"gc\",\n \t\t.foreground = maintenance_task_gc_foreground,\n \t\t.background = maintenance_task_gc_background,\n-\t\t.auto_condition = need_to_gc,\n+\t\t.auto_condition = gc_condition,\n \t},\n \t[TASK_COMMIT_GRAPH] = {\n \t\t.name = \"commit-graph\",\n\n-- \n2.51.1.851.g4ebd6896fd.dirty\n\n"},{"id":"529273","messageId":"20251021-pks-maintenance-geometric-strategy-v2-2-f0d727832b80@pks.im","threadId":"64330","inReplyTo":"20251021-pks-maintenance-geometric-strategy-v2-0-f0d727832b80@pks.im","subject":"[PATCH v2 2/9] builtin/gc: make `too_many_loose_objects()` reusable without GC config","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T14:13:24Z","receivedAt":"2025-10-21T14:13:34Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"To decide whether or not a repository needs to be repacked we estimate\nthe number of loose objects. If the number exceeds a certain threshold\nwe perform the repack, otherwise we don't.\n\nThis is done via `too_many_loose_objects()`, which takes as parameter\nthe `struct gc_config`. This configuration is only used to determine the\nthreshold. In a subsequent commit we'll add another caller of this\nfunction that wants to pass a different limit than the one stored in\nthat structure.\n\nRefactor the function accordingly so that we only take the limit as\nparameter instead of the whole structure.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 8 ++++----\n 1 file changed, 4 insertions(+), 4 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e9772eb3a30..026d3a1d714 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -447,7 +447,7 @@ static int rerere_gc_condition(struct gc_config *cfg UNUSED)\n \treturn should_gc;\n }\n \n-static int too_many_loose_objects(struct gc_config *cfg)\n+static int too_many_loose_objects(int limit)\n {\n \t/*\n \t * Quickly check if a \"gc\" is needed, by estimating how\n@@ -469,7 +469,7 @@ static int too_many_loose_objects(struct gc_config *cfg)\n \tif (!dir)\n \t\treturn 0;\n \n-\tauto_threshold = DIV_ROUND_UP(cfg->gc_auto_threshold, 256);\n+\tauto_threshold = DIV_ROUND_UP(limit, 256);\n \twhile ((ent = readdir(dir)) != NULL) {\n \t\tif (strspn(ent->d_name, \"0123456789abcdef\") != hexsz_loose ||\n \t\t    ent->d_name[hexsz_loose] != '\\0')\n@@ -703,7 +703,7 @@ static int need_to_gc(struct gc_config *cfg, struct strvec *repack_args)\n \n \t\tadd_repack_all_option(cfg, &keep_pack, repack_args);\n \t\tstring_list_clear(&keep_pack, 0);\n-\t} else if (too_many_loose_objects(cfg))\n+\t} else if (too_many_loose_objects(cfg->gc_auto_threshold))\n \t\tadd_repack_incremental_option(repack_args);\n \telse\n \t\treturn 0;\n@@ -1057,7 +1057,7 @@ int cmd_gc(int argc,\n \t\t\t\t\t     !opts.quiet && !daemonized ? COMMIT_GRAPH_WRITE_PROGRESS : 0,\n \t\t\t\t\t     NULL);\n \n-\tif (opts.auto_flag && too_many_loose_objects(&cfg))\n+\tif (opts.auto_flag && too_many_loose_objects(cfg.gc_auto_threshold))\n \t\twarning(_(\"There are too many unreachable loose objects; \"\n \t\t\t\"run 'git prune' to remove them.\"));\n \n\n-- \n2.51.1.851.g4ebd6896fd.dirty\n\n"},{"id":"529274","messageId":"20251021-pks-maintenance-geometric-strategy-v2-3-f0d727832b80@pks.im","threadId":"64330","inReplyTo":"20251021-pks-maintenance-geometric-strategy-v2-0-f0d727832b80@pks.im","subject":"[PATCH v2 3/9] builtin/maintenance: introduce \"geometric-repack\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T14:13:25Z","receivedAt":"2025-10-21T14:13:36Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"geometric-repack\" task. This task uses our geometric\nrepack infrastructure as provided by git-repack(1) itself, which is a\nstrategy that especially hosting providers tend to use to amortize the\ncosts of repacking objects.\n\nThere is one issue though with geometric repacks, namely that they\nunconditionally pack all loose objects, regardless of whether or not\nthey are reachable. This is done because it means that we can completely\nskip the reachability step, which significantly speeds up the operation.\nBut it has the big downside that we are unable to expire objects over\ntime.\n\nTo address this issue we thus use a split strategy in this new task:\nwhenever a geometric repack would merge together all packs, we instead\ndo an all-into-one repack. By default, these all-into-one repacks have\ncruft packs enabled, so unreachable objects would now be written into\ntheir own pack. Consequently, they won't be soaked up during geometric\nrepacking anymore and can be expired with the next full repack, assuming\nthat their expiry date has surpassed.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/maintenance.adoc |  11 +++\n builtin/gc.c                          | 102 +++++++++++++++++++++++++\n t/t7900-maintenance.sh                | 139 ++++++++++++++++++++++++++++++++++\n 3 files changed, 252 insertions(+)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex 2f719342183..26dc5de423f 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -75,6 +75,17 @@ maintenance.incremental-repack.auto::\n \tnumber of pack-files not in the multi-pack-index is at least the value\n \tof `maintenance.incremental-repack.auto`. The default value is 10.\n \n+maintenance.geometric-repack.auto::\n+\tThis integer config option controls how often the `geometric-repack`\n+\ttask should be run as part of `git maintenance run --auto`. If zero,\n+\tthen the `geometric-repack` task will not run with the `--auto`\n+\toption. A negative value will force the task to run every time.\n+\tOtherwise, a positive value implies the command should run either when\n+\tthere are packfiles that need to be merged together to retain the\n+\tgeometric progression, or when there are at least this many loose\n+\tobjects that would be written into a new packfile. The default value is\n+\t100.\n+\n maintenance.reflog-expire.auto::\n \tThis integer config option controls how often the `reflog-expire` task\n \tshould be run as part of `git maintenance run --auto`. If zero, then\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 026d3a1d714..2c9ecd464d2 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -34,6 +34,7 @@\n #include \"pack-objects.h\"\n #include \"path.h\"\n #include \"reflog.h\"\n+#include \"repack.h\"\n #include \"rerere.h\"\n #include \"blob.h\"\n #include \"tree.h\"\n@@ -254,6 +255,7 @@ enum maintenance_task_label {\n \tTASK_PREFETCH,\n \tTASK_LOOSE_OBJECTS,\n \tTASK_INCREMENTAL_REPACK,\n+\tTASK_GEOMETRIC_REPACK,\n \tTASK_GC,\n \tTASK_COMMIT_GRAPH,\n \tTASK_PACK_REFS,\n@@ -1566,6 +1568,101 @@ static int maintenance_task_incremental_repack(struct maintenance_run_opts *opts\n \treturn 0;\n }\n \n+static int maintenance_task_geometric_repack(struct maintenance_run_opts *opts,\n+\t\t\t\t\t     struct gc_config *cfg)\n+{\n+\tstruct pack_geometry geometry = {\n+\t\t.split_factor = 2,\n+\t};\n+\tstruct pack_objects_args po_args = {\n+\t\t.local = 1,\n+\t};\n+\tstruct existing_packs existing_packs = EXISTING_PACKS_INIT;\n+\tstruct string_list kept_packs = STRING_LIST_INIT_DUP;\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\tint ret;\n+\n+\texisting_packs.repo = the_repository;\n+\texisting_packs_collect(&existing_packs, &kept_packs);\n+\tpack_geometry_init(&geometry, &existing_packs, &po_args);\n+\tpack_geometry_split(&geometry);\n+\n+\tchild.git_cmd = 1;\n+\n+\tstrvec_pushl(&child.args, \"repack\", \"-d\", \"-l\", NULL);\n+\tif (geometry.split < geometry.pack_nr)\n+\t\tstrvec_push(&child.args, \"--geometric=2\");\n+\telse\n+\t\tadd_repack_all_option(cfg, NULL, &child.args);\n+\tif (opts->quiet)\n+\t\tstrvec_push(&child.args, \"--quiet\");\n+\tif (the_repository->settings.core_multi_pack_index)\n+\t\tstrvec_push(&child.args, \"--write-midx\");\n+\n+\tif (run_command(&child)) {\n+\t\tret = error(_(\"failed to perform geometric repack\"));\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\texisting_packs_release(&existing_packs);\n+\tpack_geometry_release(&geometry);\n+\treturn ret;\n+}\n+\n+static int geometric_repack_auto_condition(struct gc_config *cfg UNUSED)\n+{\n+\tstruct pack_geometry geometry = {\n+\t\t.split_factor = 2,\n+\t};\n+\tstruct pack_objects_args po_args = {\n+\t\t.local = 1,\n+\t};\n+\tstruct existing_packs existing_packs = EXISTING_PACKS_INIT;\n+\tstruct string_list kept_packs = STRING_LIST_INIT_DUP;\n+\tint auto_value = 100;\n+\tint ret;\n+\n+\trepo_config_get_int(the_repository, \"maintenance.geometric-repack.auto\",\n+\t\t\t    &auto_value);\n+\tif (!auto_value)\n+\t\treturn 0;\n+\tif (auto_value < 0)\n+\t\treturn 1;\n+\n+\texisting_packs.repo = the_repository;\n+\texisting_packs_collect(&existing_packs, &kept_packs);\n+\tpack_geometry_init(&geometry, &existing_packs, &po_args);\n+\tpack_geometry_split(&geometry);\n+\n+\t/*\n+\t * When we'd merge at least two packs with one another we always\n+\t * perform the repack.\n+\t */\n+\tif (geometry.split) {\n+\t\tret = 1;\n+\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t * Otherwise, we estimate the number of loose objects to determine\n+\t * whether we want to create a new packfile or not.\n+\t */\n+\tif (too_many_loose_objects(auto_value)) {\n+\t\tret = 1;\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\texisting_packs_release(&existing_packs);\n+\tpack_geometry_release(&geometry);\n+\treturn ret;\n+}\n+\n typedef int (*maintenance_task_fn)(struct maintenance_run_opts *opts,\n \t\t\t\t   struct gc_config *cfg);\n typedef int (*maintenance_auto_fn)(struct gc_config *cfg);\n@@ -1608,6 +1705,11 @@ static const struct maintenance_task tasks[] = {\n \t\t.background = maintenance_task_incremental_repack,\n \t\t.auto_condition = incremental_repack_auto_condition,\n \t},\n+\t[TASK_GEOMETRIC_REPACK] = {\n+\t\t.name = \"geometric-repack\",\n+\t\t.background = maintenance_task_geometric_repack,\n+\t\t.auto_condition = geometric_repack_auto_condition,\n+\t},\n \t[TASK_GC] = {\n \t\t.name = \"gc\",\n \t\t.foreground = maintenance_task_gc_foreground,\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex ddd273d8dc2..60029a65a35 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -465,6 +465,145 @@ test_expect_success 'maintenance.incremental-repack.auto (when config is unset)'\n \t)\n '\n \n+run_and_verify_geometric_pack () {\n+\tEXPECTED_PACKS=\"$1\" &&\n+\n+\t# Verify that we perform a geometric repack.\n+\trm -f \"trace2.txt\" &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/trace2.txt\" \\\n+\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n+\ttest_subcommand git repack -d -l --geometric=2 \\\n+\t\t--quiet --write-midx <trace2.txt &&\n+\n+\t# Verify that the number of packfiles matches our expectation.\n+\tls -l .git/objects/pack/*.pack >packfiles &&\n+\ttest_line_count = \"$EXPECTED_PACKS\" packfiles &&\n+\n+\t# And verify that there are no loose objects anymore.\n+\tcat >expect <<-\\EOF &&\n+\tinfo\n+\tpack\n+\tEOF\n+\tls .git/objects >actual &&\n+\ttest_cmp expect actual\n+}\n+\n+test_expect_success 'geometric repacking task' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tgit config set maintenance.auto false &&\n+\t\ttest_commit initial &&\n+\n+\t\t# The initial repack causes an all-into-one repack.\n+\t\tGIT_TRACE2_EVENT=\"$(pwd)/initial-repack.txt\" \\\n+\t\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n+\t\ttest_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago --quiet --write-midx <initial-repack.txt &&\n+\n+\t\t# Repacking should now cause a no-op geometric repack because\n+\t\t# no packfiles need to be combined.\n+\t\tls -l .git/objects/pack >before &&\n+\t\trun_and_verify_geometric_pack 1 &&\n+\t\tls -l .git/objects/pack >after &&\n+\t\ttest_cmp before after &&\n+\n+\t\t# This incremental change creates a new packfile that only\n+\t\t# soaks up loose objects. The packfiles are not getting merged\n+\t\t# at this point.\n+\t\ttest_commit loose &&\n+\t\trun_and_verify_geometric_pack 2 &&\n+\n+\t\t# Both packfiles have 3 objects, so the next run would cause us\n+\t\t# to merge all packfiles together. This should be turned into\n+\t\t# an all-into-one-repack.\n+\t\tGIT_TRACE2_EVENT=\"$(pwd)/all-into-one-repack.txt\" \\\n+\t\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n+\t\ttest_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago --quiet --write-midx <all-into-one-repack.txt &&\n+\n+\t\t# The geometric repack soaks up unreachable objects.\n+\t\techo blob-1 | git hash-object -w --stdin -t blob &&\n+\t\trun_and_verify_geometric_pack 2 &&\n+\n+\t\t# A second unreachable object should be written into another packfile.\n+\t\techo blob-2 | git hash-object -w --stdin -t blob &&\n+\t\trun_and_verify_geometric_pack 3 &&\n+\n+\t\t# And these two small packs should now be merged via the\n+\t\t# geometric repack. The large packfile should remain intact.\n+\t\trun_and_verify_geometric_pack 2 &&\n+\n+\t\t# If we now add two more objects and repack twice we should\n+\t\t# then see another all-into-one repack. This time around\n+\t\t# though, as we have unreachable objects, we should also see a\n+\t\t# cruft pack.\n+\t\techo blob-3 | git hash-object -w --stdin -t blob &&\n+\t\techo blob-4 | git hash-object -w --stdin -t blob &&\n+\t\trun_and_verify_geometric_pack 3 &&\n+\t\tGIT_TRACE2_EVENT=\"$(pwd)/cruft-repack.txt\" \\\n+\t\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n+\t\ttest_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago --quiet --write-midx <cruft-repack.txt &&\n+\t\tls .git/objects/pack/*.pack >packs &&\n+\t\ttest_line_count = 2 packs &&\n+\t\tls .git/objects/pack/*.mtimes >cruft &&\n+\t\ttest_line_count = 1 cruft\n+\t)\n+'\n+\n+test_geometric_repack_needed () {\n+\tNEEDED=\"$1\"\n+\tGEOMETRIC_CONFIG=\"$2\" &&\n+\trm -f trace2.txt &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/trace2.txt\" \\\n+\t\tgit ${GEOMETRIC_CONFIG:+-c maintenance.geometric-repack.$GEOMETRIC_CONFIG} \\\n+\t\tmaintenance run --auto --task=geometric-repack 2>/dev/null &&\n+\tcase \"$NEEDED\" in\n+\ttrue)\n+\t\ttest_grep \"\\[\\\"git\\\",\\\"repack\\\",\" trace2.txt;;\n+\tfalse)\n+\t\t! test_grep \"\\[\\\"git\\\",\\\"repack\\\",\" trace2.txt;;\n+\t*)\n+\t\tBUG \"invalid parameter: $NEEDED\";;\n+\tesac\n+}\n+\n+test_expect_success 'geometric repacking with --auto' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\n+\t\t# An empty repository does not need repacking, except when\n+\t\t# explicitly told to do it.\n+\t\ttest_geometric_repack_needed false &&\n+\t\ttest_geometric_repack_needed false auto=0 &&\n+\t\ttest_geometric_repack_needed false auto=1 &&\n+\t\ttest_geometric_repack_needed true auto=-1 &&\n+\n+\t\ttest_oid_init &&\n+\n+\t\t# Loose objects cause a repack when crossing the limit. Note\n+\t\t# that the number of objects gets extrapolated by having a look\n+\t\t# at the \"objects/17/\" shard.\n+\t\ttest_commit \"$(test_oid blob17_1)\" &&\n+\t\ttest_geometric_repack_needed false &&\n+\t\ttest_commit \"$(test_oid blob17_2)\" &&\n+\t\ttest_geometric_repack_needed false auto=257 &&\n+\t\ttest_geometric_repack_needed true auto=256 &&\n+\n+\t\t# Force another repack.\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_geometric_repack_needed true auto=-1 &&\n+\n+\t\t# We now have two packfiles that would be merged together. As\n+\t\t# such, the repack should always happen unless the user has\n+\t\t# disabled the auto task.\n+\t\ttest_geometric_repack_needed false auto=0 &&\n+\t\ttest_geometric_repack_needed true auto=9000\n+\t)\n+'\n+\n test_expect_success 'pack-refs task' '\n \tfor n in $(test_seq 1 5)\n \tdo\n\n-- \n2.51.1.851.g4ebd6896fd.dirty\n\n"},{"id":"529275","messageId":"20251021-pks-maintenance-geometric-strategy-v2-4-f0d727832b80@pks.im","threadId":"64330","inReplyTo":"20251021-pks-maintenance-geometric-strategy-v2-0-f0d727832b80@pks.im","subject":"[PATCH v2 4/9] builtin/maintenance: make the geometric factor configurable","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T14:13:26Z","receivedAt":"2025-10-21T14:13:39Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The geometric repacking task uses a factor of two for its geometric\nsequence, meaning that each next pack must contain at least twice as\nmany objects as the next-smaller one. In some cases it may be helpful to\nconfigure this factor though to reduce the number of packfile merges\neven further, e.g. in very big repositories. But while git-repack(1)\nitself supports doing this, the maintenance task does not give us a way\nto tune it.\n\nIntroduce a new \"maintenance.geometric-repack.splitFactor\" configuration\nto plug this gap.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/maintenance.adoc |  5 +++++\n builtin/gc.c                          |  9 ++++++++-\n t/t7900-maintenance.sh                | 32 ++++++++++++++++++++++++++++++++\n 3 files changed, 45 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex 26dc5de423f..45fdafc2c63 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -86,6 +86,11 @@ maintenance.geometric-repack.auto::\n \tobjects that would be written into a new packfile. The default value is\n \t100.\n \n+maintenance.geometric-repack.splitFactor::\n+\tThis integer config option controls the factor used for the geometric\n+\tsequence. See the `--geometric=` option in linkgit:git-repack[1] for\n+\tmore details. Defaults to `2`.\n+\n maintenance.reflog-expire.auto::\n \tThis integer config option controls how often the `reflog-expire` task\n \tshould be run as part of `git maintenance run --auto`. If zero, then\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 2c9ecd464d2..fb1a82e0304 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1582,6 +1582,9 @@ static int maintenance_task_geometric_repack(struct maintenance_run_opts *opts,\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tint ret;\n \n+\trepo_config_get_int(the_repository, \"maintenance.geometric-repack.splitFactor\",\n+\t\t\t    &geometry.split_factor);\n+\n \texisting_packs.repo = the_repository;\n \texisting_packs_collect(&existing_packs, &kept_packs);\n \tpack_geometry_init(&geometry, &existing_packs, &po_args);\n@@ -1591,7 +1594,8 @@ static int maintenance_task_geometric_repack(struct maintenance_run_opts *opts,\n \n \tstrvec_pushl(&child.args, \"repack\", \"-d\", \"-l\", NULL);\n \tif (geometry.split < geometry.pack_nr)\n-\t\tstrvec_push(&child.args, \"--geometric=2\");\n+\t\tstrvec_pushf(&child.args, \"--geometric=%d\",\n+\t\t\t     geometry.split_factor);\n \telse\n \t\tadd_repack_all_option(cfg, NULL, &child.args);\n \tif (opts->quiet)\n@@ -1632,6 +1636,9 @@ static int geometric_repack_auto_condition(struct gc_config *cfg UNUSED)\n \tif (auto_value < 0)\n \t\treturn 1;\n \n+\trepo_config_get_int(the_repository, \"maintenance.geometric-repack.splitFactor\",\n+\t\t\t    &geometry.split_factor);\n+\n \texisting_packs.repo = the_repository;\n \texisting_packs_collect(&existing_packs, &kept_packs);\n \tpack_geometry_init(&geometry, &existing_packs, &po_args);\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 60029a65a35..8f332e7fbbe 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -604,6 +604,38 @@ test_expect_success 'geometric repacking with --auto' '\n \t)\n '\n \n+test_expect_success 'geometric repacking honors configured split factor' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tgit config set maintenance.auto false &&\n+\n+\t\t# Create three different packs with 9, 2 and 1 object, respectively.\n+\t\t# This is done so that only a subset of packs would be merged\n+\t\t# together so that we can verify that `git repack` receives the\n+\t\t# correct geometric factor.\n+\t\tfor i in $(test_seq 9)\n+\t\tdo\n+\t\t\techo first-$i | git hash-object -w --stdin -t blob || return 1\n+\t\tdone &&\n+\t\tgit repack --geometric=2 -d &&\n+\n+\t\tfor i in $(test_seq 2)\n+\t\tdo\n+\t\t\techo second-$i | git hash-object -w --stdin -t blob || return 1\n+\t\tdone &&\n+\t\tgit repack --geometric=2 -d &&\n+\n+\t\techo third | git hash-object -w --stdin -t blob &&\n+\t\tgit repack --geometric=2 -d &&\n+\n+\t\ttest_geometric_repack_needed false splitFactor=2 &&\n+\t\ttest_geometric_repack_needed true splitFactor=3 &&\n+\t\ttest_subcommand git repack -d -l --geometric=3 --quiet --write-midx <trace2.txt\n+\t)\n+'\n+\n test_expect_success 'pack-refs task' '\n \tfor n in $(test_seq 1 5)\n \tdo\n\n-- \n2.51.1.851.g4ebd6896fd.dirty\n\n"},{"id":"529276","messageId":"20251021-pks-maintenance-geometric-strategy-v2-5-f0d727832b80@pks.im","threadId":"64330","inReplyTo":"20251021-pks-maintenance-geometric-strategy-v2-0-f0d727832b80@pks.im","subject":"[PATCH v2 5/9] builtin/maintenance: don't silently ignore invalid strategy","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T14:13:27Z","receivedAt":"2025-10-21T14:13:42Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When parsing maintenance strategies we completely ignore the\nuser-configured value in case it is unknown to us. This makes it\nbasically undiscoverable to the user that scheduled maintenance is\ndevolving into a no-op.\n\nChange this to instead die when seeing an unknown maintenance strategy.\nWhile at it, pull out the parsing logic into a separate function so that\nwe can reuse it in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c           | 17 +++++++++++------\n t/t7900-maintenance.sh |  5 +++++\n 2 files changed, 16 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex fb1a82e030..726d944d3b 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1855,6 +1855,13 @@ static const struct maintenance_strategy incremental_strategy = {\n \t},\n };\n \n+static struct maintenance_strategy parse_maintenance_strategy(const char *name)\n+{\n+\tif (!strcasecmp(name, \"incremental\"))\n+\t\treturn incremental_strategy;\n+\tdie(_(\"unknown maintenance strategy: '%s'\"), name);\n+}\n+\n static void initialize_task_config(struct maintenance_run_opts *opts,\n \t\t\t\t   const struct string_list *selected_tasks)\n {\n@@ -1890,12 +1897,10 @@ static void initialize_task_config(struct maintenance_run_opts *opts,\n \t * override specific aspects of our strategy.\n \t */\n \tif (opts->schedule) {\n-\t\tstrategy = none_strategy;\n-\n-\t\tif (!repo_config_get_string_tmp(the_repository, \"maintenance.strategy\", &config_str)) {\n-\t\t\tif (!strcasecmp(config_str, \"incremental\"))\n-\t\t\t\tstrategy = incremental_strategy;\n-\t\t}\n+\t\tif (!repo_config_get_string_tmp(the_repository, \"maintenance.strategy\", &config_str))\n+\t\t\tstrategy = parse_maintenance_strategy(config_str);\n+\t\telse\n+\t\t\tstrategy = none_strategy;\n \t} else {\n \t\tstrategy = default_strategy;\n \t}\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 8f332e7fbb..69fb6e9ee2 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -1264,6 +1264,11 @@ test_expect_success 'fails when running outside of a repository' '\n \tnongit test_must_fail git maintenance unregister\n '\n \n+test_expect_success 'fails when configured to use an invalid strategy' '\n+\ttest_must_fail git -c maintenance.strategy=invalid maintenance run --schedule=hourly 2>err &&\n+\ttest_grep \"unknown maintenance strategy: .invalid.\" err\n+'\n+\n test_expect_success 'register and unregister bare repo' '\n \ttest_when_finished \"git config --global --unset-all maintenance.repo || :\" &&\n \ttest_might_fail git config --global --unset-all maintenance.repo &&\n\n-- \n2.51.1.851.g4ebd6896fd.dirty\n\n"},{"id":"529277","messageId":"20251021-pks-maintenance-geometric-strategy-v2-6-f0d727832b80@pks.im","threadId":"64330","inReplyTo":"20251021-pks-maintenance-geometric-strategy-v2-0-f0d727832b80@pks.im","subject":"[PATCH v2 6/9] builtin/maintenance: run maintenance tasks depending on type","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T14:13:28Z","receivedAt":"2025-10-21T14:13:45Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We basically have three different ways to execute repository\nmaintenance:\n\n  1. Manual maintenance via `git maintenance run`.\n\n  2. Automatic maintenance via `git maintenance run --auto`.\n\n  3. Scheduled maintenance via `git maintenance run --schedule=`.\n\nAt the moment, maintenance strategies only have an effect for the last\ntype of maintenance. This is about to change in subsequent commits, but\nto do so we need to be able to skip some tasks depending on how exactly\nmaintenance was invoked.\n\nIntroduce a new maintenance type that discern between manual (1 & 2) and\nscheduled (3) maintenance. Convert the `enabled` field into a bitset so\nthat it becomes possible to specifiy which tasks exactly should run in a\nspecific context.\n\nThe types picked for existing strategies match the status quo:\n\n  - The default strategy is only ever executed as part of a manual\n    maintenance run. It is not possible to use it for scheduled\n    maintenance.\n\n  - The incremental strategy is only ever executed as part of a\n    scheduled maintenance run. It is not possible to use it for manual\n    maintenance.\n\nThe strategies will be tweaked in subsequent commits to make use of this\nnew infrastructure.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 30 +++++++++++++++++++++---------\n 1 file changed, 21 insertions(+), 9 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 726d944d3b..eff4e4886a 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1827,30 +1827,39 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \treturn result;\n }\n \n+enum maintenance_type {\n+\t/* As invoked via `git maintenance run --schedule=`. */\n+\tMAINTENANCE_TYPE_SCHEDULED = (1 << 0),\n+\t/* As invoked via `git maintenance run` and with `--auto`. */\n+\tMAINTENANCE_TYPE_MANUAL    = (1 << 1),\n+};\n+\n struct maintenance_strategy {\n \tstruct {\n-\t\tint enabled;\n+\t\tunsigned type;\n \t\tenum schedule_priority schedule;\n \t} tasks[TASK__COUNT];\n };\n \n static const struct maintenance_strategy none_strategy = { 0 };\n+\n static const struct maintenance_strategy default_strategy = {\n \t.tasks = {\n-\t\t[TASK_GC].enabled = 1,\n+\t\t[TASK_GC].type = MAINTENANCE_TYPE_MANUAL,\n \t},\n };\n+\n static const struct maintenance_strategy incremental_strategy = {\n \t.tasks = {\n-\t\t[TASK_COMMIT_GRAPH].enabled = 1,\n+\t\t[TASK_COMMIT_GRAPH].type = MAINTENANCE_TYPE_SCHEDULED,\n \t\t[TASK_COMMIT_GRAPH].schedule = SCHEDULE_HOURLY,\n-\t\t[TASK_PREFETCH].enabled = 1,\n+\t\t[TASK_PREFETCH].type = MAINTENANCE_TYPE_SCHEDULED,\n \t\t[TASK_PREFETCH].schedule = SCHEDULE_HOURLY,\n-\t\t[TASK_INCREMENTAL_REPACK].enabled = 1,\n+\t\t[TASK_INCREMENTAL_REPACK].type = MAINTENANCE_TYPE_SCHEDULED,\n \t\t[TASK_INCREMENTAL_REPACK].schedule = SCHEDULE_DAILY,\n-\t\t[TASK_LOOSE_OBJECTS].enabled = 1,\n+\t\t[TASK_LOOSE_OBJECTS].type = MAINTENANCE_TYPE_SCHEDULED,\n \t\t[TASK_LOOSE_OBJECTS].schedule = SCHEDULE_DAILY,\n-\t\t[TASK_PACK_REFS].enabled = 1,\n+\t\t[TASK_PACK_REFS].type = MAINTENANCE_TYPE_SCHEDULED,\n \t\t[TASK_PACK_REFS].schedule = SCHEDULE_WEEKLY,\n \t},\n };\n@@ -1867,6 +1876,7 @@ static void initialize_task_config(struct maintenance_run_opts *opts,\n {\n \tstruct strbuf config_name = STRBUF_INIT;\n \tstruct maintenance_strategy strategy;\n+\tenum maintenance_type type;\n \tconst char *config_str;\n \n \t/*\n@@ -1901,8 +1911,10 @@ static void initialize_task_config(struct maintenance_run_opts *opts,\n \t\t\tstrategy = parse_maintenance_strategy(config_str);\n \t\telse\n \t\t\tstrategy = none_strategy;\n+\t\ttype = MAINTENANCE_TYPE_SCHEDULED;\n \t} else {\n \t\tstrategy = default_strategy;\n+\t\ttype = MAINTENANCE_TYPE_MANUAL;\n \t}\n \n \tfor (size_t i = 0; i < TASK__COUNT; i++) {\n@@ -1912,8 +1924,8 @@ static void initialize_task_config(struct maintenance_run_opts *opts,\n \t\tstrbuf_addf(&config_name, \"maintenance.%s.enabled\",\n \t\t\t    tasks[i].name);\n \t\tif (!repo_config_get_bool(the_repository, config_name.buf, &config_value))\n-\t\t\tstrategy.tasks[i].enabled = config_value;\n-\t\tif (!strategy.tasks[i].enabled)\n+\t\t\tstrategy.tasks[i].type = config_value ? type : 0;\n+\t\tif (!(strategy.tasks[i].type & type))\n \t\t\tcontinue;\n \n \t\tif (opts->schedule) {\n\n-- \n2.51.1.851.g4ebd6896fd.dirty\n\n"},{"id":"529278","messageId":"20251021-pks-maintenance-geometric-strategy-v2-7-f0d727832b80@pks.im","threadId":"64330","inReplyTo":"20251021-pks-maintenance-geometric-strategy-v2-0-f0d727832b80@pks.im","subject":"[PATCH v2 7/9] builtin/maintenance: extend \"maintenance.strategy\" to manual maintenance","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T14:13:29Z","receivedAt":"2025-10-21T14:13:48Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"maintenance.strategy\" configuration allows users to configure how\nGit is supposed to perform repository maintenance. The idea is that we\nprovide a set of high-level strategies that may be useful in different\ncontexts, like for example when handling a large monorepo. Furthermore,\nthe strategy can be tweaked by the user by overriding specific tasks.\n\nIn its current form though, the strategy only applies to scheduled\nmaintenance. This creates something of a gap, as scheduled and manual\nmaintenance will now use _different_ strategies as the latter would\ncontinue to use git-gc(1) by default. This makes the strategies way less\nuseful than they could be on the one hand. But even more importantly,\nthe two different strategies might clash with one another, where one of\nthe strategies performs maintenance in such a way that it discards\nbenefits from the other strategy.\n\nSo ideally, it should be possible to pick one strategy that then applies\nglobally to all the different ways that we perform maintenance. This\ndoesn't necessarily mean that the strategy always does the _same_ thing\nfor every maintenance type. But it means that the strategy can configure\nthe different types to work in tandem with each other.\n\nChange the meaning of \"maintenance.strategy\" accordingly so that the\nstrategy is applied to both types, manual and scheduled. As preceding\ncommits have introduced logic to run maintenance tasks depending on this\ntype we can tweak strategies so that they perform those tasks depending\non the context.\n\nNote that this raises the question of backwards compatibility: when the\nuser has configured the \"incremental\" strategy we would have ignored\nthat strategy beforehand. Instead, repository maintenance would have\ncontinued to use git-gc(1) by default.\n\nBut luckily, we can match that behaviour by:\n\n  - Keeping all current tasks of the incremental strategy as\n    `MAINTENANCE_TYPE_SCHEDULED`. This ensures that those tasks will not\n    run during manual maintenance.\n\n  - Configuring the \"gc\" task so that it is invoked during manual\n    maintenance.\n\nLike this, the user shouldn't observe any difference in behaviour.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/maintenance.adoc | 22 ++++++++++++-------\n builtin/gc.c                          | 24 ++++++++++++++++-----\n t/t7900-maintenance.sh                | 40 +++++++++++++++++++++++++++++++++++\n 3 files changed, 73 insertions(+), 13 deletions(-)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex 45fdafc2c6..b7e90a71a3 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -16,19 +16,25 @@ detach.\n \n maintenance.strategy::\n \tThis string config option provides a way to specify one of a few\n-\trecommended schedules for background maintenance. This only affects\n-\twhich tasks are run during `git maintenance run --schedule=X`\n-\tcommands, provided no `--task=<task>` arguments are provided.\n-\tFurther, if a `maintenance.<task>.schedule` config value is set,\n-\tthen that value is used instead of the one provided by\n-\t`maintenance.strategy`. The possible strategy strings are:\n+\trecommended strategies for repository maintenance. This affects\n+\twhich tasks are run during `git maintenance run`, provided no\n+\t`--task=<task>` arguments are provided. This setting impacts manual\n+\tmaintenance, auto-maintenance as well as scheduled maintenance. The\n+\ttasks that run may be different depending on the maintenance type.\n +\n-* `none`: This default setting implies no tasks are run at any schedule.\n+The maintenance strategy can be further tweaked by setting\n+`maintenance.<task>.enabled` and `maintenance.<task>.schedule`. If set, these\n+values are used instead of the defaults provided by `maintenance.strategy`.\n++\n+The possible strategies are:\n++\n+* `none`: This strategy implies no tasks are run at all. This is the default\n+  strategy for scheduled maintenance.\n * `incremental`: This setting optimizes for performing small maintenance\n   activities that do not delete any data. This does not schedule the `gc`\n   task, but runs the `prefetch` and `commit-graph` tasks hourly, the\n   `loose-objects` and `incremental-repack` tasks daily, and the `pack-refs`\n-  task weekly.\n+  task weekly. Manual repository maintenance uses the `gc` task.\n \n maintenance.<task>.enabled::\n \tThis boolean config option controls whether the maintenance task\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex eff4e4886a..9c05905b9a 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1861,6 +1861,19 @@ static const struct maintenance_strategy incremental_strategy = {\n \t\t[TASK_LOOSE_OBJECTS].schedule = SCHEDULE_DAILY,\n \t\t[TASK_PACK_REFS].type = MAINTENANCE_TYPE_SCHEDULED,\n \t\t[TASK_PACK_REFS].schedule = SCHEDULE_WEEKLY,\n+\n+\t\t/*\n+\t\t * Historically, the \"incremental\" strategy was only available\n+\t\t * in the context of scheduled maintenance when set up via\n+\t\t * \"maintenance.strategy\". We have later expanded that config\n+\t\t * to also cover manual maintenance.\n+\t\t *\n+\t\t * To retain backwards compatibility with the previous status\n+\t\t * quo we thus run git-gc(1) in case manual maintenance was\n+\t\t * requested. This is the same as the default strategy, which\n+\t\t * would have been in use beforehand.\n+\t\t */\n+\t\t[TASK_GC].type = MAINTENANCE_TYPE_MANUAL,\n \t},\n };\n \n@@ -1904,19 +1917,20 @@ static void initialize_task_config(struct maintenance_run_opts *opts,\n \t *   - Unscheduled maintenance uses our default strategy.\n \t *\n \t * Both of these are affected by the gitconfig though, which may\n-\t * override specific aspects of our strategy.\n+\t * override specific aspects of our strategy. Furthermore, both\n+\t * strategies can be overridden by setting \"maintenance.strategy\".\n \t */\n \tif (opts->schedule) {\n-\t\tif (!repo_config_get_string_tmp(the_repository, \"maintenance.strategy\", &config_str))\n-\t\t\tstrategy = parse_maintenance_strategy(config_str);\n-\t\telse\n-\t\t\tstrategy = none_strategy;\n+\t\tstrategy = none_strategy;\n \t\ttype = MAINTENANCE_TYPE_SCHEDULED;\n \t} else {\n \t\tstrategy = default_strategy;\n \t\ttype = MAINTENANCE_TYPE_MANUAL;\n \t}\n \n+\tif (!repo_config_get_string_tmp(the_repository, \"maintenance.strategy\", &config_str))\n+\t\tstrategy = parse_maintenance_strategy(config_str);\n+\n \tfor (size_t i = 0; i < TASK__COUNT; i++) {\n \t\tint config_value;\n \ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 69fb6e9ee2..3530895bfb 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -887,6 +887,46 @@ test_expect_success 'maintenance.strategy inheritance' '\n \t\t<modified-daily.txt\n '\n \n+test_strategy () {\n+\tSTRATEGY=\"$1\"\n+\tshift\n+\n+\tcat >expect &&\n+\trm -f trace2.txt &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/trace2.txt\" \\\n+\t\tgit -c maintenance.strategy=$STRATEGY maintenance run --quiet \"$@\" &&\n+\tsed -n 's/{\"event\":\"child_start\",\"sid\":\"[^/\"]*\",.*,\"argv\":\\[\"\\(.*\\)\\\"]}/\\1/p' <trace2.txt |\n+\t\tsed 's/\",\"/ /g'  >actual\n+\ttest_cmp expect actual\n+}\n+\n+test_expect_success 'maintenance.strategy is respected' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\n+\t\ttest_must_fail git -c maintenance.strategy=unknown maintenance run 2>err &&\n+\t\ttest_grep \"unknown maintenance strategy: .unknown.\" err &&\n+\n+\t\ttest_strategy incremental <<-\\EOF &&\n+\t\tgit pack-refs --all --prune\n+\t\tgit reflog expire --all\n+\t\tgit gc --quiet --no-detach --skip-foreground-tasks\n+\t\tEOF\n+\n+\t\ttest_strategy incremental --schedule=weekly <<-\\EOF\n+\t\tgit pack-refs --all --prune\n+\t\tgit prune-packed --quiet\n+\t\tgit multi-pack-index write --no-progress\n+\t\tgit multi-pack-index expire --no-progress\n+\t\tgit multi-pack-index repack --no-progress --batch-size=1\n+\t\tgit commit-graph write --split --reachable --no-progress\n+\t\tEOF\n+\t)\n+'\n+\n test_expect_success 'register and unregister' '\n \ttest_when_finished git config --global --unset-all maintenance.repo &&\n \n\n-- \n2.51.1.851.g4ebd6896fd.dirty\n\n"},{"id":"529279","messageId":"20251021-pks-maintenance-geometric-strategy-v2-8-f0d727832b80@pks.im","threadId":"64330","inReplyTo":"20251021-pks-maintenance-geometric-strategy-v2-0-f0d727832b80@pks.im","subject":"[PATCH v2 8/9] builtin/maintenance: make \"gc\" strategy accessible","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T14:13:30Z","receivedAt":"2025-10-21T14:13:50Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While the user can pick the \"incremental\" maintenance strategy, it is\nnot possible to explicitly use the \"gc\" strategy. This has two\ndownsides:\n\n  - It is impossible to use the default \"gc\" strategy for a specific\n    repository when the strategy was globally set to a different strategy.\n\n  - It is not possible to use git-gc(1) for scheduled maintenance.\n\nAddress these issues by making making the \"gc\" strategy configurable.\nFurthermore, extend the strategy so that git-gc(1) runs for both manual\nand scheduled maintenance.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/maintenance.adoc |  2 ++\n builtin/gc.c                          |  9 ++++++---\n t/t7900-maintenance.sh                | 14 +++++++++++++-\n 3 files changed, 21 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex b7e90a71a3..b2bacdc822 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -30,6 +30,8 @@ The possible strategies are:\n +\n * `none`: This strategy implies no tasks are run at all. This is the default\n   strategy for scheduled maintenance.\n+* `gc`: This strategy runs the `gc` task. This is the default strategy for\n+  manual maintenance.\n * `incremental`: This setting optimizes for performing small maintenance\n   activities that do not delete any data. This does not schedule the `gc`\n   task, but runs the `prefetch` and `commit-graph` tasks hourly, the\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 9c05905b9a..aaff0bae15 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1843,9 +1843,10 @@ struct maintenance_strategy {\n \n static const struct maintenance_strategy none_strategy = { 0 };\n \n-static const struct maintenance_strategy default_strategy = {\n+static const struct maintenance_strategy gc_strategy = {\n \t.tasks = {\n-\t\t[TASK_GC].type = MAINTENANCE_TYPE_MANUAL,\n+\t\t[TASK_GC].type = MAINTENANCE_TYPE_MANUAL | MAINTENANCE_TYPE_SCHEDULED,\n+\t\t[TASK_GC].schedule = SCHEDULE_DAILY,\n \t},\n };\n \n@@ -1881,6 +1882,8 @@ static struct maintenance_strategy parse_maintenance_strategy(const char *name)\n {\n \tif (!strcasecmp(name, \"incremental\"))\n \t\treturn incremental_strategy;\n+\tif (!strcasecmp(name, \"gc\"))\n+\t\treturn gc_strategy;\n \tdie(_(\"unknown maintenance strategy: '%s'\"), name);\n }\n \n@@ -1924,7 +1927,7 @@ static void initialize_task_config(struct maintenance_run_opts *opts,\n \t\tstrategy = none_strategy;\n \t\ttype = MAINTENANCE_TYPE_SCHEDULED;\n \t} else {\n-\t\tstrategy = default_strategy;\n+\t\tstrategy = gc_strategy;\n \t\ttype = MAINTENANCE_TYPE_MANUAL;\n \t}\n \ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 3530895bfb..2770148fd1 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -916,7 +916,7 @@ test_expect_success 'maintenance.strategy is respected' '\n \t\tgit gc --quiet --no-detach --skip-foreground-tasks\n \t\tEOF\n \n-\t\ttest_strategy incremental --schedule=weekly <<-\\EOF\n+\t\ttest_strategy incremental --schedule=weekly <<-\\EOF &&\n \t\tgit pack-refs --all --prune\n \t\tgit prune-packed --quiet\n \t\tgit multi-pack-index write --no-progress\n@@ -924,6 +924,18 @@ test_expect_success 'maintenance.strategy is respected' '\n \t\tgit multi-pack-index repack --no-progress --batch-size=1\n \t\tgit commit-graph write --split --reachable --no-progress\n \t\tEOF\n+\n+\t\ttest_strategy gc <<-\\EOF &&\n+\t\tgit pack-refs --all --prune\n+\t\tgit reflog expire --all\n+\t\tgit gc --quiet --no-detach --skip-foreground-tasks\n+\t\tEOF\n+\n+\t\ttest_strategy gc --schedule=weekly <<-\\EOF\n+\t\tgit pack-refs --all --prune\n+\t\tgit reflog expire --all\n+\t\tgit gc --quiet --no-detach --skip-foreground-tasks\n+\t\tEOF\n \t)\n '\n \n\n-- \n2.51.1.851.g4ebd6896fd.dirty\n\n"},{"id":"529280","messageId":"20251021-pks-maintenance-geometric-strategy-v2-9-f0d727832b80@pks.im","threadId":"64330","inReplyTo":"20251021-pks-maintenance-geometric-strategy-v2-0-f0d727832b80@pks.im","subject":"[PATCH v2 9/9] builtin/maintenance: introduce \"geometric\" strategy","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T14:13:31Z","receivedAt":"2025-10-21T14:13:53Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We have two different repacking strategies in Git:\n\n  - The \"gc\" strategy uses git-gc(1).\n\n  - The \"incremental\" strategy uses multi-pack indices and `git\n    multi-pack-index repack` to merge together smaller packfiles as\n    determined by a specific batch size.\n\nThe former strategy is our old and trusted default, whereas the latter\nhas historically been used for our scheduled maintenance. But both\nstrategies have their shortcomings:\n\n  - The \"gc\" strategy performs regular all-into-one repacks. Furthermore\n    it is rather inflexible, as it is not easily possible for a user to\n    enable or disable specific subtasks.\n\n  - The \"incremental\" strategy is not a full replacement for the \"gc\"\n    strategy as it doesn't know to prune stale data.\n\nSo today, we don't have a strategy that is well-suited for large repos\nwhile being a full replacement for the \"gc\" strategy.\n\nIntroduce a new \"geometric\" strategy that aims to fill this gap. This\nstrategy invokes all the usual cleanup tasks that git-gc(1) does like\npruning reflogs and rerere caches as well as stale worktrees. But where\nit differs from both the \"gc\" and \"incremental\" strategy is that it uses\nour geometric repacking infrastructure exposed by git-repack(1) to\nrepack packfiles. The advantage of geometric repacking is that we only\nneed to perform an all-into-one repack when the object count in a repo\nhas grown significantly.\n\nOne downside of this strategy is that pruning of unreferenced objects is\nnot going to happen regularly anymore. Every geometric repack knows to\nsoak up all loose objects regardless of their reachability, and merging\ntwo or more packs doesn't consider reachability, either. Consequently,\nthe number of unreachable objects will grow over time.\n\nThis is remedied by doing an all-into-one repack instead of a geometric\nrepack whenever we determine that the geometric repack would end up\nmerging all packfiles anyway. This all-into-one repack then performs our\nusual reachability checks and writes unreachable objects into a cruft\npack. As cruft packs won't ever be merged during geometric repacks we\ncan thus phase out these objects over time.\n\nOf course, this still means that we retain unreachable objects for far\nlonger than with the \"gc\" strategy. But the maintenance strategy is\nintended especially for large repositories, where the basic assumption\nis that the set of unreachable objects will be significantly dwarfed by\nthe number of reachable objects.\n\nIf this assumption is ever proven to be too disadvantageous we could for\nexample introduce a time-based strategy: if the largest packfile has not\nbeen touched for longer than $T, we perform an all-into-one repack. But\nfor now, such a mechanism is deferred into the future as it is not clear\nyet whether it is needed in the first place.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/maintenance.adoc |  9 +++++++++\n builtin/gc.c                          | 19 +++++++++++++++++++\n t/t7900-maintenance.sh                | 20 +++++++++++++++++++-\n 3 files changed, 47 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex b2bacdc822..d0c38f03fa 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -32,6 +32,15 @@ The possible strategies are:\n   strategy for scheduled maintenance.\n * `gc`: This strategy runs the `gc` task. This is the default strategy for\n   manual maintenance.\n+* `geometric`: This strategy performs geometric repacking of packfiles and\n+  keeps auxiliary data structures up-to-date. The strategy expires data in the\n+  reflog and removes worktrees that cannot be located anymore. When the\n+  geometric repacking strategy would decide to do an all-into-one repack, then\n+  the strategy generates a cruft pack for all unreachable objects. Objects that\n+  are already part of a cruft pack will be expired.\n++\n+This repacking strategy is a full replacement for the `gc` strategy and is\n+recommended for large repositories.\n * `incremental`: This setting optimizes for performing small maintenance\n   activities that do not delete any data. This does not schedule the `gc`\n   task, but runs the `prefetch` and `commit-graph` tasks hourly, the\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex aaff0bae15..9739bb0ea2 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1878,12 +1878,31 @@ static const struct maintenance_strategy incremental_strategy = {\n \t},\n };\n \n+static const struct maintenance_strategy geometric_strategy = {\n+\t.tasks = {\n+\t\t[TASK_COMMIT_GRAPH].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t[TASK_COMMIT_GRAPH].schedule = SCHEDULE_HOURLY,\n+\t\t[TASK_GEOMETRIC_REPACK].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t[TASK_GEOMETRIC_REPACK].schedule = SCHEDULE_DAILY,\n+\t\t[TASK_PACK_REFS].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t[TASK_PACK_REFS].schedule = SCHEDULE_DAILY,\n+\t\t[TASK_RERERE_GC].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t[TASK_RERERE_GC].schedule = SCHEDULE_WEEKLY,\n+\t\t[TASK_REFLOG_EXPIRE].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t[TASK_REFLOG_EXPIRE].schedule = SCHEDULE_WEEKLY,\n+\t\t[TASK_WORKTREE_PRUNE].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t[TASK_WORKTREE_PRUNE].schedule = SCHEDULE_WEEKLY,\n+\t},\n+};\n+\n static struct maintenance_strategy parse_maintenance_strategy(const char *name)\n {\n \tif (!strcasecmp(name, \"incremental\"))\n \t\treturn incremental_strategy;\n \tif (!strcasecmp(name, \"gc\"))\n \t\treturn gc_strategy;\n+\tif (!strcasecmp(name, \"geometric\"))\n+\t\treturn geometric_strategy;\n \tdie(_(\"unknown maintenance strategy: '%s'\"), name);\n }\n \ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 2770148fd1..aedb9e7e8e 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -931,11 +931,29 @@ test_expect_success 'maintenance.strategy is respected' '\n \t\tgit gc --quiet --no-detach --skip-foreground-tasks\n \t\tEOF\n \n-\t\ttest_strategy gc --schedule=weekly <<-\\EOF\n+\t\ttest_strategy gc --schedule=weekly <<-\\EOF &&\n \t\tgit pack-refs --all --prune\n \t\tgit reflog expire --all\n \t\tgit gc --quiet --no-detach --skip-foreground-tasks\n \t\tEOF\n+\n+\t\ttest_strategy geometric <<-\\EOF &&\n+\t\tgit pack-refs --all --prune\n+\t\tgit reflog expire --all\n+\t\tgit repack -d -l --geometric=2 --quiet --write-midx\n+\t\tgit commit-graph write --split --reachable --no-progress\n+\t\tgit worktree prune --expire 3.months.ago\n+\t\tgit rerere gc\n+\t\tEOF\n+\n+\t\ttest_strategy geometric --schedule=weekly <<-\\EOF\n+\t\tgit pack-refs --all --prune\n+\t\tgit reflog expire --all\n+\t\tgit repack -d -l --geometric=2 --quiet --write-midx\n+\t\tgit commit-graph write --split --reachable --no-progress\n+\t\tgit worktree prune --expire 3.months.ago\n+\t\tgit rerere gc\n+\t\tEOF\n \t)\n '\n \n\n-- \n2.51.1.851.g4ebd6896fd.dirty\n\n"},{"id":"529510","messageId":"xmqq7bwlwoml.fsf@gitster.g","threadId":"64330","inReplyTo":"20251021-pks-maintenance-geometric-strategy-v2-0-f0d727832b80@pks.im","subject":"Re: [PATCH v2 0/9] builtin/maintenance: introduce \"geometric\" strategy","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-23T16:48:18Z","receivedAt":"2025-10-23T16:48:21Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> The series is built on top of b660e2dcb9 (Sync with 'maint', 2025-10-14)\n> with tb/incremental-midx-part-3.1 at c886af90f8 (SQUASH??? play well\n> with other topics by preemptively including \"repository.h\", 2025-09-29)\n> merged into it.\n>\n> Changes in v2:\n>   - Make the geometric factor configurable via\n>     \"maintenance.geometric-repack.splitFactor\".\n>   - Wrap some overly long lines in our tests.\n>   - Link to v1: https://lore.kernel.org/r/20251016-pks-maintenance-geometric-strategy-v1-0-18943d474203@pks.im\n>\n> Thanks!\n\nThis round looks good to me (I wasn't very careful picking typos and\nminor mistakes, but the resulting code overall looked sound).\n\nThanks.\n"},{"id":"529517","messageId":"aPp/0/kU1LMBjt7g@nand.local","threadId":"64330","inReplyTo":"aPeD75W4FjtTZDYr@pks.im","subject":"Re: [PATCH 3/8] builtin/maintenance: introduce \"geometric-repack\" task","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2025-10-23T19:19:47Z","receivedAt":"2025-10-23T19:19:57Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Tue, Oct 21, 2025 at 03:00:31PM +0200, Patrick Steinhardt wrote:\n> > OK. To make sure I understand: this limit is the minimum number of loose\n> > objects would cause the geometric-repack task to run, unless there are\n> > pack(s) which would be combined as a result of running a geometric\n> > repack, in which case we run it regardless.\n> >\n> > Is that right?\n>\n> Yeah, exactly. I was initially thinking to only frame this in the\n> context of \"how many packs would be merged\"? But the problem with that\n> is that we wouldn't ever generate _new_ packs if the repository only\n> ever grows loose objects, and consequently we also wouldn't ever merge\n> any of them.\n\nMakes total sense, I like the direction that you picked here.\n\n> > > @@ -1566,6 +1568,101 @@ static int maintenance_task_incremental_repack(struct maintenance_run_opts *opts\n> > >  \treturn 0;\n> > >  }\n> > >\n> > > +static int maintenance_task_geometric_repack(struct maintenance_run_opts *opts,\n> > > +\t\t\t\t\t     struct gc_config *cfg)\n> > > +{\n> > > +\tstruct pack_geometry geometry = {\n> > > +\t\t.split_factor = 2,\n> >\n> > I wonder if this should be configurable somewhere. It might not be a bad\n> > idea to introduce a 'repack.geometricSplitFactor' configuration\n> > variable, defaulting to two, and use that here. It would also be nice to\n> > be able to run 'git repack --geometric -d' and have it fallback to that\n> > split factor, since using \"2\" is so common that it's frustrating when I\n> > forget to type it out explicitly ;-).\n>\n> I was also pondering over this. I think the way to do so would be to\n> introduce \"maintenance.geometric-repack.splitFactor\", as that follows\n> all the other maintenance configuration we have there, as well.\n>\n> I decided to not do it yet as I wanted to keep the scope of this patch\n> series contained. But honestly, it's an easy-enough change to make, so\n> let me introduce another patch to do this.\n\nI'm glad to have ~~nerd-sniped~~ convinced you that this was an\ninteresting addition ;-). Thanks for working on it.\n\n> > > +static int geometric_repack_auto_condition(struct gc_config *cfg UNUSED)\n> > > +{\n> > > +\tstruct pack_geometry geometry = {\n> > > +\t\t.split_factor = 2,\n> > > +\t};\n> > > +\tstruct pack_objects_args po_args = {\n> > > +\t\t.local = 1,\n> > > +\t};\n> > > +\tstruct existing_packs existing_packs = EXISTING_PACKS_INIT;\n> > > +\tstruct string_list kept_packs = STRING_LIST_INIT_DUP;\n> > > +\tint auto_value = 100;\n> > > +\tint ret;\n> > > +\n> > > +\trepo_config_get_int(the_repository, \"maintenance.geometric-repack.auto\",\n> > > +\t\t\t    &auto_value);\n> > > +\tif (!auto_value)\n> > > +\t\treturn 0;\n> > > +\tif (auto_value < 0)\n> > > +\t\treturn 1;\n> > > +\n> > > +\texisting_packs.repo = the_repository;\n> > > +\texisting_packs_collect(&existing_packs, &kept_packs);\n> > > +\tpack_geometry_init(&geometry, &existing_packs, &po_args);\n> > > +\tpack_geometry_split(&geometry);\n> > > +\n> > > +\t/*\n> > > +\t * When we'd merge at least two packs with one another we always\n> > > +\t * perform the repack.\n> > > +\t */\n> > > +\tif (geometry.split) {\n> > > +\t\tret = 1;\n> > > +\t\tgoto out;\n> > > +\t}\n> >\n> > Hmm. I wish that we could somehow pass this information to the function\n> > above so that we don't have to re-discover the fact that there are packs\n> > to combine. I'm not familiar enough with the maintenance code to know\n> > how difficult that would be to do, but it looks like at least the\n> > gc_config pointer is shared between the auto condition and the task\n> > itself.\n> >\n> > That's kind of gross to tack on there, but I could see a compelling\n> > argument for passing around an extra void pointer between the two that\n> > would allow us to propagate this kind of data between the auto condition\n> > and the task itself. It's not super expensive to do so I don't think not\n> > doing it is a show-stopper at least from a performance perspective, but\n> > it does seem like a good opportunity to DRY things up a bit.\n>\n> The problem is that the auto-condition is not evaluated when running\n> without the \"--auto\" flag. We of course can conditionally compute the\n> split in case we figure that the auto-condition didn't run, but it does\n> get somewhat dirty.\n>\n> So I'd propose to defer such a change into the future in case we notice\n> that this indeed is a problem. Is that fine with you?\n\nAh, makes sense, I definitely felt like I was missing something here.\nI agree that punting on this feels like the right thing to do.\n\n> > > diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\n> > > index ddd273d8dc2..83a373fe94b 100755\n> > > --- a/t/t7900-maintenance.sh\n> > > +++ b/t/t7900-maintenance.sh\n> > > @@ -465,6 +465,143 @@ test_expect_success 'maintenance.incremental-repack.auto (when config is unset)'\n> > >  \t)\n> > >  '\n> > >\n> > > +run_and_verify_geometric_pack () {\n> > > +\tEXPECTED_PACKS=\"$1\" &&\n> > > +\n> > > +\t# Verify that we perform a geometric repack.\n> > > +\trm -f \"trace2.txt\" &&\n> > > +\tGIT_TRACE2_EVENT=\"$(pwd)/trace2.txt\" \\\n> > > +\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n> > > +\ttest_subcommand git repack -d -l --geometric=2 --quiet --write-midx <trace2.txt &&\n> >\n> > Makes sense. I do think the test_subcommand thing is a little fragile\n> > here, but verifying that the resulting pack structure forms a geometric\n> > progression feels like overkill for this test, so I think what you wrote\n> > here makes sense.\n>\n> Oh, yeah, it's fragile and somewhat gross indeed. Couldn't really find a\n> nicer way to do it though :/\n\nYeah, I don't think that there's a much better alternative. At some\nlevel, we really just care that we ran a geometric repack, and that's\nexactly what test_command is verifying. I think the fragility comes from\ncases like:\n\n - You started (or stopped) passing an ancillary argument to the command\n   in such a way that caused test_subcommand to no longer match, but the\n   change in how we call the command internally is not meaningful to the\n   test.\n\n - You stopped passing an argument (e.g., \"--geometric=2\") in a way that\n   *looks* like it would have altered the behavior of the command, but\n   actually doesn't, e.g., because you made the geometric factor of \"2\"\n   the default, or otherwise specified via the config.\n\nI think this is all just idle musing about test_subcommand in general,\nand not useful to this immediate patch series.\n\n> > > +\n> > > +\t# Verify that the number of packfiles matches our expectation.\n> > > +\tls -l .git/objects/pack/*.pack >packfiles &&\n> > > +\ttest_line_count = \"$EXPECTED_PACKS\" packfiles &&\n> > > +\n> > > +\t# And verify that there are no loose objects anymore.\n> > > +\tcat >expect <<-\\EOF &&\n> > > +\tinfo\n> > > +\tpack\n> > > +\tEOF\n> > > +\tls .git/objects >actual &&\n> >\n> > I wonder if there is an easier way to check for loose objects here that\n> > doesn't require you to know that the \"info\" and \"pack\" directories\n> > exist. Perhaps something like:\n> >\n> > test_stdout_line_count = 0 find .git/objects/?? -type f\n> >\n> > , or even\n> >\n> >     find .git/objects/?? -type f >loose.objs &&\n> >     test_must_be_empty loose.objs\n>\n> This doesn't work though in case there is not even a single sharding\n> directory:\n>\n>     find: '.git/objects/??': No such file or directory\n>\n> I didn't really have any other idea for now to do this.\n\nMmm, good point. What about using 'git count-objects -v' directly?\n\n    test_loose_object_nr() {\n      local nr=\"$1\" &&\n      git count-objects -v >count &&\n      grep '^count $nr$\" count\n    }\n\nThanks,\nTaylor\n"},{"id":"529519","messageId":"aPqCBDLwVBKKaJU6@nand.local","threadId":"64330","inReplyTo":"20251021-pks-maintenance-geometric-strategy-v2-3-f0d727832b80@pks.im","subject":"Re: [PATCH v2 3/9] builtin/maintenance: introduce \"geometric-repack\" task","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2025-10-23T19:29:08Z","receivedAt":"2025-10-23T19:29:11Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Tue, Oct 21, 2025 at 04:13:25PM +0200, Patrick Steinhardt wrote:\n> +test_expect_success 'geometric repacking task' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tgit init repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\tgit config set maintenance.auto false &&\n> +\t\ttest_commit initial &&\n> +\n> +\t\t# The initial repack causes an all-into-one repack.\n> +\t\tGIT_TRACE2_EVENT=\"$(pwd)/initial-repack.txt\" \\\n> +\t\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n> +\t\ttest_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago --quiet --write-midx <initial-repack.txt &&\n\nNot a show-stopper of course, but I thought from the cover letter that\nthese lines would have gotten wrapped. Whether or not we have lines\nlonger than 80 characters is not a hill that I'd like to die on, of\ncourse ;-). But I brought it up because I am wondering if there were\nsome changes that you meant to include as a part of this round that got\ndropped in the shuffle.\n\nThanks,\nTaylor\n"},{"id":"529520","messageId":"aPqDG+f9ZL4Oc1Ar@nand.local","threadId":"64330","inReplyTo":"20251021-pks-maintenance-geometric-strategy-v2-4-f0d727832b80@pks.im","subject":"Re: [PATCH v2 4/9] builtin/maintenance: make the geometric factor configurable","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2025-10-23T19:33:47Z","receivedAt":"2025-10-23T19:33:50Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Tue, Oct 21, 2025 at 04:13:26PM +0200, Patrick Steinhardt wrote:\n> The geometric repacking task uses a factor of two for its geometric\n> sequence, meaning that each next pack must contain at least twice as\n> many objects as the next-smaller one. In some cases it may be helpful to\n> configure this factor though to reduce the number of packfile merges\n> even further, e.g. in very big repositories. But while git-repack(1)\n> itself supports doing this, the maintenance task does not give us a way\n> to tune it.\n>\n> Introduce a new \"maintenance.geometric-repack.splitFactor\" configuration\n> to plug this gap.\n\nInteresting, this wasn't exactly what I had in my mind when reading the\nlast round, but I think this is worth doing on its own. My apologies for\nbeing ambiguous in my earlier message :-s.\n\nI was suggesting that we have a repack.geometricFactor configuration\nvariable that defaulted to two, could be overridden by --geometric=<n>,\nsuch that we could start doing \"git repack --geometric\" without having\nto write \"=2\" every time.\n\nI think that that is probably still a useful thing to do in and of\nitself, but this change doesn't preclude our ability to do that, since\nit just overwrites what we pass in to 'git repack' when calling it from\nwithin the maintenance context.\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  Documentation/config/maintenance.adoc |  5 +++++\n>  builtin/gc.c                          |  9 ++++++++-\n>  t/t7900-maintenance.sh                | 32 ++++++++++++++++++++++++++++++++\n>  3 files changed, 45 insertions(+), 1 deletion(-)\n>\n> diff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\n> index 26dc5de423f..45fdafc2c63 100644\n> --- a/Documentation/config/maintenance.adoc\n> +++ b/Documentation/config/maintenance.adoc\n> @@ -86,6 +86,11 @@ maintenance.geometric-repack.auto::\n>  \tobjects that would be written into a new packfile. The default value is\n>  \t100.\n>\n> +maintenance.geometric-repack.splitFactor::\n> +\tThis integer config option controls the factor used for the geometric\n> +\tsequence. See the `--geometric=` option in linkgit:git-repack[1] for\n> +\tmore details. Defaults to `2`.\n> +\n\nLooks good.\n\nThanks,\nTaylor\n"},{"id":"529529","messageId":"aPqewYkJFEyVohAn@nand.local","threadId":"64330","inReplyTo":"20251021-pks-maintenance-geometric-strategy-v2-5-f0d727832b80@pks.im","subject":"Re: [PATCH v2 5/9] builtin/maintenance: don't silently ignore invalid strategy","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2025-10-23T21:31:45Z","receivedAt":"2025-10-23T21:31:48Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Tue, Oct 21, 2025 at 04:13:27PM +0200, Patrick Steinhardt wrote:\n> ---\n>  builtin/gc.c           | 17 +++++++++++------\n>  t/t7900-maintenance.sh |  5 +++++\n>  2 files changed, 16 insertions(+), 6 deletions(-)\n\nAll looks good here.\n\nThanks,\nTaylor\n"},{"id":"529530","messageId":"aPqfWGN29S+kr9C/@nand.local","threadId":"64330","inReplyTo":"20251021-pks-maintenance-geometric-strategy-v2-6-f0d727832b80@pks.im","subject":"Re: [PATCH v2 6/9] builtin/maintenance: run maintenance tasks depending on type","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2025-10-23T21:34:16Z","receivedAt":"2025-10-23T21:34:18Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Tue, Oct 21, 2025 at 04:13:28PM +0200, Patrick Steinhardt wrote:\n> We basically have three different ways to execute repository\n> maintenance:\n>\n>   1. Manual maintenance via `git maintenance run`.\n>\n>   2. Automatic maintenance via `git maintenance run --auto`.\n>\n>   3. Scheduled maintenance via `git maintenance run --schedule=`.\n>\n> At the moment, maintenance strategies only have an effect for the last\n> type of maintenance. This is about to change in subsequent commits, but\n> to do so we need to be able to skip some tasks depending on how exactly\n> maintenance was invoked.\n\nThanks for writing this down; my initial thought when reading this patch\nwas that we could distinguish between scheduled tasks and manual ones\nbased on their \"schedule\" field. But this makes sense: some of the\nscheduled tasks might (or might not) be appropriate for manual runs, so\ndistinguishing as you do in this patch makes a ton of sense to me.\n\nThe rest makes sense and looks good.\n\nThanks,\nTaylor\n"},{"id":"529532","messageId":"aPqi71+zRE2KsnCk@nand.local","threadId":"64330","inReplyTo":"20251021-pks-maintenance-geometric-strategy-v2-9-f0d727832b80@pks.im","subject":"Re: [PATCH v2 9/9] builtin/maintenance: introduce \"geometric\" strategy","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2025-10-23T21:49:35Z","receivedAt":"2025-10-23T21:49:37Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Tue, Oct 21, 2025 at 04:13:31PM +0200, Patrick Steinhardt wrote:\n> We have two different repacking strategies in Git:\n>\n>   - The \"gc\" strategy uses git-gc(1).\n>\n>   - The \"incremental\" strategy uses multi-pack indices and `git\n>     multi-pack-index repack` to merge together smaller packfiles as\n>     determined by a specific batch size.\n>\n> The former strategy is our old and trusted default, whereas the latter\n> has historically been used for our scheduled maintenance. But both\n> strategies have their shortcomings:\n>\n>   - The \"gc\" strategy performs regular all-into-one repacks. Furthermore\n>     it is rather inflexible, as it is not easily possible for a user to\n>     enable or disable specific subtasks.\n>\n>   - The \"incremental\" strategy is not a full replacement for the \"gc\"\n>     strategy as it doesn't know to prune stale data.\n>\n> So today, we don't have a strategy that is well-suited for large repos\n> while being a full replacement for the \"gc\" strategy.\n\nWell put.\n\n> Introduce a new \"geometric\" strategy that aims to fill this gap. This\n> strategy invokes all the usual cleanup tasks that git-gc(1) does like\n> pruning reflogs and rerere caches as well as stale worktrees. But where\n> it differs from both the \"gc\" and \"incremental\" strategy is that it uses\n> our geometric repacking infrastructure exposed by git-repack(1) to\n> repack packfiles. The advantage of geometric repacking is that we only\n> need to perform an all-into-one repack when the object count in a repo\n> has grown significantly.\n>\n> One downside of this strategy is that pruning of unreferenced objects is\n> not going to happen regularly anymore. Every geometric repack knows to\n> soak up all loose objects regardless of their reachability, and merging\n> two or more packs doesn't consider reachability, either. Consequently,\n> the number of unreachable objects will grow over time.\n>\n> This is remedied by doing an all-into-one repack instead of a geometric\n> repack whenever we determine that the geometric repack would end up\n> merging all packfiles anyway. This all-into-one repack then performs our\n> usual reachability checks and writes unreachable objects into a cruft\n> pack. As cruft packs won't ever be merged during geometric repacks we\n> can thus phase out these objects over time.\n>\n> Of course, this still means that we retain unreachable objects for far\n> longer than with the \"gc\" strategy. But the maintenance strategy is\n> intended especially for large repositories, where the basic assumption\n> is that the set of unreachable objects will be significantly dwarfed by\n> the number of reachable objects.\n>\n> If this assumption is ever proven to be too disadvantageous we could for\n> example introduce a time-based strategy: if the largest packfile has not\n> been touched for longer than $T, we perform an all-into-one repack. But\n> for now, such a mechanism is deferred into the future as it is not clear\n> yet whether it is needed in the first place.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  Documentation/config/maintenance.adoc |  9 +++++++++\n>  builtin/gc.c                          | 19 +++++++++++++++++++\n>  t/t7900-maintenance.sh                | 20 +++++++++++++++++++-\n>  3 files changed, 47 insertions(+), 1 deletion(-)\n>\n> diff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\n> index b2bacdc822..d0c38f03fa 100644\n> --- a/Documentation/config/maintenance.adoc\n> +++ b/Documentation/config/maintenance.adoc\n> @@ -32,6 +32,15 @@ The possible strategies are:\n>    strategy for scheduled maintenance.\n>  * `gc`: This strategy runs the `gc` task. This is the default strategy for\n>    manual maintenance.\n> +* `geometric`: This strategy performs geometric repacking of packfiles and\n> +  keeps auxiliary data structures up-to-date. The strategy expires data in the\n> +  reflog and removes worktrees that cannot be located anymore. When the\n> +  geometric repacking strategy would decide to do an all-into-one repack, then\n> +  the strategy generates a cruft pack for all unreachable objects. Objects that\n> +  are already part of a cruft pack will be expired.\n> ++\n> +This repacking strategy is a full replacement for the `gc` strategy and is\n> +recommended for large repositories.\n\nNice. I always feel like it's tricky for changes like these to know\nwhere the right place is to draw the line between \"this belongs in the\ncommit message, because it will be useful to reviewers in understanding\nhow I came to this patch\" versus \"this belongs in the documentation for\nmy new feature, because it will be useful to users trying to figure out\nwhat option to use\".\n\nI like the spot where you drew that line here and I think that the\npatch message has details that are useful to reviewers (like the\nhistorical differences between all-into-one strategies and geometric\nrepacking), but that the documentation has the right user-facing details\nand explanations.\n\n\n> diff --git a/builtin/gc.c b/builtin/gc.c\n> index aaff0bae15..9739bb0ea2 100644\n> --- a/builtin/gc.c\n> +++ b/builtin/gc.c\n> @@ -1878,12 +1878,31 @@ static const struct maintenance_strategy incremental_strategy = {\n>  \t},\n>  };\n>\n> +static const struct maintenance_strategy geometric_strategy = {\n> +\t.tasks = {\n> +\t\t[TASK_COMMIT_GRAPH].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n> +\t\t[TASK_COMMIT_GRAPH].schedule = SCHEDULE_HOURLY,\n> +\t\t[TASK_GEOMETRIC_REPACK].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n> +\t\t[TASK_GEOMETRIC_REPACK].schedule = SCHEDULE_DAILY,\n> +\t\t[TASK_PACK_REFS].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n> +\t\t[TASK_PACK_REFS].schedule = SCHEDULE_DAILY,\n> +\t\t[TASK_RERERE_GC].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n> +\t\t[TASK_RERERE_GC].schedule = SCHEDULE_WEEKLY,\n> +\t\t[TASK_REFLOG_EXPIRE].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n> +\t\t[TASK_REFLOG_EXPIRE].schedule = SCHEDULE_WEEKLY,\n> +\t\t[TASK_WORKTREE_PRUNE].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n> +\t\t[TASK_WORKTREE_PRUNE].schedule = SCHEDULE_WEEKLY,\n> +\t},\n> +};\n> +\n\nWhat you wrote here all makes sense to me, so I don't have any comments\non the technical content of 'geometric_strategy'.\n\nAs an aside, I wonder if we should use a nested designated initializer\nhere? It seems a little cleaner to me than doing:\n\n    .tasks = {\n        [TASK_FOO].type = ...,\n        [TASK_FOO].schedule = ...,\n    }\n\nIt's inconsistent with the style of the rest of this file, so if you did\nmake this change I'd suggest adding a prerequisite change that modifies\nexisting strategies to match the new style. But you could imagine\nsomething like the following on top:\n\n--- 8< ---\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 9739bb0ea2..881ef6ad88 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1880,18 +1880,30 @@ static const struct maintenance_strategy incremental_strategy = {\n\n static const struct maintenance_strategy geometric_strategy = {\n \t.tasks = {\n-\t\t[TASK_COMMIT_GRAPH].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n-\t\t[TASK_COMMIT_GRAPH].schedule = SCHEDULE_HOURLY,\n-\t\t[TASK_GEOMETRIC_REPACK].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n-\t\t[TASK_GEOMETRIC_REPACK].schedule = SCHEDULE_DAILY,\n-\t\t[TASK_PACK_REFS].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n-\t\t[TASK_PACK_REFS].schedule = SCHEDULE_DAILY,\n-\t\t[TASK_RERERE_GC].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n-\t\t[TASK_RERERE_GC].schedule = SCHEDULE_WEEKLY,\n-\t\t[TASK_REFLOG_EXPIRE].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n-\t\t[TASK_REFLOG_EXPIRE].schedule = SCHEDULE_WEEKLY,\n-\t\t[TASK_WORKTREE_PRUNE].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n-\t\t[TASK_WORKTREE_PRUNE].schedule = SCHEDULE_WEEKLY,\n+\t\t[TASK_COMMIT_GRAPH] = {\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t\t.schedule = SCHEDULE_HOURLY,\n+\t\t},\n+\t\t[TASK_GEOMETRIC_REPACK] = {\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t\t.schedule = SCHEDULE_DAILY,\n+\t\t},\n+\t\t[TASK_PACK_REFS] = {\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t\t.schedule = SCHEDULE_DAILY,\n+\t\t},\n+\t\t[TASK_RERERE_GC] = {\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t\t.schedule = SCHEDULE_WEEKLY,\n+\t\t},\n+\t\t[TASK_REFLOG_EXPIRE] = {\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t\t.schedule = SCHEDULE_WEEKLY,\n+\t\t},\n+\t\t[TASK_WORKTREE_PRUNE] = {\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t\t.schedule = SCHEDULE_WEEKLY,\n+\t\t},\n \t},\n };\n\n--- >8 ---\n\nThe rest all looks good to me.\n\nThanks,\nTaylor\n"},{"id":"529533","messageId":"aPqjCiVbM/cFkdzi@nand.local","threadId":"64330","inReplyTo":"xmqq7bwlwoml.fsf@gitster.g","subject":"Re: [PATCH v2 0/9] builtin/maintenance: introduce \"geometric\" strategy","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2025-10-23T21:50:02Z","receivedAt":"2025-10-23T21:50:04Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Thu, Oct 23, 2025 at 09:48:18AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n>\n> > The series is built on top of b660e2dcb9 (Sync with 'maint', 2025-10-14)\n> > with tb/incremental-midx-part-3.1 at c886af90f8 (SQUASH??? play well\n> > with other topics by preemptively including \"repository.h\", 2025-09-29)\n> > merged into it.\n> >\n> > Changes in v2:\n> >   - Make the geometric factor configurable via\n> >     \"maintenance.geometric-repack.splitFactor\".\n> >   - Wrap some overly long lines in our tests.\n> >   - Link to v1: https://lore.kernel.org/r/20251016-pks-maintenance-geometric-strategy-v1-0-18943d474203@pks.im\n> >\n> > Thanks!\n>\n> This round looks good to me (I wasn't very careful picking typos and\n> minor mistakes, but the resulting code overall looked sound).\n\nYeah, I am happy with this round as well. I left a few thoughts\nthroughout, but none of them are blockers from my perspective.\n\nThanks for working on this, Patrick!\n\nThanks,\nTaylor\n"},{"id":"529554","messageId":"aPsSWf8W7y_XvVuI@pks.im","threadId":"64330","inReplyTo":"aPp/0/kU1LMBjt7g@nand.local","subject":"Re: [PATCH 3/8] builtin/maintenance: introduce \"geometric-repack\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-24T05:44:57Z","receivedAt":"2025-10-24T05:45:09Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Oct 23, 2025 at 03:19:47PM -0400, Taylor Blau wrote:\n> On Tue, Oct 21, 2025 at 03:00:31PM +0200, Patrick Steinhardt wrote:\n> > > > +\t# Verify that the number of packfiles matches our expectation.\n> > > > +\tls -l .git/objects/pack/*.pack >packfiles &&\n> > > > +\ttest_line_count = \"$EXPECTED_PACKS\" packfiles &&\n> > > > +\n> > > > +\t# And verify that there are no loose objects anymore.\n> > > > +\tcat >expect <<-\\EOF &&\n> > > > +\tinfo\n> > > > +\tpack\n> > > > +\tEOF\n> > > > +\tls .git/objects >actual &&\n> > >\n> > > I wonder if there is an easier way to check for loose objects here that\n> > > doesn't require you to know that the \"info\" and \"pack\" directories\n> > > exist. Perhaps something like:\n> > >\n> > > test_stdout_line_count = 0 find .git/objects/?? -type f\n> > >\n> > > , or even\n> > >\n> > >     find .git/objects/?? -type f >loose.objs &&\n> > >     test_must_be_empty loose.objs\n> >\n> > This doesn't work though in case there is not even a single sharding\n> > directory:\n> >\n> >     find: '.git/objects/??': No such file or directory\n> >\n> > I didn't really have any other idea for now to do this.\n> \n> Mmm, good point. What about using 'git count-objects -v' directly?\n> \n>     test_loose_object_nr() {\n>       local nr=\"$1\" &&\n>       git count-objects -v >count &&\n>       grep '^count $nr$\" count\n>     }\n\nI guess that works. We can even simplify this case as we really only\nwant to check that there are no loose objects at all:\n\n    git count-objects -v >count &&\n    test_grep '^count: 0$' count\n\nThanks!\n\nPatrick\n"},{"id":"529555","messageId":"aPsSZ6IlbaEi9SPY@pks.im","threadId":"64330","inReplyTo":"aPqCBDLwVBKKaJU6@nand.local","subject":"Re: [PATCH v2 3/9] builtin/maintenance: introduce \"geometric-repack\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-24T05:45:11Z","receivedAt":"2025-10-24T05:45:16Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Oct 23, 2025 at 03:29:08PM -0400, Taylor Blau wrote:\n> On Tue, Oct 21, 2025 at 04:13:25PM +0200, Patrick Steinhardt wrote:\n> > +test_expect_success 'geometric repacking task' '\n> > +\ttest_when_finished \"rm -rf repo\" &&\n> > +\tgit init repo &&\n> > +\t(\n> > +\t\tcd repo &&\n> > +\t\tgit config set maintenance.auto false &&\n> > +\t\ttest_commit initial &&\n> > +\n> > +\t\t# The initial repack causes an all-into-one repack.\n> > +\t\tGIT_TRACE2_EVENT=\"$(pwd)/initial-repack.txt\" \\\n> > +\t\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n> > +\t\ttest_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago --quiet --write-midx <initial-repack.txt &&\n> \n> Not a show-stopper of course, but I thought from the cover letter that\n> these lines would have gotten wrapped. Whether or not we have lines\n> longer than 80 characters is not a hill that I'd like to die on, of\n> course ;-). But I brought it up because I am wondering if there were\n> some changes that you meant to include as a part of this round that got\n> dropped in the shuffle.\n\nNah, I guess I merely didn't do my due diligence to also wrap other\noverly long lines. Let me fix those.\n\nPatrick\n"},{"id":"529556","messageId":"aPsSbt0pK4sbmVQj@pks.im","threadId":"64330","inReplyTo":"aPqDG+f9ZL4Oc1Ar@nand.local","subject":"Re: [PATCH v2 4/9] builtin/maintenance: make the geometric factor configurable","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-24T05:45:18Z","receivedAt":"2025-10-24T05:45:24Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Oct 23, 2025 at 03:33:47PM -0400, Taylor Blau wrote:\n> On Tue, Oct 21, 2025 at 04:13:26PM +0200, Patrick Steinhardt wrote:\n> > The geometric repacking task uses a factor of two for its geometric\n> > sequence, meaning that each next pack must contain at least twice as\n> > many objects as the next-smaller one. In some cases it may be helpful to\n> > configure this factor though to reduce the number of packfile merges\n> > even further, e.g. in very big repositories. But while git-repack(1)\n> > itself supports doing this, the maintenance task does not give us a way\n> > to tune it.\n> >\n> > Introduce a new \"maintenance.geometric-repack.splitFactor\" configuration\n> > to plug this gap.\n> \n> Interesting, this wasn't exactly what I had in my mind when reading the\n> last round, but I think this is worth doing on its own. My apologies for\n> being ambiguous in my earlier message :-s.\n> \n> I was suggesting that we have a repack.geometricFactor configuration\n> variable that defaulted to two, could be overridden by --geometric=<n>,\n> such that we could start doing \"git repack --geometric\" without having\n> to write \"=2\" every time.\n> \n> I think that that is probably still a useful thing to do in and of\n> itself, but this change doesn't preclude our ability to do that, since\n> it just overwrites what we pass in to 'git repack' when calling it from\n> within the maintenance context.\n\nYeah, I understood that suggestion, but I still think that in the\ncontext of this series here it makes more sense to piggy back onto\ngit-maintenance(1) itself so that we're in line with the other tasks\nthat we have. All of them are configurable via \"maintenance.*.foobar\"\nknobs, so I wanted to have the same architecture for the geometric task,\nas well.\n\nBut as you say, this doesn't mean that we cannot introduce a config for\ngit-repack(1) at a later point in time, and I also believe that this may\nbe a useful addition indeed. I guess the order of precedence would be\nthat \"repack.geometricFactor\" is overridden by\n\"maintenance.geometric-repack.splitFactor\", as the latter is more\nspecific.\n\nThanks!\n\nPatrick\n"},{"id":"529557","messageId":"aPsSd_4iFogbE0FL@pks.im","threadId":"64330","inReplyTo":"aPqi71+zRE2KsnCk@nand.local","subject":"Re: [PATCH v2 9/9] builtin/maintenance: introduce \"geometric\" strategy","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-24T05:45:27Z","receivedAt":"2025-10-24T05:45:32Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Oct 23, 2025 at 05:49:35PM -0400, Taylor Blau wrote:\n> On Tue, Oct 21, 2025 at 04:13:31PM +0200, Patrick Steinhardt wrote:\n> > diff --git a/builtin/gc.c b/builtin/gc.c\n> > index aaff0bae15..9739bb0ea2 100644\n> > --- a/builtin/gc.c\n> > +++ b/builtin/gc.c\n> > @@ -1878,12 +1878,31 @@ static const struct maintenance_strategy incremental_strategy = {\n> >  \t},\n> >  };\n> >\n> > +static const struct maintenance_strategy geometric_strategy = {\n> > +\t.tasks = {\n> > +\t\t[TASK_COMMIT_GRAPH].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n> > +\t\t[TASK_COMMIT_GRAPH].schedule = SCHEDULE_HOURLY,\n> > +\t\t[TASK_GEOMETRIC_REPACK].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n> > +\t\t[TASK_GEOMETRIC_REPACK].schedule = SCHEDULE_DAILY,\n> > +\t\t[TASK_PACK_REFS].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n> > +\t\t[TASK_PACK_REFS].schedule = SCHEDULE_DAILY,\n> > +\t\t[TASK_RERERE_GC].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n> > +\t\t[TASK_RERERE_GC].schedule = SCHEDULE_WEEKLY,\n> > +\t\t[TASK_REFLOG_EXPIRE].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n> > +\t\t[TASK_REFLOG_EXPIRE].schedule = SCHEDULE_WEEKLY,\n> > +\t\t[TASK_WORKTREE_PRUNE].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n> > +\t\t[TASK_WORKTREE_PRUNE].schedule = SCHEDULE_WEEKLY,\n> > +\t},\n> > +};\n> > +\n> \n> What you wrote here all makes sense to me, so I don't have any comments\n> on the technical content of 'geometric_strategy'.\n> \n> As an aside, I wonder if we should use a nested designated initializer\n> here? It seems a little cleaner to me than doing:\n> \n>     .tasks = {\n>         [TASK_FOO].type = ...,\n>         [TASK_FOO].schedule = ...,\n>     }\n> \n> It's inconsistent with the style of the rest of this file, so if you did\n> make this change I'd suggest adding a prerequisite change that modifies\n> existing strategies to match the new style. But you could imagine\n> something like the following on top:\n\nIt's more verbose, but it indeed reads a lot nicer. I'll take your\nsuggestion, thanks!\n\nPatrick\n"},{"id":"529561","messageId":"20251024-pks-maintenance-geometric-strategy-v3-0-9b5b3bdb4387@pks.im","threadId":"64330","inReplyTo":"20251016-pks-maintenance-geometric-strategy-v1-0-18943d474203@pks.im","subject":"[PATCH v3 00/10] builtin/maintenance: introduce \"geometric\" strategy","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-24T06:57:13Z","receivedAt":"2025-10-24T06:57:21Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nby default, git-maintenance(1) uses git-gc(1) to perform repository\nhousekeeping. This tool has a couple of shortcomings, most importantly\nthat it regularly does all-into-one repacks. This doesn't really work\nall that well in the context of monorepos, where you really want to\navoid repacking all objects regularly.\n\nAn alternative maintenance strategy is the \"incremental\" strategy, but\nthis strategy has two downsides:\n\n  - Strategies in general only apply to scheduled maintenance. So if you\n    run git-maintenance(1), you still end up with git-gc(1).\n\n  - The strategy is designed to not ever delete any data, but a full\n    replacment for git-gc(1) needs to also prune reflogs, rereree caches\n    and vanished worktrees.\n\nThis patch series aims to fix both of these issues.\n\nFirst, the series introduces a new \"geometric\" maintenance task, which\nmakes use of geometric repacking as exposed by git-repack(1) in the\ngeneral case. In the case where a geometric repack ends up merging all\npackfiles into one we instead do an all-into-one repack with cruft packs\nso that we can still phase out objects over time.\n\nSecond, the series extends maintenance strategies to also cover normal\nmaintenance. If the user has configured the \"geometric\" strategy, we'll\nthus use it for both manual and scheduled maintenance. For backwards\ncompatibility, the \"incremental\" strategy is changed so that it uses\ngit-gc(1) for manual maintenance and the other tasks for scheduled\nmaintenance.\n\nThe series is built on top of b660e2dcb9 (Sync with 'maint', 2025-10-14)\nwith tb/incremental-midx-part-3.1 at c886af90f8 (SQUASH??? play well\nwith other topics by preemptively including \"repository.h\", 2025-09-29)\nmerged into it.\n\nChanges in v3:\n  - More line wrapping.\n  - Improve readability of maintenance strategies by using nested\n    designated initializers.\n  - Use git-count-object(1) to count loose objects.\n  - Link to v2: https://lore.kernel.org/r/20251021-pks-maintenance-geometric-strategy-v2-0-f0d727832b80@pks.im\n\nChanges in v2:\n  - Make the geometric factor configurable via\n    \"maintenance.geometric-repack.splitFactor\".\n  - Wrap some overly long lines in our tests.\n  - Link to v1: https://lore.kernel.org/r/20251016-pks-maintenance-geometric-strategy-v1-0-18943d474203@pks.im\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (10):\n      builtin/gc: remove global `repack` variable\n      builtin/gc: make `too_many_loose_objects()` reusable without GC config\n      builtin/maintenance: introduce \"geometric-repack\" task\n      builtin/maintenance: make the geometric factor configurable\n      builtin/maintenance: don't silently ignore invalid strategy\n      builtin/maintenance: improve readability of strategies\n      builtin/maintenance: run maintenance tasks depending on type\n      builtin/maintenance: extend \"maintenance.strategy\" to manual maintenance\n      builtin/maintenance: make \"gc\" strategy accessible\n      builtin/maintenance: introduce \"geometric\" strategy\n\n Documentation/config/maintenance.adoc |  49 +++++-\n builtin/gc.c                          | 313 ++++++++++++++++++++++++++++------\n t/t7900-maintenance.sh                | 245 ++++++++++++++++++++++++++\n 3 files changed, 544 insertions(+), 63 deletions(-)\n\nRange-diff versus v2:\n\n 1:  b853ba54dca =  1:  c35408a33d0 builtin/gc: remove global `repack` variable\n 2:  9bbdfe1b9e5 =  2:  be572fe1542 builtin/gc: make `too_many_loose_objects()` reusable without GC config\n 3:  bcd82ad038e !  3:  5290f6d3e0f builtin/maintenance: introduce \"geometric-repack\" task\n    @@ t/t7900-maintenance.sh: test_expect_success 'maintenance.incremental-repack.auto\n     +\ttest_line_count = \"$EXPECTED_PACKS\" packfiles &&\n     +\n     +\t# And verify that there are no loose objects anymore.\n    -+\tcat >expect <<-\\EOF &&\n    -+\tinfo\n    -+\tpack\n    -+\tEOF\n    -+\tls .git/objects >actual &&\n    -+\ttest_cmp expect actual\n    ++\tgit count-objects -v >count &&\n    ++\ttest_grep '^count: 0$' count\n     +}\n     +\n     +test_expect_success 'geometric repacking task' '\n    @@ t/t7900-maintenance.sh: test_expect_success 'maintenance.incremental-repack.auto\n     +\t\t# The initial repack causes an all-into-one repack.\n     +\t\tGIT_TRACE2_EVENT=\"$(pwd)/initial-repack.txt\" \\\n     +\t\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n    -+\t\ttest_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago --quiet --write-midx <initial-repack.txt &&\n    ++\t\ttest_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago \\\n    ++\t\t\t--quiet --write-midx <initial-repack.txt &&\n     +\n     +\t\t# Repacking should now cause a no-op geometric repack because\n     +\t\t# no packfiles need to be combined.\n    @@ t/t7900-maintenance.sh: test_expect_success 'maintenance.incremental-repack.auto\n     +\t\t# an all-into-one-repack.\n     +\t\tGIT_TRACE2_EVENT=\"$(pwd)/all-into-one-repack.txt\" \\\n     +\t\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n    -+\t\ttest_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago --quiet --write-midx <all-into-one-repack.txt &&\n    ++\t\ttest_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago \\\n    ++\t\t\t--quiet --write-midx <all-into-one-repack.txt &&\n     +\n     +\t\t# The geometric repack soaks up unreachable objects.\n     +\t\techo blob-1 | git hash-object -w --stdin -t blob &&\n    @@ t/t7900-maintenance.sh: test_expect_success 'maintenance.incremental-repack.auto\n     +\t\trun_and_verify_geometric_pack 3 &&\n     +\t\tGIT_TRACE2_EVENT=\"$(pwd)/cruft-repack.txt\" \\\n     +\t\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n    -+\t\ttest_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago --quiet --write-midx <cruft-repack.txt &&\n    ++\t\ttest_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago \\\n    ++\t\t\t--quiet --write-midx <cruft-repack.txt &&\n     +\t\tls .git/objects/pack/*.pack >packs &&\n     +\t\ttest_line_count = 2 packs &&\n     +\t\tls .git/objects/pack/*.mtimes >cruft &&\n 4:  cb10031cc7c =  4:  7f2067fa4ec builtin/maintenance: make the geometric factor configurable\n 5:  7e8f83d4753 =  5:  7a76003215e builtin/maintenance: don't silently ignore invalid strategy\n -:  ----------- >  6:  a6383d121b2 builtin/maintenance: improve readability of strategies\n 6:  4217c37c0bf !  7:  e25c878a3ff builtin/maintenance: run maintenance tasks depending on type\n    @@ builtin/gc.c: static int maintenance_run_tasks(struct maintenance_run_opts *opts\n      \t\tenum schedule_priority schedule;\n      \t} tasks[TASK__COUNT];\n      };\n    - \n    - static const struct maintenance_strategy none_strategy = { 0 };\n    -+\n    +@@ builtin/gc.c: static const struct maintenance_strategy none_strategy = { 0 };\n      static const struct maintenance_strategy default_strategy = {\n      \t.tasks = {\n    --\t\t[TASK_GC].enabled = 1,\n    -+\t\t[TASK_GC].type = MAINTENANCE_TYPE_MANUAL,\n    + \t\t[TASK_GC] = {\n    +-\t\t\t.enabled = 1,\n    ++\t\t\t.type = MAINTENANCE_TYPE_MANUAL,\n    + \t\t},\n      \t},\n      };\n    -+\n    +@@ builtin/gc.c: static const struct maintenance_strategy default_strategy = {\n      static const struct maintenance_strategy incremental_strategy = {\n      \t.tasks = {\n    --\t\t[TASK_COMMIT_GRAPH].enabled = 1,\n    -+\t\t[TASK_COMMIT_GRAPH].type = MAINTENANCE_TYPE_SCHEDULED,\n    - \t\t[TASK_COMMIT_GRAPH].schedule = SCHEDULE_HOURLY,\n    --\t\t[TASK_PREFETCH].enabled = 1,\n    -+\t\t[TASK_PREFETCH].type = MAINTENANCE_TYPE_SCHEDULED,\n    - \t\t[TASK_PREFETCH].schedule = SCHEDULE_HOURLY,\n    --\t\t[TASK_INCREMENTAL_REPACK].enabled = 1,\n    -+\t\t[TASK_INCREMENTAL_REPACK].type = MAINTENANCE_TYPE_SCHEDULED,\n    - \t\t[TASK_INCREMENTAL_REPACK].schedule = SCHEDULE_DAILY,\n    --\t\t[TASK_LOOSE_OBJECTS].enabled = 1,\n    -+\t\t[TASK_LOOSE_OBJECTS].type = MAINTENANCE_TYPE_SCHEDULED,\n    - \t\t[TASK_LOOSE_OBJECTS].schedule = SCHEDULE_DAILY,\n    --\t\t[TASK_PACK_REFS].enabled = 1,\n    -+\t\t[TASK_PACK_REFS].type = MAINTENANCE_TYPE_SCHEDULED,\n    - \t\t[TASK_PACK_REFS].schedule = SCHEDULE_WEEKLY,\n    + \t\t[TASK_COMMIT_GRAPH] = {\n    +-\t\t\t.enabled = 1,\n    ++\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED,\n    + \t\t\t.schedule = SCHEDULE_HOURLY,\n    + \t\t},\n    + \t\t[TASK_PREFETCH] = {\n    +-\t\t\t.enabled = 1,\n    ++\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED,\n    + \t\t\t.schedule = SCHEDULE_HOURLY,\n    + \t\t},\n    + \t\t[TASK_INCREMENTAL_REPACK] = {\n    +-\t\t\t.enabled = 1,\n    ++\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED,\n    + \t\t\t.schedule = SCHEDULE_DAILY,\n    + \t\t},\n    + \t\t[TASK_LOOSE_OBJECTS] = {\n    +-\t\t\t.enabled = 1,\n    ++\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED,\n    + \t\t\t.schedule = SCHEDULE_DAILY,\n    + \t\t},\n    + \t\t[TASK_PACK_REFS] = {\n    +-\t\t\t.enabled = 1,\n    ++\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED,\n    + \t\t\t.schedule = SCHEDULE_WEEKLY,\n    + \t\t},\n      \t},\n    - };\n     @@ builtin/gc.c: static void initialize_task_config(struct maintenance_run_opts *opts,\n      {\n      \tstruct strbuf config_name = STRBUF_INIT;\n 7:  422b16a62a2 !  8:  ba147c3bf33 builtin/maintenance: extend \"maintenance.strategy\" to manual maintenance\n    @@ Documentation/config/maintenance.adoc: detach.\n     \n      ## builtin/gc.c ##\n     @@ builtin/gc.c: static const struct maintenance_strategy incremental_strategy = {\n    - \t\t[TASK_LOOSE_OBJECTS].schedule = SCHEDULE_DAILY,\n    - \t\t[TASK_PACK_REFS].type = MAINTENANCE_TYPE_SCHEDULED,\n    - \t\t[TASK_PACK_REFS].schedule = SCHEDULE_WEEKLY,\n    -+\n    + \t\t\t.type = MAINTENANCE_TYPE_SCHEDULED,\n    + \t\t\t.schedule = SCHEDULE_WEEKLY,\n    + \t\t},\n     +\t\t/*\n     +\t\t * Historically, the \"incremental\" strategy was only available\n     +\t\t * in the context of scheduled maintenance when set up via\n    @@ builtin/gc.c: static const struct maintenance_strategy incremental_strategy = {\n     +\t\t * requested. This is the same as the default strategy, which\n     +\t\t * would have been in use beforehand.\n     +\t\t */\n    -+\t\t[TASK_GC].type = MAINTENANCE_TYPE_MANUAL,\n    ++\t\t[TASK_GC] = {\n    ++\t\t\t.type = MAINTENANCE_TYPE_MANUAL,\n    ++\t\t},\n      \t},\n      };\n      \n 8:  07f5b32a22e !  9:  eebfab4acda builtin/maintenance: make \"gc\" strategy accessible\n    @@ builtin/gc.c: struct maintenance_strategy {\n     -static const struct maintenance_strategy default_strategy = {\n     +static const struct maintenance_strategy gc_strategy = {\n      \t.tasks = {\n    --\t\t[TASK_GC].type = MAINTENANCE_TYPE_MANUAL,\n    -+\t\t[TASK_GC].type = MAINTENANCE_TYPE_MANUAL | MAINTENANCE_TYPE_SCHEDULED,\n    -+\t\t[TASK_GC].schedule = SCHEDULE_DAILY,\n    + \t\t[TASK_GC] = {\n    +-\t\t\t.type = MAINTENANCE_TYPE_MANUAL,\n    ++\t\t\t.type = MAINTENANCE_TYPE_MANUAL | MAINTENANCE_TYPE_SCHEDULED,\n    ++\t\t\t.schedule = SCHEDULE_DAILY,\n    + \t\t},\n      \t},\n      };\n    - \n     @@ builtin/gc.c: static struct maintenance_strategy parse_maintenance_strategy(const char *name)\n      {\n      \tif (!strcasecmp(name, \"incremental\"))\n 9:  c597ae7f94a ! 10:  936358736f3 builtin/maintenance: introduce \"geometric\" strategy\n    @@ builtin/gc.c: static const struct maintenance_strategy incremental_strategy = {\n      \n     +static const struct maintenance_strategy geometric_strategy = {\n     +\t.tasks = {\n    -+\t\t[TASK_COMMIT_GRAPH].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n    -+\t\t[TASK_COMMIT_GRAPH].schedule = SCHEDULE_HOURLY,\n    -+\t\t[TASK_GEOMETRIC_REPACK].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n    -+\t\t[TASK_GEOMETRIC_REPACK].schedule = SCHEDULE_DAILY,\n    -+\t\t[TASK_PACK_REFS].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n    -+\t\t[TASK_PACK_REFS].schedule = SCHEDULE_DAILY,\n    -+\t\t[TASK_RERERE_GC].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n    -+\t\t[TASK_RERERE_GC].schedule = SCHEDULE_WEEKLY,\n    -+\t\t[TASK_REFLOG_EXPIRE].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n    -+\t\t[TASK_REFLOG_EXPIRE].schedule = SCHEDULE_WEEKLY,\n    -+\t\t[TASK_WORKTREE_PRUNE].type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n    -+\t\t[TASK_WORKTREE_PRUNE].schedule = SCHEDULE_WEEKLY,\n    ++\t\t[TASK_COMMIT_GRAPH] = {\n    ++\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n    ++\t\t\t.schedule = SCHEDULE_HOURLY,\n    ++\t\t},\n    ++\t\t[TASK_GEOMETRIC_REPACK] = {\n    ++\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n    ++\t\t\t.schedule = SCHEDULE_DAILY,\n    ++\t\t},\n    ++\t\t[TASK_PACK_REFS] = {\n    ++\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n    ++\t\t\t.schedule = SCHEDULE_DAILY,\n    ++\t\t},\n    ++\t\t[TASK_RERERE_GC] = {\n    ++\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n    ++\t\t\t.schedule = SCHEDULE_WEEKLY,\n    ++\t\t},\n    ++\t\t[TASK_REFLOG_EXPIRE] = {\n    ++\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n    ++\t\t\t.schedule = SCHEDULE_WEEKLY,\n    ++\t\t},\n    ++\t\t[TASK_WORKTREE_PRUNE] = {\n    ++\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n    ++\t\t\t.schedule = SCHEDULE_WEEKLY,\n    ++\t\t},\n     +\t},\n     +};\n     +\n\n---\nbase-commit: 0bb2c786c2349dd6700727153c13d81cbfb41710\nchange-id: 20251015-pks-maintenance-geometric-strategy-580c58581b01\n\n"},{"id":"529562","messageId":"20251024-pks-maintenance-geometric-strategy-v3-1-9b5b3bdb4387@pks.im","threadId":"64330","inReplyTo":"20251024-pks-maintenance-geometric-strategy-v3-0-9b5b3bdb4387@pks.im","subject":"[PATCH v3 01/10] builtin/gc: remove global `repack` variable","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-24T06:57:14Z","receivedAt":"2025-10-24T06:57:23Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The global `repack` variable is used to store all command line arguments\nthat we eventually want to pass to git-repack(1). It is being appended\nto from multiple different functions, which makes it hard to follow the\nlogic. Besides being hard to follow, it also makes it unnecessarily hard\nto reuse this infrastructure in new code.\n\nRefactor the code so that we store this variable on the stack and pass\na pointer to it around as needed. This is done so that we can reuse\n`add_repack_all_options()` in a subsequent commit.\n\nThe refactoring itself is straight-forward. One function that deserves\nattention though is `need_to_gc()`: this function determines whether or\nnot we need to execute garbage collection for `git gc --auto`, but also\nfor `git maintenance run --auto`. But besides figuring out whether we\nhave to perform GC, the function also sets up the `repack` arguments.\n\nFor `git gc --auto` it's trivial to adapt, as we already have the\non-stack variable at our fingertips. But for the maintenance condition\nit's less obvious what to do.\n\nAs it turns out, we can just use another temporary variable there that\nwe then immediately discard. If we need to perform GC we execute a child\ngit-gc(1) process to repack objects for us, and that process will have\nto recompute the arguments anyway.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 74 ++++++++++++++++++++++++++++++++++++------------------------\n 1 file changed, 45 insertions(+), 29 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e19e13d9788..e9772eb3a30 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -55,7 +55,6 @@ static const char * const builtin_gc_usage[] = {\n };\n \n static timestamp_t gc_log_expire_time;\n-static struct strvec repack = STRVEC_INIT;\n static struct tempfile *pidfile;\n static struct lock_file log_lock;\n static struct string_list pack_garbage = STRING_LIST_INIT_DUP;\n@@ -618,48 +617,50 @@ static uint64_t estimate_repack_memory(struct gc_config *cfg,\n \treturn os_cache + heap;\n }\n \n-static int keep_one_pack(struct string_list_item *item, void *data UNUSED)\n+static int keep_one_pack(struct string_list_item *item, void *data)\n {\n-\tstrvec_pushf(&repack, \"--keep-pack=%s\", basename(item->string));\n+\tstruct strvec *args = data;\n+\tstrvec_pushf(args, \"--keep-pack=%s\", basename(item->string));\n \treturn 0;\n }\n \n static void add_repack_all_option(struct gc_config *cfg,\n-\t\t\t\t  struct string_list *keep_pack)\n+\t\t\t\t  struct string_list *keep_pack,\n+\t\t\t\t  struct strvec *args)\n {\n \tif (cfg->prune_expire && !strcmp(cfg->prune_expire, \"now\")\n \t\t&& !(cfg->cruft_packs && cfg->repack_expire_to))\n-\t\tstrvec_push(&repack, \"-a\");\n+\t\tstrvec_push(args, \"-a\");\n \telse if (cfg->cruft_packs) {\n-\t\tstrvec_push(&repack, \"--cruft\");\n+\t\tstrvec_push(args, \"--cruft\");\n \t\tif (cfg->prune_expire)\n-\t\t\tstrvec_pushf(&repack, \"--cruft-expiration=%s\", cfg->prune_expire);\n+\t\t\tstrvec_pushf(args, \"--cruft-expiration=%s\", cfg->prune_expire);\n \t\tif (cfg->max_cruft_size)\n-\t\t\tstrvec_pushf(&repack, \"--max-cruft-size=%lu\",\n+\t\t\tstrvec_pushf(args, \"--max-cruft-size=%lu\",\n \t\t\t\t     cfg->max_cruft_size);\n \t\tif (cfg->repack_expire_to)\n-\t\t\tstrvec_pushf(&repack, \"--expire-to=%s\", cfg->repack_expire_to);\n+\t\t\tstrvec_pushf(args, \"--expire-to=%s\", cfg->repack_expire_to);\n \t} else {\n-\t\tstrvec_push(&repack, \"-A\");\n+\t\tstrvec_push(args, \"-A\");\n \t\tif (cfg->prune_expire)\n-\t\t\tstrvec_pushf(&repack, \"--unpack-unreachable=%s\", cfg->prune_expire);\n+\t\t\tstrvec_pushf(args, \"--unpack-unreachable=%s\", cfg->prune_expire);\n \t}\n \n \tif (keep_pack)\n-\t\tfor_each_string_list(keep_pack, keep_one_pack, NULL);\n+\t\tfor_each_string_list(keep_pack, keep_one_pack, args);\n \n \tif (cfg->repack_filter && *cfg->repack_filter)\n-\t\tstrvec_pushf(&repack, \"--filter=%s\", cfg->repack_filter);\n+\t\tstrvec_pushf(args, \"--filter=%s\", cfg->repack_filter);\n \tif (cfg->repack_filter_to && *cfg->repack_filter_to)\n-\t\tstrvec_pushf(&repack, \"--filter-to=%s\", cfg->repack_filter_to);\n+\t\tstrvec_pushf(args, \"--filter-to=%s\", cfg->repack_filter_to);\n }\n \n-static void add_repack_incremental_option(void)\n+static void add_repack_incremental_option(struct strvec *args)\n {\n-\tstrvec_push(&repack, \"--no-write-bitmap-index\");\n+\tstrvec_push(args, \"--no-write-bitmap-index\");\n }\n \n-static int need_to_gc(struct gc_config *cfg)\n+static int need_to_gc(struct gc_config *cfg, struct strvec *repack_args)\n {\n \t/*\n \t * Setting gc.auto to 0 or negative can disable the\n@@ -700,10 +701,10 @@ static int need_to_gc(struct gc_config *cfg)\n \t\t\t\tstring_list_clear(&keep_pack, 0);\n \t\t}\n \n-\t\tadd_repack_all_option(cfg, &keep_pack);\n+\t\tadd_repack_all_option(cfg, &keep_pack, repack_args);\n \t\tstring_list_clear(&keep_pack, 0);\n \t} else if (too_many_loose_objects(cfg))\n-\t\tadd_repack_incremental_option();\n+\t\tadd_repack_incremental_option(repack_args);\n \telse\n \t\treturn 0;\n \n@@ -852,6 +853,7 @@ int cmd_gc(int argc,\n \tint keep_largest_pack = -1;\n \tint skip_foreground_tasks = 0;\n \ttimestamp_t dummy;\n+\tstruct strvec repack_args = STRVEC_INIT;\n \tstruct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT;\n \tstruct gc_config cfg = GC_CONFIG_INIT;\n \tconst char *prune_expire_sentinel = \"sentinel\";\n@@ -891,7 +893,7 @@ int cmd_gc(int argc,\n \tshow_usage_with_options_if_asked(argc, argv,\n \t\t\t\t\t builtin_gc_usage, builtin_gc_options);\n \n-\tstrvec_pushl(&repack, \"repack\", \"-d\", \"-l\", NULL);\n+\tstrvec_pushl(&repack_args, \"repack\", \"-d\", \"-l\", NULL);\n \n \tgc_config(&cfg);\n \n@@ -914,14 +916,14 @@ int cmd_gc(int argc,\n \t\tdie(_(\"failed to parse prune expiry value %s\"), cfg.prune_expire);\n \n \tif (aggressive) {\n-\t\tstrvec_push(&repack, \"-f\");\n+\t\tstrvec_push(&repack_args, \"-f\");\n \t\tif (cfg.aggressive_depth > 0)\n-\t\t\tstrvec_pushf(&repack, \"--depth=%d\", cfg.aggressive_depth);\n+\t\t\tstrvec_pushf(&repack_args, \"--depth=%d\", cfg.aggressive_depth);\n \t\tif (cfg.aggressive_window > 0)\n-\t\t\tstrvec_pushf(&repack, \"--window=%d\", cfg.aggressive_window);\n+\t\t\tstrvec_pushf(&repack_args, \"--window=%d\", cfg.aggressive_window);\n \t}\n \tif (opts.quiet)\n-\t\tstrvec_push(&repack, \"-q\");\n+\t\tstrvec_push(&repack_args, \"-q\");\n \n \tif (opts.auto_flag) {\n \t\tif (cfg.detach_auto && opts.detach < 0)\n@@ -930,7 +932,7 @@ int cmd_gc(int argc,\n \t\t/*\n \t\t * Auto-gc should be least intrusive as possible.\n \t\t */\n-\t\tif (!need_to_gc(&cfg)) {\n+\t\tif (!need_to_gc(&cfg, &repack_args)) {\n \t\t\tret = 0;\n \t\t\tgoto out;\n \t\t}\n@@ -952,7 +954,7 @@ int cmd_gc(int argc,\n \t\t\tfind_base_packs(&keep_pack, cfg.big_pack_threshold);\n \t\t}\n \n-\t\tadd_repack_all_option(&cfg, &keep_pack);\n+\t\tadd_repack_all_option(&cfg, &keep_pack, &repack_args);\n \t\tstring_list_clear(&keep_pack, 0);\n \t}\n \n@@ -1014,9 +1016,9 @@ int cmd_gc(int argc,\n \n \t\trepack_cmd.git_cmd = 1;\n \t\trepack_cmd.close_object_store = 1;\n-\t\tstrvec_pushv(&repack_cmd.args, repack.v);\n+\t\tstrvec_pushv(&repack_cmd.args, repack_args.v);\n \t\tif (run_command(&repack_cmd))\n-\t\t\tdie(FAILED_RUN, repack.v[0]);\n+\t\t\tdie(FAILED_RUN, repack_args.v[0]);\n \n \t\tif (cfg.prune_expire) {\n \t\t\tstruct child_process prune_cmd = CHILD_PROCESS_INIT;\n@@ -1067,6 +1069,7 @@ int cmd_gc(int argc,\n \n out:\n \tmaintenance_run_opts_release(&opts);\n+\tstrvec_clear(&repack_args);\n \tgc_config_release(&cfg);\n \treturn 0;\n }\n@@ -1269,6 +1272,19 @@ static int maintenance_task_gc_background(struct maintenance_run_opts *opts,\n \treturn run_command(&child);\n }\n \n+static int gc_condition(struct gc_config *cfg)\n+{\n+\t/*\n+\t * Note that it's fine to drop the repack arguments here, as we execute\n+\t * git-gc(1) as a separate child process anyway. So it knows to compute\n+\t * these arguments again.\n+\t */\n+\tstruct strvec repack_args = STRVEC_INIT;\n+\tint ret = need_to_gc(cfg, &repack_args);\n+\tstrvec_clear(&repack_args);\n+\treturn ret;\n+}\n+\n static int prune_packed(struct maintenance_run_opts *opts)\n {\n \tstruct child_process child = CHILD_PROCESS_INIT;\n@@ -1596,7 +1612,7 @@ static const struct maintenance_task tasks[] = {\n \t\t.name = \"gc\",\n \t\t.foreground = maintenance_task_gc_foreground,\n \t\t.background = maintenance_task_gc_background,\n-\t\t.auto_condition = need_to_gc,\n+\t\t.auto_condition = gc_condition,\n \t},\n \t[TASK_COMMIT_GRAPH] = {\n \t\t.name = \"commit-graph\",\n\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529563","messageId":"20251024-pks-maintenance-geometric-strategy-v3-2-9b5b3bdb4387@pks.im","threadId":"64330","inReplyTo":"20251024-pks-maintenance-geometric-strategy-v3-0-9b5b3bdb4387@pks.im","subject":"[PATCH v3 02/10] builtin/gc: make `too_many_loose_objects()` reusable without GC config","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-24T06:57:15Z","receivedAt":"2025-10-24T06:57:27Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"To decide whether or not a repository needs to be repacked we estimate\nthe number of loose objects. If the number exceeds a certain threshold\nwe perform the repack, otherwise we don't.\n\nThis is done via `too_many_loose_objects()`, which takes as parameter\nthe `struct gc_config`. This configuration is only used to determine the\nthreshold. In a subsequent commit we'll add another caller of this\nfunction that wants to pass a different limit than the one stored in\nthat structure.\n\nRefactor the function accordingly so that we only take the limit as\nparameter instead of the whole structure.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 8 ++++----\n 1 file changed, 4 insertions(+), 4 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e9772eb3a30..026d3a1d714 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -447,7 +447,7 @@ static int rerere_gc_condition(struct gc_config *cfg UNUSED)\n \treturn should_gc;\n }\n \n-static int too_many_loose_objects(struct gc_config *cfg)\n+static int too_many_loose_objects(int limit)\n {\n \t/*\n \t * Quickly check if a \"gc\" is needed, by estimating how\n@@ -469,7 +469,7 @@ static int too_many_loose_objects(struct gc_config *cfg)\n \tif (!dir)\n \t\treturn 0;\n \n-\tauto_threshold = DIV_ROUND_UP(cfg->gc_auto_threshold, 256);\n+\tauto_threshold = DIV_ROUND_UP(limit, 256);\n \twhile ((ent = readdir(dir)) != NULL) {\n \t\tif (strspn(ent->d_name, \"0123456789abcdef\") != hexsz_loose ||\n \t\t    ent->d_name[hexsz_loose] != '\\0')\n@@ -703,7 +703,7 @@ static int need_to_gc(struct gc_config *cfg, struct strvec *repack_args)\n \n \t\tadd_repack_all_option(cfg, &keep_pack, repack_args);\n \t\tstring_list_clear(&keep_pack, 0);\n-\t} else if (too_many_loose_objects(cfg))\n+\t} else if (too_many_loose_objects(cfg->gc_auto_threshold))\n \t\tadd_repack_incremental_option(repack_args);\n \telse\n \t\treturn 0;\n@@ -1057,7 +1057,7 @@ int cmd_gc(int argc,\n \t\t\t\t\t     !opts.quiet && !daemonized ? COMMIT_GRAPH_WRITE_PROGRESS : 0,\n \t\t\t\t\t     NULL);\n \n-\tif (opts.auto_flag && too_many_loose_objects(&cfg))\n+\tif (opts.auto_flag && too_many_loose_objects(cfg.gc_auto_threshold))\n \t\twarning(_(\"There are too many unreachable loose objects; \"\n \t\t\t\"run 'git prune' to remove them.\"));\n \n\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529564","messageId":"20251024-pks-maintenance-geometric-strategy-v3-3-9b5b3bdb4387@pks.im","threadId":"64330","inReplyTo":"20251024-pks-maintenance-geometric-strategy-v3-0-9b5b3bdb4387@pks.im","subject":"[PATCH v3 03/10] builtin/maintenance: introduce \"geometric-repack\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-24T06:57:16Z","receivedAt":"2025-10-24T06:57:29Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"geometric-repack\" task. This task uses our geometric\nrepack infrastructure as provided by git-repack(1) itself, which is a\nstrategy that especially hosting providers tend to use to amortize the\ncosts of repacking objects.\n\nThere is one issue though with geometric repacks, namely that they\nunconditionally pack all loose objects, regardless of whether or not\nthey are reachable. This is done because it means that we can completely\nskip the reachability step, which significantly speeds up the operation.\nBut it has the big downside that we are unable to expire objects over\ntime.\n\nTo address this issue we thus use a split strategy in this new task:\nwhenever a geometric repack would merge together all packs, we instead\ndo an all-into-one repack. By default, these all-into-one repacks have\ncruft packs enabled, so unreachable objects would now be written into\ntheir own pack. Consequently, they won't be soaked up during geometric\nrepacking anymore and can be expired with the next full repack, assuming\nthat their expiry date has surpassed.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/maintenance.adoc |  11 +++\n builtin/gc.c                          | 102 +++++++++++++++++++++++++\n t/t7900-maintenance.sh                | 138 ++++++++++++++++++++++++++++++++++\n 3 files changed, 251 insertions(+)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex 2f719342183..26dc5de423f 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -75,6 +75,17 @@ maintenance.incremental-repack.auto::\n \tnumber of pack-files not in the multi-pack-index is at least the value\n \tof `maintenance.incremental-repack.auto`. The default value is 10.\n \n+maintenance.geometric-repack.auto::\n+\tThis integer config option controls how often the `geometric-repack`\n+\ttask should be run as part of `git maintenance run --auto`. If zero,\n+\tthen the `geometric-repack` task will not run with the `--auto`\n+\toption. A negative value will force the task to run every time.\n+\tOtherwise, a positive value implies the command should run either when\n+\tthere are packfiles that need to be merged together to retain the\n+\tgeometric progression, or when there are at least this many loose\n+\tobjects that would be written into a new packfile. The default value is\n+\t100.\n+\n maintenance.reflog-expire.auto::\n \tThis integer config option controls how often the `reflog-expire` task\n \tshould be run as part of `git maintenance run --auto`. If zero, then\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 026d3a1d714..2c9ecd464d2 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -34,6 +34,7 @@\n #include \"pack-objects.h\"\n #include \"path.h\"\n #include \"reflog.h\"\n+#include \"repack.h\"\n #include \"rerere.h\"\n #include \"blob.h\"\n #include \"tree.h\"\n@@ -254,6 +255,7 @@ enum maintenance_task_label {\n \tTASK_PREFETCH,\n \tTASK_LOOSE_OBJECTS,\n \tTASK_INCREMENTAL_REPACK,\n+\tTASK_GEOMETRIC_REPACK,\n \tTASK_GC,\n \tTASK_COMMIT_GRAPH,\n \tTASK_PACK_REFS,\n@@ -1566,6 +1568,101 @@ static int maintenance_task_incremental_repack(struct maintenance_run_opts *opts\n \treturn 0;\n }\n \n+static int maintenance_task_geometric_repack(struct maintenance_run_opts *opts,\n+\t\t\t\t\t     struct gc_config *cfg)\n+{\n+\tstruct pack_geometry geometry = {\n+\t\t.split_factor = 2,\n+\t};\n+\tstruct pack_objects_args po_args = {\n+\t\t.local = 1,\n+\t};\n+\tstruct existing_packs existing_packs = EXISTING_PACKS_INIT;\n+\tstruct string_list kept_packs = STRING_LIST_INIT_DUP;\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\tint ret;\n+\n+\texisting_packs.repo = the_repository;\n+\texisting_packs_collect(&existing_packs, &kept_packs);\n+\tpack_geometry_init(&geometry, &existing_packs, &po_args);\n+\tpack_geometry_split(&geometry);\n+\n+\tchild.git_cmd = 1;\n+\n+\tstrvec_pushl(&child.args, \"repack\", \"-d\", \"-l\", NULL);\n+\tif (geometry.split < geometry.pack_nr)\n+\t\tstrvec_push(&child.args, \"--geometric=2\");\n+\telse\n+\t\tadd_repack_all_option(cfg, NULL, &child.args);\n+\tif (opts->quiet)\n+\t\tstrvec_push(&child.args, \"--quiet\");\n+\tif (the_repository->settings.core_multi_pack_index)\n+\t\tstrvec_push(&child.args, \"--write-midx\");\n+\n+\tif (run_command(&child)) {\n+\t\tret = error(_(\"failed to perform geometric repack\"));\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\texisting_packs_release(&existing_packs);\n+\tpack_geometry_release(&geometry);\n+\treturn ret;\n+}\n+\n+static int geometric_repack_auto_condition(struct gc_config *cfg UNUSED)\n+{\n+\tstruct pack_geometry geometry = {\n+\t\t.split_factor = 2,\n+\t};\n+\tstruct pack_objects_args po_args = {\n+\t\t.local = 1,\n+\t};\n+\tstruct existing_packs existing_packs = EXISTING_PACKS_INIT;\n+\tstruct string_list kept_packs = STRING_LIST_INIT_DUP;\n+\tint auto_value = 100;\n+\tint ret;\n+\n+\trepo_config_get_int(the_repository, \"maintenance.geometric-repack.auto\",\n+\t\t\t    &auto_value);\n+\tif (!auto_value)\n+\t\treturn 0;\n+\tif (auto_value < 0)\n+\t\treturn 1;\n+\n+\texisting_packs.repo = the_repository;\n+\texisting_packs_collect(&existing_packs, &kept_packs);\n+\tpack_geometry_init(&geometry, &existing_packs, &po_args);\n+\tpack_geometry_split(&geometry);\n+\n+\t/*\n+\t * When we'd merge at least two packs with one another we always\n+\t * perform the repack.\n+\t */\n+\tif (geometry.split) {\n+\t\tret = 1;\n+\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t * Otherwise, we estimate the number of loose objects to determine\n+\t * whether we want to create a new packfile or not.\n+\t */\n+\tif (too_many_loose_objects(auto_value)) {\n+\t\tret = 1;\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\texisting_packs_release(&existing_packs);\n+\tpack_geometry_release(&geometry);\n+\treturn ret;\n+}\n+\n typedef int (*maintenance_task_fn)(struct maintenance_run_opts *opts,\n \t\t\t\t   struct gc_config *cfg);\n typedef int (*maintenance_auto_fn)(struct gc_config *cfg);\n@@ -1608,6 +1705,11 @@ static const struct maintenance_task tasks[] = {\n \t\t.background = maintenance_task_incremental_repack,\n \t\t.auto_condition = incremental_repack_auto_condition,\n \t},\n+\t[TASK_GEOMETRIC_REPACK] = {\n+\t\t.name = \"geometric-repack\",\n+\t\t.background = maintenance_task_geometric_repack,\n+\t\t.auto_condition = geometric_repack_auto_condition,\n+\t},\n \t[TASK_GC] = {\n \t\t.name = \"gc\",\n \t\t.foreground = maintenance_task_gc_foreground,\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex ddd273d8dc2..ace0ba83002 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -465,6 +465,144 @@ test_expect_success 'maintenance.incremental-repack.auto (when config is unset)'\n \t)\n '\n \n+run_and_verify_geometric_pack () {\n+\tEXPECTED_PACKS=\"$1\" &&\n+\n+\t# Verify that we perform a geometric repack.\n+\trm -f \"trace2.txt\" &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/trace2.txt\" \\\n+\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n+\ttest_subcommand git repack -d -l --geometric=2 \\\n+\t\t--quiet --write-midx <trace2.txt &&\n+\n+\t# Verify that the number of packfiles matches our expectation.\n+\tls -l .git/objects/pack/*.pack >packfiles &&\n+\ttest_line_count = \"$EXPECTED_PACKS\" packfiles &&\n+\n+\t# And verify that there are no loose objects anymore.\n+\tgit count-objects -v >count &&\n+\ttest_grep '^count: 0$' count\n+}\n+\n+test_expect_success 'geometric repacking task' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tgit config set maintenance.auto false &&\n+\t\ttest_commit initial &&\n+\n+\t\t# The initial repack causes an all-into-one repack.\n+\t\tGIT_TRACE2_EVENT=\"$(pwd)/initial-repack.txt\" \\\n+\t\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n+\t\ttest_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago \\\n+\t\t\t--quiet --write-midx <initial-repack.txt &&\n+\n+\t\t# Repacking should now cause a no-op geometric repack because\n+\t\t# no packfiles need to be combined.\n+\t\tls -l .git/objects/pack >before &&\n+\t\trun_and_verify_geometric_pack 1 &&\n+\t\tls -l .git/objects/pack >after &&\n+\t\ttest_cmp before after &&\n+\n+\t\t# This incremental change creates a new packfile that only\n+\t\t# soaks up loose objects. The packfiles are not getting merged\n+\t\t# at this point.\n+\t\ttest_commit loose &&\n+\t\trun_and_verify_geometric_pack 2 &&\n+\n+\t\t# Both packfiles have 3 objects, so the next run would cause us\n+\t\t# to merge all packfiles together. This should be turned into\n+\t\t# an all-into-one-repack.\n+\t\tGIT_TRACE2_EVENT=\"$(pwd)/all-into-one-repack.txt\" \\\n+\t\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n+\t\ttest_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago \\\n+\t\t\t--quiet --write-midx <all-into-one-repack.txt &&\n+\n+\t\t# The geometric repack soaks up unreachable objects.\n+\t\techo blob-1 | git hash-object -w --stdin -t blob &&\n+\t\trun_and_verify_geometric_pack 2 &&\n+\n+\t\t# A second unreachable object should be written into another packfile.\n+\t\techo blob-2 | git hash-object -w --stdin -t blob &&\n+\t\trun_and_verify_geometric_pack 3 &&\n+\n+\t\t# And these two small packs should now be merged via the\n+\t\t# geometric repack. The large packfile should remain intact.\n+\t\trun_and_verify_geometric_pack 2 &&\n+\n+\t\t# If we now add two more objects and repack twice we should\n+\t\t# then see another all-into-one repack. This time around\n+\t\t# though, as we have unreachable objects, we should also see a\n+\t\t# cruft pack.\n+\t\techo blob-3 | git hash-object -w --stdin -t blob &&\n+\t\techo blob-4 | git hash-object -w --stdin -t blob &&\n+\t\trun_and_verify_geometric_pack 3 &&\n+\t\tGIT_TRACE2_EVENT=\"$(pwd)/cruft-repack.txt\" \\\n+\t\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n+\t\ttest_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago \\\n+\t\t\t--quiet --write-midx <cruft-repack.txt &&\n+\t\tls .git/objects/pack/*.pack >packs &&\n+\t\ttest_line_count = 2 packs &&\n+\t\tls .git/objects/pack/*.mtimes >cruft &&\n+\t\ttest_line_count = 1 cruft\n+\t)\n+'\n+\n+test_geometric_repack_needed () {\n+\tNEEDED=\"$1\"\n+\tGEOMETRIC_CONFIG=\"$2\" &&\n+\trm -f trace2.txt &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/trace2.txt\" \\\n+\t\tgit ${GEOMETRIC_CONFIG:+-c maintenance.geometric-repack.$GEOMETRIC_CONFIG} \\\n+\t\tmaintenance run --auto --task=geometric-repack 2>/dev/null &&\n+\tcase \"$NEEDED\" in\n+\ttrue)\n+\t\ttest_grep \"\\[\\\"git\\\",\\\"repack\\\",\" trace2.txt;;\n+\tfalse)\n+\t\t! test_grep \"\\[\\\"git\\\",\\\"repack\\\",\" trace2.txt;;\n+\t*)\n+\t\tBUG \"invalid parameter: $NEEDED\";;\n+\tesac\n+}\n+\n+test_expect_success 'geometric repacking with --auto' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\n+\t\t# An empty repository does not need repacking, except when\n+\t\t# explicitly told to do it.\n+\t\ttest_geometric_repack_needed false &&\n+\t\ttest_geometric_repack_needed false auto=0 &&\n+\t\ttest_geometric_repack_needed false auto=1 &&\n+\t\ttest_geometric_repack_needed true auto=-1 &&\n+\n+\t\ttest_oid_init &&\n+\n+\t\t# Loose objects cause a repack when crossing the limit. Note\n+\t\t# that the number of objects gets extrapolated by having a look\n+\t\t# at the \"objects/17/\" shard.\n+\t\ttest_commit \"$(test_oid blob17_1)\" &&\n+\t\ttest_geometric_repack_needed false &&\n+\t\ttest_commit \"$(test_oid blob17_2)\" &&\n+\t\ttest_geometric_repack_needed false auto=257 &&\n+\t\ttest_geometric_repack_needed true auto=256 &&\n+\n+\t\t# Force another repack.\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_geometric_repack_needed true auto=-1 &&\n+\n+\t\t# We now have two packfiles that would be merged together. As\n+\t\t# such, the repack should always happen unless the user has\n+\t\t# disabled the auto task.\n+\t\ttest_geometric_repack_needed false auto=0 &&\n+\t\ttest_geometric_repack_needed true auto=9000\n+\t)\n+'\n+\n test_expect_success 'pack-refs task' '\n \tfor n in $(test_seq 1 5)\n \tdo\n\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529565","messageId":"20251024-pks-maintenance-geometric-strategy-v3-4-9b5b3bdb4387@pks.im","threadId":"64330","inReplyTo":"20251024-pks-maintenance-geometric-strategy-v3-0-9b5b3bdb4387@pks.im","subject":"[PATCH v3 04/10] builtin/maintenance: make the geometric factor configurable","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-24T06:57:17Z","receivedAt":"2025-10-24T06:57:32Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The geometric repacking task uses a factor of two for its geometric\nsequence, meaning that each next pack must contain at least twice as\nmany objects as the next-smaller one. In some cases it may be helpful to\nconfigure this factor though to reduce the number of packfile merges\neven further, e.g. in very big repositories. But while git-repack(1)\nitself supports doing this, the maintenance task does not give us a way\nto tune it.\n\nIntroduce a new \"maintenance.geometric-repack.splitFactor\" configuration\nto plug this gap.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/maintenance.adoc |  5 +++++\n builtin/gc.c                          |  9 ++++++++-\n t/t7900-maintenance.sh                | 32 ++++++++++++++++++++++++++++++++\n 3 files changed, 45 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex 26dc5de423f..45fdafc2c63 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -86,6 +86,11 @@ maintenance.geometric-repack.auto::\n \tobjects that would be written into a new packfile. The default value is\n \t100.\n \n+maintenance.geometric-repack.splitFactor::\n+\tThis integer config option controls the factor used for the geometric\n+\tsequence. See the `--geometric=` option in linkgit:git-repack[1] for\n+\tmore details. Defaults to `2`.\n+\n maintenance.reflog-expire.auto::\n \tThis integer config option controls how often the `reflog-expire` task\n \tshould be run as part of `git maintenance run --auto`. If zero, then\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 2c9ecd464d2..fb1a82e0304 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1582,6 +1582,9 @@ static int maintenance_task_geometric_repack(struct maintenance_run_opts *opts,\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tint ret;\n \n+\trepo_config_get_int(the_repository, \"maintenance.geometric-repack.splitFactor\",\n+\t\t\t    &geometry.split_factor);\n+\n \texisting_packs.repo = the_repository;\n \texisting_packs_collect(&existing_packs, &kept_packs);\n \tpack_geometry_init(&geometry, &existing_packs, &po_args);\n@@ -1591,7 +1594,8 @@ static int maintenance_task_geometric_repack(struct maintenance_run_opts *opts,\n \n \tstrvec_pushl(&child.args, \"repack\", \"-d\", \"-l\", NULL);\n \tif (geometry.split < geometry.pack_nr)\n-\t\tstrvec_push(&child.args, \"--geometric=2\");\n+\t\tstrvec_pushf(&child.args, \"--geometric=%d\",\n+\t\t\t     geometry.split_factor);\n \telse\n \t\tadd_repack_all_option(cfg, NULL, &child.args);\n \tif (opts->quiet)\n@@ -1632,6 +1636,9 @@ static int geometric_repack_auto_condition(struct gc_config *cfg UNUSED)\n \tif (auto_value < 0)\n \t\treturn 1;\n \n+\trepo_config_get_int(the_repository, \"maintenance.geometric-repack.splitFactor\",\n+\t\t\t    &geometry.split_factor);\n+\n \texisting_packs.repo = the_repository;\n \texisting_packs_collect(&existing_packs, &kept_packs);\n \tpack_geometry_init(&geometry, &existing_packs, &po_args);\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex ace0ba83002..e0352fd1965 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -603,6 +603,38 @@ test_expect_success 'geometric repacking with --auto' '\n \t)\n '\n \n+test_expect_success 'geometric repacking honors configured split factor' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tgit config set maintenance.auto false &&\n+\n+\t\t# Create three different packs with 9, 2 and 1 object, respectively.\n+\t\t# This is done so that only a subset of packs would be merged\n+\t\t# together so that we can verify that `git repack` receives the\n+\t\t# correct geometric factor.\n+\t\tfor i in $(test_seq 9)\n+\t\tdo\n+\t\t\techo first-$i | git hash-object -w --stdin -t blob || return 1\n+\t\tdone &&\n+\t\tgit repack --geometric=2 -d &&\n+\n+\t\tfor i in $(test_seq 2)\n+\t\tdo\n+\t\t\techo second-$i | git hash-object -w --stdin -t blob || return 1\n+\t\tdone &&\n+\t\tgit repack --geometric=2 -d &&\n+\n+\t\techo third | git hash-object -w --stdin -t blob &&\n+\t\tgit repack --geometric=2 -d &&\n+\n+\t\ttest_geometric_repack_needed false splitFactor=2 &&\n+\t\ttest_geometric_repack_needed true splitFactor=3 &&\n+\t\ttest_subcommand git repack -d -l --geometric=3 --quiet --write-midx <trace2.txt\n+\t)\n+'\n+\n test_expect_success 'pack-refs task' '\n \tfor n in $(test_seq 1 5)\n \tdo\n\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529566","messageId":"20251024-pks-maintenance-geometric-strategy-v3-5-9b5b3bdb4387@pks.im","threadId":"64330","inReplyTo":"20251024-pks-maintenance-geometric-strategy-v3-0-9b5b3bdb4387@pks.im","subject":"[PATCH v3 05/10] builtin/maintenance: don't silently ignore invalid strategy","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-24T06:57:18Z","receivedAt":"2025-10-24T06:57:37Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When parsing maintenance strategies we completely ignore the\nuser-configured value in case it is unknown to us. This makes it\nbasically undiscoverable to the user that scheduled maintenance is\ndevolving into a no-op.\n\nChange this to instead die when seeing an unknown maintenance strategy.\nWhile at it, pull out the parsing logic into a separate function so that\nwe can reuse it in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c           | 17 +++++++++++------\n t/t7900-maintenance.sh |  5 +++++\n 2 files changed, 16 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex fb1a82e0304..726d944d3bd 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1855,6 +1855,13 @@ static const struct maintenance_strategy incremental_strategy = {\n \t},\n };\n \n+static struct maintenance_strategy parse_maintenance_strategy(const char *name)\n+{\n+\tif (!strcasecmp(name, \"incremental\"))\n+\t\treturn incremental_strategy;\n+\tdie(_(\"unknown maintenance strategy: '%s'\"), name);\n+}\n+\n static void initialize_task_config(struct maintenance_run_opts *opts,\n \t\t\t\t   const struct string_list *selected_tasks)\n {\n@@ -1890,12 +1897,10 @@ static void initialize_task_config(struct maintenance_run_opts *opts,\n \t * override specific aspects of our strategy.\n \t */\n \tif (opts->schedule) {\n-\t\tstrategy = none_strategy;\n-\n-\t\tif (!repo_config_get_string_tmp(the_repository, \"maintenance.strategy\", &config_str)) {\n-\t\t\tif (!strcasecmp(config_str, \"incremental\"))\n-\t\t\t\tstrategy = incremental_strategy;\n-\t\t}\n+\t\tif (!repo_config_get_string_tmp(the_repository, \"maintenance.strategy\", &config_str))\n+\t\t\tstrategy = parse_maintenance_strategy(config_str);\n+\t\telse\n+\t\t\tstrategy = none_strategy;\n \t} else {\n \t\tstrategy = default_strategy;\n \t}\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex e0352fd1965..0fb917dd7b7 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -1263,6 +1263,11 @@ test_expect_success 'fails when running outside of a repository' '\n \tnongit test_must_fail git maintenance unregister\n '\n \n+test_expect_success 'fails when configured to use an invalid strategy' '\n+\ttest_must_fail git -c maintenance.strategy=invalid maintenance run --schedule=hourly 2>err &&\n+\ttest_grep \"unknown maintenance strategy: .invalid.\" err\n+'\n+\n test_expect_success 'register and unregister bare repo' '\n \ttest_when_finished \"git config --global --unset-all maintenance.repo || :\" &&\n \ttest_might_fail git config --global --unset-all maintenance.repo &&\n\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529567","messageId":"20251024-pks-maintenance-geometric-strategy-v3-6-9b5b3bdb4387@pks.im","threadId":"64330","inReplyTo":"20251024-pks-maintenance-geometric-strategy-v3-0-9b5b3bdb4387@pks.im","subject":"[PATCH v3 06/10] builtin/maintenance: improve readability of strategies","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-24T06:57:19Z","receivedAt":"2025-10-24T06:57:39Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Our maintenance strategies are essentially a large array of structures,\nwhere each of the tasks can be enabled and scheduled individually. With\nthe current layout though all the configuration sits on the same nesting\nlayer, which makes it a bit hard to discern which initialized fields\nbelong to what task.\n\nImprove readability of the individual tasks by using nested designated\ninitializers instead.\n\nSuggested-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 36 +++++++++++++++++++++++++-----------\n 1 file changed, 25 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 726d944d3bd..0ba6e59de14 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1835,23 +1835,37 @@ struct maintenance_strategy {\n };\n \n static const struct maintenance_strategy none_strategy = { 0 };\n+\n static const struct maintenance_strategy default_strategy = {\n \t.tasks = {\n-\t\t[TASK_GC].enabled = 1,\n+\t\t[TASK_GC] = {\n+\t\t\t.enabled = 1,\n+\t\t},\n \t},\n };\n+\n static const struct maintenance_strategy incremental_strategy = {\n \t.tasks = {\n-\t\t[TASK_COMMIT_GRAPH].enabled = 1,\n-\t\t[TASK_COMMIT_GRAPH].schedule = SCHEDULE_HOURLY,\n-\t\t[TASK_PREFETCH].enabled = 1,\n-\t\t[TASK_PREFETCH].schedule = SCHEDULE_HOURLY,\n-\t\t[TASK_INCREMENTAL_REPACK].enabled = 1,\n-\t\t[TASK_INCREMENTAL_REPACK].schedule = SCHEDULE_DAILY,\n-\t\t[TASK_LOOSE_OBJECTS].enabled = 1,\n-\t\t[TASK_LOOSE_OBJECTS].schedule = SCHEDULE_DAILY,\n-\t\t[TASK_PACK_REFS].enabled = 1,\n-\t\t[TASK_PACK_REFS].schedule = SCHEDULE_WEEKLY,\n+\t\t[TASK_COMMIT_GRAPH] = {\n+\t\t\t.enabled = 1,\n+\t\t\t.schedule = SCHEDULE_HOURLY,\n+\t\t},\n+\t\t[TASK_PREFETCH] = {\n+\t\t\t.enabled = 1,\n+\t\t\t.schedule = SCHEDULE_HOURLY,\n+\t\t},\n+\t\t[TASK_INCREMENTAL_REPACK] = {\n+\t\t\t.enabled = 1,\n+\t\t\t.schedule = SCHEDULE_DAILY,\n+\t\t},\n+\t\t[TASK_LOOSE_OBJECTS] = {\n+\t\t\t.enabled = 1,\n+\t\t\t.schedule = SCHEDULE_DAILY,\n+\t\t},\n+\t\t[TASK_PACK_REFS] = {\n+\t\t\t.enabled = 1,\n+\t\t\t.schedule = SCHEDULE_WEEKLY,\n+\t\t},\n \t},\n };\n \n\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529568","messageId":"20251024-pks-maintenance-geometric-strategy-v3-7-9b5b3bdb4387@pks.im","threadId":"64330","inReplyTo":"20251024-pks-maintenance-geometric-strategy-v3-0-9b5b3bdb4387@pks.im","subject":"[PATCH v3 07/10] builtin/maintenance: run maintenance tasks depending on type","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-24T06:57:20Z","receivedAt":"2025-10-24T06:57:42Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We basically have three different ways to execute repository\nmaintenance:\n\n  1. Manual maintenance via `git maintenance run`.\n\n  2. Automatic maintenance via `git maintenance run --auto`.\n\n  3. Scheduled maintenance via `git maintenance run --schedule=`.\n\nAt the moment, maintenance strategies only have an effect for the last\ntype of maintenance. This is about to change in subsequent commits, but\nto do so we need to be able to skip some tasks depending on how exactly\nmaintenance was invoked.\n\nIntroduce a new maintenance type that discern between manual (1 & 2) and\nscheduled (3) maintenance. Convert the `enabled` field into a bitset so\nthat it becomes possible to specifiy which tasks exactly should run in a\nspecific context.\n\nThe types picked for existing strategies match the status quo:\n\n  - The default strategy is only ever executed as part of a manual\n    maintenance run. It is not possible to use it for scheduled\n    maintenance.\n\n  - The incremental strategy is only ever executed as part of a\n    scheduled maintenance run. It is not possible to use it for manual\n    maintenance.\n\nThe strategies will be tweaked in subsequent commits to make use of this\nnew infrastructure.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 28 +++++++++++++++++++---------\n 1 file changed, 19 insertions(+), 9 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 0ba6e59de1..6cc4f98c7a 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1827,9 +1827,16 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \treturn result;\n }\n \n+enum maintenance_type {\n+\t/* As invoked via `git maintenance run --schedule=`. */\n+\tMAINTENANCE_TYPE_SCHEDULED = (1 << 0),\n+\t/* As invoked via `git maintenance run` and with `--auto`. */\n+\tMAINTENANCE_TYPE_MANUAL    = (1 << 1),\n+};\n+\n struct maintenance_strategy {\n \tstruct {\n-\t\tint enabled;\n+\t\tunsigned type;\n \t\tenum schedule_priority schedule;\n \t} tasks[TASK__COUNT];\n };\n@@ -1839,7 +1846,7 @@ static const struct maintenance_strategy none_strategy = { 0 };\n static const struct maintenance_strategy default_strategy = {\n \t.tasks = {\n \t\t[TASK_GC] = {\n-\t\t\t.enabled = 1,\n+\t\t\t.type = MAINTENANCE_TYPE_MANUAL,\n \t\t},\n \t},\n };\n@@ -1847,23 +1854,23 @@ static const struct maintenance_strategy default_strategy = {\n static const struct maintenance_strategy incremental_strategy = {\n \t.tasks = {\n \t\t[TASK_COMMIT_GRAPH] = {\n-\t\t\t.enabled = 1,\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED,\n \t\t\t.schedule = SCHEDULE_HOURLY,\n \t\t},\n \t\t[TASK_PREFETCH] = {\n-\t\t\t.enabled = 1,\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED,\n \t\t\t.schedule = SCHEDULE_HOURLY,\n \t\t},\n \t\t[TASK_INCREMENTAL_REPACK] = {\n-\t\t\t.enabled = 1,\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED,\n \t\t\t.schedule = SCHEDULE_DAILY,\n \t\t},\n \t\t[TASK_LOOSE_OBJECTS] = {\n-\t\t\t.enabled = 1,\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED,\n \t\t\t.schedule = SCHEDULE_DAILY,\n \t\t},\n \t\t[TASK_PACK_REFS] = {\n-\t\t\t.enabled = 1,\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED,\n \t\t\t.schedule = SCHEDULE_WEEKLY,\n \t\t},\n \t},\n@@ -1881,6 +1888,7 @@ static void initialize_task_config(struct maintenance_run_opts *opts,\n {\n \tstruct strbuf config_name = STRBUF_INIT;\n \tstruct maintenance_strategy strategy;\n+\tenum maintenance_type type;\n \tconst char *config_str;\n \n \t/*\n@@ -1915,8 +1923,10 @@ static void initialize_task_config(struct maintenance_run_opts *opts,\n \t\t\tstrategy = parse_maintenance_strategy(config_str);\n \t\telse\n \t\t\tstrategy = none_strategy;\n+\t\ttype = MAINTENANCE_TYPE_SCHEDULED;\n \t} else {\n \t\tstrategy = default_strategy;\n+\t\ttype = MAINTENANCE_TYPE_MANUAL;\n \t}\n \n \tfor (size_t i = 0; i < TASK__COUNT; i++) {\n@@ -1926,8 +1936,8 @@ static void initialize_task_config(struct maintenance_run_opts *opts,\n \t\tstrbuf_addf(&config_name, \"maintenance.%s.enabled\",\n \t\t\t    tasks[i].name);\n \t\tif (!repo_config_get_bool(the_repository, config_name.buf, &config_value))\n-\t\t\tstrategy.tasks[i].enabled = config_value;\n-\t\tif (!strategy.tasks[i].enabled)\n+\t\t\tstrategy.tasks[i].type = config_value ? type : 0;\n+\t\tif (!(strategy.tasks[i].type & type))\n \t\t\tcontinue;\n \n \t\tif (opts->schedule) {\n\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529569","messageId":"20251024-pks-maintenance-geometric-strategy-v3-8-9b5b3bdb4387@pks.im","threadId":"64330","inReplyTo":"20251024-pks-maintenance-geometric-strategy-v3-0-9b5b3bdb4387@pks.im","subject":"[PATCH v3 08/10] builtin/maintenance: extend \"maintenance.strategy\" to manual maintenance","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-24T06:57:21Z","receivedAt":"2025-10-24T06:57:45Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"maintenance.strategy\" configuration allows users to configure how\nGit is supposed to perform repository maintenance. The idea is that we\nprovide a set of high-level strategies that may be useful in different\ncontexts, like for example when handling a large monorepo. Furthermore,\nthe strategy can be tweaked by the user by overriding specific tasks.\n\nIn its current form though, the strategy only applies to scheduled\nmaintenance. This creates something of a gap, as scheduled and manual\nmaintenance will now use _different_ strategies as the latter would\ncontinue to use git-gc(1) by default. This makes the strategies way less\nuseful than they could be on the one hand. But even more importantly,\nthe two different strategies might clash with one another, where one of\nthe strategies performs maintenance in such a way that it discards\nbenefits from the other strategy.\n\nSo ideally, it should be possible to pick one strategy that then applies\nglobally to all the different ways that we perform maintenance. This\ndoesn't necessarily mean that the strategy always does the _same_ thing\nfor every maintenance type. But it means that the strategy can configure\nthe different types to work in tandem with each other.\n\nChange the meaning of \"maintenance.strategy\" accordingly so that the\nstrategy is applied to both types, manual and scheduled. As preceding\ncommits have introduced logic to run maintenance tasks depending on this\ntype we can tweak strategies so that they perform those tasks depending\non the context.\n\nNote that this raises the question of backwards compatibility: when the\nuser has configured the \"incremental\" strategy we would have ignored\nthat strategy beforehand. Instead, repository maintenance would have\ncontinued to use git-gc(1) by default.\n\nBut luckily, we can match that behaviour by:\n\n  - Keeping all current tasks of the incremental strategy as\n    `MAINTENANCE_TYPE_SCHEDULED`. This ensures that those tasks will not\n    run during manual maintenance.\n\n  - Configuring the \"gc\" task so that it is invoked during manual\n    maintenance.\n\nLike this, the user shouldn't observe any difference in behaviour.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/maintenance.adoc | 22 ++++++++++++-------\n builtin/gc.c                          | 25 +++++++++++++++++-----\n t/t7900-maintenance.sh                | 40 +++++++++++++++++++++++++++++++++++\n 3 files changed, 74 insertions(+), 13 deletions(-)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex 45fdafc2c63..b7e90a71a3d 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -16,19 +16,25 @@ detach.\n \n maintenance.strategy::\n \tThis string config option provides a way to specify one of a few\n-\trecommended schedules for background maintenance. This only affects\n-\twhich tasks are run during `git maintenance run --schedule=X`\n-\tcommands, provided no `--task=<task>` arguments are provided.\n-\tFurther, if a `maintenance.<task>.schedule` config value is set,\n-\tthen that value is used instead of the one provided by\n-\t`maintenance.strategy`. The possible strategy strings are:\n+\trecommended strategies for repository maintenance. This affects\n+\twhich tasks are run during `git maintenance run`, provided no\n+\t`--task=<task>` arguments are provided. This setting impacts manual\n+\tmaintenance, auto-maintenance as well as scheduled maintenance. The\n+\ttasks that run may be different depending on the maintenance type.\n +\n-* `none`: This default setting implies no tasks are run at any schedule.\n+The maintenance strategy can be further tweaked by setting\n+`maintenance.<task>.enabled` and `maintenance.<task>.schedule`. If set, these\n+values are used instead of the defaults provided by `maintenance.strategy`.\n++\n+The possible strategies are:\n++\n+* `none`: This strategy implies no tasks are run at all. This is the default\n+  strategy for scheduled maintenance.\n * `incremental`: This setting optimizes for performing small maintenance\n   activities that do not delete any data. This does not schedule the `gc`\n   task, but runs the `prefetch` and `commit-graph` tasks hourly, the\n   `loose-objects` and `incremental-repack` tasks daily, and the `pack-refs`\n-  task weekly.\n+  task weekly. Manual repository maintenance uses the `gc` task.\n \n maintenance.<task>.enabled::\n \tThis boolean config option controls whether the maintenance task\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 6cc4f98c7aa..3c0a9a2e5df 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1873,6 +1873,20 @@ static const struct maintenance_strategy incremental_strategy = {\n \t\t\t.type = MAINTENANCE_TYPE_SCHEDULED,\n \t\t\t.schedule = SCHEDULE_WEEKLY,\n \t\t},\n+\t\t/*\n+\t\t * Historically, the \"incremental\" strategy was only available\n+\t\t * in the context of scheduled maintenance when set up via\n+\t\t * \"maintenance.strategy\". We have later expanded that config\n+\t\t * to also cover manual maintenance.\n+\t\t *\n+\t\t * To retain backwards compatibility with the previous status\n+\t\t * quo we thus run git-gc(1) in case manual maintenance was\n+\t\t * requested. This is the same as the default strategy, which\n+\t\t * would have been in use beforehand.\n+\t\t */\n+\t\t[TASK_GC] = {\n+\t\t\t.type = MAINTENANCE_TYPE_MANUAL,\n+\t\t},\n \t},\n };\n \n@@ -1916,19 +1930,20 @@ static void initialize_task_config(struct maintenance_run_opts *opts,\n \t *   - Unscheduled maintenance uses our default strategy.\n \t *\n \t * Both of these are affected by the gitconfig though, which may\n-\t * override specific aspects of our strategy.\n+\t * override specific aspects of our strategy. Furthermore, both\n+\t * strategies can be overridden by setting \"maintenance.strategy\".\n \t */\n \tif (opts->schedule) {\n-\t\tif (!repo_config_get_string_tmp(the_repository, \"maintenance.strategy\", &config_str))\n-\t\t\tstrategy = parse_maintenance_strategy(config_str);\n-\t\telse\n-\t\t\tstrategy = none_strategy;\n+\t\tstrategy = none_strategy;\n \t\ttype = MAINTENANCE_TYPE_SCHEDULED;\n \t} else {\n \t\tstrategy = default_strategy;\n \t\ttype = MAINTENANCE_TYPE_MANUAL;\n \t}\n \n+\tif (!repo_config_get_string_tmp(the_repository, \"maintenance.strategy\", &config_str))\n+\t\tstrategy = parse_maintenance_strategy(config_str);\n+\n \tfor (size_t i = 0; i < TASK__COUNT; i++) {\n \t\tint config_value;\n \ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 0fb917dd7b7..5219bc17a69 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -886,6 +886,46 @@ test_expect_success 'maintenance.strategy inheritance' '\n \t\t<modified-daily.txt\n '\n \n+test_strategy () {\n+\tSTRATEGY=\"$1\"\n+\tshift\n+\n+\tcat >expect &&\n+\trm -f trace2.txt &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/trace2.txt\" \\\n+\t\tgit -c maintenance.strategy=$STRATEGY maintenance run --quiet \"$@\" &&\n+\tsed -n 's/{\"event\":\"child_start\",\"sid\":\"[^/\"]*\",.*,\"argv\":\\[\"\\(.*\\)\\\"]}/\\1/p' <trace2.txt |\n+\t\tsed 's/\",\"/ /g'  >actual\n+\ttest_cmp expect actual\n+}\n+\n+test_expect_success 'maintenance.strategy is respected' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\n+\t\ttest_must_fail git -c maintenance.strategy=unknown maintenance run 2>err &&\n+\t\ttest_grep \"unknown maintenance strategy: .unknown.\" err &&\n+\n+\t\ttest_strategy incremental <<-\\EOF &&\n+\t\tgit pack-refs --all --prune\n+\t\tgit reflog expire --all\n+\t\tgit gc --quiet --no-detach --skip-foreground-tasks\n+\t\tEOF\n+\n+\t\ttest_strategy incremental --schedule=weekly <<-\\EOF\n+\t\tgit pack-refs --all --prune\n+\t\tgit prune-packed --quiet\n+\t\tgit multi-pack-index write --no-progress\n+\t\tgit multi-pack-index expire --no-progress\n+\t\tgit multi-pack-index repack --no-progress --batch-size=1\n+\t\tgit commit-graph write --split --reachable --no-progress\n+\t\tEOF\n+\t)\n+'\n+\n test_expect_success 'register and unregister' '\n \ttest_when_finished git config --global --unset-all maintenance.repo &&\n \n\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529570","messageId":"20251024-pks-maintenance-geometric-strategy-v3-9-9b5b3bdb4387@pks.im","threadId":"64330","inReplyTo":"20251024-pks-maintenance-geometric-strategy-v3-0-9b5b3bdb4387@pks.im","subject":"[PATCH v3 09/10] builtin/maintenance: make \"gc\" strategy accessible","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-24T06:57:22Z","receivedAt":"2025-10-24T06:57:49Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While the user can pick the \"incremental\" maintenance strategy, it is\nnot possible to explicitly use the \"gc\" strategy. This has two\ndownsides:\n\n  - It is impossible to use the default \"gc\" strategy for a specific\n    repository when the strategy was globally set to a different strategy.\n\n  - It is not possible to use git-gc(1) for scheduled maintenance.\n\nAddress these issues by making making the \"gc\" strategy configurable.\nFurthermore, extend the strategy so that git-gc(1) runs for both manual\nand scheduled maintenance.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/maintenance.adoc |  2 ++\n builtin/gc.c                          |  9 ++++++---\n t/t7900-maintenance.sh                | 14 +++++++++++++-\n 3 files changed, 21 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex b7e90a71a3d..b2bacdc8220 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -30,6 +30,8 @@ The possible strategies are:\n +\n * `none`: This strategy implies no tasks are run at all. This is the default\n   strategy for scheduled maintenance.\n+* `gc`: This strategy runs the `gc` task. This is the default strategy for\n+  manual maintenance.\n * `incremental`: This setting optimizes for performing small maintenance\n   activities that do not delete any data. This does not schedule the `gc`\n   task, but runs the `prefetch` and `commit-graph` tasks hourly, the\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 3c0a9a2e5df..8cab1450095 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1843,10 +1843,11 @@ struct maintenance_strategy {\n \n static const struct maintenance_strategy none_strategy = { 0 };\n \n-static const struct maintenance_strategy default_strategy = {\n+static const struct maintenance_strategy gc_strategy = {\n \t.tasks = {\n \t\t[TASK_GC] = {\n-\t\t\t.type = MAINTENANCE_TYPE_MANUAL,\n+\t\t\t.type = MAINTENANCE_TYPE_MANUAL | MAINTENANCE_TYPE_SCHEDULED,\n+\t\t\t.schedule = SCHEDULE_DAILY,\n \t\t},\n \t},\n };\n@@ -1894,6 +1895,8 @@ static struct maintenance_strategy parse_maintenance_strategy(const char *name)\n {\n \tif (!strcasecmp(name, \"incremental\"))\n \t\treturn incremental_strategy;\n+\tif (!strcasecmp(name, \"gc\"))\n+\t\treturn gc_strategy;\n \tdie(_(\"unknown maintenance strategy: '%s'\"), name);\n }\n \n@@ -1937,7 +1940,7 @@ static void initialize_task_config(struct maintenance_run_opts *opts,\n \t\tstrategy = none_strategy;\n \t\ttype = MAINTENANCE_TYPE_SCHEDULED;\n \t} else {\n-\t\tstrategy = default_strategy;\n+\t\tstrategy = gc_strategy;\n \t\ttype = MAINTENANCE_TYPE_MANUAL;\n \t}\n \ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 5219bc17a69..85e0cea4d96 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -915,7 +915,7 @@ test_expect_success 'maintenance.strategy is respected' '\n \t\tgit gc --quiet --no-detach --skip-foreground-tasks\n \t\tEOF\n \n-\t\ttest_strategy incremental --schedule=weekly <<-\\EOF\n+\t\ttest_strategy incremental --schedule=weekly <<-\\EOF &&\n \t\tgit pack-refs --all --prune\n \t\tgit prune-packed --quiet\n \t\tgit multi-pack-index write --no-progress\n@@ -923,6 +923,18 @@ test_expect_success 'maintenance.strategy is respected' '\n \t\tgit multi-pack-index repack --no-progress --batch-size=1\n \t\tgit commit-graph write --split --reachable --no-progress\n \t\tEOF\n+\n+\t\ttest_strategy gc <<-\\EOF &&\n+\t\tgit pack-refs --all --prune\n+\t\tgit reflog expire --all\n+\t\tgit gc --quiet --no-detach --skip-foreground-tasks\n+\t\tEOF\n+\n+\t\ttest_strategy gc --schedule=weekly <<-\\EOF\n+\t\tgit pack-refs --all --prune\n+\t\tgit reflog expire --all\n+\t\tgit gc --quiet --no-detach --skip-foreground-tasks\n+\t\tEOF\n \t)\n '\n \n\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529571","messageId":"20251024-pks-maintenance-geometric-strategy-v3-10-9b5b3bdb4387@pks.im","threadId":"64330","inReplyTo":"20251024-pks-maintenance-geometric-strategy-v3-0-9b5b3bdb4387@pks.im","subject":"[PATCH v3 10/10] builtin/maintenance: introduce \"geometric\" strategy","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-24T06:57:23Z","receivedAt":"2025-10-24T06:57:52Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We have two different repacking strategies in Git:\n\n  - The \"gc\" strategy uses git-gc(1).\n\n  - The \"incremental\" strategy uses multi-pack indices and `git\n    multi-pack-index repack` to merge together smaller packfiles as\n    determined by a specific batch size.\n\nThe former strategy is our old and trusted default, whereas the latter\nhas historically been used for our scheduled maintenance. But both\nstrategies have their shortcomings:\n\n  - The \"gc\" strategy performs regular all-into-one repacks. Furthermore\n    it is rather inflexible, as it is not easily possible for a user to\n    enable or disable specific subtasks.\n\n  - The \"incremental\" strategy is not a full replacement for the \"gc\"\n    strategy as it doesn't know to prune stale data.\n\nSo today, we don't have a strategy that is well-suited for large repos\nwhile being a full replacement for the \"gc\" strategy.\n\nIntroduce a new \"geometric\" strategy that aims to fill this gap. This\nstrategy invokes all the usual cleanup tasks that git-gc(1) does like\npruning reflogs and rerere caches as well as stale worktrees. But where\nit differs from both the \"gc\" and \"incremental\" strategy is that it uses\nour geometric repacking infrastructure exposed by git-repack(1) to\nrepack packfiles. The advantage of geometric repacking is that we only\nneed to perform an all-into-one repack when the object count in a repo\nhas grown significantly.\n\nOne downside of this strategy is that pruning of unreferenced objects is\nnot going to happen regularly anymore. Every geometric repack knows to\nsoak up all loose objects regardless of their reachability, and merging\ntwo or more packs doesn't consider reachability, either. Consequently,\nthe number of unreachable objects will grow over time.\n\nThis is remedied by doing an all-into-one repack instead of a geometric\nrepack whenever we determine that the geometric repack would end up\nmerging all packfiles anyway. This all-into-one repack then performs our\nusual reachability checks and writes unreachable objects into a cruft\npack. As cruft packs won't ever be merged during geometric repacks we\ncan thus phase out these objects over time.\n\nOf course, this still means that we retain unreachable objects for far\nlonger than with the \"gc\" strategy. But the maintenance strategy is\nintended especially for large repositories, where the basic assumption\nis that the set of unreachable objects will be significantly dwarfed by\nthe number of reachable objects.\n\nIf this assumption is ever proven to be too disadvantageous we could for\nexample introduce a time-based strategy: if the largest packfile has not\nbeen touched for longer than $T, we perform an all-into-one repack. But\nfor now, such a mechanism is deferred into the future as it is not clear\nyet whether it is needed in the first place.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/maintenance.adoc |  9 +++++++++\n builtin/gc.c                          | 31 +++++++++++++++++++++++++++++++\n t/t7900-maintenance.sh                | 20 +++++++++++++++++++-\n 3 files changed, 59 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex b2bacdc8220..d0c38f03fab 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -32,6 +32,15 @@ The possible strategies are:\n   strategy for scheduled maintenance.\n * `gc`: This strategy runs the `gc` task. This is the default strategy for\n   manual maintenance.\n+* `geometric`: This strategy performs geometric repacking of packfiles and\n+  keeps auxiliary data structures up-to-date. The strategy expires data in the\n+  reflog and removes worktrees that cannot be located anymore. When the\n+  geometric repacking strategy would decide to do an all-into-one repack, then\n+  the strategy generates a cruft pack for all unreachable objects. Objects that\n+  are already part of a cruft pack will be expired.\n++\n+This repacking strategy is a full replacement for the `gc` strategy and is\n+recommended for large repositories.\n * `incremental`: This setting optimizes for performing small maintenance\n   activities that do not delete any data. This does not schedule the `gc`\n   task, but runs the `prefetch` and `commit-graph` tasks hourly, the\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 8cab1450095..19be3f87e13 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1891,12 +1891,43 @@ static const struct maintenance_strategy incremental_strategy = {\n \t},\n };\n \n+static const struct maintenance_strategy geometric_strategy = {\n+\t.tasks = {\n+\t\t[TASK_COMMIT_GRAPH] = {\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t\t.schedule = SCHEDULE_HOURLY,\n+\t\t},\n+\t\t[TASK_GEOMETRIC_REPACK] = {\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t\t.schedule = SCHEDULE_DAILY,\n+\t\t},\n+\t\t[TASK_PACK_REFS] = {\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t\t.schedule = SCHEDULE_DAILY,\n+\t\t},\n+\t\t[TASK_RERERE_GC] = {\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t\t.schedule = SCHEDULE_WEEKLY,\n+\t\t},\n+\t\t[TASK_REFLOG_EXPIRE] = {\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t\t.schedule = SCHEDULE_WEEKLY,\n+\t\t},\n+\t\t[TASK_WORKTREE_PRUNE] = {\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t\t.schedule = SCHEDULE_WEEKLY,\n+\t\t},\n+\t},\n+};\n+\n static struct maintenance_strategy parse_maintenance_strategy(const char *name)\n {\n \tif (!strcasecmp(name, \"incremental\"))\n \t\treturn incremental_strategy;\n \tif (!strcasecmp(name, \"gc\"))\n \t\treturn gc_strategy;\n+\tif (!strcasecmp(name, \"geometric\"))\n+\t\treturn geometric_strategy;\n \tdie(_(\"unknown maintenance strategy: '%s'\"), name);\n }\n \ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 85e0cea4d96..0d76693feec 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -930,11 +930,29 @@ test_expect_success 'maintenance.strategy is respected' '\n \t\tgit gc --quiet --no-detach --skip-foreground-tasks\n \t\tEOF\n \n-\t\ttest_strategy gc --schedule=weekly <<-\\EOF\n+\t\ttest_strategy gc --schedule=weekly <<-\\EOF &&\n \t\tgit pack-refs --all --prune\n \t\tgit reflog expire --all\n \t\tgit gc --quiet --no-detach --skip-foreground-tasks\n \t\tEOF\n+\n+\t\ttest_strategy geometric <<-\\EOF &&\n+\t\tgit pack-refs --all --prune\n+\t\tgit reflog expire --all\n+\t\tgit repack -d -l --geometric=2 --quiet --write-midx\n+\t\tgit commit-graph write --split --reachable --no-progress\n+\t\tgit worktree prune --expire 3.months.ago\n+\t\tgit rerere gc\n+\t\tEOF\n+\n+\t\ttest_strategy geometric --schedule=weekly <<-\\EOF\n+\t\tgit pack-refs --all --prune\n+\t\tgit reflog expire --all\n+\t\tgit repack -d -l --geometric=2 --quiet --write-midx\n+\t\tgit commit-graph write --split --reachable --no-progress\n+\t\tgit worktree prune --expire 3.months.ago\n+\t\tgit rerere gc\n+\t\tEOF\n \t)\n '\n \n\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529631","messageId":"aPvNOS2JXEHNHn28@nand.local","threadId":"64330","inReplyTo":"aPsSbt0pK4sbmVQj@pks.im","subject":"Re: [PATCH v2 4/9] builtin/maintenance: make the geometric factor configurable","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2025-10-24T19:02:17Z","receivedAt":"2025-10-24T19:02:19Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Fri, Oct 24, 2025 at 07:45:18AM +0200, Patrick Steinhardt wrote:\n> On Thu, Oct 23, 2025 at 03:33:47PM -0400, Taylor Blau wrote:\n> > On Tue, Oct 21, 2025 at 04:13:26PM +0200, Patrick Steinhardt wrote:\n> > > The geometric repacking task uses a factor of two for its geometric\n> > > sequence, meaning that each next pack must contain at least twice as\n> > > many objects as the next-smaller one. In some cases it may be helpful to\n> > > configure this factor though to reduce the number of packfile merges\n> > > even further, e.g. in very big repositories. But while git-repack(1)\n> > > itself supports doing this, the maintenance task does not give us a way\n> > > to tune it.\n> > >\n> > > Introduce a new \"maintenance.geometric-repack.splitFactor\" configuration\n> > > to plug this gap.\n> >\n> > Interesting, this wasn't exactly what I had in my mind when reading the\n> > last round, but I think this is worth doing on its own. My apologies for\n> > being ambiguous in my earlier message :-s.\n> >\n> > I was suggesting that we have a repack.geometricFactor configuration\n> > variable that defaulted to two, could be overridden by --geometric=<n>,\n> > such that we could start doing \"git repack --geometric\" without having\n> > to write \"=2\" every time.\n> >\n> > I think that that is probably still a useful thing to do in and of\n> > itself, but this change doesn't preclude our ability to do that, since\n> > it just overwrites what we pass in to 'git repack' when calling it from\n> > within the maintenance context.\n>\n> Yeah, I understood that suggestion, but I still think that in the\n> context of this series here it makes more sense to piggy back onto\n> git-maintenance(1) itself so that we're in line with the other tasks\n> that we have. All of them are configurable via \"maintenance.*.foobar\"\n> knobs, so I wanted to have the same architecture for the geometric task,\n> as well.\n>\n> But as you say, this doesn't mean that we cannot introduce a config for\n> git-repack(1) at a later point in time, and I also believe that this may\n> be a useful addition indeed. I guess the order of precedence would be\n> that \"repack.geometricFactor\" is overridden by\n> \"maintenance.geometric-repack.splitFactor\", as the latter is more\n> specific.\n\nYeah, I agree with all of that. From maintenance's perspective, it reads\nthe value of maintenance.geometric-repack.splitFactor and uses that as a\ncommand-line argument when invoking repack with '--geometric=<n>'\n\n'repack' should of course be oblivious to all of that, and whatever\nvalue it reads from 'repack.geometricFactor' should be the default when\n--geometric is passed without a value.\n\nIt is a little too bad that we can't say, \"all geometric repacking should\nuse a factor of 3\" by default easily, since that would require\nmaintenance having to read the value of 'repack.geometricFactor' itself,\nbut I think that's a trade-off that I can live with.\n\nSounds like this is all good #leftoverbits.\n\nThanks,\nTaylor\n"},{"id":"529633","messageId":"aPvNfU0B8J9sRy//@nand.local","threadId":"64330","inReplyTo":"20251024-pks-maintenance-geometric-strategy-v3-0-9b5b3bdb4387@pks.im","subject":"Re: [PATCH v3 00/10] builtin/maintenance: introduce \"geometric\" strategy","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2025-10-24T19:03:25Z","receivedAt":"2025-10-24T19:03:28Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Fri, Oct 24, 2025 at 08:57:13AM +0200, Patrick Steinhardt wrote:\n> Range-diff versus v2:\n\nThe range-diff all looks good to me, so this has my:\n\n    Acked-by: Taylor Blau <me@ttaylorr.com>\n\nThanks for working on this! I'm excited to have an easier way for more\nuses to interact with geometric repacking without having to juggle in\ntheir head when they should use that versus doing an all-into-one\nrepack.\n\nThanks,\nTaylor\n"},{"id":"529634","messageId":"xmqq4irop12y.fsf@gitster.g","threadId":"64330","inReplyTo":"aPvNfU0B8J9sRy//@nand.local","subject":"Re: [PATCH v3 00/10] builtin/maintenance: introduce \"geometric\" strategy","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-24T19:11:01Z","receivedAt":"2025-10-24T19:11:04Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Taylor Blau <me@ttaylorr.com> writes:\n\n> On Fri, Oct 24, 2025 at 08:57:13AM +0200, Patrick Steinhardt wrote:\n>> Range-diff versus v2:\n>\n> The range-diff all looks good to me, so this has my:\n>\n>     Acked-by: Taylor Blau <me@ttaylorr.com>\n>\n> Thanks for working on this! I'm excited to have an easier way for more\n> uses to interact with geometric repacking without having to juggle in\n> their head when they should use that versus doing an all-into-one\n> repack.\n\nThanks, both.  Queued.\n\nLet's mark the topic for 'next'.\n"},{"id":"529649","messageId":"20251025191550.GA279793@coredump.intra.peff.net","threadId":"64330","inReplyTo":"20251024-pks-maintenance-geometric-strategy-v3-3-9b5b3bdb4387@pks.im","subject":"Re: [PATCH v3 03/10] builtin/maintenance: introduce \"geometric-repack\" task","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-10-25T19:15:50Z","receivedAt":"2025-10-25T19:15:57Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Fri, Oct 24, 2025 at 08:57:16AM +0200, Patrick Steinhardt wrote:\n\n> +\t\t# Repacking should now cause a no-op geometric repack because\n> +\t\t# no packfiles need to be combined.\n> +\t\tls -l .git/objects/pack >before &&\n> +\t\trun_and_verify_geometric_pack 1 &&\n> +\t\tls -l .git/objects/pack >after &&\n> +\t\ttest_cmp before after &&\n\nI got a CI failure from this test like this:\n\n   + diff -u before after\n   --- before 2025-10-25 17:51:59.985025237 +0000\n   +++ after  2025-10-25 17:52:00.304026445 +0000\n   @@ -1,5 +1,5 @@\n    total 16\n   --rw-rw-r-- 1 builder builder 1252 Oct 25 17:51 multi-pack-index\n   +-rw-rw-r-- 1 builder builder 1252 Oct 25 17:52 multi-pack-index\n    -r--r--r-- 1 builder builder 1156 Oct 25 17:51 pack-68c20c4590a622a21395b4480621d55494112a83.idx\n    -r--r--r-- 1 builder builder  226 Oct 25 17:51 pack-68c20c4590a622a21395b4480621d55494112a83.pack\n    -r--r--r-- 1 builder builder   64 Oct 25 17:51 pack-68c20c4590a622a21395b4480621d55494112a83.rev\n\nI'm not sure if this is a bug or a race condition in the test. If\n\"no-op\" means \"do not generate a new pack, but do generate a new midx\"\nthen it's a race condition (the regenerated midx might move across the\nminute boundary).  If it means \"do not even generate a new midx\", then\nthere is a bug. ;)\n\nYou can generate the race at will like this:\n\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 0d76693fee..2b5141196f 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -501,6 +501,7 @@ test_expect_success 'geometric repacking task' '\n \t\t# Repacking should now cause a no-op geometric repack because\n \t\t# no packfiles need to be combined.\n \t\tls -l .git/objects/pack >before &&\n+\t\tsleep 60 &&\n \t\trun_and_verify_geometric_pack 1 &&\n \t\tls -l .git/objects/pack >after &&\n \t\ttest_cmp before after &&\n\nthough if we are going to be picky about timestamps, it probably makes\nsense to use a higher resolution. Sadly I don't think there's a portable\nway to do that with \"ls\", and \"stat\" is probably likewise something we\ncan't assume. I'd turn to perl, but I know you've been trying to avoid\ndepending on it. You can hack around it with:\n\n  test-tool chmtime -v +0 .git/objects/pack/*\n\nfor this case, I'd think.\n\n-Peff\n"},{"id":"529692","messageId":"aP8sJF45dHsVlgqt@pks.im","threadId":"64330","inReplyTo":"20251025191550.GA279793@coredump.intra.peff.net","subject":"Re: [PATCH v3 03/10] builtin/maintenance: introduce \"geometric-repack\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T08:24:11Z","receivedAt":"2025-10-27T08:24:24Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, Oct 25, 2025 at 03:15:50PM -0400, Jeff King wrote:\n> On Fri, Oct 24, 2025 at 08:57:16AM +0200, Patrick Steinhardt wrote:\n> \n> > +\t\t# Repacking should now cause a no-op geometric repack because\n> > +\t\t# no packfiles need to be combined.\n> > +\t\tls -l .git/objects/pack >before &&\n> > +\t\trun_and_verify_geometric_pack 1 &&\n> > +\t\tls -l .git/objects/pack >after &&\n> > +\t\ttest_cmp before after &&\n> \n> I got a CI failure from this test like this:\n> \n>    + diff -u before after\n>    --- before 2025-10-25 17:51:59.985025237 +0000\n>    +++ after  2025-10-25 17:52:00.304026445 +0000\n>    @@ -1,5 +1,5 @@\n>     total 16\n>    --rw-rw-r-- 1 builder builder 1252 Oct 25 17:51 multi-pack-index\n>    +-rw-rw-r-- 1 builder builder 1252 Oct 25 17:52 multi-pack-index\n>     -r--r--r-- 1 builder builder 1156 Oct 25 17:51 pack-68c20c4590a622a21395b4480621d55494112a83.idx\n>     -r--r--r-- 1 builder builder  226 Oct 25 17:51 pack-68c20c4590a622a21395b4480621d55494112a83.pack\n>     -r--r--r-- 1 builder builder   64 Oct 25 17:51 pack-68c20c4590a622a21395b4480621d55494112a83.rev\n> \n> I'm not sure if this is a bug or a race condition in the test. If\n> \"no-op\" means \"do not generate a new pack, but do generate a new midx\"\n> then it's a race condition (the regenerated midx might move across the\n> minute boundary).  If it means \"do not even generate a new midx\", then\n> there is a bug. ;)\n> \n> You can generate the race at will like this:\n> \n> diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\n> index 0d76693fee..2b5141196f 100755\n> --- a/t/t7900-maintenance.sh\n> +++ b/t/t7900-maintenance.sh\n> @@ -501,6 +501,7 @@ test_expect_success 'geometric repacking task' '\n>  \t\t# Repacking should now cause a no-op geometric repack because\n>  \t\t# no packfiles need to be combined.\n>  \t\tls -l .git/objects/pack >before &&\n> +\t\tsleep 60 &&\n>  \t\trun_and_verify_geometric_pack 1 &&\n>  \t\tls -l .git/objects/pack >after &&\n>  \t\ttest_cmp before after &&\n> \n> though if we are going to be picky about timestamps, it probably makes\n> sense to use a higher resolution. Sadly I don't think there's a portable\n> way to do that with \"ls\", and \"stat\" is probably likewise something we\n> can't assume. I'd turn to perl, but I know you've been trying to avoid\n> depending on it. You can hack around it with:\n> \n>   test-tool chmtime -v +0 .git/objects/pack/*\n> \n> for this case, I'd think.\n\nInteresting! I would say that this is an issue in git-repack(1) itself:\nif the geometric repack didn't lead to any new packs, and if all of the\npacks are already covered by a MIDX, then we still rather pointlessly\nregenerate the MIDX even though it won't cover anything new.\n\nI wonder whether we want a patch like the below one? Problem though is\nthat we'd also have to check whether any of the other options have\nchanged, otherwise we for example wouldn't generate bitmaps.\n\nIn any case though, I feel like this is a bit out of scope for this\npatch series. Other strategies that write a MIDX behave the same, so\nthis is something we can fix later on.\n\nPatrick\n\ndiff --git a/repack-midx.c b/repack-midx.c\nindex 6f6202c5bcc..efa47bb55b5 100644\n--- a/repack-midx.c\n+++ b/repack-midx.c\n@@ -285,6 +285,35 @@ static void remove_redundant_bitmaps(struct string_list *include,\n \tstrbuf_release(&path);\n }\n \n+static bool midx_needs_repack(const struct repack_write_midx_opts *opts,\n+\t\t\t      const struct string_list *include)\n+{\n+\tstruct strset set = STRSET_INIT;\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tbool needs_repack;\n+\n+\tif (opts->existing->midx_packs.nr != include->nr)\n+\t\treturn true;\n+\n+\tfor (size_t i = 0; i < opts->existing->midx_packs.nr; i++) {\n+\t\tconst char *item = opts->existing->midx_packs.items[i].string;\n+\n+\t\tstrbuf_reset(&buf);\n+\t\tstrbuf_addstr(&buf, item);\n+\t\tstrbuf_strip_suffix(&buf, \".pack\");\n+\t\tstrbuf_addstr(&buf, \".idx\");\n+\n+\t\tstrset_add(&set, buf.buf);\n+\t}\n+\n+\tneeds_repack = false;\n+\tfor (size_t i = 0; i < include->nr && !needs_repack; i++)\n+\t\tneeds_repack = !strset_contains(&set, include->items[i].string);\n+\n+\tstrset_clear(&set);\n+\treturn needs_repack;\n+}\n+\n int write_midx_included_packs(struct repack_write_midx_opts *opts)\n {\n \tstruct child_process cmd = CHILD_PROCESS_INIT;\n@@ -295,7 +324,7 @@ int write_midx_included_packs(struct repack_write_midx_opts *opts)\n \tint ret = 0;\n \n \tmidx_included_packs(&include, opts);\n-\tif (!include.nr)\n+\tif (!include.nr || !midx_needs_repack(opts, &include))\n \t\tgoto done;\n \n \tcmd.in = -1;\n\n"},{"id":"529693","messageId":"20251027-pks-maintenance-geometric-strategy-v4-0-18049e4a0adf@pks.im","threadId":"64330","inReplyTo":"20251016-pks-maintenance-geometric-strategy-v1-0-18943d474203@pks.im","subject":"[PATCH v4 00/10] builtin/maintenance: introduce \"geometric\" strategy","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T08:30:50Z","receivedAt":"2025-10-27T08:30:58Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nby default, git-maintenance(1) uses git-gc(1) to perform repository\nhousekeeping. This tool has a couple of shortcomings, most importantly\nthat it regularly does all-into-one repacks. This doesn't really work\nall that well in the context of monorepos, where you really want to\navoid repacking all objects regularly.\n\nAn alternative maintenance strategy is the \"incremental\" strategy, but\nthis strategy has two downsides:\n\n  - Strategies in general only apply to scheduled maintenance. So if you\n    run git-maintenance(1), you still end up with git-gc(1).\n\n  - The strategy is designed to not ever delete any data, but a full\n    replacment for git-gc(1) needs to also prune reflogs, rereree caches\n    and vanished worktrees.\n\nThis patch series aims to fix both of these issues.\n\nFirst, the series introduces a new \"geometric\" maintenance task, which\nmakes use of geometric repacking as exposed by git-repack(1) in the\ngeneral case. In the case where a geometric repack ends up merging all\npackfiles into one we instead do an all-into-one repack with cruft packs\nso that we can still phase out objects over time.\n\nSecond, the series extends maintenance strategies to also cover normal\nmaintenance. If the user has configured the \"geometric\" strategy, we'll\nthus use it for both manual and scheduled maintenance. For backwards\ncompatibility, the \"incremental\" strategy is changed so that it uses\ngit-gc(1) for manual maintenance and the other tasks for scheduled\nmaintenance.\n\nThe series is built on top of b660e2dcb9 (Sync with 'maint', 2025-10-14)\nwith tb/incremental-midx-part-3.1 at c886af90f8 (SQUASH??? play well\nwith other topics by preemptively including \"repository.h\", 2025-09-29)\nmerged into it.\n\nChanges in v4:\n  - Fix a flaky test because git-repack(1) always decides to rewrite the\n    MIDX, even though no packs have changed. This isn't a new issue, and\n    other maintenance tasks behave the same. So I decided to punt on it\n    for now.\n  - Link to v3: https://lore.kernel.org/r/20251024-pks-maintenance-geometric-strategy-v3-0-9b5b3bdb4387@pks.im\n\nChanges in v3:\n  - More line wrapping.\n  - Improve readability of maintenance strategies by using nested\n    designated initializers.\n  - Use git-count-object(1) to count loose objects.\n  - Link to v2: https://lore.kernel.org/r/20251021-pks-maintenance-geometric-strategy-v2-0-f0d727832b80@pks.im\n\nChanges in v2:\n  - Make the geometric factor configurable via\n    \"maintenance.geometric-repack.splitFactor\".\n  - Wrap some overly long lines in our tests.\n  - Link to v1: https://lore.kernel.org/r/20251016-pks-maintenance-geometric-strategy-v1-0-18943d474203@pks.im\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (10):\n      builtin/gc: remove global `repack` variable\n      builtin/gc: make `too_many_loose_objects()` reusable without GC config\n      builtin/maintenance: introduce \"geometric-repack\" task\n      builtin/maintenance: make the geometric factor configurable\n      builtin/maintenance: don't silently ignore invalid strategy\n      builtin/maintenance: improve readability of strategies\n      builtin/maintenance: run maintenance tasks depending on type\n      builtin/maintenance: extend \"maintenance.strategy\" to manual maintenance\n      builtin/maintenance: make \"gc\" strategy accessible\n      builtin/maintenance: introduce \"geometric\" strategy\n\n Documentation/config/maintenance.adoc |  49 +++++-\n builtin/gc.c                          | 313 ++++++++++++++++++++++++++++------\n t/t7900-maintenance.sh                | 245 ++++++++++++++++++++++++++\n 3 files changed, 544 insertions(+), 63 deletions(-)\n\nRange-diff versus v3:\n\n 1:  e1af6298ba2 =  1:  9893494c0f7 builtin/gc: remove global `repack` variable\n 2:  3009eb5fa82 =  2:  d9214040c96 builtin/gc: make `too_many_loose_objects()` reusable without GC config\n 3:  888f8576a8f !  3:  0aa6444cef2 builtin/maintenance: introduce \"geometric-repack\" task\n    @@ t/t7900-maintenance.sh: test_expect_success 'maintenance.incremental-repack.auto\n     +\n     +\t\t# Repacking should now cause a no-op geometric repack because\n     +\t\t# no packfiles need to be combined.\n    -+\t\tls -l .git/objects/pack >before &&\n    ++\t\tls -l .git/objects/pack/*.pack >before &&\n     +\t\trun_and_verify_geometric_pack 1 &&\n    -+\t\tls -l .git/objects/pack >after &&\n    ++\t\tls -l .git/objects/pack/*.pack >after &&\n     +\t\ttest_cmp before after &&\n     +\n     +\t\t# This incremental change creates a new packfile that only\n 4:  d14b6d9bfc7 =  4:  ccff4aea2fe builtin/maintenance: make the geometric factor configurable\n 5:  0b2e1dc2561 =  5:  594ee7d3765 builtin/maintenance: don't silently ignore invalid strategy\n 6:  417554d8c89 =  6:  cc5844dd05e builtin/maintenance: improve readability of strategies\n 7:  0c7f246a9f3 =  7:  6ce5c3bf93a builtin/maintenance: run maintenance tasks depending on type\n 8:  4e01332152f =  8:  bbbaee0d13e builtin/maintenance: extend \"maintenance.strategy\" to manual maintenance\n 9:  cf8adf2e039 =  9:  f4faf84e06e builtin/maintenance: make \"gc\" strategy accessible\n10:  20bfc9802d7 = 10:  fc65b12ed9e builtin/maintenance: introduce \"geometric\" strategy\n\n---\nbase-commit: 0bb2c786c2349dd6700727153c13d81cbfb41710\nchange-id: 20251015-pks-maintenance-geometric-strategy-580c58581b01\n\n"},{"id":"529694","messageId":"20251027-pks-maintenance-geometric-strategy-v4-1-18049e4a0adf@pks.im","threadId":"64330","inReplyTo":"20251027-pks-maintenance-geometric-strategy-v4-0-18049e4a0adf@pks.im","subject":"[PATCH v4 01/10] builtin/gc: remove global `repack` variable","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T08:30:51Z","receivedAt":"2025-10-27T08:31:01Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The global `repack` variable is used to store all command line arguments\nthat we eventually want to pass to git-repack(1). It is being appended\nto from multiple different functions, which makes it hard to follow the\nlogic. Besides being hard to follow, it also makes it unnecessarily hard\nto reuse this infrastructure in new code.\n\nRefactor the code so that we store this variable on the stack and pass\na pointer to it around as needed. This is done so that we can reuse\n`add_repack_all_options()` in a subsequent commit.\n\nThe refactoring itself is straight-forward. One function that deserves\nattention though is `need_to_gc()`: this function determines whether or\nnot we need to execute garbage collection for `git gc --auto`, but also\nfor `git maintenance run --auto`. But besides figuring out whether we\nhave to perform GC, the function also sets up the `repack` arguments.\n\nFor `git gc --auto` it's trivial to adapt, as we already have the\non-stack variable at our fingertips. But for the maintenance condition\nit's less obvious what to do.\n\nAs it turns out, we can just use another temporary variable there that\nwe then immediately discard. If we need to perform GC we execute a child\ngit-gc(1) process to repack objects for us, and that process will have\nto recompute the arguments anyway.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 74 ++++++++++++++++++++++++++++++++++++------------------------\n 1 file changed, 45 insertions(+), 29 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e19e13d9788..e9772eb3a30 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -55,7 +55,6 @@ static const char * const builtin_gc_usage[] = {\n };\n \n static timestamp_t gc_log_expire_time;\n-static struct strvec repack = STRVEC_INIT;\n static struct tempfile *pidfile;\n static struct lock_file log_lock;\n static struct string_list pack_garbage = STRING_LIST_INIT_DUP;\n@@ -618,48 +617,50 @@ static uint64_t estimate_repack_memory(struct gc_config *cfg,\n \treturn os_cache + heap;\n }\n \n-static int keep_one_pack(struct string_list_item *item, void *data UNUSED)\n+static int keep_one_pack(struct string_list_item *item, void *data)\n {\n-\tstrvec_pushf(&repack, \"--keep-pack=%s\", basename(item->string));\n+\tstruct strvec *args = data;\n+\tstrvec_pushf(args, \"--keep-pack=%s\", basename(item->string));\n \treturn 0;\n }\n \n static void add_repack_all_option(struct gc_config *cfg,\n-\t\t\t\t  struct string_list *keep_pack)\n+\t\t\t\t  struct string_list *keep_pack,\n+\t\t\t\t  struct strvec *args)\n {\n \tif (cfg->prune_expire && !strcmp(cfg->prune_expire, \"now\")\n \t\t&& !(cfg->cruft_packs && cfg->repack_expire_to))\n-\t\tstrvec_push(&repack, \"-a\");\n+\t\tstrvec_push(args, \"-a\");\n \telse if (cfg->cruft_packs) {\n-\t\tstrvec_push(&repack, \"--cruft\");\n+\t\tstrvec_push(args, \"--cruft\");\n \t\tif (cfg->prune_expire)\n-\t\t\tstrvec_pushf(&repack, \"--cruft-expiration=%s\", cfg->prune_expire);\n+\t\t\tstrvec_pushf(args, \"--cruft-expiration=%s\", cfg->prune_expire);\n \t\tif (cfg->max_cruft_size)\n-\t\t\tstrvec_pushf(&repack, \"--max-cruft-size=%lu\",\n+\t\t\tstrvec_pushf(args, \"--max-cruft-size=%lu\",\n \t\t\t\t     cfg->max_cruft_size);\n \t\tif (cfg->repack_expire_to)\n-\t\t\tstrvec_pushf(&repack, \"--expire-to=%s\", cfg->repack_expire_to);\n+\t\t\tstrvec_pushf(args, \"--expire-to=%s\", cfg->repack_expire_to);\n \t} else {\n-\t\tstrvec_push(&repack, \"-A\");\n+\t\tstrvec_push(args, \"-A\");\n \t\tif (cfg->prune_expire)\n-\t\t\tstrvec_pushf(&repack, \"--unpack-unreachable=%s\", cfg->prune_expire);\n+\t\t\tstrvec_pushf(args, \"--unpack-unreachable=%s\", cfg->prune_expire);\n \t}\n \n \tif (keep_pack)\n-\t\tfor_each_string_list(keep_pack, keep_one_pack, NULL);\n+\t\tfor_each_string_list(keep_pack, keep_one_pack, args);\n \n \tif (cfg->repack_filter && *cfg->repack_filter)\n-\t\tstrvec_pushf(&repack, \"--filter=%s\", cfg->repack_filter);\n+\t\tstrvec_pushf(args, \"--filter=%s\", cfg->repack_filter);\n \tif (cfg->repack_filter_to && *cfg->repack_filter_to)\n-\t\tstrvec_pushf(&repack, \"--filter-to=%s\", cfg->repack_filter_to);\n+\t\tstrvec_pushf(args, \"--filter-to=%s\", cfg->repack_filter_to);\n }\n \n-static void add_repack_incremental_option(void)\n+static void add_repack_incremental_option(struct strvec *args)\n {\n-\tstrvec_push(&repack, \"--no-write-bitmap-index\");\n+\tstrvec_push(args, \"--no-write-bitmap-index\");\n }\n \n-static int need_to_gc(struct gc_config *cfg)\n+static int need_to_gc(struct gc_config *cfg, struct strvec *repack_args)\n {\n \t/*\n \t * Setting gc.auto to 0 or negative can disable the\n@@ -700,10 +701,10 @@ static int need_to_gc(struct gc_config *cfg)\n \t\t\t\tstring_list_clear(&keep_pack, 0);\n \t\t}\n \n-\t\tadd_repack_all_option(cfg, &keep_pack);\n+\t\tadd_repack_all_option(cfg, &keep_pack, repack_args);\n \t\tstring_list_clear(&keep_pack, 0);\n \t} else if (too_many_loose_objects(cfg))\n-\t\tadd_repack_incremental_option();\n+\t\tadd_repack_incremental_option(repack_args);\n \telse\n \t\treturn 0;\n \n@@ -852,6 +853,7 @@ int cmd_gc(int argc,\n \tint keep_largest_pack = -1;\n \tint skip_foreground_tasks = 0;\n \ttimestamp_t dummy;\n+\tstruct strvec repack_args = STRVEC_INIT;\n \tstruct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT;\n \tstruct gc_config cfg = GC_CONFIG_INIT;\n \tconst char *prune_expire_sentinel = \"sentinel\";\n@@ -891,7 +893,7 @@ int cmd_gc(int argc,\n \tshow_usage_with_options_if_asked(argc, argv,\n \t\t\t\t\t builtin_gc_usage, builtin_gc_options);\n \n-\tstrvec_pushl(&repack, \"repack\", \"-d\", \"-l\", NULL);\n+\tstrvec_pushl(&repack_args, \"repack\", \"-d\", \"-l\", NULL);\n \n \tgc_config(&cfg);\n \n@@ -914,14 +916,14 @@ int cmd_gc(int argc,\n \t\tdie(_(\"failed to parse prune expiry value %s\"), cfg.prune_expire);\n \n \tif (aggressive) {\n-\t\tstrvec_push(&repack, \"-f\");\n+\t\tstrvec_push(&repack_args, \"-f\");\n \t\tif (cfg.aggressive_depth > 0)\n-\t\t\tstrvec_pushf(&repack, \"--depth=%d\", cfg.aggressive_depth);\n+\t\t\tstrvec_pushf(&repack_args, \"--depth=%d\", cfg.aggressive_depth);\n \t\tif (cfg.aggressive_window > 0)\n-\t\t\tstrvec_pushf(&repack, \"--window=%d\", cfg.aggressive_window);\n+\t\t\tstrvec_pushf(&repack_args, \"--window=%d\", cfg.aggressive_window);\n \t}\n \tif (opts.quiet)\n-\t\tstrvec_push(&repack, \"-q\");\n+\t\tstrvec_push(&repack_args, \"-q\");\n \n \tif (opts.auto_flag) {\n \t\tif (cfg.detach_auto && opts.detach < 0)\n@@ -930,7 +932,7 @@ int cmd_gc(int argc,\n \t\t/*\n \t\t * Auto-gc should be least intrusive as possible.\n \t\t */\n-\t\tif (!need_to_gc(&cfg)) {\n+\t\tif (!need_to_gc(&cfg, &repack_args)) {\n \t\t\tret = 0;\n \t\t\tgoto out;\n \t\t}\n@@ -952,7 +954,7 @@ int cmd_gc(int argc,\n \t\t\tfind_base_packs(&keep_pack, cfg.big_pack_threshold);\n \t\t}\n \n-\t\tadd_repack_all_option(&cfg, &keep_pack);\n+\t\tadd_repack_all_option(&cfg, &keep_pack, &repack_args);\n \t\tstring_list_clear(&keep_pack, 0);\n \t}\n \n@@ -1014,9 +1016,9 @@ int cmd_gc(int argc,\n \n \t\trepack_cmd.git_cmd = 1;\n \t\trepack_cmd.close_object_store = 1;\n-\t\tstrvec_pushv(&repack_cmd.args, repack.v);\n+\t\tstrvec_pushv(&repack_cmd.args, repack_args.v);\n \t\tif (run_command(&repack_cmd))\n-\t\t\tdie(FAILED_RUN, repack.v[0]);\n+\t\t\tdie(FAILED_RUN, repack_args.v[0]);\n \n \t\tif (cfg.prune_expire) {\n \t\t\tstruct child_process prune_cmd = CHILD_PROCESS_INIT;\n@@ -1067,6 +1069,7 @@ int cmd_gc(int argc,\n \n out:\n \tmaintenance_run_opts_release(&opts);\n+\tstrvec_clear(&repack_args);\n \tgc_config_release(&cfg);\n \treturn 0;\n }\n@@ -1269,6 +1272,19 @@ static int maintenance_task_gc_background(struct maintenance_run_opts *opts,\n \treturn run_command(&child);\n }\n \n+static int gc_condition(struct gc_config *cfg)\n+{\n+\t/*\n+\t * Note that it's fine to drop the repack arguments here, as we execute\n+\t * git-gc(1) as a separate child process anyway. So it knows to compute\n+\t * these arguments again.\n+\t */\n+\tstruct strvec repack_args = STRVEC_INIT;\n+\tint ret = need_to_gc(cfg, &repack_args);\n+\tstrvec_clear(&repack_args);\n+\treturn ret;\n+}\n+\n static int prune_packed(struct maintenance_run_opts *opts)\n {\n \tstruct child_process child = CHILD_PROCESS_INIT;\n@@ -1596,7 +1612,7 @@ static const struct maintenance_task tasks[] = {\n \t\t.name = \"gc\",\n \t\t.foreground = maintenance_task_gc_foreground,\n \t\t.background = maintenance_task_gc_background,\n-\t\t.auto_condition = need_to_gc,\n+\t\t.auto_condition = gc_condition,\n \t},\n \t[TASK_COMMIT_GRAPH] = {\n \t\t.name = \"commit-graph\",\n\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529695","messageId":"20251027-pks-maintenance-geometric-strategy-v4-2-18049e4a0adf@pks.im","threadId":"64330","inReplyTo":"20251027-pks-maintenance-geometric-strategy-v4-0-18049e4a0adf@pks.im","subject":"[PATCH v4 02/10] builtin/gc: make `too_many_loose_objects()` reusable without GC config","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T08:30:52Z","receivedAt":"2025-10-27T08:31:04Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"To decide whether or not a repository needs to be repacked we estimate\nthe number of loose objects. If the number exceeds a certain threshold\nwe perform the repack, otherwise we don't.\n\nThis is done via `too_many_loose_objects()`, which takes as parameter\nthe `struct gc_config`. This configuration is only used to determine the\nthreshold. In a subsequent commit we'll add another caller of this\nfunction that wants to pass a different limit than the one stored in\nthat structure.\n\nRefactor the function accordingly so that we only take the limit as\nparameter instead of the whole structure.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 8 ++++----\n 1 file changed, 4 insertions(+), 4 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e9772eb3a30..026d3a1d714 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -447,7 +447,7 @@ static int rerere_gc_condition(struct gc_config *cfg UNUSED)\n \treturn should_gc;\n }\n \n-static int too_many_loose_objects(struct gc_config *cfg)\n+static int too_many_loose_objects(int limit)\n {\n \t/*\n \t * Quickly check if a \"gc\" is needed, by estimating how\n@@ -469,7 +469,7 @@ static int too_many_loose_objects(struct gc_config *cfg)\n \tif (!dir)\n \t\treturn 0;\n \n-\tauto_threshold = DIV_ROUND_UP(cfg->gc_auto_threshold, 256);\n+\tauto_threshold = DIV_ROUND_UP(limit, 256);\n \twhile ((ent = readdir(dir)) != NULL) {\n \t\tif (strspn(ent->d_name, \"0123456789abcdef\") != hexsz_loose ||\n \t\t    ent->d_name[hexsz_loose] != '\\0')\n@@ -703,7 +703,7 @@ static int need_to_gc(struct gc_config *cfg, struct strvec *repack_args)\n \n \t\tadd_repack_all_option(cfg, &keep_pack, repack_args);\n \t\tstring_list_clear(&keep_pack, 0);\n-\t} else if (too_many_loose_objects(cfg))\n+\t} else if (too_many_loose_objects(cfg->gc_auto_threshold))\n \t\tadd_repack_incremental_option(repack_args);\n \telse\n \t\treturn 0;\n@@ -1057,7 +1057,7 @@ int cmd_gc(int argc,\n \t\t\t\t\t     !opts.quiet && !daemonized ? COMMIT_GRAPH_WRITE_PROGRESS : 0,\n \t\t\t\t\t     NULL);\n \n-\tif (opts.auto_flag && too_many_loose_objects(&cfg))\n+\tif (opts.auto_flag && too_many_loose_objects(cfg.gc_auto_threshold))\n \t\twarning(_(\"There are too many unreachable loose objects; \"\n \t\t\t\"run 'git prune' to remove them.\"));\n \n\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529696","messageId":"20251027-pks-maintenance-geometric-strategy-v4-3-18049e4a0adf@pks.im","threadId":"64330","inReplyTo":"20251027-pks-maintenance-geometric-strategy-v4-0-18049e4a0adf@pks.im","subject":"[PATCH v4 03/10] builtin/maintenance: introduce \"geometric-repack\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T08:30:53Z","receivedAt":"2025-10-27T08:31:07Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"geometric-repack\" task. This task uses our geometric\nrepack infrastructure as provided by git-repack(1) itself, which is a\nstrategy that especially hosting providers tend to use to amortize the\ncosts of repacking objects.\n\nThere is one issue though with geometric repacks, namely that they\nunconditionally pack all loose objects, regardless of whether or not\nthey are reachable. This is done because it means that we can completely\nskip the reachability step, which significantly speeds up the operation.\nBut it has the big downside that we are unable to expire objects over\ntime.\n\nTo address this issue we thus use a split strategy in this new task:\nwhenever a geometric repack would merge together all packs, we instead\ndo an all-into-one repack. By default, these all-into-one repacks have\ncruft packs enabled, so unreachable objects would now be written into\ntheir own pack. Consequently, they won't be soaked up during geometric\nrepacking anymore and can be expired with the next full repack, assuming\nthat their expiry date has surpassed.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/maintenance.adoc |  11 +++\n builtin/gc.c                          | 102 +++++++++++++++++++++++++\n t/t7900-maintenance.sh                | 138 ++++++++++++++++++++++++++++++++++\n 3 files changed, 251 insertions(+)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex 2f719342183..26dc5de423f 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -75,6 +75,17 @@ maintenance.incremental-repack.auto::\n \tnumber of pack-files not in the multi-pack-index is at least the value\n \tof `maintenance.incremental-repack.auto`. The default value is 10.\n \n+maintenance.geometric-repack.auto::\n+\tThis integer config option controls how often the `geometric-repack`\n+\ttask should be run as part of `git maintenance run --auto`. If zero,\n+\tthen the `geometric-repack` task will not run with the `--auto`\n+\toption. A negative value will force the task to run every time.\n+\tOtherwise, a positive value implies the command should run either when\n+\tthere are packfiles that need to be merged together to retain the\n+\tgeometric progression, or when there are at least this many loose\n+\tobjects that would be written into a new packfile. The default value is\n+\t100.\n+\n maintenance.reflog-expire.auto::\n \tThis integer config option controls how often the `reflog-expire` task\n \tshould be run as part of `git maintenance run --auto`. If zero, then\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 026d3a1d714..2c9ecd464d2 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -34,6 +34,7 @@\n #include \"pack-objects.h\"\n #include \"path.h\"\n #include \"reflog.h\"\n+#include \"repack.h\"\n #include \"rerere.h\"\n #include \"blob.h\"\n #include \"tree.h\"\n@@ -254,6 +255,7 @@ enum maintenance_task_label {\n \tTASK_PREFETCH,\n \tTASK_LOOSE_OBJECTS,\n \tTASK_INCREMENTAL_REPACK,\n+\tTASK_GEOMETRIC_REPACK,\n \tTASK_GC,\n \tTASK_COMMIT_GRAPH,\n \tTASK_PACK_REFS,\n@@ -1566,6 +1568,101 @@ static int maintenance_task_incremental_repack(struct maintenance_run_opts *opts\n \treturn 0;\n }\n \n+static int maintenance_task_geometric_repack(struct maintenance_run_opts *opts,\n+\t\t\t\t\t     struct gc_config *cfg)\n+{\n+\tstruct pack_geometry geometry = {\n+\t\t.split_factor = 2,\n+\t};\n+\tstruct pack_objects_args po_args = {\n+\t\t.local = 1,\n+\t};\n+\tstruct existing_packs existing_packs = EXISTING_PACKS_INIT;\n+\tstruct string_list kept_packs = STRING_LIST_INIT_DUP;\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\tint ret;\n+\n+\texisting_packs.repo = the_repository;\n+\texisting_packs_collect(&existing_packs, &kept_packs);\n+\tpack_geometry_init(&geometry, &existing_packs, &po_args);\n+\tpack_geometry_split(&geometry);\n+\n+\tchild.git_cmd = 1;\n+\n+\tstrvec_pushl(&child.args, \"repack\", \"-d\", \"-l\", NULL);\n+\tif (geometry.split < geometry.pack_nr)\n+\t\tstrvec_push(&child.args, \"--geometric=2\");\n+\telse\n+\t\tadd_repack_all_option(cfg, NULL, &child.args);\n+\tif (opts->quiet)\n+\t\tstrvec_push(&child.args, \"--quiet\");\n+\tif (the_repository->settings.core_multi_pack_index)\n+\t\tstrvec_push(&child.args, \"--write-midx\");\n+\n+\tif (run_command(&child)) {\n+\t\tret = error(_(\"failed to perform geometric repack\"));\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\texisting_packs_release(&existing_packs);\n+\tpack_geometry_release(&geometry);\n+\treturn ret;\n+}\n+\n+static int geometric_repack_auto_condition(struct gc_config *cfg UNUSED)\n+{\n+\tstruct pack_geometry geometry = {\n+\t\t.split_factor = 2,\n+\t};\n+\tstruct pack_objects_args po_args = {\n+\t\t.local = 1,\n+\t};\n+\tstruct existing_packs existing_packs = EXISTING_PACKS_INIT;\n+\tstruct string_list kept_packs = STRING_LIST_INIT_DUP;\n+\tint auto_value = 100;\n+\tint ret;\n+\n+\trepo_config_get_int(the_repository, \"maintenance.geometric-repack.auto\",\n+\t\t\t    &auto_value);\n+\tif (!auto_value)\n+\t\treturn 0;\n+\tif (auto_value < 0)\n+\t\treturn 1;\n+\n+\texisting_packs.repo = the_repository;\n+\texisting_packs_collect(&existing_packs, &kept_packs);\n+\tpack_geometry_init(&geometry, &existing_packs, &po_args);\n+\tpack_geometry_split(&geometry);\n+\n+\t/*\n+\t * When we'd merge at least two packs with one another we always\n+\t * perform the repack.\n+\t */\n+\tif (geometry.split) {\n+\t\tret = 1;\n+\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t * Otherwise, we estimate the number of loose objects to determine\n+\t * whether we want to create a new packfile or not.\n+\t */\n+\tif (too_many_loose_objects(auto_value)) {\n+\t\tret = 1;\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\texisting_packs_release(&existing_packs);\n+\tpack_geometry_release(&geometry);\n+\treturn ret;\n+}\n+\n typedef int (*maintenance_task_fn)(struct maintenance_run_opts *opts,\n \t\t\t\t   struct gc_config *cfg);\n typedef int (*maintenance_auto_fn)(struct gc_config *cfg);\n@@ -1608,6 +1705,11 @@ static const struct maintenance_task tasks[] = {\n \t\t.background = maintenance_task_incremental_repack,\n \t\t.auto_condition = incremental_repack_auto_condition,\n \t},\n+\t[TASK_GEOMETRIC_REPACK] = {\n+\t\t.name = \"geometric-repack\",\n+\t\t.background = maintenance_task_geometric_repack,\n+\t\t.auto_condition = geometric_repack_auto_condition,\n+\t},\n \t[TASK_GC] = {\n \t\t.name = \"gc\",\n \t\t.foreground = maintenance_task_gc_foreground,\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex ddd273d8dc2..842829879d8 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -465,6 +465,144 @@ test_expect_success 'maintenance.incremental-repack.auto (when config is unset)'\n \t)\n '\n \n+run_and_verify_geometric_pack () {\n+\tEXPECTED_PACKS=\"$1\" &&\n+\n+\t# Verify that we perform a geometric repack.\n+\trm -f \"trace2.txt\" &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/trace2.txt\" \\\n+\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n+\ttest_subcommand git repack -d -l --geometric=2 \\\n+\t\t--quiet --write-midx <trace2.txt &&\n+\n+\t# Verify that the number of packfiles matches our expectation.\n+\tls -l .git/objects/pack/*.pack >packfiles &&\n+\ttest_line_count = \"$EXPECTED_PACKS\" packfiles &&\n+\n+\t# And verify that there are no loose objects anymore.\n+\tgit count-objects -v >count &&\n+\ttest_grep '^count: 0$' count\n+}\n+\n+test_expect_success 'geometric repacking task' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tgit config set maintenance.auto false &&\n+\t\ttest_commit initial &&\n+\n+\t\t# The initial repack causes an all-into-one repack.\n+\t\tGIT_TRACE2_EVENT=\"$(pwd)/initial-repack.txt\" \\\n+\t\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n+\t\ttest_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago \\\n+\t\t\t--quiet --write-midx <initial-repack.txt &&\n+\n+\t\t# Repacking should now cause a no-op geometric repack because\n+\t\t# no packfiles need to be combined.\n+\t\tls -l .git/objects/pack/*.pack >before &&\n+\t\trun_and_verify_geometric_pack 1 &&\n+\t\tls -l .git/objects/pack/*.pack >after &&\n+\t\ttest_cmp before after &&\n+\n+\t\t# This incremental change creates a new packfile that only\n+\t\t# soaks up loose objects. The packfiles are not getting merged\n+\t\t# at this point.\n+\t\ttest_commit loose &&\n+\t\trun_and_verify_geometric_pack 2 &&\n+\n+\t\t# Both packfiles have 3 objects, so the next run would cause us\n+\t\t# to merge all packfiles together. This should be turned into\n+\t\t# an all-into-one-repack.\n+\t\tGIT_TRACE2_EVENT=\"$(pwd)/all-into-one-repack.txt\" \\\n+\t\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n+\t\ttest_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago \\\n+\t\t\t--quiet --write-midx <all-into-one-repack.txt &&\n+\n+\t\t# The geometric repack soaks up unreachable objects.\n+\t\techo blob-1 | git hash-object -w --stdin -t blob &&\n+\t\trun_and_verify_geometric_pack 2 &&\n+\n+\t\t# A second unreachable object should be written into another packfile.\n+\t\techo blob-2 | git hash-object -w --stdin -t blob &&\n+\t\trun_and_verify_geometric_pack 3 &&\n+\n+\t\t# And these two small packs should now be merged via the\n+\t\t# geometric repack. The large packfile should remain intact.\n+\t\trun_and_verify_geometric_pack 2 &&\n+\n+\t\t# If we now add two more objects and repack twice we should\n+\t\t# then see another all-into-one repack. This time around\n+\t\t# though, as we have unreachable objects, we should also see a\n+\t\t# cruft pack.\n+\t\techo blob-3 | git hash-object -w --stdin -t blob &&\n+\t\techo blob-4 | git hash-object -w --stdin -t blob &&\n+\t\trun_and_verify_geometric_pack 3 &&\n+\t\tGIT_TRACE2_EVENT=\"$(pwd)/cruft-repack.txt\" \\\n+\t\t\tgit maintenance run --task=geometric-repack 2>/dev/null &&\n+\t\ttest_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago \\\n+\t\t\t--quiet --write-midx <cruft-repack.txt &&\n+\t\tls .git/objects/pack/*.pack >packs &&\n+\t\ttest_line_count = 2 packs &&\n+\t\tls .git/objects/pack/*.mtimes >cruft &&\n+\t\ttest_line_count = 1 cruft\n+\t)\n+'\n+\n+test_geometric_repack_needed () {\n+\tNEEDED=\"$1\"\n+\tGEOMETRIC_CONFIG=\"$2\" &&\n+\trm -f trace2.txt &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/trace2.txt\" \\\n+\t\tgit ${GEOMETRIC_CONFIG:+-c maintenance.geometric-repack.$GEOMETRIC_CONFIG} \\\n+\t\tmaintenance run --auto --task=geometric-repack 2>/dev/null &&\n+\tcase \"$NEEDED\" in\n+\ttrue)\n+\t\ttest_grep \"\\[\\\"git\\\",\\\"repack\\\",\" trace2.txt;;\n+\tfalse)\n+\t\t! test_grep \"\\[\\\"git\\\",\\\"repack\\\",\" trace2.txt;;\n+\t*)\n+\t\tBUG \"invalid parameter: $NEEDED\";;\n+\tesac\n+}\n+\n+test_expect_success 'geometric repacking with --auto' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\n+\t\t# An empty repository does not need repacking, except when\n+\t\t# explicitly told to do it.\n+\t\ttest_geometric_repack_needed false &&\n+\t\ttest_geometric_repack_needed false auto=0 &&\n+\t\ttest_geometric_repack_needed false auto=1 &&\n+\t\ttest_geometric_repack_needed true auto=-1 &&\n+\n+\t\ttest_oid_init &&\n+\n+\t\t# Loose objects cause a repack when crossing the limit. Note\n+\t\t# that the number of objects gets extrapolated by having a look\n+\t\t# at the \"objects/17/\" shard.\n+\t\ttest_commit \"$(test_oid blob17_1)\" &&\n+\t\ttest_geometric_repack_needed false &&\n+\t\ttest_commit \"$(test_oid blob17_2)\" &&\n+\t\ttest_geometric_repack_needed false auto=257 &&\n+\t\ttest_geometric_repack_needed true auto=256 &&\n+\n+\t\t# Force another repack.\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_geometric_repack_needed true auto=-1 &&\n+\n+\t\t# We now have two packfiles that would be merged together. As\n+\t\t# such, the repack should always happen unless the user has\n+\t\t# disabled the auto task.\n+\t\ttest_geometric_repack_needed false auto=0 &&\n+\t\ttest_geometric_repack_needed true auto=9000\n+\t)\n+'\n+\n test_expect_success 'pack-refs task' '\n \tfor n in $(test_seq 1 5)\n \tdo\n\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529697","messageId":"20251027-pks-maintenance-geometric-strategy-v4-4-18049e4a0adf@pks.im","threadId":"64330","inReplyTo":"20251027-pks-maintenance-geometric-strategy-v4-0-18049e4a0adf@pks.im","subject":"[PATCH v4 04/10] builtin/maintenance: make the geometric factor configurable","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T08:30:54Z","receivedAt":"2025-10-27T08:31:10Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The geometric repacking task uses a factor of two for its geometric\nsequence, meaning that each next pack must contain at least twice as\nmany objects as the next-smaller one. In some cases it may be helpful to\nconfigure this factor though to reduce the number of packfile merges\neven further, e.g. in very big repositories. But while git-repack(1)\nitself supports doing this, the maintenance task does not give us a way\nto tune it.\n\nIntroduce a new \"maintenance.geometric-repack.splitFactor\" configuration\nto plug this gap.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/maintenance.adoc |  5 +++++\n builtin/gc.c                          |  9 ++++++++-\n t/t7900-maintenance.sh                | 32 ++++++++++++++++++++++++++++++++\n 3 files changed, 45 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex 26dc5de423f..45fdafc2c63 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -86,6 +86,11 @@ maintenance.geometric-repack.auto::\n \tobjects that would be written into a new packfile. The default value is\n \t100.\n \n+maintenance.geometric-repack.splitFactor::\n+\tThis integer config option controls the factor used for the geometric\n+\tsequence. See the `--geometric=` option in linkgit:git-repack[1] for\n+\tmore details. Defaults to `2`.\n+\n maintenance.reflog-expire.auto::\n \tThis integer config option controls how often the `reflog-expire` task\n \tshould be run as part of `git maintenance run --auto`. If zero, then\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 2c9ecd464d2..fb1a82e0304 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1582,6 +1582,9 @@ static int maintenance_task_geometric_repack(struct maintenance_run_opts *opts,\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tint ret;\n \n+\trepo_config_get_int(the_repository, \"maintenance.geometric-repack.splitFactor\",\n+\t\t\t    &geometry.split_factor);\n+\n \texisting_packs.repo = the_repository;\n \texisting_packs_collect(&existing_packs, &kept_packs);\n \tpack_geometry_init(&geometry, &existing_packs, &po_args);\n@@ -1591,7 +1594,8 @@ static int maintenance_task_geometric_repack(struct maintenance_run_opts *opts,\n \n \tstrvec_pushl(&child.args, \"repack\", \"-d\", \"-l\", NULL);\n \tif (geometry.split < geometry.pack_nr)\n-\t\tstrvec_push(&child.args, \"--geometric=2\");\n+\t\tstrvec_pushf(&child.args, \"--geometric=%d\",\n+\t\t\t     geometry.split_factor);\n \telse\n \t\tadd_repack_all_option(cfg, NULL, &child.args);\n \tif (opts->quiet)\n@@ -1632,6 +1636,9 @@ static int geometric_repack_auto_condition(struct gc_config *cfg UNUSED)\n \tif (auto_value < 0)\n \t\treturn 1;\n \n+\trepo_config_get_int(the_repository, \"maintenance.geometric-repack.splitFactor\",\n+\t\t\t    &geometry.split_factor);\n+\n \texisting_packs.repo = the_repository;\n \texisting_packs_collect(&existing_packs, &kept_packs);\n \tpack_geometry_init(&geometry, &existing_packs, &po_args);\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 842829879d8..8fda6b1a6f7 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -603,6 +603,38 @@ test_expect_success 'geometric repacking with --auto' '\n \t)\n '\n \n+test_expect_success 'geometric repacking honors configured split factor' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tgit config set maintenance.auto false &&\n+\n+\t\t# Create three different packs with 9, 2 and 1 object, respectively.\n+\t\t# This is done so that only a subset of packs would be merged\n+\t\t# together so that we can verify that `git repack` receives the\n+\t\t# correct geometric factor.\n+\t\tfor i in $(test_seq 9)\n+\t\tdo\n+\t\t\techo first-$i | git hash-object -w --stdin -t blob || return 1\n+\t\tdone &&\n+\t\tgit repack --geometric=2 -d &&\n+\n+\t\tfor i in $(test_seq 2)\n+\t\tdo\n+\t\t\techo second-$i | git hash-object -w --stdin -t blob || return 1\n+\t\tdone &&\n+\t\tgit repack --geometric=2 -d &&\n+\n+\t\techo third | git hash-object -w --stdin -t blob &&\n+\t\tgit repack --geometric=2 -d &&\n+\n+\t\ttest_geometric_repack_needed false splitFactor=2 &&\n+\t\ttest_geometric_repack_needed true splitFactor=3 &&\n+\t\ttest_subcommand git repack -d -l --geometric=3 --quiet --write-midx <trace2.txt\n+\t)\n+'\n+\n test_expect_success 'pack-refs task' '\n \tfor n in $(test_seq 1 5)\n \tdo\n\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529698","messageId":"20251027-pks-maintenance-geometric-strategy-v4-5-18049e4a0adf@pks.im","threadId":"64330","inReplyTo":"20251027-pks-maintenance-geometric-strategy-v4-0-18049e4a0adf@pks.im","subject":"[PATCH v4 05/10] builtin/maintenance: don't silently ignore invalid strategy","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T08:30:55Z","receivedAt":"2025-10-27T08:31:13Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When parsing maintenance strategies we completely ignore the\nuser-configured value in case it is unknown to us. This makes it\nbasically undiscoverable to the user that scheduled maintenance is\ndevolving into a no-op.\n\nChange this to instead die when seeing an unknown maintenance strategy.\nWhile at it, pull out the parsing logic into a separate function so that\nwe can reuse it in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c           | 17 +++++++++++------\n t/t7900-maintenance.sh |  5 +++++\n 2 files changed, 16 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex fb1a82e0304..726d944d3bd 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1855,6 +1855,13 @@ static const struct maintenance_strategy incremental_strategy = {\n \t},\n };\n \n+static struct maintenance_strategy parse_maintenance_strategy(const char *name)\n+{\n+\tif (!strcasecmp(name, \"incremental\"))\n+\t\treturn incremental_strategy;\n+\tdie(_(\"unknown maintenance strategy: '%s'\"), name);\n+}\n+\n static void initialize_task_config(struct maintenance_run_opts *opts,\n \t\t\t\t   const struct string_list *selected_tasks)\n {\n@@ -1890,12 +1897,10 @@ static void initialize_task_config(struct maintenance_run_opts *opts,\n \t * override specific aspects of our strategy.\n \t */\n \tif (opts->schedule) {\n-\t\tstrategy = none_strategy;\n-\n-\t\tif (!repo_config_get_string_tmp(the_repository, \"maintenance.strategy\", &config_str)) {\n-\t\t\tif (!strcasecmp(config_str, \"incremental\"))\n-\t\t\t\tstrategy = incremental_strategy;\n-\t\t}\n+\t\tif (!repo_config_get_string_tmp(the_repository, \"maintenance.strategy\", &config_str))\n+\t\t\tstrategy = parse_maintenance_strategy(config_str);\n+\t\telse\n+\t\t\tstrategy = none_strategy;\n \t} else {\n \t\tstrategy = default_strategy;\n \t}\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 8fda6b1a6f7..211350bf54e 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -1263,6 +1263,11 @@ test_expect_success 'fails when running outside of a repository' '\n \tnongit test_must_fail git maintenance unregister\n '\n \n+test_expect_success 'fails when configured to use an invalid strategy' '\n+\ttest_must_fail git -c maintenance.strategy=invalid maintenance run --schedule=hourly 2>err &&\n+\ttest_grep \"unknown maintenance strategy: .invalid.\" err\n+'\n+\n test_expect_success 'register and unregister bare repo' '\n \ttest_when_finished \"git config --global --unset-all maintenance.repo || :\" &&\n \ttest_might_fail git config --global --unset-all maintenance.repo &&\n\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529699","messageId":"20251027-pks-maintenance-geometric-strategy-v4-6-18049e4a0adf@pks.im","threadId":"64330","inReplyTo":"20251027-pks-maintenance-geometric-strategy-v4-0-18049e4a0adf@pks.im","subject":"[PATCH v4 06/10] builtin/maintenance: improve readability of strategies","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T08:30:56Z","receivedAt":"2025-10-27T08:31:16Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Our maintenance strategies are essentially a large array of structures,\nwhere each of the tasks can be enabled and scheduled individually. With\nthe current layout though all the configuration sits on the same nesting\nlayer, which makes it a bit hard to discern which initialized fields\nbelong to what task.\n\nImprove readability of the individual tasks by using nested designated\ninitializers instead.\n\nSuggested-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 36 +++++++++++++++++++++++++-----------\n 1 file changed, 25 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 726d944d3bd..0ba6e59de14 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1835,23 +1835,37 @@ struct maintenance_strategy {\n };\n \n static const struct maintenance_strategy none_strategy = { 0 };\n+\n static const struct maintenance_strategy default_strategy = {\n \t.tasks = {\n-\t\t[TASK_GC].enabled = 1,\n+\t\t[TASK_GC] = {\n+\t\t\t.enabled = 1,\n+\t\t},\n \t},\n };\n+\n static const struct maintenance_strategy incremental_strategy = {\n \t.tasks = {\n-\t\t[TASK_COMMIT_GRAPH].enabled = 1,\n-\t\t[TASK_COMMIT_GRAPH].schedule = SCHEDULE_HOURLY,\n-\t\t[TASK_PREFETCH].enabled = 1,\n-\t\t[TASK_PREFETCH].schedule = SCHEDULE_HOURLY,\n-\t\t[TASK_INCREMENTAL_REPACK].enabled = 1,\n-\t\t[TASK_INCREMENTAL_REPACK].schedule = SCHEDULE_DAILY,\n-\t\t[TASK_LOOSE_OBJECTS].enabled = 1,\n-\t\t[TASK_LOOSE_OBJECTS].schedule = SCHEDULE_DAILY,\n-\t\t[TASK_PACK_REFS].enabled = 1,\n-\t\t[TASK_PACK_REFS].schedule = SCHEDULE_WEEKLY,\n+\t\t[TASK_COMMIT_GRAPH] = {\n+\t\t\t.enabled = 1,\n+\t\t\t.schedule = SCHEDULE_HOURLY,\n+\t\t},\n+\t\t[TASK_PREFETCH] = {\n+\t\t\t.enabled = 1,\n+\t\t\t.schedule = SCHEDULE_HOURLY,\n+\t\t},\n+\t\t[TASK_INCREMENTAL_REPACK] = {\n+\t\t\t.enabled = 1,\n+\t\t\t.schedule = SCHEDULE_DAILY,\n+\t\t},\n+\t\t[TASK_LOOSE_OBJECTS] = {\n+\t\t\t.enabled = 1,\n+\t\t\t.schedule = SCHEDULE_DAILY,\n+\t\t},\n+\t\t[TASK_PACK_REFS] = {\n+\t\t\t.enabled = 1,\n+\t\t\t.schedule = SCHEDULE_WEEKLY,\n+\t\t},\n \t},\n };\n \n\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529700","messageId":"20251027-pks-maintenance-geometric-strategy-v4-7-18049e4a0adf@pks.im","threadId":"64330","inReplyTo":"20251027-pks-maintenance-geometric-strategy-v4-0-18049e4a0adf@pks.im","subject":"[PATCH v4 07/10] builtin/maintenance: run maintenance tasks depending on type","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T08:30:57Z","receivedAt":"2025-10-27T08:31:20Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We basically have three different ways to execute repository\nmaintenance:\n\n  1. Manual maintenance via `git maintenance run`.\n\n  2. Automatic maintenance via `git maintenance run --auto`.\n\n  3. Scheduled maintenance via `git maintenance run --schedule=`.\n\nAt the moment, maintenance strategies only have an effect for the last\ntype of maintenance. This is about to change in subsequent commits, but\nto do so we need to be able to skip some tasks depending on how exactly\nmaintenance was invoked.\n\nIntroduce a new maintenance type that discern between manual (1 & 2) and\nscheduled (3) maintenance. Convert the `enabled` field into a bitset so\nthat it becomes possible to specifiy which tasks exactly should run in a\nspecific context.\n\nThe types picked for existing strategies match the status quo:\n\n  - The default strategy is only ever executed as part of a manual\n    maintenance run. It is not possible to use it for scheduled\n    maintenance.\n\n  - The incremental strategy is only ever executed as part of a\n    scheduled maintenance run. It is not possible to use it for manual\n    maintenance.\n\nThe strategies will be tweaked in subsequent commits to make use of this\nnew infrastructure.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 28 +++++++++++++++++++---------\n 1 file changed, 19 insertions(+), 9 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 0ba6e59de1..6cc4f98c7a 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1827,9 +1827,16 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \treturn result;\n }\n \n+enum maintenance_type {\n+\t/* As invoked via `git maintenance run --schedule=`. */\n+\tMAINTENANCE_TYPE_SCHEDULED = (1 << 0),\n+\t/* As invoked via `git maintenance run` and with `--auto`. */\n+\tMAINTENANCE_TYPE_MANUAL    = (1 << 1),\n+};\n+\n struct maintenance_strategy {\n \tstruct {\n-\t\tint enabled;\n+\t\tunsigned type;\n \t\tenum schedule_priority schedule;\n \t} tasks[TASK__COUNT];\n };\n@@ -1839,7 +1846,7 @@ static const struct maintenance_strategy none_strategy = { 0 };\n static const struct maintenance_strategy default_strategy = {\n \t.tasks = {\n \t\t[TASK_GC] = {\n-\t\t\t.enabled = 1,\n+\t\t\t.type = MAINTENANCE_TYPE_MANUAL,\n \t\t},\n \t},\n };\n@@ -1847,23 +1854,23 @@ static const struct maintenance_strategy default_strategy = {\n static const struct maintenance_strategy incremental_strategy = {\n \t.tasks = {\n \t\t[TASK_COMMIT_GRAPH] = {\n-\t\t\t.enabled = 1,\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED,\n \t\t\t.schedule = SCHEDULE_HOURLY,\n \t\t},\n \t\t[TASK_PREFETCH] = {\n-\t\t\t.enabled = 1,\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED,\n \t\t\t.schedule = SCHEDULE_HOURLY,\n \t\t},\n \t\t[TASK_INCREMENTAL_REPACK] = {\n-\t\t\t.enabled = 1,\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED,\n \t\t\t.schedule = SCHEDULE_DAILY,\n \t\t},\n \t\t[TASK_LOOSE_OBJECTS] = {\n-\t\t\t.enabled = 1,\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED,\n \t\t\t.schedule = SCHEDULE_DAILY,\n \t\t},\n \t\t[TASK_PACK_REFS] = {\n-\t\t\t.enabled = 1,\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED,\n \t\t\t.schedule = SCHEDULE_WEEKLY,\n \t\t},\n \t},\n@@ -1881,6 +1888,7 @@ static void initialize_task_config(struct maintenance_run_opts *opts,\n {\n \tstruct strbuf config_name = STRBUF_INIT;\n \tstruct maintenance_strategy strategy;\n+\tenum maintenance_type type;\n \tconst char *config_str;\n \n \t/*\n@@ -1915,8 +1923,10 @@ static void initialize_task_config(struct maintenance_run_opts *opts,\n \t\t\tstrategy = parse_maintenance_strategy(config_str);\n \t\telse\n \t\t\tstrategy = none_strategy;\n+\t\ttype = MAINTENANCE_TYPE_SCHEDULED;\n \t} else {\n \t\tstrategy = default_strategy;\n+\t\ttype = MAINTENANCE_TYPE_MANUAL;\n \t}\n \n \tfor (size_t i = 0; i < TASK__COUNT; i++) {\n@@ -1926,8 +1936,8 @@ static void initialize_task_config(struct maintenance_run_opts *opts,\n \t\tstrbuf_addf(&config_name, \"maintenance.%s.enabled\",\n \t\t\t    tasks[i].name);\n \t\tif (!repo_config_get_bool(the_repository, config_name.buf, &config_value))\n-\t\t\tstrategy.tasks[i].enabled = config_value;\n-\t\tif (!strategy.tasks[i].enabled)\n+\t\t\tstrategy.tasks[i].type = config_value ? type : 0;\n+\t\tif (!(strategy.tasks[i].type & type))\n \t\t\tcontinue;\n \n \t\tif (opts->schedule) {\n\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529701","messageId":"20251027-pks-maintenance-geometric-strategy-v4-8-18049e4a0adf@pks.im","threadId":"64330","inReplyTo":"20251027-pks-maintenance-geometric-strategy-v4-0-18049e4a0adf@pks.im","subject":"[PATCH v4 08/10] builtin/maintenance: extend \"maintenance.strategy\" to manual maintenance","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T08:30:58Z","receivedAt":"2025-10-27T08:31:23Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"maintenance.strategy\" configuration allows users to configure how\nGit is supposed to perform repository maintenance. The idea is that we\nprovide a set of high-level strategies that may be useful in different\ncontexts, like for example when handling a large monorepo. Furthermore,\nthe strategy can be tweaked by the user by overriding specific tasks.\n\nIn its current form though, the strategy only applies to scheduled\nmaintenance. This creates something of a gap, as scheduled and manual\nmaintenance will now use _different_ strategies as the latter would\ncontinue to use git-gc(1) by default. This makes the strategies way less\nuseful than they could be on the one hand. But even more importantly,\nthe two different strategies might clash with one another, where one of\nthe strategies performs maintenance in such a way that it discards\nbenefits from the other strategy.\n\nSo ideally, it should be possible to pick one strategy that then applies\nglobally to all the different ways that we perform maintenance. This\ndoesn't necessarily mean that the strategy always does the _same_ thing\nfor every maintenance type. But it means that the strategy can configure\nthe different types to work in tandem with each other.\n\nChange the meaning of \"maintenance.strategy\" accordingly so that the\nstrategy is applied to both types, manual and scheduled. As preceding\ncommits have introduced logic to run maintenance tasks depending on this\ntype we can tweak strategies so that they perform those tasks depending\non the context.\n\nNote that this raises the question of backwards compatibility: when the\nuser has configured the \"incremental\" strategy we would have ignored\nthat strategy beforehand. Instead, repository maintenance would have\ncontinued to use git-gc(1) by default.\n\nBut luckily, we can match that behaviour by:\n\n  - Keeping all current tasks of the incremental strategy as\n    `MAINTENANCE_TYPE_SCHEDULED`. This ensures that those tasks will not\n    run during manual maintenance.\n\n  - Configuring the \"gc\" task so that it is invoked during manual\n    maintenance.\n\nLike this, the user shouldn't observe any difference in behaviour.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/maintenance.adoc | 22 ++++++++++++-------\n builtin/gc.c                          | 25 +++++++++++++++++-----\n t/t7900-maintenance.sh                | 40 +++++++++++++++++++++++++++++++++++\n 3 files changed, 74 insertions(+), 13 deletions(-)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex 45fdafc2c63..b7e90a71a3d 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -16,19 +16,25 @@ detach.\n \n maintenance.strategy::\n \tThis string config option provides a way to specify one of a few\n-\trecommended schedules for background maintenance. This only affects\n-\twhich tasks are run during `git maintenance run --schedule=X`\n-\tcommands, provided no `--task=<task>` arguments are provided.\n-\tFurther, if a `maintenance.<task>.schedule` config value is set,\n-\tthen that value is used instead of the one provided by\n-\t`maintenance.strategy`. The possible strategy strings are:\n+\trecommended strategies for repository maintenance. This affects\n+\twhich tasks are run during `git maintenance run`, provided no\n+\t`--task=<task>` arguments are provided. This setting impacts manual\n+\tmaintenance, auto-maintenance as well as scheduled maintenance. The\n+\ttasks that run may be different depending on the maintenance type.\n +\n-* `none`: This default setting implies no tasks are run at any schedule.\n+The maintenance strategy can be further tweaked by setting\n+`maintenance.<task>.enabled` and `maintenance.<task>.schedule`. If set, these\n+values are used instead of the defaults provided by `maintenance.strategy`.\n++\n+The possible strategies are:\n++\n+* `none`: This strategy implies no tasks are run at all. This is the default\n+  strategy for scheduled maintenance.\n * `incremental`: This setting optimizes for performing small maintenance\n   activities that do not delete any data. This does not schedule the `gc`\n   task, but runs the `prefetch` and `commit-graph` tasks hourly, the\n   `loose-objects` and `incremental-repack` tasks daily, and the `pack-refs`\n-  task weekly.\n+  task weekly. Manual repository maintenance uses the `gc` task.\n \n maintenance.<task>.enabled::\n \tThis boolean config option controls whether the maintenance task\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 6cc4f98c7aa..3c0a9a2e5df 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1873,6 +1873,20 @@ static const struct maintenance_strategy incremental_strategy = {\n \t\t\t.type = MAINTENANCE_TYPE_SCHEDULED,\n \t\t\t.schedule = SCHEDULE_WEEKLY,\n \t\t},\n+\t\t/*\n+\t\t * Historically, the \"incremental\" strategy was only available\n+\t\t * in the context of scheduled maintenance when set up via\n+\t\t * \"maintenance.strategy\". We have later expanded that config\n+\t\t * to also cover manual maintenance.\n+\t\t *\n+\t\t * To retain backwards compatibility with the previous status\n+\t\t * quo we thus run git-gc(1) in case manual maintenance was\n+\t\t * requested. This is the same as the default strategy, which\n+\t\t * would have been in use beforehand.\n+\t\t */\n+\t\t[TASK_GC] = {\n+\t\t\t.type = MAINTENANCE_TYPE_MANUAL,\n+\t\t},\n \t},\n };\n \n@@ -1916,19 +1930,20 @@ static void initialize_task_config(struct maintenance_run_opts *opts,\n \t *   - Unscheduled maintenance uses our default strategy.\n \t *\n \t * Both of these are affected by the gitconfig though, which may\n-\t * override specific aspects of our strategy.\n+\t * override specific aspects of our strategy. Furthermore, both\n+\t * strategies can be overridden by setting \"maintenance.strategy\".\n \t */\n \tif (opts->schedule) {\n-\t\tif (!repo_config_get_string_tmp(the_repository, \"maintenance.strategy\", &config_str))\n-\t\t\tstrategy = parse_maintenance_strategy(config_str);\n-\t\telse\n-\t\t\tstrategy = none_strategy;\n+\t\tstrategy = none_strategy;\n \t\ttype = MAINTENANCE_TYPE_SCHEDULED;\n \t} else {\n \t\tstrategy = default_strategy;\n \t\ttype = MAINTENANCE_TYPE_MANUAL;\n \t}\n \n+\tif (!repo_config_get_string_tmp(the_repository, \"maintenance.strategy\", &config_str))\n+\t\tstrategy = parse_maintenance_strategy(config_str);\n+\n \tfor (size_t i = 0; i < TASK__COUNT; i++) {\n \t\tint config_value;\n \ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 211350bf54e..a791a38916e 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -886,6 +886,46 @@ test_expect_success 'maintenance.strategy inheritance' '\n \t\t<modified-daily.txt\n '\n \n+test_strategy () {\n+\tSTRATEGY=\"$1\"\n+\tshift\n+\n+\tcat >expect &&\n+\trm -f trace2.txt &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/trace2.txt\" \\\n+\t\tgit -c maintenance.strategy=$STRATEGY maintenance run --quiet \"$@\" &&\n+\tsed -n 's/{\"event\":\"child_start\",\"sid\":\"[^/\"]*\",.*,\"argv\":\\[\"\\(.*\\)\\\"]}/\\1/p' <trace2.txt |\n+\t\tsed 's/\",\"/ /g'  >actual\n+\ttest_cmp expect actual\n+}\n+\n+test_expect_success 'maintenance.strategy is respected' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\n+\t\ttest_must_fail git -c maintenance.strategy=unknown maintenance run 2>err &&\n+\t\ttest_grep \"unknown maintenance strategy: .unknown.\" err &&\n+\n+\t\ttest_strategy incremental <<-\\EOF &&\n+\t\tgit pack-refs --all --prune\n+\t\tgit reflog expire --all\n+\t\tgit gc --quiet --no-detach --skip-foreground-tasks\n+\t\tEOF\n+\n+\t\ttest_strategy incremental --schedule=weekly <<-\\EOF\n+\t\tgit pack-refs --all --prune\n+\t\tgit prune-packed --quiet\n+\t\tgit multi-pack-index write --no-progress\n+\t\tgit multi-pack-index expire --no-progress\n+\t\tgit multi-pack-index repack --no-progress --batch-size=1\n+\t\tgit commit-graph write --split --reachable --no-progress\n+\t\tEOF\n+\t)\n+'\n+\n test_expect_success 'register and unregister' '\n \ttest_when_finished git config --global --unset-all maintenance.repo &&\n \n\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529702","messageId":"20251027-pks-maintenance-geometric-strategy-v4-9-18049e4a0adf@pks.im","threadId":"64330","inReplyTo":"20251027-pks-maintenance-geometric-strategy-v4-0-18049e4a0adf@pks.im","subject":"[PATCH v4 09/10] builtin/maintenance: make \"gc\" strategy accessible","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T08:30:59Z","receivedAt":"2025-10-27T08:31:26Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While the user can pick the \"incremental\" maintenance strategy, it is\nnot possible to explicitly use the \"gc\" strategy. This has two\ndownsides:\n\n  - It is impossible to use the default \"gc\" strategy for a specific\n    repository when the strategy was globally set to a different strategy.\n\n  - It is not possible to use git-gc(1) for scheduled maintenance.\n\nAddress these issues by making making the \"gc\" strategy configurable.\nFurthermore, extend the strategy so that git-gc(1) runs for both manual\nand scheduled maintenance.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/maintenance.adoc |  2 ++\n builtin/gc.c                          |  9 ++++++---\n t/t7900-maintenance.sh                | 14 +++++++++++++-\n 3 files changed, 21 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex b7e90a71a3d..b2bacdc8220 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -30,6 +30,8 @@ The possible strategies are:\n +\n * `none`: This strategy implies no tasks are run at all. This is the default\n   strategy for scheduled maintenance.\n+* `gc`: This strategy runs the `gc` task. This is the default strategy for\n+  manual maintenance.\n * `incremental`: This setting optimizes for performing small maintenance\n   activities that do not delete any data. This does not schedule the `gc`\n   task, but runs the `prefetch` and `commit-graph` tasks hourly, the\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 3c0a9a2e5df..8cab1450095 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1843,10 +1843,11 @@ struct maintenance_strategy {\n \n static const struct maintenance_strategy none_strategy = { 0 };\n \n-static const struct maintenance_strategy default_strategy = {\n+static const struct maintenance_strategy gc_strategy = {\n \t.tasks = {\n \t\t[TASK_GC] = {\n-\t\t\t.type = MAINTENANCE_TYPE_MANUAL,\n+\t\t\t.type = MAINTENANCE_TYPE_MANUAL | MAINTENANCE_TYPE_SCHEDULED,\n+\t\t\t.schedule = SCHEDULE_DAILY,\n \t\t},\n \t},\n };\n@@ -1894,6 +1895,8 @@ static struct maintenance_strategy parse_maintenance_strategy(const char *name)\n {\n \tif (!strcasecmp(name, \"incremental\"))\n \t\treturn incremental_strategy;\n+\tif (!strcasecmp(name, \"gc\"))\n+\t\treturn gc_strategy;\n \tdie(_(\"unknown maintenance strategy: '%s'\"), name);\n }\n \n@@ -1937,7 +1940,7 @@ static void initialize_task_config(struct maintenance_run_opts *opts,\n \t\tstrategy = none_strategy;\n \t\ttype = MAINTENANCE_TYPE_SCHEDULED;\n \t} else {\n-\t\tstrategy = default_strategy;\n+\t\tstrategy = gc_strategy;\n \t\ttype = MAINTENANCE_TYPE_MANUAL;\n \t}\n \ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex a791a38916e..65417a1e9c3 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -915,7 +915,7 @@ test_expect_success 'maintenance.strategy is respected' '\n \t\tgit gc --quiet --no-detach --skip-foreground-tasks\n \t\tEOF\n \n-\t\ttest_strategy incremental --schedule=weekly <<-\\EOF\n+\t\ttest_strategy incremental --schedule=weekly <<-\\EOF &&\n \t\tgit pack-refs --all --prune\n \t\tgit prune-packed --quiet\n \t\tgit multi-pack-index write --no-progress\n@@ -923,6 +923,18 @@ test_expect_success 'maintenance.strategy is respected' '\n \t\tgit multi-pack-index repack --no-progress --batch-size=1\n \t\tgit commit-graph write --split --reachable --no-progress\n \t\tEOF\n+\n+\t\ttest_strategy gc <<-\\EOF &&\n+\t\tgit pack-refs --all --prune\n+\t\tgit reflog expire --all\n+\t\tgit gc --quiet --no-detach --skip-foreground-tasks\n+\t\tEOF\n+\n+\t\ttest_strategy gc --schedule=weekly <<-\\EOF\n+\t\tgit pack-refs --all --prune\n+\t\tgit reflog expire --all\n+\t\tgit gc --quiet --no-detach --skip-foreground-tasks\n+\t\tEOF\n \t)\n '\n \n\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529703","messageId":"20251027-pks-maintenance-geometric-strategy-v4-10-18049e4a0adf@pks.im","threadId":"64330","inReplyTo":"20251027-pks-maintenance-geometric-strategy-v4-0-18049e4a0adf@pks.im","subject":"[PATCH v4 10/10] builtin/maintenance: introduce \"geometric\" strategy","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T08:31:00Z","receivedAt":"2025-10-27T08:31:30Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We have two different repacking strategies in Git:\n\n  - The \"gc\" strategy uses git-gc(1).\n\n  - The \"incremental\" strategy uses multi-pack indices and `git\n    multi-pack-index repack` to merge together smaller packfiles as\n    determined by a specific batch size.\n\nThe former strategy is our old and trusted default, whereas the latter\nhas historically been used for our scheduled maintenance. But both\nstrategies have their shortcomings:\n\n  - The \"gc\" strategy performs regular all-into-one repacks. Furthermore\n    it is rather inflexible, as it is not easily possible for a user to\n    enable or disable specific subtasks.\n\n  - The \"incremental\" strategy is not a full replacement for the \"gc\"\n    strategy as it doesn't know to prune stale data.\n\nSo today, we don't have a strategy that is well-suited for large repos\nwhile being a full replacement for the \"gc\" strategy.\n\nIntroduce a new \"geometric\" strategy that aims to fill this gap. This\nstrategy invokes all the usual cleanup tasks that git-gc(1) does like\npruning reflogs and rerere caches as well as stale worktrees. But where\nit differs from both the \"gc\" and \"incremental\" strategy is that it uses\nour geometric repacking infrastructure exposed by git-repack(1) to\nrepack packfiles. The advantage of geometric repacking is that we only\nneed to perform an all-into-one repack when the object count in a repo\nhas grown significantly.\n\nOne downside of this strategy is that pruning of unreferenced objects is\nnot going to happen regularly anymore. Every geometric repack knows to\nsoak up all loose objects regardless of their reachability, and merging\ntwo or more packs doesn't consider reachability, either. Consequently,\nthe number of unreachable objects will grow over time.\n\nThis is remedied by doing an all-into-one repack instead of a geometric\nrepack whenever we determine that the geometric repack would end up\nmerging all packfiles anyway. This all-into-one repack then performs our\nusual reachability checks and writes unreachable objects into a cruft\npack. As cruft packs won't ever be merged during geometric repacks we\ncan thus phase out these objects over time.\n\nOf course, this still means that we retain unreachable objects for far\nlonger than with the \"gc\" strategy. But the maintenance strategy is\nintended especially for large repositories, where the basic assumption\nis that the set of unreachable objects will be significantly dwarfed by\nthe number of reachable objects.\n\nIf this assumption is ever proven to be too disadvantageous we could for\nexample introduce a time-based strategy: if the largest packfile has not\nbeen touched for longer than $T, we perform an all-into-one repack. But\nfor now, such a mechanism is deferred into the future as it is not clear\nyet whether it is needed in the first place.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/maintenance.adoc |  9 +++++++++\n builtin/gc.c                          | 31 +++++++++++++++++++++++++++++++\n t/t7900-maintenance.sh                | 20 +++++++++++++++++++-\n 3 files changed, 59 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex b2bacdc8220..d0c38f03fab 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -32,6 +32,15 @@ The possible strategies are:\n   strategy for scheduled maintenance.\n * `gc`: This strategy runs the `gc` task. This is the default strategy for\n   manual maintenance.\n+* `geometric`: This strategy performs geometric repacking of packfiles and\n+  keeps auxiliary data structures up-to-date. The strategy expires data in the\n+  reflog and removes worktrees that cannot be located anymore. When the\n+  geometric repacking strategy would decide to do an all-into-one repack, then\n+  the strategy generates a cruft pack for all unreachable objects. Objects that\n+  are already part of a cruft pack will be expired.\n++\n+This repacking strategy is a full replacement for the `gc` strategy and is\n+recommended for large repositories.\n * `incremental`: This setting optimizes for performing small maintenance\n   activities that do not delete any data. This does not schedule the `gc`\n   task, but runs the `prefetch` and `commit-graph` tasks hourly, the\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 8cab1450095..19be3f87e13 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1891,12 +1891,43 @@ static const struct maintenance_strategy incremental_strategy = {\n \t},\n };\n \n+static const struct maintenance_strategy geometric_strategy = {\n+\t.tasks = {\n+\t\t[TASK_COMMIT_GRAPH] = {\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t\t.schedule = SCHEDULE_HOURLY,\n+\t\t},\n+\t\t[TASK_GEOMETRIC_REPACK] = {\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t\t.schedule = SCHEDULE_DAILY,\n+\t\t},\n+\t\t[TASK_PACK_REFS] = {\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t\t.schedule = SCHEDULE_DAILY,\n+\t\t},\n+\t\t[TASK_RERERE_GC] = {\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t\t.schedule = SCHEDULE_WEEKLY,\n+\t\t},\n+\t\t[TASK_REFLOG_EXPIRE] = {\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t\t.schedule = SCHEDULE_WEEKLY,\n+\t\t},\n+\t\t[TASK_WORKTREE_PRUNE] = {\n+\t\t\t.type = MAINTENANCE_TYPE_SCHEDULED | MAINTENANCE_TYPE_MANUAL,\n+\t\t\t.schedule = SCHEDULE_WEEKLY,\n+\t\t},\n+\t},\n+};\n+\n static struct maintenance_strategy parse_maintenance_strategy(const char *name)\n {\n \tif (!strcasecmp(name, \"incremental\"))\n \t\treturn incremental_strategy;\n \tif (!strcasecmp(name, \"gc\"))\n \t\treturn gc_strategy;\n+\tif (!strcasecmp(name, \"geometric\"))\n+\t\treturn geometric_strategy;\n \tdie(_(\"unknown maintenance strategy: '%s'\"), name);\n }\n \ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 65417a1e9c3..614184a0978 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -930,11 +930,29 @@ test_expect_success 'maintenance.strategy is respected' '\n \t\tgit gc --quiet --no-detach --skip-foreground-tasks\n \t\tEOF\n \n-\t\ttest_strategy gc --schedule=weekly <<-\\EOF\n+\t\ttest_strategy gc --schedule=weekly <<-\\EOF &&\n \t\tgit pack-refs --all --prune\n \t\tgit reflog expire --all\n \t\tgit gc --quiet --no-detach --skip-foreground-tasks\n \t\tEOF\n+\n+\t\ttest_strategy geometric <<-\\EOF &&\n+\t\tgit pack-refs --all --prune\n+\t\tgit reflog expire --all\n+\t\tgit repack -d -l --geometric=2 --quiet --write-midx\n+\t\tgit commit-graph write --split --reachable --no-progress\n+\t\tgit worktree prune --expire 3.months.ago\n+\t\tgit rerere gc\n+\t\tEOF\n+\n+\t\ttest_strategy geometric --schedule=weekly <<-\\EOF\n+\t\tgit pack-refs --all --prune\n+\t\tgit reflog expire --all\n+\t\tgit repack -d -l --geometric=2 --quiet --write-midx\n+\t\tgit commit-graph write --split --reachable --no-progress\n+\t\tgit worktree prune --expire 3.months.ago\n+\t\tgit rerere gc\n+\t\tEOF\n \t)\n '\n \n\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529729","messageId":"20251027142503.GA2758515@coredump.intra.peff.net","threadId":"64330","inReplyTo":"aP8sJF45dHsVlgqt@pks.im","subject":"Re: [PATCH v3 03/10] builtin/maintenance: introduce \"geometric-repack\" task","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-10-27T14:25:03Z","receivedAt":"2025-10-27T14:25:14Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Mon, Oct 27, 2025 at 09:24:11AM +0100, Patrick Steinhardt wrote:\n\n> Interesting! I would say that this is an issue in git-repack(1) itself:\n> if the geometric repack didn't lead to any new packs, and if all of the\n> packs are already covered by a MIDX, then we still rather pointlessly\n> regenerate the MIDX even though it won't cover anything new.\n> \n> I wonder whether we want a patch like the below one? Problem though is\n> that we'd also have to check whether any of the other options have\n> changed, otherwise we for example wouldn't generate bitmaps.\n> \n> In any case though, I feel like this is a bit out of scope for this\n> patch series. Other strategies that write a MIDX behave the same, so\n> this is something we can fix later on.\n\nI agree it's out of scope for the series, but the racy test is new. So\nwe probably at least need to make it un-racy with a comment to address\nthe root cause later.\n\nThe patch you posted looks plausibly correct, though I agree with you\nthat some thought needs to be given to changing options.\n\n-Peff\n"},{"id":"529738","messageId":"xmqqtszkjq8d.fsf@gitster.g","threadId":"64330","inReplyTo":"20251027-pks-maintenance-geometric-strategy-v4-0-18049e4a0adf@pks.im","subject":"Re: [PATCH v4 00/10] builtin/maintenance: introduce \"geometric\" strategy","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-27T15:53:22Z","receivedAt":"2025-10-27T15:53:24Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Changes in v4:\n>   - Fix a flaky test because git-repack(1) always decides to rewrite the\n>     MIDX, even though no packs have changed. This isn't a new issue, and\n>     other maintenance tasks behave the same. So I decided to punt on it\n>     for now.\n\nThanks, but this round raced against 'next', so let me fabricate the\nfollowing and queue it instead.\n\n------- >8 -------\nFrom: Patrick Steinhardt <ps@pks.im>\nDate: Mon, 27 Oct 2025 09:30:50 +0100\nSubject: [PATCH] t7900: fix a flaky test due to git-repack always regenerating .midx\n\nWhen a supposedly no-op \"git repack\" runs across a second boundary,\nbecause the command always touches the MIDX file and updates its\ntimestamp, \"ls -l $GIT_DIR/objects/pack/\" before and after the\noperation can change, which causes such a test to fail.  Only\ncompare the *.pack files in the directory before and after the\noperation to work around this flakyness.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n[jc: taken from diff to v4 from v3 that was already merged to 'next']\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n t/t7900-maintenance.sh | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 0d76693fee..614184a097 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -500,9 +500,9 @@ test_expect_success 'geometric repacking task' '\n \n \t\t# Repacking should now cause a no-op geometric repack because\n \t\t# no packfiles need to be combined.\n-\t\tls -l .git/objects/pack >before &&\n+\t\tls -l .git/objects/pack/*.pack >before &&\n \t\trun_and_verify_geometric_pack 1 &&\n-\t\tls -l .git/objects/pack >after &&\n+\t\tls -l .git/objects/pack/*.pack >after &&\n \t\ttest_cmp before after &&\n \n \t\t# This incremental change creates a new packfile that only\n-- \n2.51.2-678-g0cd646409c\n\n"},{"id":"529753","messageId":"aP_Qe2QG5w3g4Z9y@pks.im","threadId":"64330","inReplyTo":"xmqqtszkjq8d.fsf@gitster.g","subject":"Re: [PATCH v4 00/10] builtin/maintenance: introduce \"geometric\" strategy","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T20:05:15Z","receivedAt":"2025-10-27T20:05:25Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Oct 27, 2025 at 08:53:22AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > Changes in v4:\n> >   - Fix a flaky test because git-repack(1) always decides to rewrite the\n> >     MIDX, even though no packs have changed. This isn't a new issue, and\n> >     other maintenance tasks behave the same. So I decided to punt on it\n> >     for now.\n> \n> Thanks, but this round raced against 'next', so let me fabricate the\n> following and queue it instead.\n\nThanks for doing this!\n\n> ------- >8 -------\n> From: Patrick Steinhardt <ps@pks.im>\n> Date: Mon, 27 Oct 2025 09:30:50 +0100\n> Subject: [PATCH] t7900: fix a flaky test due to git-repack always regenerating .midx\n\ns/.midx/MIDX, as the MIDX file does not have a dot anywhere.\n\n> \n> When a supposedly no-op \"git repack\" runs across a second boundary,\n> because the command always touches the MIDX file and updates its\n> timestamp, \"ls -l $GIT_DIR/objects/pack/\" before and after the\n> operation can change, which causes such a test to fail.  Only\n> compare the *.pack files in the directory before and after the\n> operation to work around this flakyness.\n\nMaybe add something like the following:\n\n    Arguably, git-repack(1) should learn to not rewrite the MIDX in case\n    we know it is already up-to-date. But this is not a new problem\n    introduced via the new geometric maintenance task, so for now it\n    should be good enough to paper over the issue.\n\nBut I think this looks good enough already, so please feel free to\nignore. Happy to have my authorship with either of these versions.\n\nThanks!\n\nPatrick\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> [jc: taken from diff to v4 from v3 that was already merged to 'next']\n> Signed-off-by: Junio C Hamano <gitster@pobox.com>\n> ---\n>  t/t7900-maintenance.sh | 4 ++--\n>  1 file changed, 2 insertions(+), 2 deletions(-)\n> \n> diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\n> index 0d76693fee..614184a097 100755\n> --- a/t/t7900-maintenance.sh\n> +++ b/t/t7900-maintenance.sh\n> @@ -500,9 +500,9 @@ test_expect_success 'geometric repacking task' '\n>  \n>  \t\t# Repacking should now cause a no-op geometric repack because\n>  \t\t# no packfiles need to be combined.\n> -\t\tls -l .git/objects/pack >before &&\n> +\t\tls -l .git/objects/pack/*.pack >before &&\n>  \t\trun_and_verify_geometric_pack 1 &&\n> -\t\tls -l .git/objects/pack >after &&\n> +\t\tls -l .git/objects/pack/*.pack >after &&\n>  \t\ttest_cmp before after &&\n>  \n>  \t\t# This incremental change creates a new packfile that only\n> -- \n> 2.51.2-678-g0cd646409c\n> \n"},{"id":"529758","messageId":"xmqqbjlshxje.fsf@gitster.g","threadId":"64330","inReplyTo":"aP_Qe2QG5w3g4Z9y@pks.im","subject":"Re: [PATCH v4 00/10] builtin/maintenance: introduce \"geometric\" strategy","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-27T20:58:29Z","receivedAt":"2025-10-27T20:58:31Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Mon, Oct 27, 2025 at 08:53:22AM -0700, Junio C Hamano wrote:\n>> Patrick Steinhardt <ps@pks.im> writes:\n>> \n>> > Changes in v4:\n>> >   - Fix a flaky test because git-repack(1) always decides to rewrite the\n>> >     MIDX, even though no packs have changed. This isn't a new issue, and\n>> >     other maintenance tasks behave the same. So I decided to punt on it\n>> >     for now.\n>> \n>> Thanks, but this round raced against 'next', so let me fabricate the\n>> following and queue it instead.\n>\n> Thanks for doing this!\n>\n>> ------- >8 -------\n>> From: Patrick Steinhardt <ps@pks.im>\n>> Date: Mon, 27 Oct 2025 09:30:50 +0100\n>> Subject: [PATCH] t7900: fix a flaky test due to git-repack always regenerating .midx\n>\n> s/.midx/MIDX, as the MIDX file does not have a dot anywhere.\n>\n>> \n>> When a supposedly no-op \"git repack\" runs across a second boundary,\n>> because the command always touches the MIDX file and updates its\n>> timestamp, \"ls -l $GIT_DIR/objects/pack/\" before and after the\n>> operation can change, which causes such a test to fail.  Only\n>> compare the *.pack files in the directory before and after the\n>> operation to work around this flakyness.\n>\n> Maybe add something like the following:\n>\n>     Arguably, git-repack(1) should learn to not rewrite the MIDX in case\n>     we know it is already up-to-date. But this is not a new problem\n>     introduced via the new geometric maintenance task, so for now it\n>     should be good enough to paper over the issue.\n\nWill add.  Let me mark it for 'next' after squashing it in.\n"}]}