{"thread":{"id":"63531","subject":"[PATCH 00/11] builtin/maintenance: fix ref lock races when detaching","startedAt":"2025-05-27T14:04:31Z","lastAt":"2025-06-06T05:23:08Z","messageCount":71,"participants":["Patrick Steinhardt","Ramsay Jones","Kristoffer Haugsbakk","Karthik Nayak","Junio C Hamano","Ben Knoble","Derrick Stolee"],"isPatch":true,"patchVersion":1,"patchTotal":11},"messages":[{"id":"518980","messageId":"20250527-b4-pks-maintenance-ref-lock-race-v1-0-e1ceb2dea66e@pks.im","threadId":"63531","inReplyTo":null,"subject":"[PATCH 00/11] builtin/maintenance: fix ref lock races when detaching","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-27T14:04:26Z","receivedAt":"2025-05-27T14:04:31Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis patch series fixes races around locking the \"packed-refs\" file when\nauto-maintenance decides to repack it. This issue has been reported e.g.\nvia [1] and [2].\n\nThe root cause is that git-gc(1) used to know to detach _after_ having\nrepacked references. As such, callers wouldn't continue with their thing\nuntil we have already packed refs, and thus the race does not exist\nthere. git-maintenance(1) didn't have the same split though, so this\npatch series retrofits that logic.\n\nThe series is structured as follows:\n\n  - Patches 1 and 2 do some light refactorings.\n\n  - Patches 3 to 5 refactor how we set up the list of tasks to not rely\n    on globals anymore. Instead, we now have a single source of truth\n    for which tasks exactly will be run.\n\n  - The remaining patches introduce the split of before/after-detach\n    tasks and wire them up for \"pack-refs\", \"reflog-expire\" and \"gc\"\n    tasks.\n\nThanks!\n\nPatrick\n\n[1]: <CAJR-fbZ4X1+gN75m2dUvocR6NkowLOZ9F26cjBy8w1qd181OoQ@mail.gmail.com>\n[2]: <CANi7bVAkNc+gY1NoXfJuDRjxjZLTgL8Lfn8_ZmWsvLAoiLPkNg@mail.gmail.com>\n\n---\nPatrick Steinhardt (11):\n      builtin/gc: use designated field initializers for maintenance tasks\n      builtin/gc: drop redundant local variable\n      builtin/maintenance: centralize configuration of explicit tasks\n      builtin/maintenance: mark \"--task=\" and \"--schedule=\" as incompatible\n      builtin/maintenance: stop modifying global array of tasks\n      builtin/maintenance: extract function to run tasks\n      builtin/maintenance: fix typedef for function pointers\n      builtin/maintenance: let tasks do maintenance before and after detach\n      builtin/maintenance: fix locking race when packing refs and reflogs\n      builtin/gc: avoid global state in `gc_before_repack()`\n      builtin/maintenance: fix locking race when handling \"gc\" task\n\n builtin/gc.c           | 386 +++++++++++++++++++++++++++----------------------\n t/t7900-maintenance.sh |  19 ++-\n 2 files changed, 229 insertions(+), 176 deletions(-)\n\n\n---\nbase-commit: 845c48a16a7f7b2c44d8cb137b16a4a1f0140229\nchange-id: 20250527-b4-pks-maintenance-ref-lock-race-11ae5d68e06f\n\n"},{"id":"518981","messageId":"20250527-b4-pks-maintenance-ref-lock-race-v1-1-e1ceb2dea66e@pks.im","threadId":"63531","inReplyTo":"20250527-b4-pks-maintenance-ref-lock-race-v1-0-e1ceb2dea66e@pks.im","subject":"[PATCH 01/11] builtin/gc: use designated field initializers for maintenance tasks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-27T14:04:27Z","receivedAt":"2025-05-27T14:04:32Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Convert the array of maintenance tasks to use designated field\ninitializers. This makes it easier to add more fields to the struct\nwithout having to modify all tasks.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 54 +++++++++++++++++++++++++++---------------------------\n 1 file changed, 27 insertions(+), 27 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e33ba946e43..54fc7f299a9 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1550,49 +1550,49 @@ enum maintenance_task_label {\n \n static struct maintenance_task tasks[] = {\n \t[TASK_PREFETCH] = {\n-\t\t\"prefetch\",\n-\t\tmaintenance_task_prefetch,\n+\t\t.name = \"prefetch\",\n+\t\t.fn = maintenance_task_prefetch,\n \t},\n \t[TASK_LOOSE_OBJECTS] = {\n-\t\t\"loose-objects\",\n-\t\tmaintenance_task_loose_objects,\n-\t\tloose_object_auto_condition,\n+\t\t.name = \"loose-objects\",\n+\t\t.fn = maintenance_task_loose_objects,\n+\t\t.auto_condition = loose_object_auto_condition,\n \t},\n \t[TASK_INCREMENTAL_REPACK] = {\n-\t\t\"incremental-repack\",\n-\t\tmaintenance_task_incremental_repack,\n-\t\tincremental_repack_auto_condition,\n+\t\t.name = \"incremental-repack\",\n+\t\t.fn = maintenance_task_incremental_repack,\n+\t\t.auto_condition = incremental_repack_auto_condition,\n \t},\n \t[TASK_GC] = {\n-\t\t\"gc\",\n-\t\tmaintenance_task_gc,\n-\t\tneed_to_gc,\n-\t\t1,\n+\t\t.name = \"gc\",\n+\t\t.fn = maintenance_task_gc,\n+\t\t.auto_condition = need_to_gc,\n+\t\t.enabled = 1,\n \t},\n \t[TASK_COMMIT_GRAPH] = {\n-\t\t\"commit-graph\",\n-\t\tmaintenance_task_commit_graph,\n-\t\tshould_write_commit_graph,\n+\t\t.name = \"commit-graph\",\n+\t\t.fn = maintenance_task_commit_graph,\n+\t\t.auto_condition = should_write_commit_graph,\n \t},\n \t[TASK_PACK_REFS] = {\n-\t\t\"pack-refs\",\n-\t\tmaintenance_task_pack_refs,\n-\t\tpack_refs_condition,\n+\t\t.name = \"pack-refs\",\n+\t\t.fn = maintenance_task_pack_refs,\n+\t\t.auto_condition = pack_refs_condition,\n \t},\n \t[TASK_REFLOG_EXPIRE] = {\n-\t\t\"reflog-expire\",\n-\t\tmaintenance_task_reflog_expire,\n-\t\treflog_expire_condition,\n+\t\t.name = \"reflog-expire\",\n+\t\t.fn = maintenance_task_reflog_expire,\n+\t\t.auto_condition = reflog_expire_condition,\n \t},\n \t[TASK_WORKTREE_PRUNE] = {\n-\t\t\"worktree-prune\",\n-\t\tmaintenance_task_worktree_prune,\n-\t\tworktree_prune_condition,\n+\t\t.name = \"worktree-prune\",\n+\t\t.fn = maintenance_task_worktree_prune,\n+\t\t.auto_condition = worktree_prune_condition,\n \t},\n \t[TASK_RERERE_GC] = {\n-\t\t\"rerere-gc\",\n-\t\tmaintenance_task_rerere_gc,\n-\t\trerere_gc_condition,\n+\t\t.name = \"rerere-gc\",\n+\t\t.fn = maintenance_task_rerere_gc,\n+\t\t.auto_condition = rerere_gc_condition,\n \t},\n };\n \n\n-- \n2.49.0.1266.g31b7d2e469.dirty\n\n"},{"id":"518982","messageId":"20250527-b4-pks-maintenance-ref-lock-race-v1-2-e1ceb2dea66e@pks.im","threadId":"63531","inReplyTo":"20250527-b4-pks-maintenance-ref-lock-race-v1-0-e1ceb2dea66e@pks.im","subject":"[PATCH 02/11] builtin/gc: drop redundant local variable","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-27T14:04:28Z","receivedAt":"2025-05-27T14:04:33Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We have two different variables that track the quietness for git-gc(1):\n\n  - The local variable `quiet`, which we wire up.\n\n  - The `quiet` field of `struct maintenance_run_opts`.\n\nThis leads to confusion which of these variables should be used and what\nthe respective effect is.\n\nSimplify this logic by dropping the local variable in favor of the\noptions field.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 11 +++++------\n 1 file changed, 5 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 54fc7f299a9..7adda8d2d0d 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -818,7 +818,6 @@ int cmd_gc(int argc,\n \t   struct repository *repo UNUSED)\n {\n \tint aggressive = 0;\n-\tint quiet = 0;\n \tint force = 0;\n \tconst char *name;\n \tpid_t pid;\n@@ -831,7 +830,7 @@ int cmd_gc(int argc,\n \tconst char *prune_expire_arg = prune_expire_sentinel;\n \tint ret;\n \tstruct option builtin_gc_options[] = {\n-\t\tOPT__QUIET(&quiet, N_(\"suppress progress reporting\")),\n+\t\tOPT__QUIET(&opts.quiet, N_(\"suppress progress reporting\")),\n \t\t{\n \t\t\t.type = OPTION_STRING,\n \t\t\t.long_name = \"prune\",\n@@ -891,7 +890,7 @@ int cmd_gc(int argc,\n \t\tif (cfg.aggressive_window > 0)\n \t\t\tstrvec_pushf(&repack, \"--window=%d\", cfg.aggressive_window);\n \t}\n-\tif (quiet)\n+\tif (opts.quiet)\n \t\tstrvec_push(&repack, \"-q\");\n \n \tif (opts.auto_flag) {\n@@ -906,7 +905,7 @@ int cmd_gc(int argc,\n \t\t\tgoto out;\n \t\t}\n \n-\t\tif (!quiet) {\n+\t\tif (!opts.quiet) {\n \t\t\tif (opts.detach > 0)\n \t\t\t\tfprintf(stderr, _(\"Auto packing the repository in background for optimum performance.\\n\"));\n \t\t\telse\n@@ -991,7 +990,7 @@ int cmd_gc(int argc,\n \t\t\tstrvec_pushl(&prune_cmd.args, \"prune\", \"--expire\", NULL);\n \t\t\t/* run `git prune` even if using cruft packs */\n \t\t\tstrvec_push(&prune_cmd.args, cfg.prune_expire);\n-\t\t\tif (quiet)\n+\t\t\tif (opts.quiet)\n \t\t\t\tstrvec_push(&prune_cmd.args, \"--no-progress\");\n \t\t\tif (repo_has_promisor_remote(the_repository))\n \t\t\t\tstrvec_push(&prune_cmd.args,\n@@ -1019,7 +1018,7 @@ int cmd_gc(int argc,\n \n \tif (the_repository->settings.gc_write_commit_graph == 1)\n \t\twrite_commit_graph_reachable(the_repository->objects->odb,\n-\t\t\t\t\t     !quiet && !daemonized ? COMMIT_GRAPH_WRITE_PROGRESS : 0,\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\n-- \n2.49.0.1266.g31b7d2e469.dirty\n\n"},{"id":"518983","messageId":"20250527-b4-pks-maintenance-ref-lock-race-v1-3-e1ceb2dea66e@pks.im","threadId":"63531","inReplyTo":"20250527-b4-pks-maintenance-ref-lock-race-v1-0-e1ceb2dea66e@pks.im","subject":"[PATCH 03/11] builtin/maintenance: centralize configuration of explicit tasks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-27T14:04:29Z","receivedAt":"2025-05-27T14:04:34Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Users of git-maintenance(1) can explicitly ask it to run specific tasks\nby passing the `--task=` command line option. This option can be passed\nmultiple times, which causes us to execute tasks in the same order as\nthe tasks have been provided by the user.\n\nThe order in which tasks are run is computed in `task_option_parse()`:\nevery time we parse such a command line argument, we modify the global\narray of tasks by seting the selected index for that specific task.\nThis has two downsides:\n\n  - We modify global state, which makes it hard to follow the logic.\n\n  - The configuration of tasks is split across multiple different\n    functions, so it is not easy to figure out the different factors\n    that play a role in selecting tasks.\n\nRefactor the logic so that `task_option_parse()` does not modify global\nstate anymore. Instead, this function now only collects the list of\nconfigured tasks. The logic to configure ordering of the respective\ntasks is then deferred to `initialize_task_config()`.\n\nThis refactoring solves the second problem, that the configuration of\ntasks is spread across multiple different locations. The first problem,\nthat we modify global state, will be fixed in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 47 ++++++++++++++++++++++++-----------------------\n 1 file changed, 24 insertions(+), 23 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 7adda8d2d0d..c4af9b11287 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1690,15 +1690,22 @@ static void initialize_maintenance_strategy(void)\n \t}\n }\n \n-static void initialize_task_config(int schedule)\n+static void initialize_task_config(const struct string_list *selected_tasks,\n+\t\t\t\t   int schedule)\n {\n-\tint i;\n \tstruct strbuf config_name = STRBUF_INIT;\n \n+\tfor (size_t i = 0; i < TASK__COUNT; i++)\n+\t\ttasks[i].selected_order = -1;\n+\tfor (size_t i = 0; i < selected_tasks->nr; i++) {\n+\t\tstruct maintenance_task *task = selected_tasks->items[i].util;\n+\t\ttask->selected_order = i;\n+\t}\n+\n \tif (schedule)\n \t\tinitialize_maintenance_strategy();\n \n-\tfor (i = 0; i < TASK__COUNT; i++) {\n+\tfor (size_t i = 0; i < TASK__COUNT; i++) {\n \t\tint config_value;\n \t\tchar *config_str;\n \n@@ -1722,33 +1729,28 @@ static void initialize_task_config(int schedule)\n \tstrbuf_release(&config_name);\n }\n \n-static int task_option_parse(const struct option *opt UNUSED,\n+static int task_option_parse(const struct option *opt,\n \t\t\t     const char *arg, int unset)\n {\n-\tint i, num_selected = 0;\n-\tstruct maintenance_task *task = NULL;\n+\tstruct string_list *selected_tasks = opt->value;\n+\tsize_t i;\n \n \tBUG_ON_OPT_NEG(unset);\n \n-\tfor (i = 0; i < TASK__COUNT; i++) {\n-\t\tif (tasks[i].selected_order >= 0)\n-\t\t\tnum_selected++;\n-\t\tif (!strcasecmp(tasks[i].name, arg)) {\n-\t\t\ttask = &tasks[i];\n-\t\t}\n-\t}\n-\n-\tif (!task) {\n+\tfor (i = 0; i < TASK__COUNT; i++)\n+\t\tif (!strcasecmp(tasks[i].name, arg))\n+\t\t\tbreak;\n+\tif (i >= TASK__COUNT) {\n \t\terror(_(\"'%s' is not a valid task\"), arg);\n \t\treturn 1;\n \t}\n \n-\tif (task->selected_order >= 0) {\n+\tif (unsorted_string_list_has_string(selected_tasks, arg)) {\n \t\terror(_(\"task '%s' cannot be selected multiple times\"), arg);\n \t\treturn 1;\n \t}\n \n-\ttask->selected_order = num_selected + 1;\n+\tstring_list_append(selected_tasks, arg)->util = &tasks[i];\n \n \treturn 0;\n }\n@@ -1756,8 +1758,8 @@ static int task_option_parse(const struct option *opt UNUSED,\n static int maintenance_run(int argc, const char **argv, const char *prefix,\n \t\t\t   struct repository *repo UNUSED)\n {\n-\tint i;\n \tstruct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT;\n+\tstruct string_list selected_tasks = STRING_LIST_INIT_DUP;\n \tstruct gc_config cfg = GC_CONFIG_INIT;\n \tstruct option builtin_maintenance_run_options[] = {\n \t\tOPT_BOOL(0, \"auto\", &opts.auto_flag,\n@@ -1769,7 +1771,7 @@ static int maintenance_run(int argc, const char **argv, const char *prefix,\n \t\t\t     maintenance_opt_schedule),\n \t\tOPT_BOOL(0, \"quiet\", &opts.quiet,\n \t\t\t N_(\"do not report progress or other information over stderr\")),\n-\t\tOPT_CALLBACK_F(0, \"task\", NULL, N_(\"task\"),\n+\t\tOPT_CALLBACK_F(0, \"task\", &selected_tasks, N_(\"task\"),\n \t\t\tN_(\"run a specific task\"),\n \t\t\tPARSE_OPT_NONEG, task_option_parse),\n \t\tOPT_END()\n@@ -1778,9 +1780,6 @@ static int maintenance_run(int argc, const char **argv, const char *prefix,\n \n \topts.quiet = !isatty(2);\n \n-\tfor (i = 0; i < TASK__COUNT; i++)\n-\t\ttasks[i].selected_order = -1;\n-\n \targc = parse_options(argc, argv, prefix,\n \t\t\t     builtin_maintenance_run_options,\n \t\t\t     builtin_maintenance_run_usage,\n@@ -1790,13 +1789,15 @@ static int maintenance_run(int argc, const char **argv, const char *prefix,\n \t\tdie(_(\"use at most one of --auto and --schedule=<frequency>\"));\n \n \tgc_config(&cfg);\n-\tinitialize_task_config(opts.schedule);\n+\tinitialize_task_config(&selected_tasks, opts.schedule);\n \n \tif (argc != 0)\n \t\tusage_with_options(builtin_maintenance_run_usage,\n \t\t\t\t   builtin_maintenance_run_options);\n \n \tret = maintenance_run_tasks(&opts, &cfg);\n+\n+\tstring_list_clear(&selected_tasks, 0);\n \tgc_config_release(&cfg);\n \treturn ret;\n }\n\n-- \n2.49.0.1266.g31b7d2e469.dirty\n\n"},{"id":"518984","messageId":"20250527-b4-pks-maintenance-ref-lock-race-v1-4-e1ceb2dea66e@pks.im","threadId":"63531","inReplyTo":"20250527-b4-pks-maintenance-ref-lock-race-v1-0-e1ceb2dea66e@pks.im","subject":"[PATCH 04/11] builtin/maintenance: mark \"--task=\" and \"--schedule=\" as incompatible","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-27T14:04:30Z","receivedAt":"2025-05-27T14:04:35Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"--task=\" option explicitly allows the user to say which maintenance\ntasks should be run, whereas \"--schedule=\" only respects the maintenance\nstrategy configured for a specific repository. As such, it is sensible\nto accept both options at the same time.\n\nMark them as incompatible with one another. While at it, also convert\nthe existing logic that marks \"--auto\" and \"--schedule=\" as incompatible\nto use `die_for_incompatible_opt2()`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c           | 6 ++++--\n t/t7900-maintenance.sh | 7 ++++++-\n 2 files changed, 10 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex c4af9b11287..57d7602596a 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1785,8 +1785,10 @@ static int maintenance_run(int argc, const char **argv, const char *prefix,\n \t\t\t     builtin_maintenance_run_usage,\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n \n-\tif (opts.auto_flag && opts.schedule)\n-\t\tdie(_(\"use at most one of --auto and --schedule=<frequency>\"));\n+\tdie_for_incompatible_opt2(opts.auto_flag, \"--auto\",\n+\t\t\t\t  opts.schedule, \"--schedule=\");\n+\tdie_for_incompatible_opt2(selected_tasks.nr, \"--task=\",\n+\t\t\t\t  opts.schedule, \"--schedule=\");\n \n \tgc_config(&cfg);\n \tinitialize_task_config(&selected_tasks, opts.schedule);\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 8cf89e285f4..1ada5246606 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -610,7 +610,12 @@ test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.aut\n \n test_expect_success '--auto and --schedule incompatible' '\n \ttest_must_fail git maintenance run --auto --schedule=daily 2>err &&\n-\ttest_grep \"at most one\" err\n+\ttest_grep \"cannot be used together\" err\n+'\n+\n+test_expect_success '--task and --schedule incompatible' '\n+\ttest_must_fail git maintenance run --task=pack-refs --schedule=daily 2>err &&\n+\ttest_grep \"cannot be used together\" err\n '\n \n test_expect_success 'invalid --schedule value' '\n\n-- \n2.49.0.1266.g31b7d2e469.dirty\n\n"},{"id":"518988","messageId":"20250527-b4-pks-maintenance-ref-lock-race-v1-5-e1ceb2dea66e@pks.im","threadId":"63531","inReplyTo":"20250527-b4-pks-maintenance-ref-lock-race-v1-0-e1ceb2dea66e@pks.im","subject":"[PATCH 05/11] builtin/maintenance: stop modifying global array of tasks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-27T14:04:31Z","receivedAt":"2025-05-27T14:04:36Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When configuring maintenance tasks run by git-maintenance(1) we do so by\nmodifying the global array of tasks directly. This is already quite bad\non its own, as global state makes for logic that is hard to follow.\n\nEven more importantly though we use multiple different fields to track\nwhether or not a task should be run:\n\n  - \"enabled\" tracks the \"maintenance.*.enabled\" config key. This field\n    disables execution of a task, unless the user has explicitly asked\n    for the task.\n\n  - \"selected_order\" tracks the order in which jobs have been asked for\n    by the user via the \"--task=\" command line option. It overrides\n    everything else, but only has an effect if at least one job has been\n    selected.\n\n  - \"schedule\" tracks the schedule priority for a job, that is how often\n    it should run. This field only plays a role when the user has passed\n    the \"--schedule=\" command line option.\n\nAll of this makes it non-trivial to figure out which job really should\nbe running right now. The logic to configure these fields and the logic\nthat interprets them is distributed across multiple functions, making it\neven harder to follow it.\n\nRefactor the logic so that we stop modifying global state. Instead, we\nnow compute which jobs should be run in `initialize_task_config()`,\nrepresented as an array of jobs to run that is stored in the options\nstructure. Like this, all logic becomes self-contained and any users of\nthis array only need to iterate through the tasks and execute them one\nby one.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 206 ++++++++++++++++++++++++++++++++---------------------------\n 1 file changed, 112 insertions(+), 94 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 57d7602596a..4d636237cac 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -251,7 +251,24 @@ static enum schedule_priority parse_schedule(const char *value)\n \treturn SCHEDULE_NONE;\n }\n \n+enum maintenance_task_label {\n+\tTASK_PREFETCH,\n+\tTASK_LOOSE_OBJECTS,\n+\tTASK_INCREMENTAL_REPACK,\n+\tTASK_GC,\n+\tTASK_COMMIT_GRAPH,\n+\tTASK_PACK_REFS,\n+\tTASK_REFLOG_EXPIRE,\n+\tTASK_WORKTREE_PRUNE,\n+\tTASK_RERERE_GC,\n+\n+\t/* Leave as final value */\n+\tTASK__COUNT\n+};\n+\n struct maintenance_run_opts {\n+\tenum maintenance_task_label *tasks;\n+\tsize_t tasks_nr, tasks_alloc;\n \tint auto_flag;\n \tint detach;\n \tint quiet;\n@@ -261,6 +278,11 @@ struct maintenance_run_opts {\n \t.detach = -1, \\\n }\n \n+static void maintenance_run_opts_release(struct maintenance_run_opts *opts)\n+{\n+\tfree(opts->tasks);\n+}\n+\n static int pack_refs_condition(UNUSED struct gc_config *cfg)\n {\n \t/*\n@@ -1032,6 +1054,7 @@ int cmd_gc(int argc,\n \t}\n \n out:\n+\tmaintenance_run_opts_release(&opts);\n \tgc_config_release(&cfg);\n \treturn 0;\n }\n@@ -1524,30 +1547,9 @@ struct maintenance_task {\n \tconst char *name;\n \tmaintenance_task_fn *fn;\n \tmaintenance_auto_fn *auto_condition;\n-\tunsigned enabled:1;\n-\n-\tenum schedule_priority schedule;\n-\n-\t/* -1 if not selected. */\n-\tint selected_order;\n-};\n-\n-enum maintenance_task_label {\n-\tTASK_PREFETCH,\n-\tTASK_LOOSE_OBJECTS,\n-\tTASK_INCREMENTAL_REPACK,\n-\tTASK_GC,\n-\tTASK_COMMIT_GRAPH,\n-\tTASK_PACK_REFS,\n-\tTASK_REFLOG_EXPIRE,\n-\tTASK_WORKTREE_PRUNE,\n-\tTASK_RERERE_GC,\n-\n-\t/* Leave as final value */\n-\tTASK__COUNT\n };\n \n-static struct maintenance_task tasks[] = {\n+static const struct maintenance_task tasks[] = {\n \t[TASK_PREFETCH] = {\n \t\t.name = \"prefetch\",\n \t\t.fn = maintenance_task_prefetch,\n@@ -1566,7 +1568,6 @@ static struct maintenance_task tasks[] = {\n \t\t.name = \"gc\",\n \t\t.fn = maintenance_task_gc,\n \t\t.auto_condition = need_to_gc,\n-\t\t.enabled = 1,\n \t},\n \t[TASK_COMMIT_GRAPH] = {\n \t\t.name = \"commit-graph\",\n@@ -1595,18 +1596,9 @@ static struct maintenance_task tasks[] = {\n \t},\n };\n \n-static int compare_tasks_by_selection(const void *a_, const void *b_)\n-{\n-\tconst struct maintenance_task *a = a_;\n-\tconst struct maintenance_task *b = b_;\n-\n-\treturn b->selected_order - a->selected_order;\n-}\n-\n static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \t\t\t\t struct gc_config *cfg)\n {\n-\tint i, found_selected = 0;\n \tint result = 0;\n \tstruct lock_file lk;\n \tstruct repository *r = the_repository;\n@@ -1635,95 +1627,120 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \t\ttrace2_region_leave(\"maintenance\", \"detach\", the_repository);\n \t}\n \n-\tfor (i = 0; !found_selected && i < TASK__COUNT; i++)\n-\t\tfound_selected = tasks[i].selected_order >= 0;\n-\n-\tif (found_selected)\n-\t\tQSORT(tasks, TASK__COUNT, compare_tasks_by_selection);\n-\n-\tfor (i = 0; i < TASK__COUNT; i++) {\n-\t\tif (found_selected && tasks[i].selected_order < 0)\n-\t\t\tcontinue;\n-\n-\t\tif (!found_selected && !tasks[i].enabled)\n-\t\t\tcontinue;\n-\n+\tfor (size_t i = 0; i < opts->tasks_nr; i++) {\n \t\tif (opts->auto_flag &&\n-\t\t    (!tasks[i].auto_condition ||\n-\t\t     !tasks[i].auto_condition(cfg)))\n-\t\t\tcontinue;\n-\n-\t\tif (opts->schedule && tasks[i].schedule < opts->schedule)\n+\t\t    (!tasks[opts->tasks[i]].auto_condition ||\n+\t\t     !tasks[opts->tasks[i]].auto_condition(cfg)))\n \t\t\tcontinue;\n \n-\t\ttrace2_region_enter(\"maintenance\", tasks[i].name, r);\n-\t\tif (tasks[i].fn(opts, cfg)) {\n-\t\t\terror(_(\"task '%s' failed\"), tasks[i].name);\n+\t\ttrace2_region_enter(\"maintenance\", tasks[opts->tasks[i]].name, r);\n+\t\tif (tasks[opts->tasks[i]].fn(opts, cfg)) {\n+\t\t\terror(_(\"task '%s' failed\"), tasks[opts->tasks[i]].name);\n \t\t\tresult = 1;\n \t\t}\n-\t\ttrace2_region_leave(\"maintenance\", tasks[i].name, r);\n+\t\ttrace2_region_leave(\"maintenance\", tasks[opts->tasks[i]].name, r);\n \t}\n \n \trollback_lock_file(&lk);\n \treturn result;\n }\n \n-static void initialize_maintenance_strategy(void)\n+struct maintenance_strategy {\n+\tstruct {\n+\t\tint enabled;\n+\t\tenum schedule_priority schedule;\n+\t} tasks[TASK__COUNT];\n+};\n+\n+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},\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},\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+\tstruct strbuf config_name = STRBUF_INIT;\n+\tstruct maintenance_strategy strategy;\n \tconst char *config_str;\n \n-\tif (git_config_get_string_tmp(\"maintenance.strategy\", &config_str))\n-\t\treturn;\n+\t/*\n+\t * In case the user has asked us to run tasks explicitly we only use\n+\t * those specified tasks. Specifically, we do _not_ want to consult the\n+\t * config or maintenance strategy.\n+\t */\n+\tif (selected_tasks->nr) {\n+\t\tfor (size_t i = 0; i < selected_tasks->nr; i++) {\n+\t\t\tenum maintenance_task_label label = (intptr_t)selected_tasks->items[i].util;;\n+\t\t\tALLOC_GROW(opts->tasks, opts->tasks_nr + 1, opts->tasks_alloc);\n+\t\t\topts->tasks[opts->tasks_nr++] = label;\n+\t\t}\n \n-\tif (!strcasecmp(config_str, \"incremental\")) {\n-\t\ttasks[TASK_GC].schedule = SCHEDULE_NONE;\n-\t\ttasks[TASK_COMMIT_GRAPH].enabled = 1;\n-\t\ttasks[TASK_COMMIT_GRAPH].schedule = SCHEDULE_HOURLY;\n-\t\ttasks[TASK_PREFETCH].enabled = 1;\n-\t\ttasks[TASK_PREFETCH].schedule = SCHEDULE_HOURLY;\n-\t\ttasks[TASK_INCREMENTAL_REPACK].enabled = 1;\n-\t\ttasks[TASK_INCREMENTAL_REPACK].schedule = SCHEDULE_DAILY;\n-\t\ttasks[TASK_LOOSE_OBJECTS].enabled = 1;\n-\t\ttasks[TASK_LOOSE_OBJECTS].schedule = SCHEDULE_DAILY;\n-\t\ttasks[TASK_PACK_REFS].enabled = 1;\n-\t\ttasks[TASK_PACK_REFS].schedule = SCHEDULE_WEEKLY;\n+\t\treturn;\n \t}\n-}\n \n-static void initialize_task_config(const struct string_list *selected_tasks,\n-\t\t\t\t   int schedule)\n-{\n-\tstruct strbuf config_name = STRBUF_INIT;\n+\t/*\n+\t * Otherwise, the strategy depends on whether we run as part of a\n+\t * scheduled job or not:\n+\t *\n+\t *   - Scheduled maintenance does not perform any housekeeping by\n+\t *     default, but requires the user to pick a maintenance strategy.\n+\t *\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 */\n+\tif (opts->schedule) {\n+\t\tstrategy = none_strategy;\n \n-\tfor (size_t i = 0; i < TASK__COUNT; i++)\n-\t\ttasks[i].selected_order = -1;\n-\tfor (size_t i = 0; i < selected_tasks->nr; i++) {\n-\t\tstruct maintenance_task *task = selected_tasks->items[i].util;\n-\t\ttask->selected_order = i;\n+\t\tif (!git_config_get_string_tmp(\"maintenance.strategy\", &config_str)) {\n+\t\t\tif (!strcasecmp(config_str, \"incremental\"))\n+\t\t\t\tstrategy = incremental_strategy;\n+\t\t}\n+\t} else {\n+\t\tstrategy = default_strategy;\n \t}\n \n-\tif (schedule)\n-\t\tinitialize_maintenance_strategy();\n-\n \tfor (size_t i = 0; i < TASK__COUNT; i++) {\n \t\tint config_value;\n-\t\tchar *config_str;\n \n \t\tstrbuf_reset(&config_name);\n \t\tstrbuf_addf(&config_name, \"maintenance.%s.enabled\",\n \t\t\t    tasks[i].name);\n-\n \t\tif (!git_config_get_bool(config_name.buf, &config_value))\n-\t\t\ttasks[i].enabled = config_value;\n-\n-\t\tstrbuf_reset(&config_name);\n-\t\tstrbuf_addf(&config_name, \"maintenance.%s.schedule\",\n-\t\t\t    tasks[i].name);\n+\t\t\tstrategy.tasks[i].enabled = config_value;\n+\t\tif (!strategy.tasks[i].enabled)\n+\t\t\tcontinue;\n \n-\t\tif (!git_config_get_string(config_name.buf, &config_str)) {\n-\t\t\ttasks[i].schedule = parse_schedule(config_str);\n-\t\t\tfree(config_str);\n+\t\tif (opts->schedule) {\n+\t\t\tstrbuf_reset(&config_name);\n+\t\t\tstrbuf_addf(&config_name, \"maintenance.%s.schedule\",\n+\t\t\t\t    tasks[i].name);\n+\t\t\tif (!git_config_get_string_tmp(config_name.buf, &config_str))\n+\t\t\t\tstrategy.tasks[i].schedule = parse_schedule(config_str);\n+\t\t\tif (strategy.tasks[i].schedule < opts->schedule)\n+\t\t\t\tcontinue;\n \t\t}\n+\n+\t\tALLOC_GROW(opts->tasks, opts->tasks_nr + 1, opts->tasks_alloc);\n+\t\topts->tasks[opts->tasks_nr++] = i;\n \t}\n \n \tstrbuf_release(&config_name);\n@@ -1750,7 +1767,7 @@ static int task_option_parse(const struct option *opt,\n \t\treturn 1;\n \t}\n \n-\tstring_list_append(selected_tasks, arg)->util = &tasks[i];\n+\tstring_list_append(selected_tasks, arg)->util = (void *)(intptr_t)i;\n \n \treturn 0;\n }\n@@ -1791,7 +1808,7 @@ static int maintenance_run(int argc, const char **argv, const char *prefix,\n \t\t\t\t  opts.schedule, \"--schedule=\");\n \n \tgc_config(&cfg);\n-\tinitialize_task_config(&selected_tasks, opts.schedule);\n+\tinitialize_task_config(&opts, &selected_tasks);\n \n \tif (argc != 0)\n \t\tusage_with_options(builtin_maintenance_run_usage,\n@@ -1800,6 +1817,7 @@ static int maintenance_run(int argc, const char **argv, const char *prefix,\n \tret = maintenance_run_tasks(&opts, &cfg);\n \n \tstring_list_clear(&selected_tasks, 0);\n+\tmaintenance_run_opts_release(&opts);\n \tgc_config_release(&cfg);\n \treturn ret;\n }\n\n-- \n2.49.0.1266.g31b7d2e469.dirty\n\n"},{"id":"518985","messageId":"20250527-b4-pks-maintenance-ref-lock-race-v1-6-e1ceb2dea66e@pks.im","threadId":"63531","inReplyTo":"20250527-b4-pks-maintenance-ref-lock-race-v1-0-e1ceb2dea66e@pks.im","subject":"[PATCH 06/11] builtin/maintenance: extract function to run tasks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-27T14:04:32Z","receivedAt":"2025-05-27T14:04:37Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Extract the function to run maintenance tasks. This function will be\nreused in a subsequent commit where we introduce a split between\nmaintenance tasks that run before and after daemonizing the process.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 35 +++++++++++++++++++++++------------\n 1 file changed, 23 insertions(+), 12 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 4d636237cac..cfbf9d8a2b9 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1596,6 +1596,27 @@ static const struct maintenance_task tasks[] = {\n \t},\n };\n \n+static int maybe_run_task(const struct maintenance_task *task,\n+\t\t\t  struct repository *repo,\n+\t\t\t  struct maintenance_run_opts *opts,\n+\t\t\t  struct gc_config *cfg)\n+{\n+\tint ret = 0;\n+\n+\tif (opts->auto_flag &&\n+\t    (!task->auto_condition || !task->auto_condition(cfg)))\n+\t\treturn 0;\n+\n+\ttrace2_region_enter(\"maintenance\", task->name, repo);\n+\tif (task->fn(opts, cfg)) {\n+\t\terror(_(\"task '%s' failed\"), task->name);\n+\t\tret = 1;\n+\t}\n+\ttrace2_region_leave(\"maintenance\", task->name, repo);\n+\n+\treturn ret;\n+}\n+\n static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \t\t\t\t struct gc_config *cfg)\n {\n@@ -1627,19 +1648,9 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \t\ttrace2_region_leave(\"maintenance\", \"detach\", the_repository);\n \t}\n \n-\tfor (size_t i = 0; i < opts->tasks_nr; i++) {\n-\t\tif (opts->auto_flag &&\n-\t\t    (!tasks[opts->tasks[i]].auto_condition ||\n-\t\t     !tasks[opts->tasks[i]].auto_condition(cfg)))\n-\t\t\tcontinue;\n-\n-\t\ttrace2_region_enter(\"maintenance\", tasks[opts->tasks[i]].name, r);\n-\t\tif (tasks[opts->tasks[i]].fn(opts, cfg)) {\n-\t\t\terror(_(\"task '%s' failed\"), tasks[opts->tasks[i]].name);\n+\tfor (size_t i = 0; i < opts->tasks_nr; i++)\n+\t\tif (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg))\n \t\t\tresult = 1;\n-\t\t}\n-\t\ttrace2_region_leave(\"maintenance\", tasks[opts->tasks[i]].name, r);\n-\t}\n \n \trollback_lock_file(&lk);\n \treturn result;\n\n-- \n2.49.0.1266.g31b7d2e469.dirty\n\n"},{"id":"518986","messageId":"20250527-b4-pks-maintenance-ref-lock-race-v1-7-e1ceb2dea66e@pks.im","threadId":"63531","inReplyTo":"20250527-b4-pks-maintenance-ref-lock-race-v1-0-e1ceb2dea66e@pks.im","subject":"[PATCH 07/11] builtin/maintenance: fix typedef for function pointers","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-27T14:04:33Z","receivedAt":"2025-05-27T14:04:38Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The typedefs for `maintenance_task_fn` and `maintenance_auto_fn` are\nsomewhat confusingly not true function pointers. As such, any user of\nthose typedefs needs to manually add the pointer to make use of them.\n\nFix this by making these true function pointers.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 10 +++++-----\n 1 file changed, 5 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex cfbf9d8a2b9..447e5800846 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1533,20 +1533,20 @@ static int maintenance_task_incremental_repack(struct maintenance_run_opts *opts\n \treturn 0;\n }\n \n-typedef int maintenance_task_fn(struct maintenance_run_opts *opts,\n-\t\t\t\tstruct gc_config *cfg);\n+typedef int (*maintenance_task_fn)(struct maintenance_run_opts *opts,\n+\t\t\t\t   struct gc_config *cfg);\n \n /*\n  * An auto condition function returns 1 if the task should run\n  * and 0 if the task should NOT run. See needs_to_gc() for an\n  * example.\n  */\n-typedef int maintenance_auto_fn(struct gc_config *cfg);\n+typedef int (*maintenance_auto_fn)(struct gc_config *cfg);\n \n struct maintenance_task {\n \tconst char *name;\n-\tmaintenance_task_fn *fn;\n-\tmaintenance_auto_fn *auto_condition;\n+\tmaintenance_task_fn fn;\n+\tmaintenance_auto_fn auto_condition;\n };\n \n static const struct maintenance_task tasks[] = {\n\n-- \n2.49.0.1266.g31b7d2e469.dirty\n\n"},{"id":"518987","messageId":"20250527-b4-pks-maintenance-ref-lock-race-v1-8-e1ceb2dea66e@pks.im","threadId":"63531","inReplyTo":"20250527-b4-pks-maintenance-ref-lock-race-v1-0-e1ceb2dea66e@pks.im","subject":"[PATCH 08/11] builtin/maintenance: let tasks do maintenance before and after detach","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-27T14:04:34Z","receivedAt":"2025-05-27T14:04:40Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Both git-gc(1) and git-maintenance(1) have logic to daemonize so that\nthe maintenance tasks are performed in the background. git-gc(1) has\nsome special logic though to not perform _all_ housekeeping tasks in the\nbackground: both references and reflogs are still handled synchronously\nni the foreground.\n\nThis split exists because otherwise it may easily happen that git-gc(1)\nkeeps for the \"packed-refs\" file locked for an extended amount of time,\nwhere the next Git command that wants to modify any reference could now\nfail. This was especially important in the past, where git-gc(1) was\nstill executed directly as part of our automatic maintenance: git-gc(1)\nwas invoked via `git gc --auto --detach`, so we knew to handle most of\nthe maintenance tasks in the background while doing those parts that may\ncause locking issues in the foreground.\n\nWe have since moved to git-maintenance(1), which is a more flexible\nreplacement for git-gc(1). By default this command runs git-gc(1), only,\nbut it can be configured to run different tasks, as well. This command\ndoes not know about the split between maintenance tasks that should run\nbefore and after detach though, and this has led to several bug reports\nabout spurious locking errors for the \"packed-refs\" file.\n\nPrepare for a fix by introducing this split for maintenance tasks. Note\nthat this commit does not yet change any of the tasks, so there should\nnot (yet) be a change in behaviour.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 40 +++++++++++++++++++++++++---------------\n 1 file changed, 25 insertions(+), 15 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 447e5800846..57f3bbf5344 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1545,53 +1545,54 @@ typedef int (*maintenance_auto_fn)(struct gc_config *cfg);\n \n struct maintenance_task {\n \tconst char *name;\n-\tmaintenance_task_fn fn;\n+\tmaintenance_task_fn before_detach;\n+\tmaintenance_task_fn after_detach;\n \tmaintenance_auto_fn auto_condition;\n };\n \n static const struct maintenance_task tasks[] = {\n \t[TASK_PREFETCH] = {\n \t\t.name = \"prefetch\",\n-\t\t.fn = maintenance_task_prefetch,\n+\t\t.after_detach = maintenance_task_prefetch,\n \t},\n \t[TASK_LOOSE_OBJECTS] = {\n \t\t.name = \"loose-objects\",\n-\t\t.fn = maintenance_task_loose_objects,\n+\t\t.after_detach = maintenance_task_loose_objects,\n \t\t.auto_condition = loose_object_auto_condition,\n \t},\n \t[TASK_INCREMENTAL_REPACK] = {\n \t\t.name = \"incremental-repack\",\n-\t\t.fn = maintenance_task_incremental_repack,\n+\t\t.after_detach = maintenance_task_incremental_repack,\n \t\t.auto_condition = incremental_repack_auto_condition,\n \t},\n \t[TASK_GC] = {\n \t\t.name = \"gc\",\n-\t\t.fn = maintenance_task_gc,\n+\t\t.after_detach = maintenance_task_gc,\n \t\t.auto_condition = need_to_gc,\n \t},\n \t[TASK_COMMIT_GRAPH] = {\n \t\t.name = \"commit-graph\",\n-\t\t.fn = maintenance_task_commit_graph,\n+\t\t.after_detach = maintenance_task_commit_graph,\n \t\t.auto_condition = should_write_commit_graph,\n \t},\n \t[TASK_PACK_REFS] = {\n \t\t.name = \"pack-refs\",\n-\t\t.fn = maintenance_task_pack_refs,\n+\t\t.after_detach = maintenance_task_pack_refs,\n \t\t.auto_condition = pack_refs_condition,\n \t},\n \t[TASK_REFLOG_EXPIRE] = {\n \t\t.name = \"reflog-expire\",\n-\t\t.fn = maintenance_task_reflog_expire,\n+\t\t.after_detach = maintenance_task_reflog_expire,\n \t\t.auto_condition = reflog_expire_condition,\n \t},\n \t[TASK_WORKTREE_PRUNE] = {\n \t\t.name = \"worktree-prune\",\n-\t\t.fn = maintenance_task_worktree_prune,\n+\t\t.after_detach = maintenance_task_worktree_prune,\n \t\t.auto_condition = worktree_prune_condition,\n \t},\n \t[TASK_RERERE_GC] = {\n \t\t.name = \"rerere-gc\",\n-\t\t.fn = maintenance_task_rerere_gc,\n+\t\t.after_detach = maintenance_task_rerere_gc,\n \t\t.auto_condition = rerere_gc_condition,\n \t},\n };\n@@ -1599,20 +1600,25 @@ static const struct maintenance_task tasks[] = {\n static int maybe_run_task(const struct maintenance_task *task,\n \t\t\t  struct repository *repo,\n \t\t\t  struct maintenance_run_opts *opts,\n-\t\t\t  struct gc_config *cfg)\n+\t\t\t  struct gc_config *cfg,\n+\t\t\t  int before)\n {\n+\tmaintenance_task_fn fn = before ? task->before_detach : task->after_detach;\n+\tconst char *region = before ? \"maintenance before\" : \"maintenance\";\n \tint ret = 0;\n \n+\tif (!fn)\n+\t\treturn 0;\n \tif (opts->auto_flag &&\n \t    (!task->auto_condition || !task->auto_condition(cfg)))\n \t\treturn 0;\n \n-\ttrace2_region_enter(\"maintenance\", task->name, repo);\n-\tif (task->fn(opts, cfg)) {\n+\ttrace2_region_enter(region, task->name, repo);\n+\tif (fn(opts, cfg)) {\n \t\terror(_(\"task '%s' failed\"), task->name);\n \t\tret = 1;\n \t}\n-\ttrace2_region_leave(\"maintenance\", task->name, repo);\n+\ttrace2_region_leave(region, task->name, repo);\n \n \treturn ret;\n }\n@@ -1641,6 +1647,10 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \t}\n \tfree(lock_path);\n \n+\tfor (size_t i = 0; i < opts->tasks_nr; i++)\n+\t\tif (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg, 1))\n+\t\t\tresult = 1;\n+\n \t/* Failure to daemonize is ok, we'll continue in foreground. */\n \tif (opts->detach > 0) {\n \t\ttrace2_region_enter(\"maintenance\", \"detach\", the_repository);\n@@ -1649,7 +1659,7 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \t}\n \n \tfor (size_t i = 0; i < opts->tasks_nr; i++)\n-\t\tif (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg))\n+\t\tif (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg, 0))\n \t\t\tresult = 1;\n \n \trollback_lock_file(&lk);\n\n-- \n2.49.0.1266.g31b7d2e469.dirty\n\n"},{"id":"518989","messageId":"20250527-b4-pks-maintenance-ref-lock-race-v1-9-e1ceb2dea66e@pks.im","threadId":"63531","inReplyTo":"20250527-b4-pks-maintenance-ref-lock-race-v1-0-e1ceb2dea66e@pks.im","subject":"[PATCH 09/11] builtin/maintenance: fix locking race when packing refs and reflogs","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-27T14:04:35Z","receivedAt":"2025-05-27T14:04:41Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"As explained in the preceding commit, git-gc(1) knows to detach only\nafter it has already packed references and reflogs. This is done to\navoid racing around their respective lockfiles.\n\nAdapt git-maintenance(1) accordingly and run the \"pack-refs\" and\n\"reflog-expire\" tasks before detaching. Note that the \"gc\" task has the\nsame issue, but the fix is a bit more involved there and will thus be\ndone in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 57f3bbf5344..e5d1114bd2d 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1577,12 +1577,12 @@ static const struct maintenance_task tasks[] = {\n \t},\n \t[TASK_PACK_REFS] = {\n \t\t.name = \"pack-refs\",\n-\t\t.after_detach = maintenance_task_pack_refs,\n+\t\t.before_detach = maintenance_task_pack_refs,\n \t\t.auto_condition = pack_refs_condition,\n \t},\n \t[TASK_REFLOG_EXPIRE] = {\n \t\t.name = \"reflog-expire\",\n-\t\t.after_detach = maintenance_task_reflog_expire,\n+\t\t.before_detach = maintenance_task_reflog_expire,\n \t\t.auto_condition = reflog_expire_condition,\n \t},\n \t[TASK_WORKTREE_PRUNE] = {\n\n-- \n2.49.0.1266.g31b7d2e469.dirty\n\n"},{"id":"518990","messageId":"20250527-b4-pks-maintenance-ref-lock-race-v1-11-e1ceb2dea66e@pks.im","threadId":"63531","inReplyTo":"20250527-b4-pks-maintenance-ref-lock-race-v1-0-e1ceb2dea66e@pks.im","subject":"[PATCH 11/11] builtin/maintenance: fix locking race when handling \"gc\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-27T14:04:37Z","receivedAt":"2025-05-27T14:04:42Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"gc\" task has a similar locking race as the one that we have fixed\nfor the \"pack-refs\" and \"reflog-expire\" tasks in preceding commits. Fix\nthis by splitting up the logic of the \"gc\" task:\n\n  - Before detaching we execute `gc_before_repack()`, which contains the\n    logic that git-gc(1) itself would execute before detaching.\n\n  - After detaching we spawn git-gc(1), but with a new hidden flag that\n    suppresses calling `gc_before_repack()`.\n\nLike this we have roughly the same logic as git-gc(1) itself and know to\nrepack refs and reflogs before detaching, thus fixing the race.\n\nNote that `gc_before_repack()` is renamed to `gc_before_detach()` to\nbetter reflect what this function does.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c           | 39 ++++++++++++++++++++++++++-------------\n t/t7900-maintenance.sh | 12 ++++++------\n 2 files changed, 32 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 174357b9c25..2cf61efcee9 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -816,7 +816,7 @@ static int report_last_gc_error(void)\n \treturn ret;\n }\n \n-static int gc_before_repack(struct maintenance_run_opts *opts,\n+static int gc_before_detach(struct maintenance_run_opts *opts,\n \t\t\t    struct gc_config *cfg)\n {\n \tif (cfg->pack_refs && maintenance_task_pack_refs(opts, cfg))\n@@ -837,6 +837,7 @@ int cmd_gc(int argc,\n \tpid_t pid;\n \tint daemonized = 0;\n \tint keep_largest_pack = -1;\n+\tint skip_maintenance_before_detach = 0;\n \ttimestamp_t dummy;\n \tstruct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT;\n \tstruct gc_config cfg = GC_CONFIG_INIT;\n@@ -869,6 +870,8 @@ int cmd_gc(int argc,\n \t\t\t N_(\"repack all other packs except the largest pack\")),\n \t\tOPT_STRING(0, \"expire-to\", &cfg.repack_expire_to, N_(\"dir\"),\n \t\t\t   N_(\"pack prefix to store a pack containing pruned objects\")),\n+\t\tOPT_HIDDEN_BOOL(0, \"skip-maintenance-before-detach\", &skip_maintenance_before_detach,\n+\t\t\t   N_(\"skip maintenance steps typically done before detaching\")),\n \t\tOPT_END()\n \t};\n \n@@ -952,14 +955,16 @@ int cmd_gc(int argc,\n \t\t\tgoto out;\n \t\t}\n \n-\t\tif (lock_repo_for_gc(force, &pid)) {\n-\t\t\tret = 0;\n-\t\t\tgoto out;\n-\t\t}\n+\t\tif (!skip_maintenance_before_detach) {\n+\t\t\tif (lock_repo_for_gc(force, &pid)) {\n+\t\t\t\tret = 0;\n+\t\t\t\tgoto out;\n+\t\t\t}\n \n-\t\tif (gc_before_repack(&opts, &cfg) < 0)\n-\t\t\texit(127);\n-\t\tdelete_tempfile(&pidfile);\n+\t\t\tif (gc_before_detach(&opts, &cfg) < 0)\n+\t\t\t\texit(127);\n+\t\t\tdelete_tempfile(&pidfile);\n+\t\t}\n \n \t\t/*\n \t\t * failure to daemonize is ok, we'll continue\n@@ -988,8 +993,8 @@ int cmd_gc(int argc,\n \t\tfree(path);\n \t}\n \n-\tif (opts.detach <= 0)\n-\t\tgc_before_repack(&opts, &cfg);\n+\tif (opts.detach <= 0 && !skip_maintenance_before_detach)\n+\t\tgc_before_detach(&opts, &cfg);\n \n \tif (!repository_format_precious_objects) {\n \t\tstruct child_process repack_cmd = CHILD_PROCESS_INIT;\n@@ -1225,8 +1230,14 @@ static int maintenance_task_prefetch(struct maintenance_run_opts *opts,\n \treturn 0;\n }\n \n-static int maintenance_task_gc(struct maintenance_run_opts *opts,\n-\t\t\t       struct gc_config *cfg UNUSED)\n+static int maintenance_task_gc_before_detach(struct maintenance_run_opts *opts,\n+\t\t\t\t\t     struct gc_config *cfg)\n+{\n+\treturn gc_before_detach(opts, cfg);\n+}\n+\n+static int maintenance_task_gc_after_detach(struct maintenance_run_opts *opts,\n+\t\t\t\t\t    struct gc_config *cfg UNUSED)\n {\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \n@@ -1240,6 +1251,7 @@ static int maintenance_task_gc(struct maintenance_run_opts *opts,\n \telse\n \t\tstrvec_push(&child.args, \"--no-quiet\");\n \tstrvec_push(&child.args, \"--no-detach\");\n+\tstrvec_push(&child.args, \"--skip-maintenance-before-detach\");\n \n \treturn run_command(&child);\n }\n@@ -1561,7 +1573,8 @@ static const struct maintenance_task tasks[] = {\n \t},\n \t[TASK_GC] = {\n \t\t.name = \"gc\",\n-\t\t.after_detach = maintenance_task_gc,\n+\t\t.before_detach = maintenance_task_gc_before_detach,\n+\t\t.after_detach = maintenance_task_gc_after_detach,\n \t\t.auto_condition = need_to_gc,\n \t},\n \t[TASK_COMMIT_GRAPH] = {\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 1ada5246606..e09a36ab021 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -49,9 +49,9 @@ test_expect_success 'run [--auto|--quiet]' '\n \t\tgit maintenance run --auto 2>/dev/null &&\n \tGIT_TRACE2_EVENT=\"$(pwd)/run-no-quiet.txt\" \\\n \t\tgit maintenance run --no-quiet 2>/dev/null &&\n-\ttest_subcommand git gc --quiet --no-detach <run-no-auto.txt &&\n-\ttest_subcommand ! git gc --auto --quiet --no-detach <run-auto.txt &&\n-\ttest_subcommand git gc --no-quiet --no-detach <run-no-quiet.txt\n+\ttest_subcommand git gc --quiet --no-detach --skip-maintenance-before-detach <run-no-auto.txt &&\n+\ttest_subcommand ! git gc --auto --quiet --no-detach --skip-maintenance-before-detach <run-auto.txt &&\n+\ttest_subcommand git gc --no-quiet --no-detach --skip-maintenance-before-detach <run-no-quiet.txt\n '\n \n test_expect_success 'maintenance.auto config option' '\n@@ -154,9 +154,9 @@ test_expect_success 'run --task=<task>' '\n \t\tgit maintenance run --task=commit-graph 2>/dev/null &&\n \tGIT_TRACE2_EVENT=\"$(pwd)/run-both.txt\" \\\n \t\tgit maintenance run --task=commit-graph --task=gc 2>/dev/null &&\n-\ttest_subcommand ! git gc --quiet --no-detach <run-commit-graph.txt &&\n-\ttest_subcommand git gc --quiet --no-detach <run-gc.txt &&\n-\ttest_subcommand git gc --quiet --no-detach <run-both.txt &&\n+\ttest_subcommand ! git gc --quiet --no-detach --skip-maintenance-before-detach <run-commit-graph.txt &&\n+\ttest_subcommand git gc --quiet --no-detach --skip-maintenance-before-detach <run-gc.txt &&\n+\ttest_subcommand git gc --quiet --no-detach --skip-maintenance-before-detach <run-both.txt &&\n \ttest_subcommand git commit-graph write --split --reachable --no-progress <run-commit-graph.txt &&\n \ttest_subcommand ! git commit-graph write --split --reachable --no-progress <run-gc.txt &&\n \ttest_subcommand git commit-graph write --split --reachable --no-progress <run-both.txt\n\n-- \n2.49.0.1266.g31b7d2e469.dirty\n\n"},{"id":"518991","messageId":"20250527-b4-pks-maintenance-ref-lock-race-v1-10-e1ceb2dea66e@pks.im","threadId":"63531","inReplyTo":"20250527-b4-pks-maintenance-ref-lock-race-v1-0-e1ceb2dea66e@pks.im","subject":"[PATCH 10/11] builtin/gc: avoid global state in `gc_before_repack()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-27T14:04:36Z","receivedAt":"2025-05-27T14:04:42Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The `gc_before_repack()` should only ever run once in git-gc(1), but we\nmay end up calling it twice when the \"--detach\" flag is passed. The\nduplicated call is avoided though via a static flag in this function.\n\nThis pattern is somewhat unintuitive though. Refactor it to drop the\nstatic flag and instead guard the second call of `gc_before_repack()`\nvia `opts.detach`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 24 +++++++++---------------\n 1 file changed, 9 insertions(+), 15 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e5d1114bd2d..174357b9c25 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -816,22 +816,14 @@ static int report_last_gc_error(void)\n \treturn ret;\n }\n \n-static void gc_before_repack(struct maintenance_run_opts *opts,\n-\t\t\t     struct gc_config *cfg)\n+static int gc_before_repack(struct maintenance_run_opts *opts,\n+\t\t\t    struct gc_config *cfg)\n {\n-\t/*\n-\t * We may be called twice, as both the pre- and\n-\t * post-daemonized phases will call us, but running these\n-\t * commands more than once is pointless and wasteful.\n-\t */\n-\tstatic int done = 0;\n-\tif (done++)\n-\t\treturn;\n-\n \tif (cfg->pack_refs && maintenance_task_pack_refs(opts, cfg))\n-\t\tdie(FAILED_RUN, \"pack-refs\");\n+\t\treturn error(FAILED_RUN, \"pack-refs\");\n \tif (cfg->prune_reflogs && maintenance_task_reflog_expire(opts, cfg))\n-\t\tdie(FAILED_RUN, \"reflog\");\n+\t\treturn error(FAILED_RUN, \"reflog\");\n+\treturn 0;\n }\n \n int cmd_gc(int argc,\n@@ -965,7 +957,8 @@ int cmd_gc(int argc,\n \t\t\tgoto out;\n \t\t}\n \n-\t\tgc_before_repack(&opts, &cfg); /* dies on failure */\n+\t\tif (gc_before_repack(&opts, &cfg) < 0)\n+\t\t\texit(127);\n \t\tdelete_tempfile(&pidfile);\n \n \t\t/*\n@@ -995,7 +988,8 @@ int cmd_gc(int argc,\n \t\tfree(path);\n \t}\n \n-\tgc_before_repack(&opts, &cfg);\n+\tif (opts.detach <= 0)\n+\t\tgc_before_repack(&opts, &cfg);\n \n \tif (!repository_format_precious_objects) {\n \t\tstruct child_process repack_cmd = CHILD_PROCESS_INIT;\n\n-- \n2.49.0.1266.g31b7d2e469.dirty\n\n"},{"id":"519006","messageId":"3f6863f8-7fb9-4516-a36b-f55243794dd0@ramsayjones.plus.com","threadId":"63531","inReplyTo":"20250527-b4-pks-maintenance-ref-lock-race-v1-4-e1ceb2dea66e@pks.im","subject":"Re: [PATCH 04/11] builtin/maintenance: mark \"--task=\" and \"--schedule=\" as incompatible","fromName":"Ramsay Jones","fromEmail":"ramsay@ramsayjones.plus.com","sentAt":"2025-05-27T16:43:23Z","receivedAt":"2025-05-27T16:46:34Z","isPatch":true,"sender":{"key":"ramsay@ramsayjones.plus.com","avatar":"https://avatars.githubusercontent.com/u/33702710?v=4"},"body":"\n\nOn 27/05/2025 15:04, Patrick Steinhardt wrote:\n> The \"--task=\" option explicitly allows the user to say which maintenance\n> tasks should be run, whereas \"--schedule=\" only respects the maintenance\n> strategy configured for a specific repository. As such, it is sensible\n\ns/is sensible/is not sensible/ ?\n\n> to accept both options at the same time.\n> \n> Mark them as incompatible with one another. While at it, also convert\n> the existing logic that marks \"--auto\" and \"--schedule=\" as incompatible\n> to use `die_for_incompatible_opt2()`.\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n[snip]\n\nATB,\nRamsay Jones\n\n"},{"id":"519009","messageId":"5c9f9bc1-da0d-4952-a424-1d2c99fce0d3@ramsayjones.plus.com","threadId":"63531","inReplyTo":"20250527-b4-pks-maintenance-ref-lock-race-v1-8-e1ceb2dea66e@pks.im","subject":"Re: [PATCH 08/11] builtin/maintenance: let tasks do maintenance before and after detach","fromName":"Ramsay Jones","fromEmail":"ramsay@ramsayjones.plus.com","sentAt":"2025-05-27T17:01:21Z","receivedAt":"2025-05-27T17:01:24Z","isPatch":true,"sender":{"key":"ramsay@ramsayjones.plus.com","avatar":"https://avatars.githubusercontent.com/u/33702710?v=4"},"body":"\n\nOn 27/05/2025 15:04, Patrick Steinhardt wrote:\n> Both git-gc(1) and git-maintenance(1) have logic to daemonize so that\n> the maintenance tasks are performed in the background. git-gc(1) has\n> some special logic though to not perform _all_ housekeeping tasks in the\n> background: both references and reflogs are still handled synchronously\n> ni the foreground.\n\ns/ni/in/\n\n> \n> This split exists because otherwise it may easily happen that git-gc(1)\n> keeps for the \"packed-refs\" file locked for an extended amount of time,\n\ns/keeps for the/keeps the/\n\n> where the next Git command that wants to modify any reference could now\n> fail. This was especially important in the past, where git-gc(1) was\n> still executed directly as part of our automatic maintenance: git-gc(1)\n> was invoked via `git gc --auto --detach`, so we knew to handle most of\n> the maintenance tasks in the background while doing those parts that may\n> cause locking issues in the foreground.\n> \n> We have since moved to git-maintenance(1), which is a more flexible\n> replacement for git-gc(1). By default this command runs git-gc(1), only,\n> but it can be configured to run different tasks, as well. This command\n> does not know about the split between maintenance tasks that should run\n> before and after detach though, and this has led to several bug reports\n> about spurious locking errors for the \"packed-refs\" file.\n> \n> Prepare for a fix by introducing this split for maintenance tasks. Note\n> that this commit does not yet change any of the tasks, so there should\n> not (yet) be a change in behaviour.\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n[snip]\n\nATB,\nRamsay Jones\n\n"},{"id":"519048","messageId":"aDa1BYwzhMdqmp4S@pks.im","threadId":"63531","inReplyTo":"5c9f9bc1-da0d-4952-a424-1d2c99fce0d3@ramsayjones.plus.com","subject":"Re: [PATCH 08/11] builtin/maintenance: let tasks do maintenance before and after detach","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-28T07:02:29Z","receivedAt":"2025-05-28T07:02:41Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, May 27, 2025 at 06:01:21PM +0100, Ramsay Jones wrote:\n> On 27/05/2025 15:04, Patrick Steinhardt wrote:\n> > Both git-gc(1) and git-maintenance(1) have logic to daemonize so that\n> > the maintenance tasks are performed in the background. git-gc(1) has\n> > some special logic though to not perform _all_ housekeeping tasks in the\n> > background: both references and reflogs are still handled synchronously\n> > ni the foreground.\n> \n> s/ni/in/\n> \n> > \n> > This split exists because otherwise it may easily happen that git-gc(1)\n> > keeps for the \"packed-refs\" file locked for an extended amount of time,\n> \n> s/keeps for the/keeps the/\n\nAnd these I've got fixed now, as well. I'll wait a bit for more feedback\nthough before sending out these fixes.\n\nThanks!\n\nPatrick\n"},{"id":"519049","messageId":"aDa1E8aWHmy22TRi@pks.im","threadId":"63531","inReplyTo":"3f6863f8-7fb9-4516-a36b-f55243794dd0@ramsayjones.plus.com","subject":"Re: [PATCH 04/11] builtin/maintenance: mark \"--task=\" and \"--schedule=\" as incompatible","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-28T07:02:43Z","receivedAt":"2025-05-28T07:02:47Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, May 27, 2025 at 05:43:23PM +0100, Ramsay Jones wrote:\n> On 27/05/2025 15:04, Patrick Steinhardt wrote:\n> > The \"--task=\" option explicitly allows the user to say which maintenance\n> > tasks should be run, whereas \"--schedule=\" only respects the maintenance\n> > strategy configured for a specific repository. As such, it is sensible\n> \n> s/is sensible/is not sensible/ ?\n\nOh, obviously, yeah. Thanks!\n\nPatrick\n"},{"id":"519250","messageId":"20250530-b4-pks-maintenance-ref-lock-race-v2-0-d04e2f93e51f@pks.im","threadId":"63531","inReplyTo":"20250527-b4-pks-maintenance-ref-lock-race-v1-0-e1ceb2dea66e@pks.im","subject":"[PATCH v2 00/12] builtin/maintenance: fix ref lock races when detaching","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-30T15:08:26Z","receivedAt":"2025-05-30T15:08:41Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis patch series fixes races around locking the \"packed-refs\" file when\nauto-maintenance decides to repack it. This issue has been reported e.g.\nvia [1] and [2].\n\nThe root cause is that git-gc(1) used to know to detach _after_ having\nrepacked references. As such, callers wouldn't continue with their thing\nuntil we have already packed refs, and thus the race does not exist\nthere. git-maintenance(1) didn't have the same split though, so this\npatch series retrofits that logic.\n\nThe series is structured as follows:\n\n  - Patches 1 and 2 do some light refactorings.\n\n  - Patches 3 to 5 refactor how we set up the list of tasks to not rely\n    on globals anymore. Instead, we now have a single source of truth\n    for which tasks exactly will be run.\n\n  - The remaining patches introduce the split of before/after-detach\n    tasks and wire them up for \"pack-refs\", \"reflog-expire\" and \"gc\"\n    tasks.\n\nChanges in v2:\n  - A couple of commit message improvements.\n  - Introduce `die(NULL)` to die with the correct exit code but no error\n    message. This gets rid of some magic numbers.\n  - Introduce an enum to discern the phases before and after detach.\n  - Link to v1: https://lore.kernel.org/r/20250527-b4-pks-maintenance-ref-lock-race-v1-0-e1ceb2dea66e@pks.im\n\nThanks!\n\nPatrick\n\n[1]: <CAJR-fbZ4X1+gN75m2dUvocR6NkowLOZ9F26cjBy8w1qd181OoQ@mail.gmail.com>\n[2]: <CANi7bVAkNc+gY1NoXfJuDRjxjZLTgL8Lfn8_ZmWsvLAoiLPkNg@mail.gmail.com>\n\n---\nPatrick Steinhardt (12):\n      builtin/gc: use designated field initializers for maintenance tasks\n      builtin/gc: drop redundant local variable\n      builtin/maintenance: centralize configuration of explicit tasks\n      builtin/maintenance: mark \"--task=\" and \"--schedule=\" as incompatible\n      builtin/maintenance: stop modifying global array of tasks\n      builtin/maintenance: extract function to run tasks\n      builtin/maintenance: fix typedef for function pointers\n      builtin/maintenance: let tasks do maintenance before and after detach\n      builtin/maintenance: fix locking race when packing refs and reflogs\n      usage: allow dying without writing an error message\n      builtin/gc: avoid global state in `gc_before_repack()`\n      builtin/maintenance: fix locking race when handling \"gc\" task\n\n builtin/am.c                |   4 +-\n builtin/checkout.c          |   4 +-\n builtin/fetch.c             |   2 +-\n builtin/gc.c                | 394 +++++++++++++++++++++++++-------------------\n builtin/submodule--helper.c |  12 +-\n t/t7900-maintenance.sh      |  19 ++-\n usage.c                     |   2 +\n 7 files changed, 250 insertions(+), 187 deletions(-)\n\nRange-diff versus v1:\n\n 1:  74fcc4e2251 =  1:  87df070a9e7 builtin/gc: use designated field initializers for maintenance tasks\n 2:  1cc513a7b0f =  2:  e2acea10f7e builtin/gc: drop redundant local variable\n 3:  be8c8a98892 =  3:  48a5e25c8bc builtin/maintenance: centralize configuration of explicit tasks\n 4:  b19fa152c81 !  4:  680b36e2fa6 builtin/maintenance: mark \"--task=\" and \"--schedule=\" as incompatible\n    @@ Commit message\n     \n         The \"--task=\" option explicitly allows the user to say which maintenance\n         tasks should be run, whereas \"--schedule=\" only respects the maintenance\n    -    strategy configured for a specific repository. As such, it is sensible\n    -    to accept both options at the same time.\n    +    strategy configured for a specific repository. As such, it is not\n    +    sensible to accept both options at the same time.\n     \n         Mark them as incompatible with one another. While at it, also convert\n         the existing logic that marks \"--auto\" and \"--schedule=\" as incompatible\n 5:  8f692f30829 =  5:  9eaabb93edd builtin/maintenance: stop modifying global array of tasks\n 6:  fc0ea110c01 =  6:  ffee3ca3c6c builtin/maintenance: extract function to run tasks\n 7:  b5821ef6cfe =  7:  66e1bb2111b builtin/maintenance: fix typedef for function pointers\n 8:  42a9210e445 !  8:  4eed6a8dc9c builtin/maintenance: let tasks do maintenance before and after detach\n    @@ Commit message\n         the maintenance tasks are performed in the background. git-gc(1) has\n         some special logic though to not perform _all_ housekeeping tasks in the\n         background: both references and reflogs are still handled synchronously\n    -    ni the foreground.\n    +    in the foreground.\n     \n         This split exists because otherwise it may easily happen that git-gc(1)\n    -    keeps for the \"packed-refs\" file locked for an extended amount of time,\n    +    keeps the \"packed-refs\" file locked for an extended amount of time,\n         where the next Git command that wants to modify any reference could now\n         fail. This was especially important in the past, where git-gc(1) was\n         still executed directly as part of our automatic maintenance: git-gc(1)\n    @@ builtin/gc.c: typedef int (*maintenance_auto_fn)(struct gc_config *cfg);\n      \t\t.auto_condition = rerere_gc_condition,\n      \t},\n      };\n    -@@ builtin/gc.c: static const struct maintenance_task tasks[] = {\n    + \n    ++enum task_phase {\n    ++\tTASK_PHASE_BEFORE_DETACH,\n    ++\tTASK_PHASE_AFTER_DETACH,\n    ++};\n    ++\n      static int maybe_run_task(const struct maintenance_task *task,\n      \t\t\t  struct repository *repo,\n      \t\t\t  struct maintenance_run_opts *opts,\n     -\t\t\t  struct gc_config *cfg)\n     +\t\t\t  struct gc_config *cfg,\n    -+\t\t\t  int before)\n    ++\t\t\t  enum task_phase phase)\n      {\n    ++\tint before = (phase == TASK_PHASE_BEFORE_DETACH);\n     +\tmaintenance_task_fn fn = before ? task->before_detach : task->after_detach;\n     +\tconst char *region = before ? \"maintenance before\" : \"maintenance\";\n      \tint ret = 0;\n    @@ builtin/gc.c: static int maintenance_run_tasks(struct maintenance_run_opts *opts\n      \tfree(lock_path);\n      \n     +\tfor (size_t i = 0; i < opts->tasks_nr; i++)\n    -+\t\tif (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg, 1))\n    ++\t\tif (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg,\n    ++\t\t\t\t   TASK_PHASE_BEFORE_DETACH))\n     +\t\t\tresult = 1;\n     +\n      \t/* Failure to daemonize is ok, we'll continue in foreground. */\n    @@ builtin/gc.c: static int maintenance_run_tasks(struct maintenance_run_opts *opts\n      \n      \tfor (size_t i = 0; i < opts->tasks_nr; i++)\n     -\t\tif (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg))\n    -+\t\tif (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg, 0))\n    ++\t\tif (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg,\n    ++\t\t\t\t   TASK_PHASE_AFTER_DETACH))\n      \t\t\tresult = 1;\n      \n      \trollback_lock_file(&lk);\n 9:  7859d3b9b4f =  9:  41ae51294e6 builtin/maintenance: fix locking race when packing refs and reflogs\n -:  ----------- > 10:  2e1b4deb668 usage: allow dying without writing an error message\n10:  18bce954787 ! 11:  7d9be688eb4 builtin/gc: avoid global state in `gc_before_repack()`\n    @@ builtin/gc.c: int cmd_gc(int argc,\n      \n     -\t\tgc_before_repack(&opts, &cfg); /* dies on failure */\n     +\t\tif (gc_before_repack(&opts, &cfg) < 0)\n    -+\t\t\texit(127);\n    ++\t\t\tdie(NULL);\n      \t\tdelete_tempfile(&pidfile);\n      \n      \t\t/*\n11:  ff92709bf6c ! 12:  291498849b8 builtin/maintenance: fix locking race when handling \"gc\" task\n    @@ builtin/gc.c: int cmd_gc(int argc,\n     +\t\t\t}\n      \n     -\t\tif (gc_before_repack(&opts, &cfg) < 0)\n    --\t\t\texit(127);\n    +-\t\t\tdie(NULL);\n     -\t\tdelete_tempfile(&pidfile);\n     +\t\t\tif (gc_before_detach(&opts, &cfg) < 0)\n    -+\t\t\t\texit(127);\n    ++\t\t\t\tdie(NULL);\n     +\t\t\tdelete_tempfile(&pidfile);\n     +\t\t}\n      \n\n---\nbase-commit: 845c48a16a7f7b2c44d8cb137b16a4a1f0140229\nchange-id: 20250527-b4-pks-maintenance-ref-lock-race-11ae5d68e06f\n\n"},{"id":"519251","messageId":"20250530-b4-pks-maintenance-ref-lock-race-v2-1-d04e2f93e51f@pks.im","threadId":"63531","inReplyTo":"20250530-b4-pks-maintenance-ref-lock-race-v2-0-d04e2f93e51f@pks.im","subject":"[PATCH v2 01/12] builtin/gc: use designated field initializers for maintenance tasks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-30T15:08:27Z","receivedAt":"2025-05-30T15:08:43Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Convert the array of maintenance tasks to use designated field\ninitializers. This makes it easier to add more fields to the struct\nwithout having to modify all tasks.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 54 +++++++++++++++++++++++++++---------------------------\n 1 file changed, 27 insertions(+), 27 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e33ba946e43..54fc7f299a9 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1550,49 +1550,49 @@ enum maintenance_task_label {\n \n static struct maintenance_task tasks[] = {\n \t[TASK_PREFETCH] = {\n-\t\t\"prefetch\",\n-\t\tmaintenance_task_prefetch,\n+\t\t.name = \"prefetch\",\n+\t\t.fn = maintenance_task_prefetch,\n \t},\n \t[TASK_LOOSE_OBJECTS] = {\n-\t\t\"loose-objects\",\n-\t\tmaintenance_task_loose_objects,\n-\t\tloose_object_auto_condition,\n+\t\t.name = \"loose-objects\",\n+\t\t.fn = maintenance_task_loose_objects,\n+\t\t.auto_condition = loose_object_auto_condition,\n \t},\n \t[TASK_INCREMENTAL_REPACK] = {\n-\t\t\"incremental-repack\",\n-\t\tmaintenance_task_incremental_repack,\n-\t\tincremental_repack_auto_condition,\n+\t\t.name = \"incremental-repack\",\n+\t\t.fn = maintenance_task_incremental_repack,\n+\t\t.auto_condition = incremental_repack_auto_condition,\n \t},\n \t[TASK_GC] = {\n-\t\t\"gc\",\n-\t\tmaintenance_task_gc,\n-\t\tneed_to_gc,\n-\t\t1,\n+\t\t.name = \"gc\",\n+\t\t.fn = maintenance_task_gc,\n+\t\t.auto_condition = need_to_gc,\n+\t\t.enabled = 1,\n \t},\n \t[TASK_COMMIT_GRAPH] = {\n-\t\t\"commit-graph\",\n-\t\tmaintenance_task_commit_graph,\n-\t\tshould_write_commit_graph,\n+\t\t.name = \"commit-graph\",\n+\t\t.fn = maintenance_task_commit_graph,\n+\t\t.auto_condition = should_write_commit_graph,\n \t},\n \t[TASK_PACK_REFS] = {\n-\t\t\"pack-refs\",\n-\t\tmaintenance_task_pack_refs,\n-\t\tpack_refs_condition,\n+\t\t.name = \"pack-refs\",\n+\t\t.fn = maintenance_task_pack_refs,\n+\t\t.auto_condition = pack_refs_condition,\n \t},\n \t[TASK_REFLOG_EXPIRE] = {\n-\t\t\"reflog-expire\",\n-\t\tmaintenance_task_reflog_expire,\n-\t\treflog_expire_condition,\n+\t\t.name = \"reflog-expire\",\n+\t\t.fn = maintenance_task_reflog_expire,\n+\t\t.auto_condition = reflog_expire_condition,\n \t},\n \t[TASK_WORKTREE_PRUNE] = {\n-\t\t\"worktree-prune\",\n-\t\tmaintenance_task_worktree_prune,\n-\t\tworktree_prune_condition,\n+\t\t.name = \"worktree-prune\",\n+\t\t.fn = maintenance_task_worktree_prune,\n+\t\t.auto_condition = worktree_prune_condition,\n \t},\n \t[TASK_RERERE_GC] = {\n-\t\t\"rerere-gc\",\n-\t\tmaintenance_task_rerere_gc,\n-\t\trerere_gc_condition,\n+\t\t.name = \"rerere-gc\",\n+\t\t.fn = maintenance_task_rerere_gc,\n+\t\t.auto_condition = rerere_gc_condition,\n \t},\n };\n \n\n-- \n2.50.0.rc0.604.gd4ff7b7c86.dirty\n\n"},{"id":"519252","messageId":"20250530-b4-pks-maintenance-ref-lock-race-v2-2-d04e2f93e51f@pks.im","threadId":"63531","inReplyTo":"20250530-b4-pks-maintenance-ref-lock-race-v2-0-d04e2f93e51f@pks.im","subject":"[PATCH v2 02/12] builtin/gc: drop redundant local variable","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-30T15:08:28Z","receivedAt":"2025-05-30T15:08:43Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We have two different variables that track the quietness for git-gc(1):\n\n  - The local variable `quiet`, which we wire up.\n\n  - The `quiet` field of `struct maintenance_run_opts`.\n\nThis leads to confusion which of these variables should be used and what\nthe respective effect is.\n\nSimplify this logic by dropping the local variable in favor of the\noptions field.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 11 +++++------\n 1 file changed, 5 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 54fc7f299a9..7adda8d2d0d 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -818,7 +818,6 @@ int cmd_gc(int argc,\n \t   struct repository *repo UNUSED)\n {\n \tint aggressive = 0;\n-\tint quiet = 0;\n \tint force = 0;\n \tconst char *name;\n \tpid_t pid;\n@@ -831,7 +830,7 @@ int cmd_gc(int argc,\n \tconst char *prune_expire_arg = prune_expire_sentinel;\n \tint ret;\n \tstruct option builtin_gc_options[] = {\n-\t\tOPT__QUIET(&quiet, N_(\"suppress progress reporting\")),\n+\t\tOPT__QUIET(&opts.quiet, N_(\"suppress progress reporting\")),\n \t\t{\n \t\t\t.type = OPTION_STRING,\n \t\t\t.long_name = \"prune\",\n@@ -891,7 +890,7 @@ int cmd_gc(int argc,\n \t\tif (cfg.aggressive_window > 0)\n \t\t\tstrvec_pushf(&repack, \"--window=%d\", cfg.aggressive_window);\n \t}\n-\tif (quiet)\n+\tif (opts.quiet)\n \t\tstrvec_push(&repack, \"-q\");\n \n \tif (opts.auto_flag) {\n@@ -906,7 +905,7 @@ int cmd_gc(int argc,\n \t\t\tgoto out;\n \t\t}\n \n-\t\tif (!quiet) {\n+\t\tif (!opts.quiet) {\n \t\t\tif (opts.detach > 0)\n \t\t\t\tfprintf(stderr, _(\"Auto packing the repository in background for optimum performance.\\n\"));\n \t\t\telse\n@@ -991,7 +990,7 @@ int cmd_gc(int argc,\n \t\t\tstrvec_pushl(&prune_cmd.args, \"prune\", \"--expire\", NULL);\n \t\t\t/* run `git prune` even if using cruft packs */\n \t\t\tstrvec_push(&prune_cmd.args, cfg.prune_expire);\n-\t\t\tif (quiet)\n+\t\t\tif (opts.quiet)\n \t\t\t\tstrvec_push(&prune_cmd.args, \"--no-progress\");\n \t\t\tif (repo_has_promisor_remote(the_repository))\n \t\t\t\tstrvec_push(&prune_cmd.args,\n@@ -1019,7 +1018,7 @@ int cmd_gc(int argc,\n \n \tif (the_repository->settings.gc_write_commit_graph == 1)\n \t\twrite_commit_graph_reachable(the_repository->objects->odb,\n-\t\t\t\t\t     !quiet && !daemonized ? COMMIT_GRAPH_WRITE_PROGRESS : 0,\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\n-- \n2.50.0.rc0.604.gd4ff7b7c86.dirty\n\n"},{"id":"519253","messageId":"20250530-b4-pks-maintenance-ref-lock-race-v2-4-d04e2f93e51f@pks.im","threadId":"63531","inReplyTo":"20250530-b4-pks-maintenance-ref-lock-race-v2-0-d04e2f93e51f@pks.im","subject":"[PATCH v2 04/12] builtin/maintenance: mark \"--task=\" and \"--schedule=\" as incompatible","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-30T15:08:30Z","receivedAt":"2025-05-30T15:08:44Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"--task=\" option explicitly allows the user to say which maintenance\ntasks should be run, whereas \"--schedule=\" only respects the maintenance\nstrategy configured for a specific repository. As such, it is not\nsensible to accept both options at the same time.\n\nMark them as incompatible with one another. While at it, also convert\nthe existing logic that marks \"--auto\" and \"--schedule=\" as incompatible\nto use `die_for_incompatible_opt2()`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c           | 6 ++++--\n t/t7900-maintenance.sh | 7 ++++++-\n 2 files changed, 10 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex c4af9b11287..57d7602596a 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1785,8 +1785,10 @@ static int maintenance_run(int argc, const char **argv, const char *prefix,\n \t\t\t     builtin_maintenance_run_usage,\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n \n-\tif (opts.auto_flag && opts.schedule)\n-\t\tdie(_(\"use at most one of --auto and --schedule=<frequency>\"));\n+\tdie_for_incompatible_opt2(opts.auto_flag, \"--auto\",\n+\t\t\t\t  opts.schedule, \"--schedule=\");\n+\tdie_for_incompatible_opt2(selected_tasks.nr, \"--task=\",\n+\t\t\t\t  opts.schedule, \"--schedule=\");\n \n \tgc_config(&cfg);\n \tinitialize_task_config(&selected_tasks, opts.schedule);\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 8cf89e285f4..1ada5246606 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -610,7 +610,12 @@ test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.aut\n \n test_expect_success '--auto and --schedule incompatible' '\n \ttest_must_fail git maintenance run --auto --schedule=daily 2>err &&\n-\ttest_grep \"at most one\" err\n+\ttest_grep \"cannot be used together\" err\n+'\n+\n+test_expect_success '--task and --schedule incompatible' '\n+\ttest_must_fail git maintenance run --task=pack-refs --schedule=daily 2>err &&\n+\ttest_grep \"cannot be used together\" err\n '\n \n test_expect_success 'invalid --schedule value' '\n\n-- \n2.50.0.rc0.604.gd4ff7b7c86.dirty\n\n"},{"id":"519254","messageId":"20250530-b4-pks-maintenance-ref-lock-race-v2-3-d04e2f93e51f@pks.im","threadId":"63531","inReplyTo":"20250530-b4-pks-maintenance-ref-lock-race-v2-0-d04e2f93e51f@pks.im","subject":"[PATCH v2 03/12] builtin/maintenance: centralize configuration of explicit tasks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-30T15:08:29Z","receivedAt":"2025-05-30T15:08:44Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Users of git-maintenance(1) can explicitly ask it to run specific tasks\nby passing the `--task=` command line option. This option can be passed\nmultiple times, which causes us to execute tasks in the same order as\nthe tasks have been provided by the user.\n\nThe order in which tasks are run is computed in `task_option_parse()`:\nevery time we parse such a command line argument, we modify the global\narray of tasks by seting the selected index for that specific task.\nThis has two downsides:\n\n  - We modify global state, which makes it hard to follow the logic.\n\n  - The configuration of tasks is split across multiple different\n    functions, so it is not easy to figure out the different factors\n    that play a role in selecting tasks.\n\nRefactor the logic so that `task_option_parse()` does not modify global\nstate anymore. Instead, this function now only collects the list of\nconfigured tasks. The logic to configure ordering of the respective\ntasks is then deferred to `initialize_task_config()`.\n\nThis refactoring solves the second problem, that the configuration of\ntasks is spread across multiple different locations. The first problem,\nthat we modify global state, will be fixed in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 47 ++++++++++++++++++++++++-----------------------\n 1 file changed, 24 insertions(+), 23 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 7adda8d2d0d..c4af9b11287 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1690,15 +1690,22 @@ static void initialize_maintenance_strategy(void)\n \t}\n }\n \n-static void initialize_task_config(int schedule)\n+static void initialize_task_config(const struct string_list *selected_tasks,\n+\t\t\t\t   int schedule)\n {\n-\tint i;\n \tstruct strbuf config_name = STRBUF_INIT;\n \n+\tfor (size_t i = 0; i < TASK__COUNT; i++)\n+\t\ttasks[i].selected_order = -1;\n+\tfor (size_t i = 0; i < selected_tasks->nr; i++) {\n+\t\tstruct maintenance_task *task = selected_tasks->items[i].util;\n+\t\ttask->selected_order = i;\n+\t}\n+\n \tif (schedule)\n \t\tinitialize_maintenance_strategy();\n \n-\tfor (i = 0; i < TASK__COUNT; i++) {\n+\tfor (size_t i = 0; i < TASK__COUNT; i++) {\n \t\tint config_value;\n \t\tchar *config_str;\n \n@@ -1722,33 +1729,28 @@ static void initialize_task_config(int schedule)\n \tstrbuf_release(&config_name);\n }\n \n-static int task_option_parse(const struct option *opt UNUSED,\n+static int task_option_parse(const struct option *opt,\n \t\t\t     const char *arg, int unset)\n {\n-\tint i, num_selected = 0;\n-\tstruct maintenance_task *task = NULL;\n+\tstruct string_list *selected_tasks = opt->value;\n+\tsize_t i;\n \n \tBUG_ON_OPT_NEG(unset);\n \n-\tfor (i = 0; i < TASK__COUNT; i++) {\n-\t\tif (tasks[i].selected_order >= 0)\n-\t\t\tnum_selected++;\n-\t\tif (!strcasecmp(tasks[i].name, arg)) {\n-\t\t\ttask = &tasks[i];\n-\t\t}\n-\t}\n-\n-\tif (!task) {\n+\tfor (i = 0; i < TASK__COUNT; i++)\n+\t\tif (!strcasecmp(tasks[i].name, arg))\n+\t\t\tbreak;\n+\tif (i >= TASK__COUNT) {\n \t\terror(_(\"'%s' is not a valid task\"), arg);\n \t\treturn 1;\n \t}\n \n-\tif (task->selected_order >= 0) {\n+\tif (unsorted_string_list_has_string(selected_tasks, arg)) {\n \t\terror(_(\"task '%s' cannot be selected multiple times\"), arg);\n \t\treturn 1;\n \t}\n \n-\ttask->selected_order = num_selected + 1;\n+\tstring_list_append(selected_tasks, arg)->util = &tasks[i];\n \n \treturn 0;\n }\n@@ -1756,8 +1758,8 @@ static int task_option_parse(const struct option *opt UNUSED,\n static int maintenance_run(int argc, const char **argv, const char *prefix,\n \t\t\t   struct repository *repo UNUSED)\n {\n-\tint i;\n \tstruct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT;\n+\tstruct string_list selected_tasks = STRING_LIST_INIT_DUP;\n \tstruct gc_config cfg = GC_CONFIG_INIT;\n \tstruct option builtin_maintenance_run_options[] = {\n \t\tOPT_BOOL(0, \"auto\", &opts.auto_flag,\n@@ -1769,7 +1771,7 @@ static int maintenance_run(int argc, const char **argv, const char *prefix,\n \t\t\t     maintenance_opt_schedule),\n \t\tOPT_BOOL(0, \"quiet\", &opts.quiet,\n \t\t\t N_(\"do not report progress or other information over stderr\")),\n-\t\tOPT_CALLBACK_F(0, \"task\", NULL, N_(\"task\"),\n+\t\tOPT_CALLBACK_F(0, \"task\", &selected_tasks, N_(\"task\"),\n \t\t\tN_(\"run a specific task\"),\n \t\t\tPARSE_OPT_NONEG, task_option_parse),\n \t\tOPT_END()\n@@ -1778,9 +1780,6 @@ static int maintenance_run(int argc, const char **argv, const char *prefix,\n \n \topts.quiet = !isatty(2);\n \n-\tfor (i = 0; i < TASK__COUNT; i++)\n-\t\ttasks[i].selected_order = -1;\n-\n \targc = parse_options(argc, argv, prefix,\n \t\t\t     builtin_maintenance_run_options,\n \t\t\t     builtin_maintenance_run_usage,\n@@ -1790,13 +1789,15 @@ static int maintenance_run(int argc, const char **argv, const char *prefix,\n \t\tdie(_(\"use at most one of --auto and --schedule=<frequency>\"));\n \n \tgc_config(&cfg);\n-\tinitialize_task_config(opts.schedule);\n+\tinitialize_task_config(&selected_tasks, opts.schedule);\n \n \tif (argc != 0)\n \t\tusage_with_options(builtin_maintenance_run_usage,\n \t\t\t\t   builtin_maintenance_run_options);\n \n \tret = maintenance_run_tasks(&opts, &cfg);\n+\n+\tstring_list_clear(&selected_tasks, 0);\n \tgc_config_release(&cfg);\n \treturn ret;\n }\n\n-- \n2.50.0.rc0.604.gd4ff7b7c86.dirty\n\n"},{"id":"519256","messageId":"20250530-b4-pks-maintenance-ref-lock-race-v2-5-d04e2f93e51f@pks.im","threadId":"63531","inReplyTo":"20250530-b4-pks-maintenance-ref-lock-race-v2-0-d04e2f93e51f@pks.im","subject":"[PATCH v2 05/12] builtin/maintenance: stop modifying global array of tasks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-30T15:08:31Z","receivedAt":"2025-05-30T15:08:45Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When configuring maintenance tasks run by git-maintenance(1) we do so by\nmodifying the global array of tasks directly. This is already quite bad\non its own, as global state makes for logic that is hard to follow.\n\nEven more importantly though we use multiple different fields to track\nwhether or not a task should be run:\n\n  - \"enabled\" tracks the \"maintenance.*.enabled\" config key. This field\n    disables execution of a task, unless the user has explicitly asked\n    for the task.\n\n  - \"selected_order\" tracks the order in which jobs have been asked for\n    by the user via the \"--task=\" command line option. It overrides\n    everything else, but only has an effect if at least one job has been\n    selected.\n\n  - \"schedule\" tracks the schedule priority for a job, that is how often\n    it should run. This field only plays a role when the user has passed\n    the \"--schedule=\" command line option.\n\nAll of this makes it non-trivial to figure out which job really should\nbe running right now. The logic to configure these fields and the logic\nthat interprets them is distributed across multiple functions, making it\neven harder to follow it.\n\nRefactor the logic so that we stop modifying global state. Instead, we\nnow compute which jobs should be run in `initialize_task_config()`,\nrepresented as an array of jobs to run that is stored in the options\nstructure. Like this, all logic becomes self-contained and any users of\nthis array only need to iterate through the tasks and execute them one\nby one.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 206 ++++++++++++++++++++++++++++++++---------------------------\n 1 file changed, 112 insertions(+), 94 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 57d7602596a..4d636237cac 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -251,7 +251,24 @@ static enum schedule_priority parse_schedule(const char *value)\n \treturn SCHEDULE_NONE;\n }\n \n+enum maintenance_task_label {\n+\tTASK_PREFETCH,\n+\tTASK_LOOSE_OBJECTS,\n+\tTASK_INCREMENTAL_REPACK,\n+\tTASK_GC,\n+\tTASK_COMMIT_GRAPH,\n+\tTASK_PACK_REFS,\n+\tTASK_REFLOG_EXPIRE,\n+\tTASK_WORKTREE_PRUNE,\n+\tTASK_RERERE_GC,\n+\n+\t/* Leave as final value */\n+\tTASK__COUNT\n+};\n+\n struct maintenance_run_opts {\n+\tenum maintenance_task_label *tasks;\n+\tsize_t tasks_nr, tasks_alloc;\n \tint auto_flag;\n \tint detach;\n \tint quiet;\n@@ -261,6 +278,11 @@ struct maintenance_run_opts {\n \t.detach = -1, \\\n }\n \n+static void maintenance_run_opts_release(struct maintenance_run_opts *opts)\n+{\n+\tfree(opts->tasks);\n+}\n+\n static int pack_refs_condition(UNUSED struct gc_config *cfg)\n {\n \t/*\n@@ -1032,6 +1054,7 @@ int cmd_gc(int argc,\n \t}\n \n out:\n+\tmaintenance_run_opts_release(&opts);\n \tgc_config_release(&cfg);\n \treturn 0;\n }\n@@ -1524,30 +1547,9 @@ struct maintenance_task {\n \tconst char *name;\n \tmaintenance_task_fn *fn;\n \tmaintenance_auto_fn *auto_condition;\n-\tunsigned enabled:1;\n-\n-\tenum schedule_priority schedule;\n-\n-\t/* -1 if not selected. */\n-\tint selected_order;\n-};\n-\n-enum maintenance_task_label {\n-\tTASK_PREFETCH,\n-\tTASK_LOOSE_OBJECTS,\n-\tTASK_INCREMENTAL_REPACK,\n-\tTASK_GC,\n-\tTASK_COMMIT_GRAPH,\n-\tTASK_PACK_REFS,\n-\tTASK_REFLOG_EXPIRE,\n-\tTASK_WORKTREE_PRUNE,\n-\tTASK_RERERE_GC,\n-\n-\t/* Leave as final value */\n-\tTASK__COUNT\n };\n \n-static struct maintenance_task tasks[] = {\n+static const struct maintenance_task tasks[] = {\n \t[TASK_PREFETCH] = {\n \t\t.name = \"prefetch\",\n \t\t.fn = maintenance_task_prefetch,\n@@ -1566,7 +1568,6 @@ static struct maintenance_task tasks[] = {\n \t\t.name = \"gc\",\n \t\t.fn = maintenance_task_gc,\n \t\t.auto_condition = need_to_gc,\n-\t\t.enabled = 1,\n \t},\n \t[TASK_COMMIT_GRAPH] = {\n \t\t.name = \"commit-graph\",\n@@ -1595,18 +1596,9 @@ static struct maintenance_task tasks[] = {\n \t},\n };\n \n-static int compare_tasks_by_selection(const void *a_, const void *b_)\n-{\n-\tconst struct maintenance_task *a = a_;\n-\tconst struct maintenance_task *b = b_;\n-\n-\treturn b->selected_order - a->selected_order;\n-}\n-\n static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \t\t\t\t struct gc_config *cfg)\n {\n-\tint i, found_selected = 0;\n \tint result = 0;\n \tstruct lock_file lk;\n \tstruct repository *r = the_repository;\n@@ -1635,95 +1627,120 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \t\ttrace2_region_leave(\"maintenance\", \"detach\", the_repository);\n \t}\n \n-\tfor (i = 0; !found_selected && i < TASK__COUNT; i++)\n-\t\tfound_selected = tasks[i].selected_order >= 0;\n-\n-\tif (found_selected)\n-\t\tQSORT(tasks, TASK__COUNT, compare_tasks_by_selection);\n-\n-\tfor (i = 0; i < TASK__COUNT; i++) {\n-\t\tif (found_selected && tasks[i].selected_order < 0)\n-\t\t\tcontinue;\n-\n-\t\tif (!found_selected && !tasks[i].enabled)\n-\t\t\tcontinue;\n-\n+\tfor (size_t i = 0; i < opts->tasks_nr; i++) {\n \t\tif (opts->auto_flag &&\n-\t\t    (!tasks[i].auto_condition ||\n-\t\t     !tasks[i].auto_condition(cfg)))\n-\t\t\tcontinue;\n-\n-\t\tif (opts->schedule && tasks[i].schedule < opts->schedule)\n+\t\t    (!tasks[opts->tasks[i]].auto_condition ||\n+\t\t     !tasks[opts->tasks[i]].auto_condition(cfg)))\n \t\t\tcontinue;\n \n-\t\ttrace2_region_enter(\"maintenance\", tasks[i].name, r);\n-\t\tif (tasks[i].fn(opts, cfg)) {\n-\t\t\terror(_(\"task '%s' failed\"), tasks[i].name);\n+\t\ttrace2_region_enter(\"maintenance\", tasks[opts->tasks[i]].name, r);\n+\t\tif (tasks[opts->tasks[i]].fn(opts, cfg)) {\n+\t\t\terror(_(\"task '%s' failed\"), tasks[opts->tasks[i]].name);\n \t\t\tresult = 1;\n \t\t}\n-\t\ttrace2_region_leave(\"maintenance\", tasks[i].name, r);\n+\t\ttrace2_region_leave(\"maintenance\", tasks[opts->tasks[i]].name, r);\n \t}\n \n \trollback_lock_file(&lk);\n \treturn result;\n }\n \n-static void initialize_maintenance_strategy(void)\n+struct maintenance_strategy {\n+\tstruct {\n+\t\tint enabled;\n+\t\tenum schedule_priority schedule;\n+\t} tasks[TASK__COUNT];\n+};\n+\n+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},\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},\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+\tstruct strbuf config_name = STRBUF_INIT;\n+\tstruct maintenance_strategy strategy;\n \tconst char *config_str;\n \n-\tif (git_config_get_string_tmp(\"maintenance.strategy\", &config_str))\n-\t\treturn;\n+\t/*\n+\t * In case the user has asked us to run tasks explicitly we only use\n+\t * those specified tasks. Specifically, we do _not_ want to consult the\n+\t * config or maintenance strategy.\n+\t */\n+\tif (selected_tasks->nr) {\n+\t\tfor (size_t i = 0; i < selected_tasks->nr; i++) {\n+\t\t\tenum maintenance_task_label label = (intptr_t)selected_tasks->items[i].util;;\n+\t\t\tALLOC_GROW(opts->tasks, opts->tasks_nr + 1, opts->tasks_alloc);\n+\t\t\topts->tasks[opts->tasks_nr++] = label;\n+\t\t}\n \n-\tif (!strcasecmp(config_str, \"incremental\")) {\n-\t\ttasks[TASK_GC].schedule = SCHEDULE_NONE;\n-\t\ttasks[TASK_COMMIT_GRAPH].enabled = 1;\n-\t\ttasks[TASK_COMMIT_GRAPH].schedule = SCHEDULE_HOURLY;\n-\t\ttasks[TASK_PREFETCH].enabled = 1;\n-\t\ttasks[TASK_PREFETCH].schedule = SCHEDULE_HOURLY;\n-\t\ttasks[TASK_INCREMENTAL_REPACK].enabled = 1;\n-\t\ttasks[TASK_INCREMENTAL_REPACK].schedule = SCHEDULE_DAILY;\n-\t\ttasks[TASK_LOOSE_OBJECTS].enabled = 1;\n-\t\ttasks[TASK_LOOSE_OBJECTS].schedule = SCHEDULE_DAILY;\n-\t\ttasks[TASK_PACK_REFS].enabled = 1;\n-\t\ttasks[TASK_PACK_REFS].schedule = SCHEDULE_WEEKLY;\n+\t\treturn;\n \t}\n-}\n \n-static void initialize_task_config(const struct string_list *selected_tasks,\n-\t\t\t\t   int schedule)\n-{\n-\tstruct strbuf config_name = STRBUF_INIT;\n+\t/*\n+\t * Otherwise, the strategy depends on whether we run as part of a\n+\t * scheduled job or not:\n+\t *\n+\t *   - Scheduled maintenance does not perform any housekeeping by\n+\t *     default, but requires the user to pick a maintenance strategy.\n+\t *\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 */\n+\tif (opts->schedule) {\n+\t\tstrategy = none_strategy;\n \n-\tfor (size_t i = 0; i < TASK__COUNT; i++)\n-\t\ttasks[i].selected_order = -1;\n-\tfor (size_t i = 0; i < selected_tasks->nr; i++) {\n-\t\tstruct maintenance_task *task = selected_tasks->items[i].util;\n-\t\ttask->selected_order = i;\n+\t\tif (!git_config_get_string_tmp(\"maintenance.strategy\", &config_str)) {\n+\t\t\tif (!strcasecmp(config_str, \"incremental\"))\n+\t\t\t\tstrategy = incremental_strategy;\n+\t\t}\n+\t} else {\n+\t\tstrategy = default_strategy;\n \t}\n \n-\tif (schedule)\n-\t\tinitialize_maintenance_strategy();\n-\n \tfor (size_t i = 0; i < TASK__COUNT; i++) {\n \t\tint config_value;\n-\t\tchar *config_str;\n \n \t\tstrbuf_reset(&config_name);\n \t\tstrbuf_addf(&config_name, \"maintenance.%s.enabled\",\n \t\t\t    tasks[i].name);\n-\n \t\tif (!git_config_get_bool(config_name.buf, &config_value))\n-\t\t\ttasks[i].enabled = config_value;\n-\n-\t\tstrbuf_reset(&config_name);\n-\t\tstrbuf_addf(&config_name, \"maintenance.%s.schedule\",\n-\t\t\t    tasks[i].name);\n+\t\t\tstrategy.tasks[i].enabled = config_value;\n+\t\tif (!strategy.tasks[i].enabled)\n+\t\t\tcontinue;\n \n-\t\tif (!git_config_get_string(config_name.buf, &config_str)) {\n-\t\t\ttasks[i].schedule = parse_schedule(config_str);\n-\t\t\tfree(config_str);\n+\t\tif (opts->schedule) {\n+\t\t\tstrbuf_reset(&config_name);\n+\t\t\tstrbuf_addf(&config_name, \"maintenance.%s.schedule\",\n+\t\t\t\t    tasks[i].name);\n+\t\t\tif (!git_config_get_string_tmp(config_name.buf, &config_str))\n+\t\t\t\tstrategy.tasks[i].schedule = parse_schedule(config_str);\n+\t\t\tif (strategy.tasks[i].schedule < opts->schedule)\n+\t\t\t\tcontinue;\n \t\t}\n+\n+\t\tALLOC_GROW(opts->tasks, opts->tasks_nr + 1, opts->tasks_alloc);\n+\t\topts->tasks[opts->tasks_nr++] = i;\n \t}\n \n \tstrbuf_release(&config_name);\n@@ -1750,7 +1767,7 @@ static int task_option_parse(const struct option *opt,\n \t\treturn 1;\n \t}\n \n-\tstring_list_append(selected_tasks, arg)->util = &tasks[i];\n+\tstring_list_append(selected_tasks, arg)->util = (void *)(intptr_t)i;\n \n \treturn 0;\n }\n@@ -1791,7 +1808,7 @@ static int maintenance_run(int argc, const char **argv, const char *prefix,\n \t\t\t\t  opts.schedule, \"--schedule=\");\n \n \tgc_config(&cfg);\n-\tinitialize_task_config(&selected_tasks, opts.schedule);\n+\tinitialize_task_config(&opts, &selected_tasks);\n \n \tif (argc != 0)\n \t\tusage_with_options(builtin_maintenance_run_usage,\n@@ -1800,6 +1817,7 @@ static int maintenance_run(int argc, const char **argv, const char *prefix,\n \tret = maintenance_run_tasks(&opts, &cfg);\n \n \tstring_list_clear(&selected_tasks, 0);\n+\tmaintenance_run_opts_release(&opts);\n \tgc_config_release(&cfg);\n \treturn ret;\n }\n\n-- \n2.50.0.rc0.604.gd4ff7b7c86.dirty\n\n"},{"id":"519255","messageId":"20250530-b4-pks-maintenance-ref-lock-race-v2-7-d04e2f93e51f@pks.im","threadId":"63531","inReplyTo":"20250530-b4-pks-maintenance-ref-lock-race-v2-0-d04e2f93e51f@pks.im","subject":"[PATCH v2 07/12] builtin/maintenance: fix typedef for function pointers","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-30T15:08:33Z","receivedAt":"2025-05-30T15:08:46Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The typedefs for `maintenance_task_fn` and `maintenance_auto_fn` are\nsomewhat confusingly not true function pointers. As such, any user of\nthose typedefs needs to manually add the pointer to make use of them.\n\nFix this by making these true function pointers.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 10 +++++-----\n 1 file changed, 5 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex cfbf9d8a2b9..447e5800846 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1533,20 +1533,20 @@ static int maintenance_task_incremental_repack(struct maintenance_run_opts *opts\n \treturn 0;\n }\n \n-typedef int maintenance_task_fn(struct maintenance_run_opts *opts,\n-\t\t\t\tstruct gc_config *cfg);\n+typedef int (*maintenance_task_fn)(struct maintenance_run_opts *opts,\n+\t\t\t\t   struct gc_config *cfg);\n \n /*\n  * An auto condition function returns 1 if the task should run\n  * and 0 if the task should NOT run. See needs_to_gc() for an\n  * example.\n  */\n-typedef int maintenance_auto_fn(struct gc_config *cfg);\n+typedef int (*maintenance_auto_fn)(struct gc_config *cfg);\n \n struct maintenance_task {\n \tconst char *name;\n-\tmaintenance_task_fn *fn;\n-\tmaintenance_auto_fn *auto_condition;\n+\tmaintenance_task_fn fn;\n+\tmaintenance_auto_fn auto_condition;\n };\n \n static const struct maintenance_task tasks[] = {\n\n-- \n2.50.0.rc0.604.gd4ff7b7c86.dirty\n\n"},{"id":"519257","messageId":"20250530-b4-pks-maintenance-ref-lock-race-v2-6-d04e2f93e51f@pks.im","threadId":"63531","inReplyTo":"20250530-b4-pks-maintenance-ref-lock-race-v2-0-d04e2f93e51f@pks.im","subject":"[PATCH v2 06/12] builtin/maintenance: extract function to run tasks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-30T15:08:32Z","receivedAt":"2025-05-30T15:08:46Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Extract the function to run maintenance tasks. This function will be\nreused in a subsequent commit where we introduce a split between\nmaintenance tasks that run before and after daemonizing the process.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 35 +++++++++++++++++++++++------------\n 1 file changed, 23 insertions(+), 12 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 4d636237cac..cfbf9d8a2b9 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1596,6 +1596,27 @@ static const struct maintenance_task tasks[] = {\n \t},\n };\n \n+static int maybe_run_task(const struct maintenance_task *task,\n+\t\t\t  struct repository *repo,\n+\t\t\t  struct maintenance_run_opts *opts,\n+\t\t\t  struct gc_config *cfg)\n+{\n+\tint ret = 0;\n+\n+\tif (opts->auto_flag &&\n+\t    (!task->auto_condition || !task->auto_condition(cfg)))\n+\t\treturn 0;\n+\n+\ttrace2_region_enter(\"maintenance\", task->name, repo);\n+\tif (task->fn(opts, cfg)) {\n+\t\terror(_(\"task '%s' failed\"), task->name);\n+\t\tret = 1;\n+\t}\n+\ttrace2_region_leave(\"maintenance\", task->name, repo);\n+\n+\treturn ret;\n+}\n+\n static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \t\t\t\t struct gc_config *cfg)\n {\n@@ -1627,19 +1648,9 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \t\ttrace2_region_leave(\"maintenance\", \"detach\", the_repository);\n \t}\n \n-\tfor (size_t i = 0; i < opts->tasks_nr; i++) {\n-\t\tif (opts->auto_flag &&\n-\t\t    (!tasks[opts->tasks[i]].auto_condition ||\n-\t\t     !tasks[opts->tasks[i]].auto_condition(cfg)))\n-\t\t\tcontinue;\n-\n-\t\ttrace2_region_enter(\"maintenance\", tasks[opts->tasks[i]].name, r);\n-\t\tif (tasks[opts->tasks[i]].fn(opts, cfg)) {\n-\t\t\terror(_(\"task '%s' failed\"), tasks[opts->tasks[i]].name);\n+\tfor (size_t i = 0; i < opts->tasks_nr; i++)\n+\t\tif (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg))\n \t\t\tresult = 1;\n-\t\t}\n-\t\ttrace2_region_leave(\"maintenance\", tasks[opts->tasks[i]].name, r);\n-\t}\n \n \trollback_lock_file(&lk);\n \treturn result;\n\n-- \n2.50.0.rc0.604.gd4ff7b7c86.dirty\n\n"},{"id":"519258","messageId":"20250530-b4-pks-maintenance-ref-lock-race-v2-8-d04e2f93e51f@pks.im","threadId":"63531","inReplyTo":"20250530-b4-pks-maintenance-ref-lock-race-v2-0-d04e2f93e51f@pks.im","subject":"[PATCH v2 08/12] builtin/maintenance: let tasks do maintenance before and after detach","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-30T15:08:34Z","receivedAt":"2025-05-30T15:08:47Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Both git-gc(1) and git-maintenance(1) have logic to daemonize so that\nthe maintenance tasks are performed in the background. git-gc(1) has\nsome special logic though to not perform _all_ housekeeping tasks in the\nbackground: both references and reflogs are still handled synchronously\nin the foreground.\n\nThis split exists because otherwise it may easily happen that git-gc(1)\nkeeps the \"packed-refs\" file locked for an extended amount of time,\nwhere the next Git command that wants to modify any reference could now\nfail. This was especially important in the past, where git-gc(1) was\nstill executed directly as part of our automatic maintenance: git-gc(1)\nwas invoked via `git gc --auto --detach`, so we knew to handle most of\nthe maintenance tasks in the background while doing those parts that may\ncause locking issues in the foreground.\n\nWe have since moved to git-maintenance(1), which is a more flexible\nreplacement for git-gc(1). By default this command runs git-gc(1), only,\nbut it can be configured to run different tasks, as well. This command\ndoes not know about the split between maintenance tasks that should run\nbefore and after detach though, and this has led to several bug reports\nabout spurious locking errors for the \"packed-refs\" file.\n\nPrepare for a fix by introducing this split for maintenance tasks. Note\nthat this commit does not yet change any of the tasks, so there should\nnot (yet) be a change in behaviour.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 48 +++++++++++++++++++++++++++++++++---------------\n 1 file changed, 33 insertions(+), 15 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 447e5800846..f64bae0a825 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1545,74 +1545,86 @@ typedef int (*maintenance_auto_fn)(struct gc_config *cfg);\n \n struct maintenance_task {\n \tconst char *name;\n-\tmaintenance_task_fn fn;\n+\tmaintenance_task_fn before_detach;\n+\tmaintenance_task_fn after_detach;\n \tmaintenance_auto_fn auto_condition;\n };\n \n static const struct maintenance_task tasks[] = {\n \t[TASK_PREFETCH] = {\n \t\t.name = \"prefetch\",\n-\t\t.fn = maintenance_task_prefetch,\n+\t\t.after_detach = maintenance_task_prefetch,\n \t},\n \t[TASK_LOOSE_OBJECTS] = {\n \t\t.name = \"loose-objects\",\n-\t\t.fn = maintenance_task_loose_objects,\n+\t\t.after_detach = maintenance_task_loose_objects,\n \t\t.auto_condition = loose_object_auto_condition,\n \t},\n \t[TASK_INCREMENTAL_REPACK] = {\n \t\t.name = \"incremental-repack\",\n-\t\t.fn = maintenance_task_incremental_repack,\n+\t\t.after_detach = maintenance_task_incremental_repack,\n \t\t.auto_condition = incremental_repack_auto_condition,\n \t},\n \t[TASK_GC] = {\n \t\t.name = \"gc\",\n-\t\t.fn = maintenance_task_gc,\n+\t\t.after_detach = maintenance_task_gc,\n \t\t.auto_condition = need_to_gc,\n \t},\n \t[TASK_COMMIT_GRAPH] = {\n \t\t.name = \"commit-graph\",\n-\t\t.fn = maintenance_task_commit_graph,\n+\t\t.after_detach = maintenance_task_commit_graph,\n \t\t.auto_condition = should_write_commit_graph,\n \t},\n \t[TASK_PACK_REFS] = {\n \t\t.name = \"pack-refs\",\n-\t\t.fn = maintenance_task_pack_refs,\n+\t\t.after_detach = maintenance_task_pack_refs,\n \t\t.auto_condition = pack_refs_condition,\n \t},\n \t[TASK_REFLOG_EXPIRE] = {\n \t\t.name = \"reflog-expire\",\n-\t\t.fn = maintenance_task_reflog_expire,\n+\t\t.after_detach = maintenance_task_reflog_expire,\n \t\t.auto_condition = reflog_expire_condition,\n \t},\n \t[TASK_WORKTREE_PRUNE] = {\n \t\t.name = \"worktree-prune\",\n-\t\t.fn = maintenance_task_worktree_prune,\n+\t\t.after_detach = maintenance_task_worktree_prune,\n \t\t.auto_condition = worktree_prune_condition,\n \t},\n \t[TASK_RERERE_GC] = {\n \t\t.name = \"rerere-gc\",\n-\t\t.fn = maintenance_task_rerere_gc,\n+\t\t.after_detach = maintenance_task_rerere_gc,\n \t\t.auto_condition = rerere_gc_condition,\n \t},\n };\n \n+enum task_phase {\n+\tTASK_PHASE_BEFORE_DETACH,\n+\tTASK_PHASE_AFTER_DETACH,\n+};\n+\n static int maybe_run_task(const struct maintenance_task *task,\n \t\t\t  struct repository *repo,\n \t\t\t  struct maintenance_run_opts *opts,\n-\t\t\t  struct gc_config *cfg)\n+\t\t\t  struct gc_config *cfg,\n+\t\t\t  enum task_phase phase)\n {\n+\tint before = (phase == TASK_PHASE_BEFORE_DETACH);\n+\tmaintenance_task_fn fn = before ? task->before_detach : task->after_detach;\n+\tconst char *region = before ? \"maintenance before\" : \"maintenance\";\n \tint ret = 0;\n \n+\tif (!fn)\n+\t\treturn 0;\n \tif (opts->auto_flag &&\n \t    (!task->auto_condition || !task->auto_condition(cfg)))\n \t\treturn 0;\n \n-\ttrace2_region_enter(\"maintenance\", task->name, repo);\n-\tif (task->fn(opts, cfg)) {\n+\ttrace2_region_enter(region, task->name, repo);\n+\tif (fn(opts, cfg)) {\n \t\terror(_(\"task '%s' failed\"), task->name);\n \t\tret = 1;\n \t}\n-\ttrace2_region_leave(\"maintenance\", task->name, repo);\n+\ttrace2_region_leave(region, task->name, repo);\n \n \treturn ret;\n }\n@@ -1641,6 +1653,11 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \t}\n \tfree(lock_path);\n \n+\tfor (size_t i = 0; i < opts->tasks_nr; i++)\n+\t\tif (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg,\n+\t\t\t\t   TASK_PHASE_BEFORE_DETACH))\n+\t\t\tresult = 1;\n+\n \t/* Failure to daemonize is ok, we'll continue in foreground. */\n \tif (opts->detach > 0) {\n \t\ttrace2_region_enter(\"maintenance\", \"detach\", the_repository);\n@@ -1649,7 +1666,8 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \t}\n \n \tfor (size_t i = 0; i < opts->tasks_nr; i++)\n-\t\tif (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg))\n+\t\tif (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg,\n+\t\t\t\t   TASK_PHASE_AFTER_DETACH))\n \t\t\tresult = 1;\n \n \trollback_lock_file(&lk);\n\n-- \n2.50.0.rc0.604.gd4ff7b7c86.dirty\n\n"},{"id":"519259","messageId":"20250530-b4-pks-maintenance-ref-lock-race-v2-9-d04e2f93e51f@pks.im","threadId":"63531","inReplyTo":"20250530-b4-pks-maintenance-ref-lock-race-v2-0-d04e2f93e51f@pks.im","subject":"[PATCH v2 09/12] builtin/maintenance: fix locking race when packing refs and reflogs","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-30T15:08:35Z","receivedAt":"2025-05-30T15:08:49Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"As explained in the preceding commit, git-gc(1) knows to detach only\nafter it has already packed references and reflogs. This is done to\navoid racing around their respective lockfiles.\n\nAdapt git-maintenance(1) accordingly and run the \"pack-refs\" and\n\"reflog-expire\" tasks before detaching. Note that the \"gc\" task has the\nsame issue, but the fix is a bit more involved there and will thus be\ndone in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex f64bae0a825..e92015887a7 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1577,12 +1577,12 @@ static const struct maintenance_task tasks[] = {\n \t},\n \t[TASK_PACK_REFS] = {\n \t\t.name = \"pack-refs\",\n-\t\t.after_detach = maintenance_task_pack_refs,\n+\t\t.before_detach = maintenance_task_pack_refs,\n \t\t.auto_condition = pack_refs_condition,\n \t},\n \t[TASK_REFLOG_EXPIRE] = {\n \t\t.name = \"reflog-expire\",\n-\t\t.after_detach = maintenance_task_reflog_expire,\n+\t\t.before_detach = maintenance_task_reflog_expire,\n \t\t.auto_condition = reflog_expire_condition,\n \t},\n \t[TASK_WORKTREE_PRUNE] = {\n\n-- \n2.50.0.rc0.604.gd4ff7b7c86.dirty\n\n"},{"id":"519260","messageId":"20250530-b4-pks-maintenance-ref-lock-race-v2-10-d04e2f93e51f@pks.im","threadId":"63531","inReplyTo":"20250530-b4-pks-maintenance-ref-lock-race-v2-0-d04e2f93e51f@pks.im","subject":"[PATCH v2 10/12] usage: allow dying without writing an error message","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-30T15:08:36Z","receivedAt":"2025-05-30T15:08:50Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Sometimes code wants to die in a situation where it already has written\nan error message. To use the same error code as `die()` we have to open\ncode the code with a call to `exit(128)` in such cases, which is easy to\nget wrong and leaves magical numbers all over our codebase.\n\nTeach `die_message_builtin()` to not print any error when passed a\n`NULL` pointer as error string. Like this, such users can now call\n`die(NULL)` to achieve the same result without any hardcoded error\ncodes.\n\nAdapt a couple of builtins to use this new pattern to demonstrate that\nthere is a need for such a helper.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/am.c                |  4 ++--\n builtin/checkout.c          |  4 ++--\n builtin/fetch.c             |  2 +-\n builtin/submodule--helper.c | 12 ++++++------\n usage.c                     |  2 ++\n 5 files changed, 13 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/am.c b/builtin/am.c\nindex e32a3b4c973..a800003340f 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -1000,7 +1000,7 @@ static void am_setup(struct am_state *state, enum patch_format patch_format,\n \n \tif (!patch_format) {\n \t\tfprintf_ln(stderr, _(\"Patch format detection failed.\"));\n-\t\texit(128);\n+\t\tdie(NULL);\n \t}\n \n \tif (mkdir(state->dir, 0777) < 0 && errno != EEXIST)\n@@ -1178,7 +1178,7 @@ static void NORETURN die_user_resolve(const struct am_state *state)\n \t\tstrbuf_release(&sb);\n \t}\n \n-\texit(128);\n+\tdie(NULL);\n }\n \n /**\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex d185982f3a6..536192d3456 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -838,7 +838,7 @@ static int merge_working_tree(const struct checkout_opts *opts,\n \t\tinit_tree_desc(&trees[0], &tree->object.oid,\n \t\t\t       tree->buffer, tree->size);\n \t\tif (parse_tree(new_tree) < 0)\n-\t\t\texit(128);\n+\t\t\tdie(NULL);\n \t\ttree = new_tree;\n \t\tinit_tree_desc(&trees[1], &tree->object.oid,\n \t\t\t       tree->buffer, tree->size);\n@@ -913,7 +913,7 @@ static int merge_working_tree(const struct checkout_opts *opts,\n \t\t\t\t\t\t     work,\n \t\t\t\t\t\t     old_tree);\n \t\t\tif (ret < 0)\n-\t\t\t\texit(128);\n+\t\t\t\tdie(NULL);\n \t\t\tret = reset_tree(new_tree,\n \t\t\t\t\t opts, 0,\n \t\t\t\t\t writeout_error, new_branch_info);\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex cda6eaf1fd6..b0800ea5829 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -992,7 +992,7 @@ static int update_local_ref(struct ref *ref,\n \t\tfast_forward = repo_in_merge_bases(the_repository, current,\n \t\t\t\t\t\t   updated);\n \t\tif (fast_forward < 0)\n-\t\t\texit(128);\n+\t\t\tdie(NULL);\n \t\tforced_updates_ms += (getnanotime() - t_before) / 1000000;\n \t} else {\n \t\tfast_forward = 1;\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 53da2116ddf..4255caca579 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -303,7 +303,7 @@ static void runcommand_in_submodule_cb(const struct cache_entry *list_item,\n \tchar *displaypath;\n \n \tif (validate_submodule_path(path) < 0)\n-\t\texit(128);\n+\t\tdie(NULL);\n \n \tdisplaypath = get_submodule_displaypath(path, info->prefix,\n \t\t\t\t\t\tinfo->super_prefix);\n@@ -643,7 +643,7 @@ static void status_submodule(const char *path, const struct object_id *ce_oid,\n \t};\n \n \tif (validate_submodule_path(path) < 0)\n-\t\texit(128);\n+\t\tdie(NULL);\n \n \tif (!submodule_from_path(the_repository, null_oid(the_hash_algo), path))\n \t\tdie(_(\"no submodule mapping found in .gitmodules for path '%s'\"),\n@@ -1257,7 +1257,7 @@ static void sync_submodule(const char *path, const char *prefix,\n \t\treturn;\n \n \tif (validate_submodule_path(path) < 0)\n-\t\texit(128);\n+\t\tdie(NULL);\n \n \tsub = submodule_from_path(the_repository, null_oid(the_hash_algo), path);\n \n@@ -1402,7 +1402,7 @@ static void deinit_submodule(const char *path, const char *prefix,\n \tchar *sub_git_dir = xstrfmt(\"%s/.git\", path);\n \n \tif (validate_submodule_path(path) < 0)\n-\t\texit(128);\n+\t\tdie(NULL);\n \n \tsub = submodule_from_path(the_repository, null_oid(the_hash_algo), path);\n \n@@ -1724,7 +1724,7 @@ static int clone_submodule(const struct module_clone_data *clone_data,\n \tchar *to_free = NULL;\n \n \tif (validate_submodule_path(clone_data_path) < 0)\n-\t\texit(128);\n+\t\tdie(NULL);\n \n \tif (!is_absolute_path(clone_data->path))\n \t\tclone_data_path = to_free = xstrfmt(\"%s/%s\", repo_get_work_tree(the_repository),\n@@ -3524,7 +3524,7 @@ static int module_add(int argc, const char **argv, const char *prefix,\n \tstrip_dir_trailing_slashes(add_data.sm_path);\n \n \tif (validate_submodule_path(add_data.sm_path) < 0)\n-\t\texit(128);\n+\t\tdie(NULL);\n \n \tdie_on_index_match(add_data.sm_path, force);\n \tdie_on_repo_without_commits(add_data.sm_path);\ndiff --git a/usage.c b/usage.c\nindex 38b46bbbfe7..cd7b57d6446 100644\n--- a/usage.c\n+++ b/usage.c\n@@ -67,6 +67,8 @@ static NORETURN void usage_builtin(const char *err, va_list params)\n \n static void die_message_builtin(const char *err, va_list params)\n {\n+\tif (!err)\n+\t\treturn;\n \ttrace2_cmd_error_va(err, params);\n \tvreportf(_(\"fatal: \"), err, params);\n }\n\n-- \n2.50.0.rc0.604.gd4ff7b7c86.dirty\n\n"},{"id":"519261","messageId":"20250530-b4-pks-maintenance-ref-lock-race-v2-11-d04e2f93e51f@pks.im","threadId":"63531","inReplyTo":"20250530-b4-pks-maintenance-ref-lock-race-v2-0-d04e2f93e51f@pks.im","subject":"[PATCH v2 11/12] builtin/gc: avoid global state in `gc_before_repack()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-30T15:08:37Z","receivedAt":"2025-05-30T15:08:51Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The `gc_before_repack()` should only ever run once in git-gc(1), but we\nmay end up calling it twice when the \"--detach\" flag is passed. The\nduplicated call is avoided though via a static flag in this function.\n\nThis pattern is somewhat unintuitive though. Refactor it to drop the\nstatic flag and instead guard the second call of `gc_before_repack()`\nvia `opts.detach`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 24 +++++++++---------------\n 1 file changed, 9 insertions(+), 15 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e92015887a7..e910a99e033 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -816,22 +816,14 @@ static int report_last_gc_error(void)\n \treturn ret;\n }\n \n-static void gc_before_repack(struct maintenance_run_opts *opts,\n-\t\t\t     struct gc_config *cfg)\n+static int gc_before_repack(struct maintenance_run_opts *opts,\n+\t\t\t    struct gc_config *cfg)\n {\n-\t/*\n-\t * We may be called twice, as both the pre- and\n-\t * post-daemonized phases will call us, but running these\n-\t * commands more than once is pointless and wasteful.\n-\t */\n-\tstatic int done = 0;\n-\tif (done++)\n-\t\treturn;\n-\n \tif (cfg->pack_refs && maintenance_task_pack_refs(opts, cfg))\n-\t\tdie(FAILED_RUN, \"pack-refs\");\n+\t\treturn error(FAILED_RUN, \"pack-refs\");\n \tif (cfg->prune_reflogs && maintenance_task_reflog_expire(opts, cfg))\n-\t\tdie(FAILED_RUN, \"reflog\");\n+\t\treturn error(FAILED_RUN, \"reflog\");\n+\treturn 0;\n }\n \n int cmd_gc(int argc,\n@@ -965,7 +957,8 @@ int cmd_gc(int argc,\n \t\t\tgoto out;\n \t\t}\n \n-\t\tgc_before_repack(&opts, &cfg); /* dies on failure */\n+\t\tif (gc_before_repack(&opts, &cfg) < 0)\n+\t\t\tdie(NULL);\n \t\tdelete_tempfile(&pidfile);\n \n \t\t/*\n@@ -995,7 +988,8 @@ int cmd_gc(int argc,\n \t\tfree(path);\n \t}\n \n-\tgc_before_repack(&opts, &cfg);\n+\tif (opts.detach <= 0)\n+\t\tgc_before_repack(&opts, &cfg);\n \n \tif (!repository_format_precious_objects) {\n \t\tstruct child_process repack_cmd = CHILD_PROCESS_INIT;\n\n-- \n2.50.0.rc0.604.gd4ff7b7c86.dirty\n\n"},{"id":"519262","messageId":"20250530-b4-pks-maintenance-ref-lock-race-v2-12-d04e2f93e51f@pks.im","threadId":"63531","inReplyTo":"20250530-b4-pks-maintenance-ref-lock-race-v2-0-d04e2f93e51f@pks.im","subject":"[PATCH v2 12/12] builtin/maintenance: fix locking race when handling \"gc\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-30T15:08:38Z","receivedAt":"2025-05-30T15:08:51Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"gc\" task has a similar locking race as the one that we have fixed\nfor the \"pack-refs\" and \"reflog-expire\" tasks in preceding commits. Fix\nthis by splitting up the logic of the \"gc\" task:\n\n  - Before detaching we execute `gc_before_repack()`, which contains the\n    logic that git-gc(1) itself would execute before detaching.\n\n  - After detaching we spawn git-gc(1), but with a new hidden flag that\n    suppresses calling `gc_before_repack()`.\n\nLike this we have roughly the same logic as git-gc(1) itself and know to\nrepack refs and reflogs before detaching, thus fixing the race.\n\nNote that `gc_before_repack()` is renamed to `gc_before_detach()` to\nbetter reflect what this function does.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c           | 39 ++++++++++++++++++++++++++-------------\n t/t7900-maintenance.sh | 12 ++++++------\n 2 files changed, 32 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e910a99e033..dfbf20491e3 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -816,7 +816,7 @@ static int report_last_gc_error(void)\n \treturn ret;\n }\n \n-static int gc_before_repack(struct maintenance_run_opts *opts,\n+static int gc_before_detach(struct maintenance_run_opts *opts,\n \t\t\t    struct gc_config *cfg)\n {\n \tif (cfg->pack_refs && maintenance_task_pack_refs(opts, cfg))\n@@ -837,6 +837,7 @@ int cmd_gc(int argc,\n \tpid_t pid;\n \tint daemonized = 0;\n \tint keep_largest_pack = -1;\n+\tint skip_maintenance_before_detach = 0;\n \ttimestamp_t dummy;\n \tstruct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT;\n \tstruct gc_config cfg = GC_CONFIG_INIT;\n@@ -869,6 +870,8 @@ int cmd_gc(int argc,\n \t\t\t N_(\"repack all other packs except the largest pack\")),\n \t\tOPT_STRING(0, \"expire-to\", &cfg.repack_expire_to, N_(\"dir\"),\n \t\t\t   N_(\"pack prefix to store a pack containing pruned objects\")),\n+\t\tOPT_HIDDEN_BOOL(0, \"skip-maintenance-before-detach\", &skip_maintenance_before_detach,\n+\t\t\t   N_(\"skip maintenance steps typically done before detaching\")),\n \t\tOPT_END()\n \t};\n \n@@ -952,14 +955,16 @@ int cmd_gc(int argc,\n \t\t\tgoto out;\n \t\t}\n \n-\t\tif (lock_repo_for_gc(force, &pid)) {\n-\t\t\tret = 0;\n-\t\t\tgoto out;\n-\t\t}\n+\t\tif (!skip_maintenance_before_detach) {\n+\t\t\tif (lock_repo_for_gc(force, &pid)) {\n+\t\t\t\tret = 0;\n+\t\t\t\tgoto out;\n+\t\t\t}\n \n-\t\tif (gc_before_repack(&opts, &cfg) < 0)\n-\t\t\tdie(NULL);\n-\t\tdelete_tempfile(&pidfile);\n+\t\t\tif (gc_before_detach(&opts, &cfg) < 0)\n+\t\t\t\tdie(NULL);\n+\t\t\tdelete_tempfile(&pidfile);\n+\t\t}\n \n \t\t/*\n \t\t * failure to daemonize is ok, we'll continue\n@@ -988,8 +993,8 @@ int cmd_gc(int argc,\n \t\tfree(path);\n \t}\n \n-\tif (opts.detach <= 0)\n-\t\tgc_before_repack(&opts, &cfg);\n+\tif (opts.detach <= 0 && !skip_maintenance_before_detach)\n+\t\tgc_before_detach(&opts, &cfg);\n \n \tif (!repository_format_precious_objects) {\n \t\tstruct child_process repack_cmd = CHILD_PROCESS_INIT;\n@@ -1225,8 +1230,14 @@ static int maintenance_task_prefetch(struct maintenance_run_opts *opts,\n \treturn 0;\n }\n \n-static int maintenance_task_gc(struct maintenance_run_opts *opts,\n-\t\t\t       struct gc_config *cfg UNUSED)\n+static int maintenance_task_gc_before_detach(struct maintenance_run_opts *opts,\n+\t\t\t\t\t     struct gc_config *cfg)\n+{\n+\treturn gc_before_detach(opts, cfg);\n+}\n+\n+static int maintenance_task_gc_after_detach(struct maintenance_run_opts *opts,\n+\t\t\t\t\t    struct gc_config *cfg UNUSED)\n {\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \n@@ -1240,6 +1251,7 @@ static int maintenance_task_gc(struct maintenance_run_opts *opts,\n \telse\n \t\tstrvec_push(&child.args, \"--no-quiet\");\n \tstrvec_push(&child.args, \"--no-detach\");\n+\tstrvec_push(&child.args, \"--skip-maintenance-before-detach\");\n \n \treturn run_command(&child);\n }\n@@ -1561,7 +1573,8 @@ static const struct maintenance_task tasks[] = {\n \t},\n \t[TASK_GC] = {\n \t\t.name = \"gc\",\n-\t\t.after_detach = maintenance_task_gc,\n+\t\t.before_detach = maintenance_task_gc_before_detach,\n+\t\t.after_detach = maintenance_task_gc_after_detach,\n \t\t.auto_condition = need_to_gc,\n \t},\n \t[TASK_COMMIT_GRAPH] = {\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 1ada5246606..e09a36ab021 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -49,9 +49,9 @@ test_expect_success 'run [--auto|--quiet]' '\n \t\tgit maintenance run --auto 2>/dev/null &&\n \tGIT_TRACE2_EVENT=\"$(pwd)/run-no-quiet.txt\" \\\n \t\tgit maintenance run --no-quiet 2>/dev/null &&\n-\ttest_subcommand git gc --quiet --no-detach <run-no-auto.txt &&\n-\ttest_subcommand ! git gc --auto --quiet --no-detach <run-auto.txt &&\n-\ttest_subcommand git gc --no-quiet --no-detach <run-no-quiet.txt\n+\ttest_subcommand git gc --quiet --no-detach --skip-maintenance-before-detach <run-no-auto.txt &&\n+\ttest_subcommand ! git gc --auto --quiet --no-detach --skip-maintenance-before-detach <run-auto.txt &&\n+\ttest_subcommand git gc --no-quiet --no-detach --skip-maintenance-before-detach <run-no-quiet.txt\n '\n \n test_expect_success 'maintenance.auto config option' '\n@@ -154,9 +154,9 @@ test_expect_success 'run --task=<task>' '\n \t\tgit maintenance run --task=commit-graph 2>/dev/null &&\n \tGIT_TRACE2_EVENT=\"$(pwd)/run-both.txt\" \\\n \t\tgit maintenance run --task=commit-graph --task=gc 2>/dev/null &&\n-\ttest_subcommand ! git gc --quiet --no-detach <run-commit-graph.txt &&\n-\ttest_subcommand git gc --quiet --no-detach <run-gc.txt &&\n-\ttest_subcommand git gc --quiet --no-detach <run-both.txt &&\n+\ttest_subcommand ! git gc --quiet --no-detach --skip-maintenance-before-detach <run-commit-graph.txt &&\n+\ttest_subcommand git gc --quiet --no-detach --skip-maintenance-before-detach <run-gc.txt &&\n+\ttest_subcommand git gc --quiet --no-detach --skip-maintenance-before-detach <run-both.txt &&\n \ttest_subcommand git commit-graph write --split --reachable --no-progress <run-commit-graph.txt &&\n \ttest_subcommand ! git commit-graph write --split --reachable --no-progress <run-gc.txt &&\n \ttest_subcommand git commit-graph write --split --reachable --no-progress <run-both.txt\n\n-- \n2.50.0.rc0.604.gd4ff7b7c86.dirty\n\n"},{"id":"519385","messageId":"20250602-b4-pks-maintenance-ref-lock-race-v3-0-587d44252dcb@pks.im","threadId":"63531","inReplyTo":"20250527-b4-pks-maintenance-ref-lock-race-v1-0-e1ceb2dea66e@pks.im","subject":"[PATCH v3 00/12] builtin/maintenance: fix ref lock races when detaching","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-02T07:17:26Z","receivedAt":"2025-06-02T07:17:33Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis patch series fixes races around locking the \"packed-refs\" file when\nauto-maintenance decides to repack it. This issue has been reported e.g.\nvia [1] and [2].\n\nThe root cause is that git-gc(1) used to know to detach _after_ having\nrepacked references. As such, callers wouldn't continue with their thing\nuntil we have already packed refs, and thus the race does not exist\nthere. git-maintenance(1) didn't have the same split though, so this\npatch series retrofits that logic.\n\nThe series is structured as follows:\n\n  - Patches 1 and 2 do some light refactorings.\n\n  - Patches 3 to 5 refactor how we set up the list of tasks to not rely\n    on globals anymore. Instead, we now have a single source of truth\n    for which tasks exactly will be run.\n\n  - The remaining patches introduce the split of before/after-detach\n    tasks and wire them up for \"pack-refs\", \"reflog-expire\" and \"gc\"\n    tasks.\n\nChanges in v2:\n  - A couple of commit message improvements.\n  - Introduce `die(NULL)` to die with the correct exit code but no error\n    message. This gets rid of some magic numbers.\n  - Introduce an enum to discern the phases before and after detach.\n  - Link to v1: https://lore.kernel.org/r/20250527-b4-pks-maintenance-ref-lock-race-v1-0-e1ceb2dea66e@pks.im\n\nChanges in v3:\n  - Rework logic to talk about foreground/background tasks instead of\n    before/after detach.\n  - Link to v2: https://lore.kernel.org/r/20250530-b4-pks-maintenance-ref-lock-race-v2-0-d04e2f93e51f@pks.im\n\nThanks!\n\nPatrick\n\n[1]: <CAJR-fbZ4X1+gN75m2dUvocR6NkowLOZ9F26cjBy8w1qd181OoQ@mail.gmail.com>\n[2]: <CANi7bVAkNc+gY1NoXfJuDRjxjZLTgL8Lfn8_ZmWsvLAoiLPkNg@mail.gmail.com>\n\n---\nPatrick Steinhardt (12):\n      builtin/gc: use designated field initializers for maintenance tasks\n      builtin/gc: drop redundant local variable\n      builtin/maintenance: centralize configuration of explicit tasks\n      builtin/maintenance: mark \"--task=\" and \"--schedule=\" as incompatible\n      builtin/maintenance: stop modifying global array of tasks\n      builtin/maintenance: extract function to run tasks\n      builtin/maintenance: fix typedef for function pointers\n      builtin/maintenance: split into foreground and background tasks\n      builtin/maintenance: fix locking race when packing refs and reflogs\n      usage: allow dying without writing an error message\n      builtin/gc: avoid global state in `gc_before_repack()`\n      builtin/maintenance: fix locking race when handling \"gc\" task\n\n builtin/am.c                |   4 +-\n builtin/checkout.c          |   4 +-\n builtin/fetch.c             |   2 +-\n builtin/gc.c                | 410 +++++++++++++++++++++++++-------------------\n builtin/submodule--helper.c |  12 +-\n t/t7900-maintenance.sh      |  19 +-\n usage.c                     |   2 +\n 7 files changed, 263 insertions(+), 190 deletions(-)\n\nRange-diff versus v2:\n\n 1:  2c90fadd640 =  1:  00fa348fb9c builtin/gc: use designated field initializers for maintenance tasks\n 2:  ebd37a92895 =  2:  d7a2072da28 builtin/gc: drop redundant local variable\n 3:  90863ae9957 =  3:  2eef35388e1 builtin/maintenance: centralize configuration of explicit tasks\n 4:  f0c05004ea3 =  4:  372efcf7fec builtin/maintenance: mark \"--task=\" and \"--schedule=\" as incompatible\n 5:  85c8273d4f4 =  5:  979f27b19ce builtin/maintenance: stop modifying global array of tasks\n 6:  a9d49980f8a =  6:  b89c8d237ed builtin/maintenance: extract function to run tasks\n 7:  35a78300572 =  7:  96554899fd9 builtin/maintenance: fix typedef for function pointers\n 8:  b5fe34348aa !  8:  b79befc82be builtin/maintenance: let tasks do maintenance before and after detach\n    @@ Metadata\n     Author: Patrick Steinhardt <ps@pks.im>\n     \n      ## Commit message ##\n    -    builtin/maintenance: let tasks do maintenance before and after detach\n    +    builtin/maintenance: split into foreground and background tasks\n     \n         Both git-gc(1) and git-maintenance(1) have logic to daemonize so that\n         the maintenance tasks are performed in the background. git-gc(1) has\n    @@ Commit message\n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n      ## builtin/gc.c ##\n    -@@ builtin/gc.c: typedef int (*maintenance_auto_fn)(struct gc_config *cfg);\n    +@@ builtin/gc.c: static int maintenance_task_incremental_repack(struct maintenance_run_opts *opts\n    + \n    + typedef int (*maintenance_task_fn)(struct maintenance_run_opts *opts,\n    + \t\t\t\t   struct gc_config *cfg);\n    +-\n    +-/*\n    +- * An auto condition function returns 1 if the task should run\n    +- * and 0 if the task should NOT run. See needs_to_gc() for an\n    +- * example.\n    +- */\n    + typedef int (*maintenance_auto_fn)(struct gc_config *cfg);\n      \n      struct maintenance_task {\n      \tconst char *name;\n     -\tmaintenance_task_fn fn;\n    -+\tmaintenance_task_fn before_detach;\n    -+\tmaintenance_task_fn after_detach;\n    ++\n    ++\t/*\n    ++\t * Work that will be executed before detaching. This should not include\n    ++\t * tasks that may run for an extended amount of time as it does cause\n    ++\t * auto-maintenance to block until foreground tasks have been run.\n    ++\t */\n    ++\tmaintenance_task_fn foreground;\n    ++\n    ++\t/*\n    ++\t * Work that will be executed after detaching. When not detaching the\n    ++\t * work will be run in the foreground, as well.\n    ++\t */\n    ++\tmaintenance_task_fn background;\n    ++\n    ++\t/*\n    ++\t * An auto condition function returns 1 if the task should run and 0 if\n    ++\t * the task should NOT run. See needs_to_gc() for an example.\n    ++\t */\n      \tmaintenance_auto_fn auto_condition;\n      };\n      \n    @@ builtin/gc.c: typedef int (*maintenance_auto_fn)(struct gc_config *cfg);\n      \t[TASK_PREFETCH] = {\n      \t\t.name = \"prefetch\",\n     -\t\t.fn = maintenance_task_prefetch,\n    -+\t\t.after_detach = maintenance_task_prefetch,\n    ++\t\t.background = maintenance_task_prefetch,\n      \t},\n      \t[TASK_LOOSE_OBJECTS] = {\n      \t\t.name = \"loose-objects\",\n     -\t\t.fn = maintenance_task_loose_objects,\n    -+\t\t.after_detach = maintenance_task_loose_objects,\n    ++\t\t.background = maintenance_task_loose_objects,\n      \t\t.auto_condition = loose_object_auto_condition,\n      \t},\n      \t[TASK_INCREMENTAL_REPACK] = {\n      \t\t.name = \"incremental-repack\",\n     -\t\t.fn = maintenance_task_incremental_repack,\n    -+\t\t.after_detach = maintenance_task_incremental_repack,\n    ++\t\t.background = maintenance_task_incremental_repack,\n      \t\t.auto_condition = incremental_repack_auto_condition,\n      \t},\n      \t[TASK_GC] = {\n      \t\t.name = \"gc\",\n     -\t\t.fn = maintenance_task_gc,\n    -+\t\t.after_detach = maintenance_task_gc,\n    ++\t\t.background = maintenance_task_gc,\n      \t\t.auto_condition = need_to_gc,\n      \t},\n      \t[TASK_COMMIT_GRAPH] = {\n      \t\t.name = \"commit-graph\",\n     -\t\t.fn = maintenance_task_commit_graph,\n    -+\t\t.after_detach = maintenance_task_commit_graph,\n    ++\t\t.background = maintenance_task_commit_graph,\n      \t\t.auto_condition = should_write_commit_graph,\n      \t},\n      \t[TASK_PACK_REFS] = {\n      \t\t.name = \"pack-refs\",\n     -\t\t.fn = maintenance_task_pack_refs,\n    -+\t\t.after_detach = maintenance_task_pack_refs,\n    ++\t\t.background = maintenance_task_pack_refs,\n      \t\t.auto_condition = pack_refs_condition,\n      \t},\n      \t[TASK_REFLOG_EXPIRE] = {\n      \t\t.name = \"reflog-expire\",\n     -\t\t.fn = maintenance_task_reflog_expire,\n    -+\t\t.after_detach = maintenance_task_reflog_expire,\n    ++\t\t.background = maintenance_task_reflog_expire,\n      \t\t.auto_condition = reflog_expire_condition,\n      \t},\n      \t[TASK_WORKTREE_PRUNE] = {\n      \t\t.name = \"worktree-prune\",\n     -\t\t.fn = maintenance_task_worktree_prune,\n    -+\t\t.after_detach = maintenance_task_worktree_prune,\n    ++\t\t.background = maintenance_task_worktree_prune,\n      \t\t.auto_condition = worktree_prune_condition,\n      \t},\n      \t[TASK_RERERE_GC] = {\n      \t\t.name = \"rerere-gc\",\n     -\t\t.fn = maintenance_task_rerere_gc,\n    -+\t\t.after_detach = maintenance_task_rerere_gc,\n    ++\t\t.background = maintenance_task_rerere_gc,\n      \t\t.auto_condition = rerere_gc_condition,\n      \t},\n      };\n      \n     +enum task_phase {\n    -+\tTASK_PHASE_BEFORE_DETACH,\n    -+\tTASK_PHASE_AFTER_DETACH,\n    ++\tTASK_PHASE_FOREGROUND,\n    ++\tTASK_PHASE_BACKGROUND,\n     +};\n     +\n      static int maybe_run_task(const struct maintenance_task *task,\n    @@ builtin/gc.c: typedef int (*maintenance_auto_fn)(struct gc_config *cfg);\n     +\t\t\t  struct gc_config *cfg,\n     +\t\t\t  enum task_phase phase)\n      {\n    -+\tint before = (phase == TASK_PHASE_BEFORE_DETACH);\n    -+\tmaintenance_task_fn fn = before ? task->before_detach : task->after_detach;\n    -+\tconst char *region = before ? \"maintenance before\" : \"maintenance\";\n    ++\tint foreground = (phase == TASK_PHASE_FOREGROUND);\n    ++\tmaintenance_task_fn fn = foreground ? task->foreground : task->background;\n    ++\tconst char *region = foreground ? \"maintenance foreground\" : \"maintenance\";\n      \tint ret = 0;\n      \n     +\tif (!fn)\n    @@ builtin/gc.c: static int maintenance_run_tasks(struct maintenance_run_opts *opts\n      \n     +\tfor (size_t i = 0; i < opts->tasks_nr; i++)\n     +\t\tif (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg,\n    -+\t\t\t\t   TASK_PHASE_BEFORE_DETACH))\n    ++\t\t\t\t   TASK_PHASE_FOREGROUND))\n     +\t\t\tresult = 1;\n     +\n      \t/* Failure to daemonize is ok, we'll continue in foreground. */\n    @@ builtin/gc.c: static int maintenance_run_tasks(struct maintenance_run_opts *opts\n      \tfor (size_t i = 0; i < opts->tasks_nr; i++)\n     -\t\tif (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg))\n     +\t\tif (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg,\n    -+\t\t\t\t   TASK_PHASE_AFTER_DETACH))\n    ++\t\t\t\t   TASK_PHASE_BACKGROUND))\n      \t\t\tresult = 1;\n      \n      \trollback_lock_file(&lk);\n 9:  79766b0afa0 <  -:  ----------- builtin/maintenance: fix locking race when packing refs and reflogs\n -:  ----------- >  9:  8b3f82b871d builtin/maintenance: fix locking race when packing refs and reflogs\n10:  d74a6a3fe7f = 10:  7c61970f4aa usage: allow dying without writing an error message\n11:  f8e77819f6e = 11:  39a1aa9dcb0 builtin/gc: avoid global state in `gc_before_repack()`\n12:  31007c209cb ! 12:  8b263d0fc2b builtin/maintenance: fix locking race when handling \"gc\" task\n    @@ Commit message\n         for the \"pack-refs\" and \"reflog-expire\" tasks in preceding commits. Fix\n         this by splitting up the logic of the \"gc\" task:\n     \n    -      - Before detaching we execute `gc_before_repack()`, which contains the\n    -        logic that git-gc(1) itself would execute before detaching.\n    +      - We execute `gc_before_repack()` in the foreground, which contains\n    +        the logic that git-gc(1) itself would execute in the foreground, as\n    +        well.\n     \n    -      - After detaching we spawn git-gc(1), but with a new hidden flag that\n    +      - We spawn git-gc(1) after detaching, but with a new hidden flag that\n             suppresses calling `gc_before_repack()`.\n     \n         Like this we have roughly the same logic as git-gc(1) itself and know to\n         repack refs and reflogs before detaching, thus fixing the race.\n     \n    -    Note that `gc_before_repack()` is renamed to `gc_before_detach()` to\n    +    Note that `gc_before_repack()` is renamed to `gc_foreground_tasks()` to\n         better reflect what this function does.\n     \n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n    @@ builtin/gc.c: static int report_last_gc_error(void)\n      }\n      \n     -static int gc_before_repack(struct maintenance_run_opts *opts,\n    -+static int gc_before_detach(struct maintenance_run_opts *opts,\n    - \t\t\t    struct gc_config *cfg)\n    +-\t\t\t    struct gc_config *cfg)\n    ++static int gc_foreground_tasks(struct maintenance_run_opts *opts,\n    ++\t\t\t       struct gc_config *cfg)\n      {\n      \tif (cfg->pack_refs && maintenance_task_pack_refs(opts, cfg))\n    + \t\treturn error(FAILED_RUN, \"pack-refs\");\n     @@ builtin/gc.c: int cmd_gc(int argc,\n      \tpid_t pid;\n      \tint daemonized = 0;\n      \tint keep_largest_pack = -1;\n    -+\tint skip_maintenance_before_detach = 0;\n    ++\tint skip_foreground_tasks = 0;\n      \ttimestamp_t dummy;\n      \tstruct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT;\n      \tstruct gc_config cfg = GC_CONFIG_INIT;\n    @@ builtin/gc.c: int cmd_gc(int argc,\n      \t\t\t N_(\"repack all other packs except the largest pack\")),\n      \t\tOPT_STRING(0, \"expire-to\", &cfg.repack_expire_to, N_(\"dir\"),\n      \t\t\t   N_(\"pack prefix to store a pack containing pruned objects\")),\n    -+\t\tOPT_HIDDEN_BOOL(0, \"skip-maintenance-before-detach\", &skip_maintenance_before_detach,\n    -+\t\t\t   N_(\"skip maintenance steps typically done before detaching\")),\n    ++\t\tOPT_HIDDEN_BOOL(0, \"skip-foreground-tasks\", &skip_foreground_tasks,\n    ++\t\t\t   N_(\"skip maintenance tasks typically done in the foreground\")),\n      \t\tOPT_END()\n      \t};\n      \n    @@ builtin/gc.c: int cmd_gc(int argc,\n     -\t\t\tret = 0;\n     -\t\t\tgoto out;\n     -\t\t}\n    -+\t\tif (!skip_maintenance_before_detach) {\n    ++\t\tif (!skip_foreground_tasks) {\n     +\t\t\tif (lock_repo_for_gc(force, &pid)) {\n     +\t\t\t\tret = 0;\n     +\t\t\t\tgoto out;\n    @@ builtin/gc.c: int cmd_gc(int argc,\n     -\t\tif (gc_before_repack(&opts, &cfg) < 0)\n     -\t\t\tdie(NULL);\n     -\t\tdelete_tempfile(&pidfile);\n    -+\t\t\tif (gc_before_detach(&opts, &cfg) < 0)\n    ++\t\t\tif (gc_foreground_tasks(&opts, &cfg) < 0)\n     +\t\t\t\tdie(NULL);\n     +\t\t\tdelete_tempfile(&pidfile);\n     +\t\t}\n    @@ builtin/gc.c: int cmd_gc(int argc,\n      \n     -\tif (opts.detach <= 0)\n     -\t\tgc_before_repack(&opts, &cfg);\n    -+\tif (opts.detach <= 0 && !skip_maintenance_before_detach)\n    -+\t\tgc_before_detach(&opts, &cfg);\n    ++\tif (opts.detach <= 0 && !skip_foreground_tasks)\n    ++\t\tgc_foreground_tasks(&opts, &cfg);\n      \n      \tif (!repository_format_precious_objects) {\n      \t\tstruct child_process repack_cmd = CHILD_PROCESS_INIT;\n    @@ builtin/gc.c: static int maintenance_task_prefetch(struct maintenance_run_opts *\n      \n     -static int maintenance_task_gc(struct maintenance_run_opts *opts,\n     -\t\t\t       struct gc_config *cfg UNUSED)\n    -+static int maintenance_task_gc_before_detach(struct maintenance_run_opts *opts,\n    -+\t\t\t\t\t     struct gc_config *cfg)\n    ++static int maintenance_task_gc_foreground(struct maintenance_run_opts *opts,\n    ++\t\t\t\t\t  struct gc_config *cfg)\n     +{\n    -+\treturn gc_before_detach(opts, cfg);\n    ++\treturn gc_foreground_tasks(opts, cfg);\n     +}\n     +\n    -+static int maintenance_task_gc_after_detach(struct maintenance_run_opts *opts,\n    -+\t\t\t\t\t    struct gc_config *cfg UNUSED)\n    ++static int maintenance_task_gc_background(struct maintenance_run_opts *opts,\n    ++\t\t\t\t\t  struct gc_config *cfg UNUSED)\n      {\n      \tstruct child_process child = CHILD_PROCESS_INIT;\n      \n    @@ builtin/gc.c: static int maintenance_task_gc(struct maintenance_run_opts *opts,\n      \telse\n      \t\tstrvec_push(&child.args, \"--no-quiet\");\n      \tstrvec_push(&child.args, \"--no-detach\");\n    -+\tstrvec_push(&child.args, \"--skip-maintenance-before-detach\");\n    ++\tstrvec_push(&child.args, \"--skip-foreground-tasks\");\n      \n      \treturn run_command(&child);\n      }\n    @@ builtin/gc.c: static const struct maintenance_task tasks[] = {\n      \t},\n      \t[TASK_GC] = {\n      \t\t.name = \"gc\",\n    --\t\t.after_detach = maintenance_task_gc,\n    -+\t\t.before_detach = maintenance_task_gc_before_detach,\n    -+\t\t.after_detach = maintenance_task_gc_after_detach,\n    +-\t\t.background = maintenance_task_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},\n      \t[TASK_COMMIT_GRAPH] = {\n    @@ t/t7900-maintenance.sh: test_expect_success 'run [--auto|--quiet]' '\n     -\ttest_subcommand git gc --quiet --no-detach <run-no-auto.txt &&\n     -\ttest_subcommand ! git gc --auto --quiet --no-detach <run-auto.txt &&\n     -\ttest_subcommand git gc --no-quiet --no-detach <run-no-quiet.txt\n    -+\ttest_subcommand git gc --quiet --no-detach --skip-maintenance-before-detach <run-no-auto.txt &&\n    -+\ttest_subcommand ! git gc --auto --quiet --no-detach --skip-maintenance-before-detach <run-auto.txt &&\n    -+\ttest_subcommand git gc --no-quiet --no-detach --skip-maintenance-before-detach <run-no-quiet.txt\n    ++\ttest_subcommand git gc --quiet --no-detach --skip-foreground-tasks <run-no-auto.txt &&\n    ++\ttest_subcommand ! git gc --auto --quiet --no-detach --skip-foreground-tasks <run-auto.txt &&\n    ++\ttest_subcommand git gc --no-quiet --no-detach --skip-foreground-tasks <run-no-quiet.txt\n      '\n      \n      test_expect_success 'maintenance.auto config option' '\n    @@ t/t7900-maintenance.sh: test_expect_success 'run --task=<task>' '\n     -\ttest_subcommand ! git gc --quiet --no-detach <run-commit-graph.txt &&\n     -\ttest_subcommand git gc --quiet --no-detach <run-gc.txt &&\n     -\ttest_subcommand git gc --quiet --no-detach <run-both.txt &&\n    -+\ttest_subcommand ! git gc --quiet --no-detach --skip-maintenance-before-detach <run-commit-graph.txt &&\n    -+\ttest_subcommand git gc --quiet --no-detach --skip-maintenance-before-detach <run-gc.txt &&\n    -+\ttest_subcommand git gc --quiet --no-detach --skip-maintenance-before-detach <run-both.txt &&\n    ++\ttest_subcommand ! git gc --quiet --no-detach --skip-foreground-tasks <run-commit-graph.txt &&\n    ++\ttest_subcommand git gc --quiet --no-detach --skip-foreground-tasks <run-gc.txt &&\n    ++\ttest_subcommand git gc --quiet --no-detach --skip-foreground-tasks <run-both.txt &&\n      \ttest_subcommand git commit-graph write --split --reachable --no-progress <run-commit-graph.txt &&\n      \ttest_subcommand ! git commit-graph write --split --reachable --no-progress <run-gc.txt &&\n      \ttest_subcommand git commit-graph write --split --reachable --no-progress <run-both.txt\n\n---\nbase-commit: 845c48a16a7f7b2c44d8cb137b16a4a1f0140229\nchange-id: 20250527-b4-pks-maintenance-ref-lock-race-11ae5d68e06f\n\n"},{"id":"519386","messageId":"20250602-b4-pks-maintenance-ref-lock-race-v3-1-587d44252dcb@pks.im","threadId":"63531","inReplyTo":"20250602-b4-pks-maintenance-ref-lock-race-v3-0-587d44252dcb@pks.im","subject":"[PATCH v3 01/12] builtin/gc: use designated field initializers for maintenance tasks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-02T07:17:27Z","receivedAt":"2025-06-02T07:17:35Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Convert the array of maintenance tasks to use designated field\ninitializers. This makes it easier to add more fields to the struct\nwithout having to modify all tasks.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 54 +++++++++++++++++++++++++++---------------------------\n 1 file changed, 27 insertions(+), 27 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e33ba946e43..54fc7f299a9 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1550,49 +1550,49 @@ enum maintenance_task_label {\n \n static struct maintenance_task tasks[] = {\n \t[TASK_PREFETCH] = {\n-\t\t\"prefetch\",\n-\t\tmaintenance_task_prefetch,\n+\t\t.name = \"prefetch\",\n+\t\t.fn = maintenance_task_prefetch,\n \t},\n \t[TASK_LOOSE_OBJECTS] = {\n-\t\t\"loose-objects\",\n-\t\tmaintenance_task_loose_objects,\n-\t\tloose_object_auto_condition,\n+\t\t.name = \"loose-objects\",\n+\t\t.fn = maintenance_task_loose_objects,\n+\t\t.auto_condition = loose_object_auto_condition,\n \t},\n \t[TASK_INCREMENTAL_REPACK] = {\n-\t\t\"incremental-repack\",\n-\t\tmaintenance_task_incremental_repack,\n-\t\tincremental_repack_auto_condition,\n+\t\t.name = \"incremental-repack\",\n+\t\t.fn = maintenance_task_incremental_repack,\n+\t\t.auto_condition = incremental_repack_auto_condition,\n \t},\n \t[TASK_GC] = {\n-\t\t\"gc\",\n-\t\tmaintenance_task_gc,\n-\t\tneed_to_gc,\n-\t\t1,\n+\t\t.name = \"gc\",\n+\t\t.fn = maintenance_task_gc,\n+\t\t.auto_condition = need_to_gc,\n+\t\t.enabled = 1,\n \t},\n \t[TASK_COMMIT_GRAPH] = {\n-\t\t\"commit-graph\",\n-\t\tmaintenance_task_commit_graph,\n-\t\tshould_write_commit_graph,\n+\t\t.name = \"commit-graph\",\n+\t\t.fn = maintenance_task_commit_graph,\n+\t\t.auto_condition = should_write_commit_graph,\n \t},\n \t[TASK_PACK_REFS] = {\n-\t\t\"pack-refs\",\n-\t\tmaintenance_task_pack_refs,\n-\t\tpack_refs_condition,\n+\t\t.name = \"pack-refs\",\n+\t\t.fn = maintenance_task_pack_refs,\n+\t\t.auto_condition = pack_refs_condition,\n \t},\n \t[TASK_REFLOG_EXPIRE] = {\n-\t\t\"reflog-expire\",\n-\t\tmaintenance_task_reflog_expire,\n-\t\treflog_expire_condition,\n+\t\t.name = \"reflog-expire\",\n+\t\t.fn = maintenance_task_reflog_expire,\n+\t\t.auto_condition = reflog_expire_condition,\n \t},\n \t[TASK_WORKTREE_PRUNE] = {\n-\t\t\"worktree-prune\",\n-\t\tmaintenance_task_worktree_prune,\n-\t\tworktree_prune_condition,\n+\t\t.name = \"worktree-prune\",\n+\t\t.fn = maintenance_task_worktree_prune,\n+\t\t.auto_condition = worktree_prune_condition,\n \t},\n \t[TASK_RERERE_GC] = {\n-\t\t\"rerere-gc\",\n-\t\tmaintenance_task_rerere_gc,\n-\t\trerere_gc_condition,\n+\t\t.name = \"rerere-gc\",\n+\t\t.fn = maintenance_task_rerere_gc,\n+\t\t.auto_condition = rerere_gc_condition,\n \t},\n };\n \n\n-- \n2.50.0.rc0.629.g846fc57c9e.dirty\n\n"},{"id":"519387","messageId":"20250602-b4-pks-maintenance-ref-lock-race-v3-2-587d44252dcb@pks.im","threadId":"63531","inReplyTo":"20250602-b4-pks-maintenance-ref-lock-race-v3-0-587d44252dcb@pks.im","subject":"[PATCH v3 02/12] builtin/gc: drop redundant local variable","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-02T07:17:28Z","receivedAt":"2025-06-02T07:17:35Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We have two different variables that track the quietness for git-gc(1):\n\n  - The local variable `quiet`, which we wire up.\n\n  - The `quiet` field of `struct maintenance_run_opts`.\n\nThis leads to confusion which of these variables should be used and what\nthe respective effect is.\n\nSimplify this logic by dropping the local variable in favor of the\noptions field.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 11 +++++------\n 1 file changed, 5 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 54fc7f299a9..7adda8d2d0d 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -818,7 +818,6 @@ int cmd_gc(int argc,\n \t   struct repository *repo UNUSED)\n {\n \tint aggressive = 0;\n-\tint quiet = 0;\n \tint force = 0;\n \tconst char *name;\n \tpid_t pid;\n@@ -831,7 +830,7 @@ int cmd_gc(int argc,\n \tconst char *prune_expire_arg = prune_expire_sentinel;\n \tint ret;\n \tstruct option builtin_gc_options[] = {\n-\t\tOPT__QUIET(&quiet, N_(\"suppress progress reporting\")),\n+\t\tOPT__QUIET(&opts.quiet, N_(\"suppress progress reporting\")),\n \t\t{\n \t\t\t.type = OPTION_STRING,\n \t\t\t.long_name = \"prune\",\n@@ -891,7 +890,7 @@ int cmd_gc(int argc,\n \t\tif (cfg.aggressive_window > 0)\n \t\t\tstrvec_pushf(&repack, \"--window=%d\", cfg.aggressive_window);\n \t}\n-\tif (quiet)\n+\tif (opts.quiet)\n \t\tstrvec_push(&repack, \"-q\");\n \n \tif (opts.auto_flag) {\n@@ -906,7 +905,7 @@ int cmd_gc(int argc,\n \t\t\tgoto out;\n \t\t}\n \n-\t\tif (!quiet) {\n+\t\tif (!opts.quiet) {\n \t\t\tif (opts.detach > 0)\n \t\t\t\tfprintf(stderr, _(\"Auto packing the repository in background for optimum performance.\\n\"));\n \t\t\telse\n@@ -991,7 +990,7 @@ int cmd_gc(int argc,\n \t\t\tstrvec_pushl(&prune_cmd.args, \"prune\", \"--expire\", NULL);\n \t\t\t/* run `git prune` even if using cruft packs */\n \t\t\tstrvec_push(&prune_cmd.args, cfg.prune_expire);\n-\t\t\tif (quiet)\n+\t\t\tif (opts.quiet)\n \t\t\t\tstrvec_push(&prune_cmd.args, \"--no-progress\");\n \t\t\tif (repo_has_promisor_remote(the_repository))\n \t\t\t\tstrvec_push(&prune_cmd.args,\n@@ -1019,7 +1018,7 @@ int cmd_gc(int argc,\n \n \tif (the_repository->settings.gc_write_commit_graph == 1)\n \t\twrite_commit_graph_reachable(the_repository->objects->odb,\n-\t\t\t\t\t     !quiet && !daemonized ? COMMIT_GRAPH_WRITE_PROGRESS : 0,\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\n-- \n2.50.0.rc0.629.g846fc57c9e.dirty\n\n"},{"id":"519388","messageId":"20250602-b4-pks-maintenance-ref-lock-race-v3-3-587d44252dcb@pks.im","threadId":"63531","inReplyTo":"20250602-b4-pks-maintenance-ref-lock-race-v3-0-587d44252dcb@pks.im","subject":"[PATCH v3 03/12] builtin/maintenance: centralize configuration of explicit tasks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-02T07:17:29Z","receivedAt":"2025-06-02T07:17:36Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Users of git-maintenance(1) can explicitly ask it to run specific tasks\nby passing the `--task=` command line option. This option can be passed\nmultiple times, which causes us to execute tasks in the same order as\nthe tasks have been provided by the user.\n\nThe order in which tasks are run is computed in `task_option_parse()`:\nevery time we parse such a command line argument, we modify the global\narray of tasks by seting the selected index for that specific task.\nThis has two downsides:\n\n  - We modify global state, which makes it hard to follow the logic.\n\n  - The configuration of tasks is split across multiple different\n    functions, so it is not easy to figure out the different factors\n    that play a role in selecting tasks.\n\nRefactor the logic so that `task_option_parse()` does not modify global\nstate anymore. Instead, this function now only collects the list of\nconfigured tasks. The logic to configure ordering of the respective\ntasks is then deferred to `initialize_task_config()`.\n\nThis refactoring solves the second problem, that the configuration of\ntasks is spread across multiple different locations. The first problem,\nthat we modify global state, will be fixed in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 47 ++++++++++++++++++++++++-----------------------\n 1 file changed, 24 insertions(+), 23 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 7adda8d2d0d..c4af9b11287 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1690,15 +1690,22 @@ static void initialize_maintenance_strategy(void)\n \t}\n }\n \n-static void initialize_task_config(int schedule)\n+static void initialize_task_config(const struct string_list *selected_tasks,\n+\t\t\t\t   int schedule)\n {\n-\tint i;\n \tstruct strbuf config_name = STRBUF_INIT;\n \n+\tfor (size_t i = 0; i < TASK__COUNT; i++)\n+\t\ttasks[i].selected_order = -1;\n+\tfor (size_t i = 0; i < selected_tasks->nr; i++) {\n+\t\tstruct maintenance_task *task = selected_tasks->items[i].util;\n+\t\ttask->selected_order = i;\n+\t}\n+\n \tif (schedule)\n \t\tinitialize_maintenance_strategy();\n \n-\tfor (i = 0; i < TASK__COUNT; i++) {\n+\tfor (size_t i = 0; i < TASK__COUNT; i++) {\n \t\tint config_value;\n \t\tchar *config_str;\n \n@@ -1722,33 +1729,28 @@ static void initialize_task_config(int schedule)\n \tstrbuf_release(&config_name);\n }\n \n-static int task_option_parse(const struct option *opt UNUSED,\n+static int task_option_parse(const struct option *opt,\n \t\t\t     const char *arg, int unset)\n {\n-\tint i, num_selected = 0;\n-\tstruct maintenance_task *task = NULL;\n+\tstruct string_list *selected_tasks = opt->value;\n+\tsize_t i;\n \n \tBUG_ON_OPT_NEG(unset);\n \n-\tfor (i = 0; i < TASK__COUNT; i++) {\n-\t\tif (tasks[i].selected_order >= 0)\n-\t\t\tnum_selected++;\n-\t\tif (!strcasecmp(tasks[i].name, arg)) {\n-\t\t\ttask = &tasks[i];\n-\t\t}\n-\t}\n-\n-\tif (!task) {\n+\tfor (i = 0; i < TASK__COUNT; i++)\n+\t\tif (!strcasecmp(tasks[i].name, arg))\n+\t\t\tbreak;\n+\tif (i >= TASK__COUNT) {\n \t\terror(_(\"'%s' is not a valid task\"), arg);\n \t\treturn 1;\n \t}\n \n-\tif (task->selected_order >= 0) {\n+\tif (unsorted_string_list_has_string(selected_tasks, arg)) {\n \t\terror(_(\"task '%s' cannot be selected multiple times\"), arg);\n \t\treturn 1;\n \t}\n \n-\ttask->selected_order = num_selected + 1;\n+\tstring_list_append(selected_tasks, arg)->util = &tasks[i];\n \n \treturn 0;\n }\n@@ -1756,8 +1758,8 @@ static int task_option_parse(const struct option *opt UNUSED,\n static int maintenance_run(int argc, const char **argv, const char *prefix,\n \t\t\t   struct repository *repo UNUSED)\n {\n-\tint i;\n \tstruct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT;\n+\tstruct string_list selected_tasks = STRING_LIST_INIT_DUP;\n \tstruct gc_config cfg = GC_CONFIG_INIT;\n \tstruct option builtin_maintenance_run_options[] = {\n \t\tOPT_BOOL(0, \"auto\", &opts.auto_flag,\n@@ -1769,7 +1771,7 @@ static int maintenance_run(int argc, const char **argv, const char *prefix,\n \t\t\t     maintenance_opt_schedule),\n \t\tOPT_BOOL(0, \"quiet\", &opts.quiet,\n \t\t\t N_(\"do not report progress or other information over stderr\")),\n-\t\tOPT_CALLBACK_F(0, \"task\", NULL, N_(\"task\"),\n+\t\tOPT_CALLBACK_F(0, \"task\", &selected_tasks, N_(\"task\"),\n \t\t\tN_(\"run a specific task\"),\n \t\t\tPARSE_OPT_NONEG, task_option_parse),\n \t\tOPT_END()\n@@ -1778,9 +1780,6 @@ static int maintenance_run(int argc, const char **argv, const char *prefix,\n \n \topts.quiet = !isatty(2);\n \n-\tfor (i = 0; i < TASK__COUNT; i++)\n-\t\ttasks[i].selected_order = -1;\n-\n \targc = parse_options(argc, argv, prefix,\n \t\t\t     builtin_maintenance_run_options,\n \t\t\t     builtin_maintenance_run_usage,\n@@ -1790,13 +1789,15 @@ static int maintenance_run(int argc, const char **argv, const char *prefix,\n \t\tdie(_(\"use at most one of --auto and --schedule=<frequency>\"));\n \n \tgc_config(&cfg);\n-\tinitialize_task_config(opts.schedule);\n+\tinitialize_task_config(&selected_tasks, opts.schedule);\n \n \tif (argc != 0)\n \t\tusage_with_options(builtin_maintenance_run_usage,\n \t\t\t\t   builtin_maintenance_run_options);\n \n \tret = maintenance_run_tasks(&opts, &cfg);\n+\n+\tstring_list_clear(&selected_tasks, 0);\n \tgc_config_release(&cfg);\n \treturn ret;\n }\n\n-- \n2.50.0.rc0.629.g846fc57c9e.dirty\n\n"},{"id":"519389","messageId":"20250602-b4-pks-maintenance-ref-lock-race-v3-4-587d44252dcb@pks.im","threadId":"63531","inReplyTo":"20250602-b4-pks-maintenance-ref-lock-race-v3-0-587d44252dcb@pks.im","subject":"[PATCH v3 04/12] builtin/maintenance: mark \"--task=\" and \"--schedule=\" as incompatible","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-02T07:17:30Z","receivedAt":"2025-06-02T07:17:37Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"--task=\" option explicitly allows the user to say which maintenance\ntasks should be run, whereas \"--schedule=\" only respects the maintenance\nstrategy configured for a specific repository. As such, it is not\nsensible to accept both options at the same time.\n\nMark them as incompatible with one another. While at it, also convert\nthe existing logic that marks \"--auto\" and \"--schedule=\" as incompatible\nto use `die_for_incompatible_opt2()`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c           | 6 ++++--\n t/t7900-maintenance.sh | 7 ++++++-\n 2 files changed, 10 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex c4af9b11287..57d7602596a 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1785,8 +1785,10 @@ static int maintenance_run(int argc, const char **argv, const char *prefix,\n \t\t\t     builtin_maintenance_run_usage,\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n \n-\tif (opts.auto_flag && opts.schedule)\n-\t\tdie(_(\"use at most one of --auto and --schedule=<frequency>\"));\n+\tdie_for_incompatible_opt2(opts.auto_flag, \"--auto\",\n+\t\t\t\t  opts.schedule, \"--schedule=\");\n+\tdie_for_incompatible_opt2(selected_tasks.nr, \"--task=\",\n+\t\t\t\t  opts.schedule, \"--schedule=\");\n \n \tgc_config(&cfg);\n \tinitialize_task_config(&selected_tasks, opts.schedule);\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 8cf89e285f4..1ada5246606 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -610,7 +610,12 @@ test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.aut\n \n test_expect_success '--auto and --schedule incompatible' '\n \ttest_must_fail git maintenance run --auto --schedule=daily 2>err &&\n-\ttest_grep \"at most one\" err\n+\ttest_grep \"cannot be used together\" err\n+'\n+\n+test_expect_success '--task and --schedule incompatible' '\n+\ttest_must_fail git maintenance run --task=pack-refs --schedule=daily 2>err &&\n+\ttest_grep \"cannot be used together\" err\n '\n \n test_expect_success 'invalid --schedule value' '\n\n-- \n2.50.0.rc0.629.g846fc57c9e.dirty\n\n"},{"id":"519390","messageId":"20250602-b4-pks-maintenance-ref-lock-race-v3-5-587d44252dcb@pks.im","threadId":"63531","inReplyTo":"20250602-b4-pks-maintenance-ref-lock-race-v3-0-587d44252dcb@pks.im","subject":"[PATCH v3 05/12] builtin/maintenance: stop modifying global array of tasks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-02T07:17:31Z","receivedAt":"2025-06-02T07:17:38Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When configuring maintenance tasks run by git-maintenance(1) we do so by\nmodifying the global array of tasks directly. This is already quite bad\non its own, as global state makes for logic that is hard to follow.\n\nEven more importantly though we use multiple different fields to track\nwhether or not a task should be run:\n\n  - \"enabled\" tracks the \"maintenance.*.enabled\" config key. This field\n    disables execution of a task, unless the user has explicitly asked\n    for the task.\n\n  - \"selected_order\" tracks the order in which jobs have been asked for\n    by the user via the \"--task=\" command line option. It overrides\n    everything else, but only has an effect if at least one job has been\n    selected.\n\n  - \"schedule\" tracks the schedule priority for a job, that is how often\n    it should run. This field only plays a role when the user has passed\n    the \"--schedule=\" command line option.\n\nAll of this makes it non-trivial to figure out which job really should\nbe running right now. The logic to configure these fields and the logic\nthat interprets them is distributed across multiple functions, making it\neven harder to follow it.\n\nRefactor the logic so that we stop modifying global state. Instead, we\nnow compute which jobs should be run in `initialize_task_config()`,\nrepresented as an array of jobs to run that is stored in the options\nstructure. Like this, all logic becomes self-contained and any users of\nthis array only need to iterate through the tasks and execute them one\nby one.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 206 ++++++++++++++++++++++++++++++++---------------------------\n 1 file changed, 112 insertions(+), 94 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 57d7602596a..4d636237cac 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -251,7 +251,24 @@ static enum schedule_priority parse_schedule(const char *value)\n \treturn SCHEDULE_NONE;\n }\n \n+enum maintenance_task_label {\n+\tTASK_PREFETCH,\n+\tTASK_LOOSE_OBJECTS,\n+\tTASK_INCREMENTAL_REPACK,\n+\tTASK_GC,\n+\tTASK_COMMIT_GRAPH,\n+\tTASK_PACK_REFS,\n+\tTASK_REFLOG_EXPIRE,\n+\tTASK_WORKTREE_PRUNE,\n+\tTASK_RERERE_GC,\n+\n+\t/* Leave as final value */\n+\tTASK__COUNT\n+};\n+\n struct maintenance_run_opts {\n+\tenum maintenance_task_label *tasks;\n+\tsize_t tasks_nr, tasks_alloc;\n \tint auto_flag;\n \tint detach;\n \tint quiet;\n@@ -261,6 +278,11 @@ struct maintenance_run_opts {\n \t.detach = -1, \\\n }\n \n+static void maintenance_run_opts_release(struct maintenance_run_opts *opts)\n+{\n+\tfree(opts->tasks);\n+}\n+\n static int pack_refs_condition(UNUSED struct gc_config *cfg)\n {\n \t/*\n@@ -1032,6 +1054,7 @@ int cmd_gc(int argc,\n \t}\n \n out:\n+\tmaintenance_run_opts_release(&opts);\n \tgc_config_release(&cfg);\n \treturn 0;\n }\n@@ -1524,30 +1547,9 @@ struct maintenance_task {\n \tconst char *name;\n \tmaintenance_task_fn *fn;\n \tmaintenance_auto_fn *auto_condition;\n-\tunsigned enabled:1;\n-\n-\tenum schedule_priority schedule;\n-\n-\t/* -1 if not selected. */\n-\tint selected_order;\n-};\n-\n-enum maintenance_task_label {\n-\tTASK_PREFETCH,\n-\tTASK_LOOSE_OBJECTS,\n-\tTASK_INCREMENTAL_REPACK,\n-\tTASK_GC,\n-\tTASK_COMMIT_GRAPH,\n-\tTASK_PACK_REFS,\n-\tTASK_REFLOG_EXPIRE,\n-\tTASK_WORKTREE_PRUNE,\n-\tTASK_RERERE_GC,\n-\n-\t/* Leave as final value */\n-\tTASK__COUNT\n };\n \n-static struct maintenance_task tasks[] = {\n+static const struct maintenance_task tasks[] = {\n \t[TASK_PREFETCH] = {\n \t\t.name = \"prefetch\",\n \t\t.fn = maintenance_task_prefetch,\n@@ -1566,7 +1568,6 @@ static struct maintenance_task tasks[] = {\n \t\t.name = \"gc\",\n \t\t.fn = maintenance_task_gc,\n \t\t.auto_condition = need_to_gc,\n-\t\t.enabled = 1,\n \t},\n \t[TASK_COMMIT_GRAPH] = {\n \t\t.name = \"commit-graph\",\n@@ -1595,18 +1596,9 @@ static struct maintenance_task tasks[] = {\n \t},\n };\n \n-static int compare_tasks_by_selection(const void *a_, const void *b_)\n-{\n-\tconst struct maintenance_task *a = a_;\n-\tconst struct maintenance_task *b = b_;\n-\n-\treturn b->selected_order - a->selected_order;\n-}\n-\n static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \t\t\t\t struct gc_config *cfg)\n {\n-\tint i, found_selected = 0;\n \tint result = 0;\n \tstruct lock_file lk;\n \tstruct repository *r = the_repository;\n@@ -1635,95 +1627,120 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \t\ttrace2_region_leave(\"maintenance\", \"detach\", the_repository);\n \t}\n \n-\tfor (i = 0; !found_selected && i < TASK__COUNT; i++)\n-\t\tfound_selected = tasks[i].selected_order >= 0;\n-\n-\tif (found_selected)\n-\t\tQSORT(tasks, TASK__COUNT, compare_tasks_by_selection);\n-\n-\tfor (i = 0; i < TASK__COUNT; i++) {\n-\t\tif (found_selected && tasks[i].selected_order < 0)\n-\t\t\tcontinue;\n-\n-\t\tif (!found_selected && !tasks[i].enabled)\n-\t\t\tcontinue;\n-\n+\tfor (size_t i = 0; i < opts->tasks_nr; i++) {\n \t\tif (opts->auto_flag &&\n-\t\t    (!tasks[i].auto_condition ||\n-\t\t     !tasks[i].auto_condition(cfg)))\n-\t\t\tcontinue;\n-\n-\t\tif (opts->schedule && tasks[i].schedule < opts->schedule)\n+\t\t    (!tasks[opts->tasks[i]].auto_condition ||\n+\t\t     !tasks[opts->tasks[i]].auto_condition(cfg)))\n \t\t\tcontinue;\n \n-\t\ttrace2_region_enter(\"maintenance\", tasks[i].name, r);\n-\t\tif (tasks[i].fn(opts, cfg)) {\n-\t\t\terror(_(\"task '%s' failed\"), tasks[i].name);\n+\t\ttrace2_region_enter(\"maintenance\", tasks[opts->tasks[i]].name, r);\n+\t\tif (tasks[opts->tasks[i]].fn(opts, cfg)) {\n+\t\t\terror(_(\"task '%s' failed\"), tasks[opts->tasks[i]].name);\n \t\t\tresult = 1;\n \t\t}\n-\t\ttrace2_region_leave(\"maintenance\", tasks[i].name, r);\n+\t\ttrace2_region_leave(\"maintenance\", tasks[opts->tasks[i]].name, r);\n \t}\n \n \trollback_lock_file(&lk);\n \treturn result;\n }\n \n-static void initialize_maintenance_strategy(void)\n+struct maintenance_strategy {\n+\tstruct {\n+\t\tint enabled;\n+\t\tenum schedule_priority schedule;\n+\t} tasks[TASK__COUNT];\n+};\n+\n+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},\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},\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+\tstruct strbuf config_name = STRBUF_INIT;\n+\tstruct maintenance_strategy strategy;\n \tconst char *config_str;\n \n-\tif (git_config_get_string_tmp(\"maintenance.strategy\", &config_str))\n-\t\treturn;\n+\t/*\n+\t * In case the user has asked us to run tasks explicitly we only use\n+\t * those specified tasks. Specifically, we do _not_ want to consult the\n+\t * config or maintenance strategy.\n+\t */\n+\tif (selected_tasks->nr) {\n+\t\tfor (size_t i = 0; i < selected_tasks->nr; i++) {\n+\t\t\tenum maintenance_task_label label = (intptr_t)selected_tasks->items[i].util;;\n+\t\t\tALLOC_GROW(opts->tasks, opts->tasks_nr + 1, opts->tasks_alloc);\n+\t\t\topts->tasks[opts->tasks_nr++] = label;\n+\t\t}\n \n-\tif (!strcasecmp(config_str, \"incremental\")) {\n-\t\ttasks[TASK_GC].schedule = SCHEDULE_NONE;\n-\t\ttasks[TASK_COMMIT_GRAPH].enabled = 1;\n-\t\ttasks[TASK_COMMIT_GRAPH].schedule = SCHEDULE_HOURLY;\n-\t\ttasks[TASK_PREFETCH].enabled = 1;\n-\t\ttasks[TASK_PREFETCH].schedule = SCHEDULE_HOURLY;\n-\t\ttasks[TASK_INCREMENTAL_REPACK].enabled = 1;\n-\t\ttasks[TASK_INCREMENTAL_REPACK].schedule = SCHEDULE_DAILY;\n-\t\ttasks[TASK_LOOSE_OBJECTS].enabled = 1;\n-\t\ttasks[TASK_LOOSE_OBJECTS].schedule = SCHEDULE_DAILY;\n-\t\ttasks[TASK_PACK_REFS].enabled = 1;\n-\t\ttasks[TASK_PACK_REFS].schedule = SCHEDULE_WEEKLY;\n+\t\treturn;\n \t}\n-}\n \n-static void initialize_task_config(const struct string_list *selected_tasks,\n-\t\t\t\t   int schedule)\n-{\n-\tstruct strbuf config_name = STRBUF_INIT;\n+\t/*\n+\t * Otherwise, the strategy depends on whether we run as part of a\n+\t * scheduled job or not:\n+\t *\n+\t *   - Scheduled maintenance does not perform any housekeeping by\n+\t *     default, but requires the user to pick a maintenance strategy.\n+\t *\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 */\n+\tif (opts->schedule) {\n+\t\tstrategy = none_strategy;\n \n-\tfor (size_t i = 0; i < TASK__COUNT; i++)\n-\t\ttasks[i].selected_order = -1;\n-\tfor (size_t i = 0; i < selected_tasks->nr; i++) {\n-\t\tstruct maintenance_task *task = selected_tasks->items[i].util;\n-\t\ttask->selected_order = i;\n+\t\tif (!git_config_get_string_tmp(\"maintenance.strategy\", &config_str)) {\n+\t\t\tif (!strcasecmp(config_str, \"incremental\"))\n+\t\t\t\tstrategy = incremental_strategy;\n+\t\t}\n+\t} else {\n+\t\tstrategy = default_strategy;\n \t}\n \n-\tif (schedule)\n-\t\tinitialize_maintenance_strategy();\n-\n \tfor (size_t i = 0; i < TASK__COUNT; i++) {\n \t\tint config_value;\n-\t\tchar *config_str;\n \n \t\tstrbuf_reset(&config_name);\n \t\tstrbuf_addf(&config_name, \"maintenance.%s.enabled\",\n \t\t\t    tasks[i].name);\n-\n \t\tif (!git_config_get_bool(config_name.buf, &config_value))\n-\t\t\ttasks[i].enabled = config_value;\n-\n-\t\tstrbuf_reset(&config_name);\n-\t\tstrbuf_addf(&config_name, \"maintenance.%s.schedule\",\n-\t\t\t    tasks[i].name);\n+\t\t\tstrategy.tasks[i].enabled = config_value;\n+\t\tif (!strategy.tasks[i].enabled)\n+\t\t\tcontinue;\n \n-\t\tif (!git_config_get_string(config_name.buf, &config_str)) {\n-\t\t\ttasks[i].schedule = parse_schedule(config_str);\n-\t\t\tfree(config_str);\n+\t\tif (opts->schedule) {\n+\t\t\tstrbuf_reset(&config_name);\n+\t\t\tstrbuf_addf(&config_name, \"maintenance.%s.schedule\",\n+\t\t\t\t    tasks[i].name);\n+\t\t\tif (!git_config_get_string_tmp(config_name.buf, &config_str))\n+\t\t\t\tstrategy.tasks[i].schedule = parse_schedule(config_str);\n+\t\t\tif (strategy.tasks[i].schedule < opts->schedule)\n+\t\t\t\tcontinue;\n \t\t}\n+\n+\t\tALLOC_GROW(opts->tasks, opts->tasks_nr + 1, opts->tasks_alloc);\n+\t\topts->tasks[opts->tasks_nr++] = i;\n \t}\n \n \tstrbuf_release(&config_name);\n@@ -1750,7 +1767,7 @@ static int task_option_parse(const struct option *opt,\n \t\treturn 1;\n \t}\n \n-\tstring_list_append(selected_tasks, arg)->util = &tasks[i];\n+\tstring_list_append(selected_tasks, arg)->util = (void *)(intptr_t)i;\n \n \treturn 0;\n }\n@@ -1791,7 +1808,7 @@ static int maintenance_run(int argc, const char **argv, const char *prefix,\n \t\t\t\t  opts.schedule, \"--schedule=\");\n \n \tgc_config(&cfg);\n-\tinitialize_task_config(&selected_tasks, opts.schedule);\n+\tinitialize_task_config(&opts, &selected_tasks);\n \n \tif (argc != 0)\n \t\tusage_with_options(builtin_maintenance_run_usage,\n@@ -1800,6 +1817,7 @@ static int maintenance_run(int argc, const char **argv, const char *prefix,\n \tret = maintenance_run_tasks(&opts, &cfg);\n \n \tstring_list_clear(&selected_tasks, 0);\n+\tmaintenance_run_opts_release(&opts);\n \tgc_config_release(&cfg);\n \treturn ret;\n }\n\n-- \n2.50.0.rc0.629.g846fc57c9e.dirty\n\n"},{"id":"519391","messageId":"20250602-b4-pks-maintenance-ref-lock-race-v3-6-587d44252dcb@pks.im","threadId":"63531","inReplyTo":"20250602-b4-pks-maintenance-ref-lock-race-v3-0-587d44252dcb@pks.im","subject":"[PATCH v3 06/12] builtin/maintenance: extract function to run tasks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-02T07:17:32Z","receivedAt":"2025-06-02T07:17:39Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Extract the function to run maintenance tasks. This function will be\nreused in a subsequent commit where we introduce a split between\nmaintenance tasks that run before and after daemonizing the process.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 35 +++++++++++++++++++++++------------\n 1 file changed, 23 insertions(+), 12 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 4d636237cac..cfbf9d8a2b9 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1596,6 +1596,27 @@ static const struct maintenance_task tasks[] = {\n \t},\n };\n \n+static int maybe_run_task(const struct maintenance_task *task,\n+\t\t\t  struct repository *repo,\n+\t\t\t  struct maintenance_run_opts *opts,\n+\t\t\t  struct gc_config *cfg)\n+{\n+\tint ret = 0;\n+\n+\tif (opts->auto_flag &&\n+\t    (!task->auto_condition || !task->auto_condition(cfg)))\n+\t\treturn 0;\n+\n+\ttrace2_region_enter(\"maintenance\", task->name, repo);\n+\tif (task->fn(opts, cfg)) {\n+\t\terror(_(\"task '%s' failed\"), task->name);\n+\t\tret = 1;\n+\t}\n+\ttrace2_region_leave(\"maintenance\", task->name, repo);\n+\n+\treturn ret;\n+}\n+\n static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \t\t\t\t struct gc_config *cfg)\n {\n@@ -1627,19 +1648,9 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \t\ttrace2_region_leave(\"maintenance\", \"detach\", the_repository);\n \t}\n \n-\tfor (size_t i = 0; i < opts->tasks_nr; i++) {\n-\t\tif (opts->auto_flag &&\n-\t\t    (!tasks[opts->tasks[i]].auto_condition ||\n-\t\t     !tasks[opts->tasks[i]].auto_condition(cfg)))\n-\t\t\tcontinue;\n-\n-\t\ttrace2_region_enter(\"maintenance\", tasks[opts->tasks[i]].name, r);\n-\t\tif (tasks[opts->tasks[i]].fn(opts, cfg)) {\n-\t\t\terror(_(\"task '%s' failed\"), tasks[opts->tasks[i]].name);\n+\tfor (size_t i = 0; i < opts->tasks_nr; i++)\n+\t\tif (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg))\n \t\t\tresult = 1;\n-\t\t}\n-\t\ttrace2_region_leave(\"maintenance\", tasks[opts->tasks[i]].name, r);\n-\t}\n \n \trollback_lock_file(&lk);\n \treturn result;\n\n-- \n2.50.0.rc0.629.g846fc57c9e.dirty\n\n"},{"id":"519392","messageId":"20250602-b4-pks-maintenance-ref-lock-race-v3-7-587d44252dcb@pks.im","threadId":"63531","inReplyTo":"20250602-b4-pks-maintenance-ref-lock-race-v3-0-587d44252dcb@pks.im","subject":"[PATCH v3 07/12] builtin/maintenance: fix typedef for function pointers","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-02T07:17:33Z","receivedAt":"2025-06-02T07:17:40Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The typedefs for `maintenance_task_fn` and `maintenance_auto_fn` are\nsomewhat confusingly not true function pointers. As such, any user of\nthose typedefs needs to manually add the pointer to make use of them.\n\nFix this by making these true function pointers.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 10 +++++-----\n 1 file changed, 5 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex cfbf9d8a2b9..447e5800846 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1533,20 +1533,20 @@ static int maintenance_task_incremental_repack(struct maintenance_run_opts *opts\n \treturn 0;\n }\n \n-typedef int maintenance_task_fn(struct maintenance_run_opts *opts,\n-\t\t\t\tstruct gc_config *cfg);\n+typedef int (*maintenance_task_fn)(struct maintenance_run_opts *opts,\n+\t\t\t\t   struct gc_config *cfg);\n \n /*\n  * An auto condition function returns 1 if the task should run\n  * and 0 if the task should NOT run. See needs_to_gc() for an\n  * example.\n  */\n-typedef int maintenance_auto_fn(struct gc_config *cfg);\n+typedef int (*maintenance_auto_fn)(struct gc_config *cfg);\n \n struct maintenance_task {\n \tconst char *name;\n-\tmaintenance_task_fn *fn;\n-\tmaintenance_auto_fn *auto_condition;\n+\tmaintenance_task_fn fn;\n+\tmaintenance_auto_fn auto_condition;\n };\n \n static const struct maintenance_task tasks[] = {\n\n-- \n2.50.0.rc0.629.g846fc57c9e.dirty\n\n"},{"id":"519393","messageId":"20250602-b4-pks-maintenance-ref-lock-race-v3-8-587d44252dcb@pks.im","threadId":"63531","inReplyTo":"20250602-b4-pks-maintenance-ref-lock-race-v3-0-587d44252dcb@pks.im","subject":"[PATCH v3 08/12] builtin/maintenance: split into foreground and background tasks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-02T07:17:34Z","receivedAt":"2025-06-02T07:17:40Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Both git-gc(1) and git-maintenance(1) have logic to daemonize so that\nthe maintenance tasks are performed in the background. git-gc(1) has\nsome special logic though to not perform _all_ housekeeping tasks in the\nbackground: both references and reflogs are still handled synchronously\nin the foreground.\n\nThis split exists because otherwise it may easily happen that git-gc(1)\nkeeps the \"packed-refs\" file locked for an extended amount of time,\nwhere the next Git command that wants to modify any reference could now\nfail. This was especially important in the past, where git-gc(1) was\nstill executed directly as part of our automatic maintenance: git-gc(1)\nwas invoked via `git gc --auto --detach`, so we knew to handle most of\nthe maintenance tasks in the background while doing those parts that may\ncause locking issues in the foreground.\n\nWe have since moved to git-maintenance(1), which is a more flexible\nreplacement for git-gc(1). By default this command runs git-gc(1), only,\nbut it can be configured to run different tasks, as well. This command\ndoes not know about the split between maintenance tasks that should run\nbefore and after detach though, and this has led to several bug reports\nabout spurious locking errors for the \"packed-refs\" file.\n\nPrepare for a fix by introducing this split for maintenance tasks. Note\nthat this commit does not yet change any of the tasks, so there should\nnot (yet) be a change in behaviour.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 70 ++++++++++++++++++++++++++++++++++++++++++------------------\n 1 file changed, 49 insertions(+), 21 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 447e5800846..72a695853e5 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1535,84 +1535,106 @@ static int maintenance_task_incremental_repack(struct maintenance_run_opts *opts\n \n typedef int (*maintenance_task_fn)(struct maintenance_run_opts *opts,\n \t\t\t\t   struct gc_config *cfg);\n-\n-/*\n- * An auto condition function returns 1 if the task should run\n- * and 0 if the task should NOT run. See needs_to_gc() for an\n- * example.\n- */\n typedef int (*maintenance_auto_fn)(struct gc_config *cfg);\n \n struct maintenance_task {\n \tconst char *name;\n-\tmaintenance_task_fn fn;\n+\n+\t/*\n+\t * Work that will be executed before detaching. This should not include\n+\t * tasks that may run for an extended amount of time as it does cause\n+\t * auto-maintenance to block until foreground tasks have been run.\n+\t */\n+\tmaintenance_task_fn foreground;\n+\n+\t/*\n+\t * Work that will be executed after detaching. When not detaching the\n+\t * work will be run in the foreground, as well.\n+\t */\n+\tmaintenance_task_fn background;\n+\n+\t/*\n+\t * An auto condition function returns 1 if the task should run and 0 if\n+\t * the task should NOT run. See needs_to_gc() for an example.\n+\t */\n \tmaintenance_auto_fn auto_condition;\n };\n \n static const struct maintenance_task tasks[] = {\n \t[TASK_PREFETCH] = {\n \t\t.name = \"prefetch\",\n-\t\t.fn = maintenance_task_prefetch,\n+\t\t.background = maintenance_task_prefetch,\n \t},\n \t[TASK_LOOSE_OBJECTS] = {\n \t\t.name = \"loose-objects\",\n-\t\t.fn = maintenance_task_loose_objects,\n+\t\t.background = maintenance_task_loose_objects,\n \t\t.auto_condition = loose_object_auto_condition,\n \t},\n \t[TASK_INCREMENTAL_REPACK] = {\n \t\t.name = \"incremental-repack\",\n-\t\t.fn = maintenance_task_incremental_repack,\n+\t\t.background = maintenance_task_incremental_repack,\n \t\t.auto_condition = incremental_repack_auto_condition,\n \t},\n \t[TASK_GC] = {\n \t\t.name = \"gc\",\n-\t\t.fn = maintenance_task_gc,\n+\t\t.background = maintenance_task_gc,\n \t\t.auto_condition = need_to_gc,\n \t},\n \t[TASK_COMMIT_GRAPH] = {\n \t\t.name = \"commit-graph\",\n-\t\t.fn = maintenance_task_commit_graph,\n+\t\t.background = maintenance_task_commit_graph,\n \t\t.auto_condition = should_write_commit_graph,\n \t},\n \t[TASK_PACK_REFS] = {\n \t\t.name = \"pack-refs\",\n-\t\t.fn = maintenance_task_pack_refs,\n+\t\t.background = maintenance_task_pack_refs,\n \t\t.auto_condition = pack_refs_condition,\n \t},\n \t[TASK_REFLOG_EXPIRE] = {\n \t\t.name = \"reflog-expire\",\n-\t\t.fn = maintenance_task_reflog_expire,\n+\t\t.background = maintenance_task_reflog_expire,\n \t\t.auto_condition = reflog_expire_condition,\n \t},\n \t[TASK_WORKTREE_PRUNE] = {\n \t\t.name = \"worktree-prune\",\n-\t\t.fn = maintenance_task_worktree_prune,\n+\t\t.background = maintenance_task_worktree_prune,\n \t\t.auto_condition = worktree_prune_condition,\n \t},\n \t[TASK_RERERE_GC] = {\n \t\t.name = \"rerere-gc\",\n-\t\t.fn = maintenance_task_rerere_gc,\n+\t\t.background = maintenance_task_rerere_gc,\n \t\t.auto_condition = rerere_gc_condition,\n \t},\n };\n \n+enum task_phase {\n+\tTASK_PHASE_FOREGROUND,\n+\tTASK_PHASE_BACKGROUND,\n+};\n+\n static int maybe_run_task(const struct maintenance_task *task,\n \t\t\t  struct repository *repo,\n \t\t\t  struct maintenance_run_opts *opts,\n-\t\t\t  struct gc_config *cfg)\n+\t\t\t  struct gc_config *cfg,\n+\t\t\t  enum task_phase phase)\n {\n+\tint foreground = (phase == TASK_PHASE_FOREGROUND);\n+\tmaintenance_task_fn fn = foreground ? task->foreground : task->background;\n+\tconst char *region = foreground ? \"maintenance foreground\" : \"maintenance\";\n \tint ret = 0;\n \n+\tif (!fn)\n+\t\treturn 0;\n \tif (opts->auto_flag &&\n \t    (!task->auto_condition || !task->auto_condition(cfg)))\n \t\treturn 0;\n \n-\ttrace2_region_enter(\"maintenance\", task->name, repo);\n-\tif (task->fn(opts, cfg)) {\n+\ttrace2_region_enter(region, task->name, repo);\n+\tif (fn(opts, cfg)) {\n \t\terror(_(\"task '%s' failed\"), task->name);\n \t\tret = 1;\n \t}\n-\ttrace2_region_leave(\"maintenance\", task->name, repo);\n+\ttrace2_region_leave(region, task->name, repo);\n \n \treturn ret;\n }\n@@ -1641,6 +1663,11 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \t}\n \tfree(lock_path);\n \n+\tfor (size_t i = 0; i < opts->tasks_nr; i++)\n+\t\tif (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg,\n+\t\t\t\t   TASK_PHASE_FOREGROUND))\n+\t\t\tresult = 1;\n+\n \t/* Failure to daemonize is ok, we'll continue in foreground. */\n \tif (opts->detach > 0) {\n \t\ttrace2_region_enter(\"maintenance\", \"detach\", the_repository);\n@@ -1649,7 +1676,8 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \t}\n \n \tfor (size_t i = 0; i < opts->tasks_nr; i++)\n-\t\tif (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg))\n+\t\tif (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg,\n+\t\t\t\t   TASK_PHASE_BACKGROUND))\n \t\t\tresult = 1;\n \n \trollback_lock_file(&lk);\n\n-- \n2.50.0.rc0.629.g846fc57c9e.dirty\n\n"},{"id":"519395","messageId":"20250602-b4-pks-maintenance-ref-lock-race-v3-9-587d44252dcb@pks.im","threadId":"63531","inReplyTo":"20250602-b4-pks-maintenance-ref-lock-race-v3-0-587d44252dcb@pks.im","subject":"[PATCH v3 09/12] builtin/maintenance: fix locking race when packing refs and reflogs","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-02T07:17:35Z","receivedAt":"2025-06-02T07:17:41Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"As explained in the preceding commit, git-gc(1) knows to detach only\nafter it has already packed references and reflogs. This is done to\navoid racing around their respective lockfiles.\n\nAdapt git-maintenance(1) accordingly and run the \"pack-refs\" and\n\"reflog-expire\" tasks in the foreground. Note that the \"gc\" task has the\nsame issue, but the fix is a bit more involved there and will thus be\ndone in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 72a695853e5..fdd0dd09be7 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1587,12 +1587,12 @@ static const struct maintenance_task tasks[] = {\n \t},\n \t[TASK_PACK_REFS] = {\n \t\t.name = \"pack-refs\",\n-\t\t.background = maintenance_task_pack_refs,\n+\t\t.foreground = maintenance_task_pack_refs,\n \t\t.auto_condition = pack_refs_condition,\n \t},\n \t[TASK_REFLOG_EXPIRE] = {\n \t\t.name = \"reflog-expire\",\n-\t\t.background = maintenance_task_reflog_expire,\n+\t\t.foreground = maintenance_task_reflog_expire,\n \t\t.auto_condition = reflog_expire_condition,\n \t},\n \t[TASK_WORKTREE_PRUNE] = {\n\n-- \n2.50.0.rc0.629.g846fc57c9e.dirty\n\n"},{"id":"519394","messageId":"20250602-b4-pks-maintenance-ref-lock-race-v3-10-587d44252dcb@pks.im","threadId":"63531","inReplyTo":"20250602-b4-pks-maintenance-ref-lock-race-v3-0-587d44252dcb@pks.im","subject":"[PATCH v3 10/12] usage: allow dying without writing an error message","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-02T07:17:36Z","receivedAt":"2025-06-02T07:17:42Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Sometimes code wants to die in a situation where it already has written\nan error message. To use the same error code as `die()` we have to open\ncode the code with a call to `exit(128)` in such cases, which is easy to\nget wrong and leaves magical numbers all over our codebase.\n\nTeach `die_message_builtin()` to not print any error when passed a\n`NULL` pointer as error string. Like this, such users can now call\n`die(NULL)` to achieve the same result without any hardcoded error\ncodes.\n\nAdapt a couple of builtins to use this new pattern to demonstrate that\nthere is a need for such a helper.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/am.c                |  4 ++--\n builtin/checkout.c          |  4 ++--\n builtin/fetch.c             |  2 +-\n builtin/submodule--helper.c | 12 ++++++------\n usage.c                     |  2 ++\n 5 files changed, 13 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/am.c b/builtin/am.c\nindex e32a3b4c973..a800003340f 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -1000,7 +1000,7 @@ static void am_setup(struct am_state *state, enum patch_format patch_format,\n \n \tif (!patch_format) {\n \t\tfprintf_ln(stderr, _(\"Patch format detection failed.\"));\n-\t\texit(128);\n+\t\tdie(NULL);\n \t}\n \n \tif (mkdir(state->dir, 0777) < 0 && errno != EEXIST)\n@@ -1178,7 +1178,7 @@ static void NORETURN die_user_resolve(const struct am_state *state)\n \t\tstrbuf_release(&sb);\n \t}\n \n-\texit(128);\n+\tdie(NULL);\n }\n \n /**\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex d185982f3a6..536192d3456 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -838,7 +838,7 @@ static int merge_working_tree(const struct checkout_opts *opts,\n \t\tinit_tree_desc(&trees[0], &tree->object.oid,\n \t\t\t       tree->buffer, tree->size);\n \t\tif (parse_tree(new_tree) < 0)\n-\t\t\texit(128);\n+\t\t\tdie(NULL);\n \t\ttree = new_tree;\n \t\tinit_tree_desc(&trees[1], &tree->object.oid,\n \t\t\t       tree->buffer, tree->size);\n@@ -913,7 +913,7 @@ static int merge_working_tree(const struct checkout_opts *opts,\n \t\t\t\t\t\t     work,\n \t\t\t\t\t\t     old_tree);\n \t\t\tif (ret < 0)\n-\t\t\t\texit(128);\n+\t\t\t\tdie(NULL);\n \t\t\tret = reset_tree(new_tree,\n \t\t\t\t\t opts, 0,\n \t\t\t\t\t writeout_error, new_branch_info);\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex cda6eaf1fd6..b0800ea5829 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -992,7 +992,7 @@ static int update_local_ref(struct ref *ref,\n \t\tfast_forward = repo_in_merge_bases(the_repository, current,\n \t\t\t\t\t\t   updated);\n \t\tif (fast_forward < 0)\n-\t\t\texit(128);\n+\t\t\tdie(NULL);\n \t\tforced_updates_ms += (getnanotime() - t_before) / 1000000;\n \t} else {\n \t\tfast_forward = 1;\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 53da2116ddf..4255caca579 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -303,7 +303,7 @@ static void runcommand_in_submodule_cb(const struct cache_entry *list_item,\n \tchar *displaypath;\n \n \tif (validate_submodule_path(path) < 0)\n-\t\texit(128);\n+\t\tdie(NULL);\n \n \tdisplaypath = get_submodule_displaypath(path, info->prefix,\n \t\t\t\t\t\tinfo->super_prefix);\n@@ -643,7 +643,7 @@ static void status_submodule(const char *path, const struct object_id *ce_oid,\n \t};\n \n \tif (validate_submodule_path(path) < 0)\n-\t\texit(128);\n+\t\tdie(NULL);\n \n \tif (!submodule_from_path(the_repository, null_oid(the_hash_algo), path))\n \t\tdie(_(\"no submodule mapping found in .gitmodules for path '%s'\"),\n@@ -1257,7 +1257,7 @@ static void sync_submodule(const char *path, const char *prefix,\n \t\treturn;\n \n \tif (validate_submodule_path(path) < 0)\n-\t\texit(128);\n+\t\tdie(NULL);\n \n \tsub = submodule_from_path(the_repository, null_oid(the_hash_algo), path);\n \n@@ -1402,7 +1402,7 @@ static void deinit_submodule(const char *path, const char *prefix,\n \tchar *sub_git_dir = xstrfmt(\"%s/.git\", path);\n \n \tif (validate_submodule_path(path) < 0)\n-\t\texit(128);\n+\t\tdie(NULL);\n \n \tsub = submodule_from_path(the_repository, null_oid(the_hash_algo), path);\n \n@@ -1724,7 +1724,7 @@ static int clone_submodule(const struct module_clone_data *clone_data,\n \tchar *to_free = NULL;\n \n \tif (validate_submodule_path(clone_data_path) < 0)\n-\t\texit(128);\n+\t\tdie(NULL);\n \n \tif (!is_absolute_path(clone_data->path))\n \t\tclone_data_path = to_free = xstrfmt(\"%s/%s\", repo_get_work_tree(the_repository),\n@@ -3524,7 +3524,7 @@ static int module_add(int argc, const char **argv, const char *prefix,\n \tstrip_dir_trailing_slashes(add_data.sm_path);\n \n \tif (validate_submodule_path(add_data.sm_path) < 0)\n-\t\texit(128);\n+\t\tdie(NULL);\n \n \tdie_on_index_match(add_data.sm_path, force);\n \tdie_on_repo_without_commits(add_data.sm_path);\ndiff --git a/usage.c b/usage.c\nindex 38b46bbbfe7..cd7b57d6446 100644\n--- a/usage.c\n+++ b/usage.c\n@@ -67,6 +67,8 @@ static NORETURN void usage_builtin(const char *err, va_list params)\n \n static void die_message_builtin(const char *err, va_list params)\n {\n+\tif (!err)\n+\t\treturn;\n \ttrace2_cmd_error_va(err, params);\n \tvreportf(_(\"fatal: \"), err, params);\n }\n\n-- \n2.50.0.rc0.629.g846fc57c9e.dirty\n\n"},{"id":"519396","messageId":"20250602-b4-pks-maintenance-ref-lock-race-v3-11-587d44252dcb@pks.im","threadId":"63531","inReplyTo":"20250602-b4-pks-maintenance-ref-lock-race-v3-0-587d44252dcb@pks.im","subject":"[PATCH v3 11/12] builtin/gc: avoid global state in `gc_before_repack()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-02T07:17:37Z","receivedAt":"2025-06-02T07:17:43Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The `gc_before_repack()` should only ever run once in git-gc(1), but we\nmay end up calling it twice when the \"--detach\" flag is passed. The\nduplicated call is avoided though via a static flag in this function.\n\nThis pattern is somewhat unintuitive though. Refactor it to drop the\nstatic flag and instead guard the second call of `gc_before_repack()`\nvia `opts.detach`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 24 +++++++++---------------\n 1 file changed, 9 insertions(+), 15 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex fdd0dd09be7..4a5c4b20442 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -816,22 +816,14 @@ static int report_last_gc_error(void)\n \treturn ret;\n }\n \n-static void gc_before_repack(struct maintenance_run_opts *opts,\n-\t\t\t     struct gc_config *cfg)\n+static int gc_before_repack(struct maintenance_run_opts *opts,\n+\t\t\t    struct gc_config *cfg)\n {\n-\t/*\n-\t * We may be called twice, as both the pre- and\n-\t * post-daemonized phases will call us, but running these\n-\t * commands more than once is pointless and wasteful.\n-\t */\n-\tstatic int done = 0;\n-\tif (done++)\n-\t\treturn;\n-\n \tif (cfg->pack_refs && maintenance_task_pack_refs(opts, cfg))\n-\t\tdie(FAILED_RUN, \"pack-refs\");\n+\t\treturn error(FAILED_RUN, \"pack-refs\");\n \tif (cfg->prune_reflogs && maintenance_task_reflog_expire(opts, cfg))\n-\t\tdie(FAILED_RUN, \"reflog\");\n+\t\treturn error(FAILED_RUN, \"reflog\");\n+\treturn 0;\n }\n \n int cmd_gc(int argc,\n@@ -965,7 +957,8 @@ int cmd_gc(int argc,\n \t\t\tgoto out;\n \t\t}\n \n-\t\tgc_before_repack(&opts, &cfg); /* dies on failure */\n+\t\tif (gc_before_repack(&opts, &cfg) < 0)\n+\t\t\tdie(NULL);\n \t\tdelete_tempfile(&pidfile);\n \n \t\t/*\n@@ -995,7 +988,8 @@ int cmd_gc(int argc,\n \t\tfree(path);\n \t}\n \n-\tgc_before_repack(&opts, &cfg);\n+\tif (opts.detach <= 0)\n+\t\tgc_before_repack(&opts, &cfg);\n \n \tif (!repository_format_precious_objects) {\n \t\tstruct child_process repack_cmd = CHILD_PROCESS_INIT;\n\n-- \n2.50.0.rc0.629.g846fc57c9e.dirty\n\n"},{"id":"519397","messageId":"20250602-b4-pks-maintenance-ref-lock-race-v3-12-587d44252dcb@pks.im","threadId":"63531","inReplyTo":"20250602-b4-pks-maintenance-ref-lock-race-v3-0-587d44252dcb@pks.im","subject":"[PATCH v3 12/12] builtin/maintenance: fix locking race when handling \"gc\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-02T07:17:38Z","receivedAt":"2025-06-02T07:17:43Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"gc\" task has a similar locking race as the one that we have fixed\nfor the \"pack-refs\" and \"reflog-expire\" tasks in preceding commits. Fix\nthis by splitting up the logic of the \"gc\" task:\n\n  - We execute `gc_before_repack()` in the foreground, which contains\n    the logic that git-gc(1) itself would execute in the foreground, as\n    well.\n\n  - We spawn git-gc(1) after detaching, but with a new hidden flag that\n    suppresses calling `gc_before_repack()`.\n\nLike this we have roughly the same logic as git-gc(1) itself and know to\nrepack refs and reflogs before detaching, thus fixing the race.\n\nNote that `gc_before_repack()` is renamed to `gc_foreground_tasks()` to\nbetter reflect what this function does.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c           | 41 +++++++++++++++++++++++++++--------------\n t/t7900-maintenance.sh | 12 ++++++------\n 2 files changed, 33 insertions(+), 20 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 4a5c4b20442..b5e6519d597 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -816,8 +816,8 @@ static int report_last_gc_error(void)\n \treturn ret;\n }\n \n-static int gc_before_repack(struct maintenance_run_opts *opts,\n-\t\t\t    struct gc_config *cfg)\n+static int gc_foreground_tasks(struct maintenance_run_opts *opts,\n+\t\t\t       struct gc_config *cfg)\n {\n \tif (cfg->pack_refs && maintenance_task_pack_refs(opts, cfg))\n \t\treturn error(FAILED_RUN, \"pack-refs\");\n@@ -837,6 +837,7 @@ int cmd_gc(int argc,\n \tpid_t pid;\n \tint daemonized = 0;\n \tint keep_largest_pack = -1;\n+\tint skip_foreground_tasks = 0;\n \ttimestamp_t dummy;\n \tstruct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT;\n \tstruct gc_config cfg = GC_CONFIG_INIT;\n@@ -869,6 +870,8 @@ int cmd_gc(int argc,\n \t\t\t N_(\"repack all other packs except the largest pack\")),\n \t\tOPT_STRING(0, \"expire-to\", &cfg.repack_expire_to, N_(\"dir\"),\n \t\t\t   N_(\"pack prefix to store a pack containing pruned objects\")),\n+\t\tOPT_HIDDEN_BOOL(0, \"skip-foreground-tasks\", &skip_foreground_tasks,\n+\t\t\t   N_(\"skip maintenance tasks typically done in the foreground\")),\n \t\tOPT_END()\n \t};\n \n@@ -952,14 +955,16 @@ int cmd_gc(int argc,\n \t\t\tgoto out;\n \t\t}\n \n-\t\tif (lock_repo_for_gc(force, &pid)) {\n-\t\t\tret = 0;\n-\t\t\tgoto out;\n-\t\t}\n+\t\tif (!skip_foreground_tasks) {\n+\t\t\tif (lock_repo_for_gc(force, &pid)) {\n+\t\t\t\tret = 0;\n+\t\t\t\tgoto out;\n+\t\t\t}\n \n-\t\tif (gc_before_repack(&opts, &cfg) < 0)\n-\t\t\tdie(NULL);\n-\t\tdelete_tempfile(&pidfile);\n+\t\t\tif (gc_foreground_tasks(&opts, &cfg) < 0)\n+\t\t\t\tdie(NULL);\n+\t\t\tdelete_tempfile(&pidfile);\n+\t\t}\n \n \t\t/*\n \t\t * failure to daemonize is ok, we'll continue\n@@ -988,8 +993,8 @@ int cmd_gc(int argc,\n \t\tfree(path);\n \t}\n \n-\tif (opts.detach <= 0)\n-\t\tgc_before_repack(&opts, &cfg);\n+\tif (opts.detach <= 0 && !skip_foreground_tasks)\n+\t\tgc_foreground_tasks(&opts, &cfg);\n \n \tif (!repository_format_precious_objects) {\n \t\tstruct child_process repack_cmd = CHILD_PROCESS_INIT;\n@@ -1225,8 +1230,14 @@ static int maintenance_task_prefetch(struct maintenance_run_opts *opts,\n \treturn 0;\n }\n \n-static int maintenance_task_gc(struct maintenance_run_opts *opts,\n-\t\t\t       struct gc_config *cfg UNUSED)\n+static int maintenance_task_gc_foreground(struct maintenance_run_opts *opts,\n+\t\t\t\t\t  struct gc_config *cfg)\n+{\n+\treturn gc_foreground_tasks(opts, cfg);\n+}\n+\n+static int maintenance_task_gc_background(struct maintenance_run_opts *opts,\n+\t\t\t\t\t  struct gc_config *cfg UNUSED)\n {\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \n@@ -1240,6 +1251,7 @@ static int maintenance_task_gc(struct maintenance_run_opts *opts,\n \telse\n \t\tstrvec_push(&child.args, \"--no-quiet\");\n \tstrvec_push(&child.args, \"--no-detach\");\n+\tstrvec_push(&child.args, \"--skip-foreground-tasks\");\n \n \treturn run_command(&child);\n }\n@@ -1571,7 +1583,8 @@ static const struct maintenance_task tasks[] = {\n \t},\n \t[TASK_GC] = {\n \t\t.name = \"gc\",\n-\t\t.background = maintenance_task_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},\n \t[TASK_COMMIT_GRAPH] = {\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 1ada5246606..ddd273d8dc2 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -49,9 +49,9 @@ test_expect_success 'run [--auto|--quiet]' '\n \t\tgit maintenance run --auto 2>/dev/null &&\n \tGIT_TRACE2_EVENT=\"$(pwd)/run-no-quiet.txt\" \\\n \t\tgit maintenance run --no-quiet 2>/dev/null &&\n-\ttest_subcommand git gc --quiet --no-detach <run-no-auto.txt &&\n-\ttest_subcommand ! git gc --auto --quiet --no-detach <run-auto.txt &&\n-\ttest_subcommand git gc --no-quiet --no-detach <run-no-quiet.txt\n+\ttest_subcommand git gc --quiet --no-detach --skip-foreground-tasks <run-no-auto.txt &&\n+\ttest_subcommand ! git gc --auto --quiet --no-detach --skip-foreground-tasks <run-auto.txt &&\n+\ttest_subcommand git gc --no-quiet --no-detach --skip-foreground-tasks <run-no-quiet.txt\n '\n \n test_expect_success 'maintenance.auto config option' '\n@@ -154,9 +154,9 @@ test_expect_success 'run --task=<task>' '\n \t\tgit maintenance run --task=commit-graph 2>/dev/null &&\n \tGIT_TRACE2_EVENT=\"$(pwd)/run-both.txt\" \\\n \t\tgit maintenance run --task=commit-graph --task=gc 2>/dev/null &&\n-\ttest_subcommand ! git gc --quiet --no-detach <run-commit-graph.txt &&\n-\ttest_subcommand git gc --quiet --no-detach <run-gc.txt &&\n-\ttest_subcommand git gc --quiet --no-detach <run-both.txt &&\n+\ttest_subcommand ! git gc --quiet --no-detach --skip-foreground-tasks <run-commit-graph.txt &&\n+\ttest_subcommand git gc --quiet --no-detach --skip-foreground-tasks <run-gc.txt &&\n+\ttest_subcommand git gc --quiet --no-detach --skip-foreground-tasks <run-both.txt &&\n \ttest_subcommand git commit-graph write --split --reachable --no-progress <run-commit-graph.txt &&\n \ttest_subcommand ! git commit-graph write --split --reachable --no-progress <run-gc.txt &&\n \ttest_subcommand git commit-graph write --split --reachable --no-progress <run-both.txt\n\n-- \n2.50.0.rc0.629.g846fc57c9e.dirty\n\n"},{"id":"519406","messageId":"cab8e175-e977-41b1-b53d-6b9170c9e416@app.fastmail.com","threadId":"63531","inReplyTo":"20250602-b4-pks-maintenance-ref-lock-race-v3-9-587d44252dcb@pks.im","subject":"Re: [PATCH v3 09/12] builtin/maintenance: fix locking race when packing refs and reflogs","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-06-02T10:03:21Z","receivedAt":"2025-06-02T10:03:43Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Mon, Jun 2, 2025, at 09:17, Patrick Steinhardt wrote:\n> As explained in the preceding commit, git-gc(1) knows to detach only\n> after it has already packed references and reflogs. This is done to\n\nI’m a naïve reader.  When I read this I immediately thought that reflogs\ncan be packed now.  But going by the last paragraph it is packed\nreferences and expired reflogs?\n\n> avoid racing around their respective lockfiles.\n>\n> Adapt git-maintenance(1) accordingly and run the \"pack-refs\" and\n> \"reflog-expire\" tasks in the foreground. Note that the \"gc\" task has the\n> same issue, but the fix is a bit more involved there and will thus be\n> done in a subsequent commit.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n"},{"id":"519427","messageId":"CAOLa=ZSy5XoKJ2OaKv3DukY9ko-pkpa2zHLQneLwq=9+wJnYWg@mail.gmail.com","threadId":"63531","inReplyTo":"20250602-b4-pks-maintenance-ref-lock-race-v3-1-587d44252dcb@pks.im","subject":"Re: [PATCH v3 01/12] builtin/gc: use designated field initializers for maintenance tasks","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-06-02T10:35:38Z","receivedAt":"2025-06-02T10:35:40Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Convert the array of maintenance tasks to use designated field\n> initializers. This makes it easier to add more fields to the struct\n> without having to modify all tasks.\n>\n\nI was wondering why we don't catch missing field initializers when not\nusing designators. Seems like we explicitly set\n'-Wno-missing-field-initializers'. Perhaps something to slowly tackle\nover time.\n\nThe patch looks good.\n\n[snip]\n"},{"id":"519540","messageId":"aD6Xh8kKUu0Y7sr-@pks.im","threadId":"63531","inReplyTo":"cab8e175-e977-41b1-b53d-6b9170c9e416@app.fastmail.com","subject":"Re: [PATCH v3 09/12] builtin/maintenance: fix locking race when packing refs and reflogs","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-03T06:34:47Z","receivedAt":"2025-06-03T06:34:57Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Jun 02, 2025 at 12:03:21PM +0200, Kristoffer Haugsbakk wrote:\n> On Mon, Jun 2, 2025, at 09:17, Patrick Steinhardt wrote:\n> > As explained in the preceding commit, git-gc(1) knows to detach only\n> > after it has already packed references and reflogs. This is done to\n> \n> I’m a naïve reader.  When I read this I immediately thought that reflogs\n> can be packed now.  But going by the last paragraph it is packed\n> references and expired reflogs?\n\nYeah, I see how this is misleading. But indeed, it is packing refs and\nexpiring reflogs. I've rephrased this locally, but if that's okay with\nyou I'll hold off sending a new version for just this change.\n\nThanks!\n\nPatrick\n"},{"id":"519542","messageId":"99de4d2e-ee78-463c-ba92-0f21bb1a4f27@app.fastmail.com","threadId":"63531","inReplyTo":"aD6Xh8kKUu0Y7sr-@pks.im","subject":"Re: [PATCH v3 09/12] builtin/maintenance: fix locking race when packing refs and reflogs","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-06-03T07:08:06Z","receivedAt":"2025-06-03T07:08:37Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Tue, Jun 3, 2025, at 08:34, Patrick Steinhardt wrote:\n>> [snip]\n>> I’m a naïve reader.  When I read this I immediately thought that reflogs\n>> can be packed now.  But going by the last paragraph it is packed\n>> references and expired reflogs?\n>\n> Yeah, I see how this is misleading. But indeed, it is packing refs and\n> expiring reflogs. I've rephrased this locally, but if that's okay with\n> you I'll hold off sending a new version for just this change.\n\nCertainly!  I forgot to prefix: nit\n\n-- \nKristoffer Haugsbakk\n\n \n"},{"id":"519543","messageId":"CAOLa=ZTLvL52UpZZbXtMefHPE+s5PWhUR4ZLgB6T_JMFjqfBZg@mail.gmail.com","threadId":"63531","inReplyTo":"20250602-b4-pks-maintenance-ref-lock-race-v3-10-587d44252dcb@pks.im","subject":"Re: [PATCH v3 10/12] usage: allow dying without writing an error message","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-06-03T08:31:01Z","receivedAt":"2025-06-03T08:31:02Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Sometimes code wants to die in a situation where it already has written\n> an error message. To use the same error code as `die()` we have to open\n> code the code with a call to `exit(128)` in such cases, which is easy to\n\nNit: This reads a little weird.\n\n> get wrong and leaves magical numbers all over our codebase.\n>\n> Teach `die_message_builtin()` to not print any error when passed a\n> `NULL` pointer as error string. Like this, such users can now call\n> `die(NULL)` to achieve the same result without any hardcoded error\n> codes.\n>\n> Adapt a couple of builtins to use this new pattern to demonstrate that\n> there is a need for such a helper.\n>\n\n[snip]\n"},{"id":"519544","messageId":"256e9e60-fdea-461b-b43c-8e6024b773a2@app.fastmail.com","threadId":"63531","inReplyTo":"CAOLa=ZTLvL52UpZZbXtMefHPE+s5PWhUR4ZLgB6T_JMFjqfBZg@mail.gmail.com","subject":"Re: [PATCH v3 10/12] usage: allow dying without writing an error message","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-06-03T08:33:40Z","receivedAt":"2025-06-03T08:34:07Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Tue, Jun 3, 2025, at 10:31, Karthik Nayak wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n>\n>> Sometimes code wants to die in a situation where it already has written\n>> an error message. To use the same error code as `die()` we have to open\n>> code the code with a call to `exit(128)` in such cases, which is easy to\n>\n> Nit: This reads a little weird.\n\nMaybe s/to open code the code/to open-code the code/\n"},{"id":"519545","messageId":"CAOLa=ZQTYTNiUhFW+kOqMro0rpWVTkyKop7az_6h7zgM8njRhw@mail.gmail.com","threadId":"63531","inReplyTo":"256e9e60-fdea-461b-b43c-8e6024b773a2@app.fastmail.com","subject":"Re: [PATCH v3 10/12] usage: allow dying without writing an error message","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-06-03T09:04:56Z","receivedAt":"2025-06-03T09:05:23Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"On Tue, Jun 3, 2025 at 10:34 AM Kristoffer Haugsbakk\n<kristofferhaugsbakk@fastmail.com> wrote:\n>\n> On Tue, Jun 3, 2025, at 10:31, Karthik Nayak wrote:\n> > Patrick Steinhardt <ps@pks.im> writes:\n> >\n> >> Sometimes code wants to die in a situation where it already has written\n> >> an error message. To use the same error code as `die()` we have to open\n> >> code the code with a call to `exit(128)` in such cases, which is easy to\n> >\n> > Nit: This reads a little weird.\n>\n> Maybe s/to open code the code/to open-code the code/\n\nAh, that makes so much difference.\n"},{"id":"519546","messageId":"aD7SoaRg4UV6ktJL@pks.im","threadId":"63531","inReplyTo":"CAOLa=ZQTYTNiUhFW+kOqMro0rpWVTkyKop7az_6h7zgM8njRhw@mail.gmail.com","subject":"Re: [PATCH v3 10/12] usage: allow dying without writing an error message","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-03T10:46:57Z","receivedAt":"2025-06-03T10:47:03Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Jun 03, 2025 at 11:04:56AM +0200, Karthik Nayak wrote:\n> On Tue, Jun 3, 2025 at 10:34 AM Kristoffer Haugsbakk\n> <kristofferhaugsbakk@fastmail.com> wrote:\n> >\n> > On Tue, Jun 3, 2025, at 10:31, Karthik Nayak wrote:\n> > > Patrick Steinhardt <ps@pks.im> writes:\n> > >\n> > >> Sometimes code wants to die in a situation where it already has written\n> > >> an error message. To use the same error code as `die()` we have to open\n> > >> code the code with a call to `exit(128)` in such cases, which is easy to\n> > >\n> > > Nit: This reads a little weird.\n> >\n> > Maybe s/to open code the code/to open-code the code/\n> \n> Ah, that makes so much difference.\n\nThat reads better, but I guess we can improve it even further. How about\nthis instead:\n\n    To retain the same error code as `die()` we have to use `exit(128)`,\n    which is easy to get wrong and leaves magical numbers all over our\n    codebase.\n\nPatrick\n"},{"id":"519553","messageId":"8dca137e-04a5-4609-89ca-c89c31ce2b2d@app.fastmail.com","threadId":"63531","inReplyTo":"aD7SoaRg4UV6ktJL@pks.im","subject":"Re: [PATCH v3 10/12] usage: allow dying without writing an error message","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-06-03T13:45:35Z","receivedAt":"2025-06-03T13:46:04Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Tue, Jun 3, 2025, at 12:46, Patrick Steinhardt wrote:\n>> Ah, that makes so much difference.\n>\n> That reads better, but I guess we can improve it even further. How about\n> this instead:\n>\n>     To retain the same error code as `die()` we have to use `exit(128)`,\n>     which is easy to get wrong and leaves magical numbers all over our\n>     codebase.\n\nLooks great.  Just s/magical numbers/magic numbers/\n"},{"id":"519556","messageId":"20250603-b4-pks-maintenance-ref-lock-race-v4-0-52f5cf7b7e99@pks.im","threadId":"63531","inReplyTo":"20250527-b4-pks-maintenance-ref-lock-race-v1-0-e1ceb2dea66e@pks.im","subject":"[PATCH v4 00/12] builtin/maintenance: fix ref lock races when detaching","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-03T14:01:08Z","receivedAt":"2025-06-03T14:01:21Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis patch series fixes races around locking the \"packed-refs\" file when\nauto-maintenance decides to repack it. This issue has been reported e.g.\nvia [1] and [2].\n\nThe root cause is that git-gc(1) used to know to detach _after_ having\nrepacked references. As such, callers wouldn't continue with their thing\nuntil we have already packed refs, and thus the race does not exist\nthere. git-maintenance(1) didn't have the same split though, so this\npatch series retrofits that logic.\n\nThe series is structured as follows:\n\n  - Patches 1 and 2 do some light refactorings.\n\n  - Patches 3 to 5 refactor how we set up the list of tasks to not rely\n    on globals anymore. Instead, we now have a single source of truth\n    for which tasks exactly will be run.\n\n  - The remaining patches introduce the split of before/after-detach\n    tasks and wire them up for \"pack-refs\", \"reflog-expire\" and \"gc\"\n    tasks.\n\nChanges in v2:\n  - A couple of commit message improvements.\n  - Introduce `die(NULL)` to die with the correct exit code but no error\n    message. This gets rid of some magic numbers.\n  - Introduce an enum to discern the phases before and after detach.\n  - Link to v1: https://lore.kernel.org/r/20250527-b4-pks-maintenance-ref-lock-race-v1-0-e1ceb2dea66e@pks.im\n\nChanges in v3:\n  - Rework logic to talk about foreground/background tasks instead of\n    before/after detach.\n  - Link to v2: https://lore.kernel.org/r/20250530-b4-pks-maintenance-ref-lock-race-v2-0-d04e2f93e51f@pks.im\n\nChanges in v4:\n  - Some more massaging of commit messages.\n  - Link to v3: https://lore.kernel.org/r/20250602-b4-pks-maintenance-ref-lock-race-v3-0-587d44252dcb@pks.im\n\nThanks!\n\nPatrick\n\n[1]: <CAJR-fbZ4X1+gN75m2dUvocR6NkowLOZ9F26cjBy8w1qd181OoQ@mail.gmail.com>\n[2]: <CANi7bVAkNc+gY1NoXfJuDRjxjZLTgL8Lfn8_ZmWsvLAoiLPkNg@mail.gmail.com>\n\n---\nPatrick Steinhardt (12):\n      builtin/gc: use designated field initializers for maintenance tasks\n      builtin/gc: drop redundant local variable\n      builtin/maintenance: centralize configuration of explicit tasks\n      builtin/maintenance: mark \"--task=\" and \"--schedule=\" as incompatible\n      builtin/maintenance: stop modifying global array of tasks\n      builtin/maintenance: extract function to run tasks\n      builtin/maintenance: fix typedef for function pointers\n      builtin/maintenance: split into foreground and background tasks\n      builtin/maintenance: fix locking race with refs and reflogs tasks\n      usage: allow dying without writing an error message\n      builtin/gc: avoid global state in `gc_before_repack()`\n      builtin/maintenance: fix locking race when handling \"gc\" task\n\n builtin/am.c                |   4 +-\n builtin/checkout.c          |   4 +-\n builtin/fetch.c             |   2 +-\n builtin/gc.c                | 410 +++++++++++++++++++++++++-------------------\n builtin/submodule--helper.c |  12 +-\n t/t7900-maintenance.sh      |  19 +-\n usage.c                     |   2 +\n 7 files changed, 263 insertions(+), 190 deletions(-)\n\nRange-diff versus v3:\n\n 1:  e46a65951b9 =  1:  280f13d2895 builtin/gc: use designated field initializers for maintenance tasks\n 2:  73cd67f3e1a =  2:  16a017fb819 builtin/gc: drop redundant local variable\n 3:  a02452a6d6f =  3:  0ab3344ddb0 builtin/maintenance: centralize configuration of explicit tasks\n 4:  ccd7691e4d5 =  4:  69e768cb54e builtin/maintenance: mark \"--task=\" and \"--schedule=\" as incompatible\n 5:  0e243fd81e6 =  5:  295e9e5ee9f builtin/maintenance: stop modifying global array of tasks\n 6:  c95bd62823e =  6:  d94b0c86622 builtin/maintenance: extract function to run tasks\n 7:  43d28434d8e =  7:  0bbba671cd0 builtin/maintenance: fix typedef for function pointers\n 8:  d5740a5c9d9 =  8:  4ce38539bb6 builtin/maintenance: split into foreground and background tasks\n 9:  168eb3a9372 !  9:  28092b9bed1 builtin/maintenance: fix locking race when packing refs and reflogs\n    @@ Metadata\n     Author: Patrick Steinhardt <ps@pks.im>\n     \n      ## Commit message ##\n    -    builtin/maintenance: fix locking race when packing refs and reflogs\n    +    builtin/maintenance: fix locking race with refs and reflogs tasks\n     \n         As explained in the preceding commit, git-gc(1) knows to detach only\n    -    after it has already packed references and reflogs. This is done to\n    -    avoid racing around their respective lockfiles.\n    +    after it has already packed references and expired reflogs. This is done\n    +    to avoid racing around their respective lockfiles.\n     \n         Adapt git-maintenance(1) accordingly and run the \"pack-refs\" and\n         \"reflog-expire\" tasks in the foreground. Note that the \"gc\" task has the\n10:  0ff01f6e2aa ! 10:  b8ed080c67d usage: allow dying without writing an error message\n    @@ Commit message\n         usage: allow dying without writing an error message\n     \n         Sometimes code wants to die in a situation where it already has written\n    -    an error message. To use the same error code as `die()` we have to open\n    -    code the code with a call to `exit(128)` in such cases, which is easy to\n    -    get wrong and leaves magical numbers all over our codebase.\n    +    an error message. To use the same error code as `die()` we have to use\n    +    `exit(128)`, which is easy to get wrong and leaves magic numbers all\n    +    over our codebase.\n     \n         Teach `die_message_builtin()` to not print any error when passed a\n         `NULL` pointer as error string. Like this, such users can now call\n11:  93f53000e47 = 11:  5b149886263 builtin/gc: avoid global state in `gc_before_repack()`\n12:  01095d1bf88 = 12:  9ba01f143b3 builtin/maintenance: fix locking race when handling \"gc\" task\n\n---\nbase-commit: 845c48a16a7f7b2c44d8cb137b16a4a1f0140229\nchange-id: 20250527-b4-pks-maintenance-ref-lock-race-11ae5d68e06f\n\n"},{"id":"519558","messageId":"20250603-b4-pks-maintenance-ref-lock-race-v4-1-52f5cf7b7e99@pks.im","threadId":"63531","inReplyTo":"20250603-b4-pks-maintenance-ref-lock-race-v4-0-52f5cf7b7e99@pks.im","subject":"[PATCH v4 01/12] builtin/gc: use designated field initializers for maintenance tasks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-03T14:01:09Z","receivedAt":"2025-06-03T14:01:22Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Convert the array of maintenance tasks to use designated field\ninitializers. This makes it easier to add more fields to the struct\nwithout having to modify all tasks.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 54 +++++++++++++++++++++++++++---------------------------\n 1 file changed, 27 insertions(+), 27 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e33ba946e43..54fc7f299a9 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1550,49 +1550,49 @@ enum maintenance_task_label {\n \n static struct maintenance_task tasks[] = {\n \t[TASK_PREFETCH] = {\n-\t\t\"prefetch\",\n-\t\tmaintenance_task_prefetch,\n+\t\t.name = \"prefetch\",\n+\t\t.fn = maintenance_task_prefetch,\n \t},\n \t[TASK_LOOSE_OBJECTS] = {\n-\t\t\"loose-objects\",\n-\t\tmaintenance_task_loose_objects,\n-\t\tloose_object_auto_condition,\n+\t\t.name = \"loose-objects\",\n+\t\t.fn = maintenance_task_loose_objects,\n+\t\t.auto_condition = loose_object_auto_condition,\n \t},\n \t[TASK_INCREMENTAL_REPACK] = {\n-\t\t\"incremental-repack\",\n-\t\tmaintenance_task_incremental_repack,\n-\t\tincremental_repack_auto_condition,\n+\t\t.name = \"incremental-repack\",\n+\t\t.fn = maintenance_task_incremental_repack,\n+\t\t.auto_condition = incremental_repack_auto_condition,\n \t},\n \t[TASK_GC] = {\n-\t\t\"gc\",\n-\t\tmaintenance_task_gc,\n-\t\tneed_to_gc,\n-\t\t1,\n+\t\t.name = \"gc\",\n+\t\t.fn = maintenance_task_gc,\n+\t\t.auto_condition = need_to_gc,\n+\t\t.enabled = 1,\n \t},\n \t[TASK_COMMIT_GRAPH] = {\n-\t\t\"commit-graph\",\n-\t\tmaintenance_task_commit_graph,\n-\t\tshould_write_commit_graph,\n+\t\t.name = \"commit-graph\",\n+\t\t.fn = maintenance_task_commit_graph,\n+\t\t.auto_condition = should_write_commit_graph,\n \t},\n \t[TASK_PACK_REFS] = {\n-\t\t\"pack-refs\",\n-\t\tmaintenance_task_pack_refs,\n-\t\tpack_refs_condition,\n+\t\t.name = \"pack-refs\",\n+\t\t.fn = maintenance_task_pack_refs,\n+\t\t.auto_condition = pack_refs_condition,\n \t},\n \t[TASK_REFLOG_EXPIRE] = {\n-\t\t\"reflog-expire\",\n-\t\tmaintenance_task_reflog_expire,\n-\t\treflog_expire_condition,\n+\t\t.name = \"reflog-expire\",\n+\t\t.fn = maintenance_task_reflog_expire,\n+\t\t.auto_condition = reflog_expire_condition,\n \t},\n \t[TASK_WORKTREE_PRUNE] = {\n-\t\t\"worktree-prune\",\n-\t\tmaintenance_task_worktree_prune,\n-\t\tworktree_prune_condition,\n+\t\t.name = \"worktree-prune\",\n+\t\t.fn = maintenance_task_worktree_prune,\n+\t\t.auto_condition = worktree_prune_condition,\n \t},\n \t[TASK_RERERE_GC] = {\n-\t\t\"rerere-gc\",\n-\t\tmaintenance_task_rerere_gc,\n-\t\trerere_gc_condition,\n+\t\t.name = \"rerere-gc\",\n+\t\t.fn = maintenance_task_rerere_gc,\n+\t\t.auto_condition = rerere_gc_condition,\n \t},\n };\n \n\n-- \n2.50.0.rc0.629.g846fc57c9e.dirty\n\n"},{"id":"519557","messageId":"20250603-b4-pks-maintenance-ref-lock-race-v4-2-52f5cf7b7e99@pks.im","threadId":"63531","inReplyTo":"20250603-b4-pks-maintenance-ref-lock-race-v4-0-52f5cf7b7e99@pks.im","subject":"[PATCH v4 02/12] builtin/gc: drop redundant local variable","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-03T14:01:10Z","receivedAt":"2025-06-03T14:01:23Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We have two different variables that track the quietness for git-gc(1):\n\n  - The local variable `quiet`, which we wire up.\n\n  - The `quiet` field of `struct maintenance_run_opts`.\n\nThis leads to confusion which of these variables should be used and what\nthe respective effect is.\n\nSimplify this logic by dropping the local variable in favor of the\noptions field.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 11 +++++------\n 1 file changed, 5 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 54fc7f299a9..7adda8d2d0d 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -818,7 +818,6 @@ int cmd_gc(int argc,\n \t   struct repository *repo UNUSED)\n {\n \tint aggressive = 0;\n-\tint quiet = 0;\n \tint force = 0;\n \tconst char *name;\n \tpid_t pid;\n@@ -831,7 +830,7 @@ int cmd_gc(int argc,\n \tconst char *prune_expire_arg = prune_expire_sentinel;\n \tint ret;\n \tstruct option builtin_gc_options[] = {\n-\t\tOPT__QUIET(&quiet, N_(\"suppress progress reporting\")),\n+\t\tOPT__QUIET(&opts.quiet, N_(\"suppress progress reporting\")),\n \t\t{\n \t\t\t.type = OPTION_STRING,\n \t\t\t.long_name = \"prune\",\n@@ -891,7 +890,7 @@ int cmd_gc(int argc,\n \t\tif (cfg.aggressive_window > 0)\n \t\t\tstrvec_pushf(&repack, \"--window=%d\", cfg.aggressive_window);\n \t}\n-\tif (quiet)\n+\tif (opts.quiet)\n \t\tstrvec_push(&repack, \"-q\");\n \n \tif (opts.auto_flag) {\n@@ -906,7 +905,7 @@ int cmd_gc(int argc,\n \t\t\tgoto out;\n \t\t}\n \n-\t\tif (!quiet) {\n+\t\tif (!opts.quiet) {\n \t\t\tif (opts.detach > 0)\n \t\t\t\tfprintf(stderr, _(\"Auto packing the repository in background for optimum performance.\\n\"));\n \t\t\telse\n@@ -991,7 +990,7 @@ int cmd_gc(int argc,\n \t\t\tstrvec_pushl(&prune_cmd.args, \"prune\", \"--expire\", NULL);\n \t\t\t/* run `git prune` even if using cruft packs */\n \t\t\tstrvec_push(&prune_cmd.args, cfg.prune_expire);\n-\t\t\tif (quiet)\n+\t\t\tif (opts.quiet)\n \t\t\t\tstrvec_push(&prune_cmd.args, \"--no-progress\");\n \t\t\tif (repo_has_promisor_remote(the_repository))\n \t\t\t\tstrvec_push(&prune_cmd.args,\n@@ -1019,7 +1018,7 @@ int cmd_gc(int argc,\n \n \tif (the_repository->settings.gc_write_commit_graph == 1)\n \t\twrite_commit_graph_reachable(the_repository->objects->odb,\n-\t\t\t\t\t     !quiet && !daemonized ? COMMIT_GRAPH_WRITE_PROGRESS : 0,\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\n-- \n2.50.0.rc0.629.g846fc57c9e.dirty\n\n"},{"id":"519559","messageId":"20250603-b4-pks-maintenance-ref-lock-race-v4-3-52f5cf7b7e99@pks.im","threadId":"63531","inReplyTo":"20250603-b4-pks-maintenance-ref-lock-race-v4-0-52f5cf7b7e99@pks.im","subject":"[PATCH v4 03/12] builtin/maintenance: centralize configuration of explicit tasks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-03T14:01:11Z","receivedAt":"2025-06-03T14:01:24Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Users of git-maintenance(1) can explicitly ask it to run specific tasks\nby passing the `--task=` command line option. This option can be passed\nmultiple times, which causes us to execute tasks in the same order as\nthe tasks have been provided by the user.\n\nThe order in which tasks are run is computed in `task_option_parse()`:\nevery time we parse such a command line argument, we modify the global\narray of tasks by seting the selected index for that specific task.\nThis has two downsides:\n\n  - We modify global state, which makes it hard to follow the logic.\n\n  - The configuration of tasks is split across multiple different\n    functions, so it is not easy to figure out the different factors\n    that play a role in selecting tasks.\n\nRefactor the logic so that `task_option_parse()` does not modify global\nstate anymore. Instead, this function now only collects the list of\nconfigured tasks. The logic to configure ordering of the respective\ntasks is then deferred to `initialize_task_config()`.\n\nThis refactoring solves the second problem, that the configuration of\ntasks is spread across multiple different locations. The first problem,\nthat we modify global state, will be fixed in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 47 ++++++++++++++++++++++++-----------------------\n 1 file changed, 24 insertions(+), 23 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 7adda8d2d0d..c4af9b11287 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1690,15 +1690,22 @@ static void initialize_maintenance_strategy(void)\n \t}\n }\n \n-static void initialize_task_config(int schedule)\n+static void initialize_task_config(const struct string_list *selected_tasks,\n+\t\t\t\t   int schedule)\n {\n-\tint i;\n \tstruct strbuf config_name = STRBUF_INIT;\n \n+\tfor (size_t i = 0; i < TASK__COUNT; i++)\n+\t\ttasks[i].selected_order = -1;\n+\tfor (size_t i = 0; i < selected_tasks->nr; i++) {\n+\t\tstruct maintenance_task *task = selected_tasks->items[i].util;\n+\t\ttask->selected_order = i;\n+\t}\n+\n \tif (schedule)\n \t\tinitialize_maintenance_strategy();\n \n-\tfor (i = 0; i < TASK__COUNT; i++) {\n+\tfor (size_t i = 0; i < TASK__COUNT; i++) {\n \t\tint config_value;\n \t\tchar *config_str;\n \n@@ -1722,33 +1729,28 @@ static void initialize_task_config(int schedule)\n \tstrbuf_release(&config_name);\n }\n \n-static int task_option_parse(const struct option *opt UNUSED,\n+static int task_option_parse(const struct option *opt,\n \t\t\t     const char *arg, int unset)\n {\n-\tint i, num_selected = 0;\n-\tstruct maintenance_task *task = NULL;\n+\tstruct string_list *selected_tasks = opt->value;\n+\tsize_t i;\n \n \tBUG_ON_OPT_NEG(unset);\n \n-\tfor (i = 0; i < TASK__COUNT; i++) {\n-\t\tif (tasks[i].selected_order >= 0)\n-\t\t\tnum_selected++;\n-\t\tif (!strcasecmp(tasks[i].name, arg)) {\n-\t\t\ttask = &tasks[i];\n-\t\t}\n-\t}\n-\n-\tif (!task) {\n+\tfor (i = 0; i < TASK__COUNT; i++)\n+\t\tif (!strcasecmp(tasks[i].name, arg))\n+\t\t\tbreak;\n+\tif (i >= TASK__COUNT) {\n \t\terror(_(\"'%s' is not a valid task\"), arg);\n \t\treturn 1;\n \t}\n \n-\tif (task->selected_order >= 0) {\n+\tif (unsorted_string_list_has_string(selected_tasks, arg)) {\n \t\terror(_(\"task '%s' cannot be selected multiple times\"), arg);\n \t\treturn 1;\n \t}\n \n-\ttask->selected_order = num_selected + 1;\n+\tstring_list_append(selected_tasks, arg)->util = &tasks[i];\n \n \treturn 0;\n }\n@@ -1756,8 +1758,8 @@ static int task_option_parse(const struct option *opt UNUSED,\n static int maintenance_run(int argc, const char **argv, const char *prefix,\n \t\t\t   struct repository *repo UNUSED)\n {\n-\tint i;\n \tstruct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT;\n+\tstruct string_list selected_tasks = STRING_LIST_INIT_DUP;\n \tstruct gc_config cfg = GC_CONFIG_INIT;\n \tstruct option builtin_maintenance_run_options[] = {\n \t\tOPT_BOOL(0, \"auto\", &opts.auto_flag,\n@@ -1769,7 +1771,7 @@ static int maintenance_run(int argc, const char **argv, const char *prefix,\n \t\t\t     maintenance_opt_schedule),\n \t\tOPT_BOOL(0, \"quiet\", &opts.quiet,\n \t\t\t N_(\"do not report progress or other information over stderr\")),\n-\t\tOPT_CALLBACK_F(0, \"task\", NULL, N_(\"task\"),\n+\t\tOPT_CALLBACK_F(0, \"task\", &selected_tasks, N_(\"task\"),\n \t\t\tN_(\"run a specific task\"),\n \t\t\tPARSE_OPT_NONEG, task_option_parse),\n \t\tOPT_END()\n@@ -1778,9 +1780,6 @@ static int maintenance_run(int argc, const char **argv, const char *prefix,\n \n \topts.quiet = !isatty(2);\n \n-\tfor (i = 0; i < TASK__COUNT; i++)\n-\t\ttasks[i].selected_order = -1;\n-\n \targc = parse_options(argc, argv, prefix,\n \t\t\t     builtin_maintenance_run_options,\n \t\t\t     builtin_maintenance_run_usage,\n@@ -1790,13 +1789,15 @@ static int maintenance_run(int argc, const char **argv, const char *prefix,\n \t\tdie(_(\"use at most one of --auto and --schedule=<frequency>\"));\n \n \tgc_config(&cfg);\n-\tinitialize_task_config(opts.schedule);\n+\tinitialize_task_config(&selected_tasks, opts.schedule);\n \n \tif (argc != 0)\n \t\tusage_with_options(builtin_maintenance_run_usage,\n \t\t\t\t   builtin_maintenance_run_options);\n \n \tret = maintenance_run_tasks(&opts, &cfg);\n+\n+\tstring_list_clear(&selected_tasks, 0);\n \tgc_config_release(&cfg);\n \treturn ret;\n }\n\n-- \n2.50.0.rc0.629.g846fc57c9e.dirty\n\n"},{"id":"519560","messageId":"20250603-b4-pks-maintenance-ref-lock-race-v4-5-52f5cf7b7e99@pks.im","threadId":"63531","inReplyTo":"20250603-b4-pks-maintenance-ref-lock-race-v4-0-52f5cf7b7e99@pks.im","subject":"[PATCH v4 05/12] builtin/maintenance: stop modifying global array of tasks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-03T14:01:13Z","receivedAt":"2025-06-03T14:01:25Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When configuring maintenance tasks run by git-maintenance(1) we do so by\nmodifying the global array of tasks directly. This is already quite bad\non its own, as global state makes for logic that is hard to follow.\n\nEven more importantly though we use multiple different fields to track\nwhether or not a task should be run:\n\n  - \"enabled\" tracks the \"maintenance.*.enabled\" config key. This field\n    disables execution of a task, unless the user has explicitly asked\n    for the task.\n\n  - \"selected_order\" tracks the order in which jobs have been asked for\n    by the user via the \"--task=\" command line option. It overrides\n    everything else, but only has an effect if at least one job has been\n    selected.\n\n  - \"schedule\" tracks the schedule priority for a job, that is how often\n    it should run. This field only plays a role when the user has passed\n    the \"--schedule=\" command line option.\n\nAll of this makes it non-trivial to figure out which job really should\nbe running right now. The logic to configure these fields and the logic\nthat interprets them is distributed across multiple functions, making it\neven harder to follow it.\n\nRefactor the logic so that we stop modifying global state. Instead, we\nnow compute which jobs should be run in `initialize_task_config()`,\nrepresented as an array of jobs to run that is stored in the options\nstructure. Like this, all logic becomes self-contained and any users of\nthis array only need to iterate through the tasks and execute them one\nby one.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 206 ++++++++++++++++++++++++++++++++---------------------------\n 1 file changed, 112 insertions(+), 94 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 57d7602596a..4d636237cac 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -251,7 +251,24 @@ static enum schedule_priority parse_schedule(const char *value)\n \treturn SCHEDULE_NONE;\n }\n \n+enum maintenance_task_label {\n+\tTASK_PREFETCH,\n+\tTASK_LOOSE_OBJECTS,\n+\tTASK_INCREMENTAL_REPACK,\n+\tTASK_GC,\n+\tTASK_COMMIT_GRAPH,\n+\tTASK_PACK_REFS,\n+\tTASK_REFLOG_EXPIRE,\n+\tTASK_WORKTREE_PRUNE,\n+\tTASK_RERERE_GC,\n+\n+\t/* Leave as final value */\n+\tTASK__COUNT\n+};\n+\n struct maintenance_run_opts {\n+\tenum maintenance_task_label *tasks;\n+\tsize_t tasks_nr, tasks_alloc;\n \tint auto_flag;\n \tint detach;\n \tint quiet;\n@@ -261,6 +278,11 @@ struct maintenance_run_opts {\n \t.detach = -1, \\\n }\n \n+static void maintenance_run_opts_release(struct maintenance_run_opts *opts)\n+{\n+\tfree(opts->tasks);\n+}\n+\n static int pack_refs_condition(UNUSED struct gc_config *cfg)\n {\n \t/*\n@@ -1032,6 +1054,7 @@ int cmd_gc(int argc,\n \t}\n \n out:\n+\tmaintenance_run_opts_release(&opts);\n \tgc_config_release(&cfg);\n \treturn 0;\n }\n@@ -1524,30 +1547,9 @@ struct maintenance_task {\n \tconst char *name;\n \tmaintenance_task_fn *fn;\n \tmaintenance_auto_fn *auto_condition;\n-\tunsigned enabled:1;\n-\n-\tenum schedule_priority schedule;\n-\n-\t/* -1 if not selected. */\n-\tint selected_order;\n-};\n-\n-enum maintenance_task_label {\n-\tTASK_PREFETCH,\n-\tTASK_LOOSE_OBJECTS,\n-\tTASK_INCREMENTAL_REPACK,\n-\tTASK_GC,\n-\tTASK_COMMIT_GRAPH,\n-\tTASK_PACK_REFS,\n-\tTASK_REFLOG_EXPIRE,\n-\tTASK_WORKTREE_PRUNE,\n-\tTASK_RERERE_GC,\n-\n-\t/* Leave as final value */\n-\tTASK__COUNT\n };\n \n-static struct maintenance_task tasks[] = {\n+static const struct maintenance_task tasks[] = {\n \t[TASK_PREFETCH] = {\n \t\t.name = \"prefetch\",\n \t\t.fn = maintenance_task_prefetch,\n@@ -1566,7 +1568,6 @@ static struct maintenance_task tasks[] = {\n \t\t.name = \"gc\",\n \t\t.fn = maintenance_task_gc,\n \t\t.auto_condition = need_to_gc,\n-\t\t.enabled = 1,\n \t},\n \t[TASK_COMMIT_GRAPH] = {\n \t\t.name = \"commit-graph\",\n@@ -1595,18 +1596,9 @@ static struct maintenance_task tasks[] = {\n \t},\n };\n \n-static int compare_tasks_by_selection(const void *a_, const void *b_)\n-{\n-\tconst struct maintenance_task *a = a_;\n-\tconst struct maintenance_task *b = b_;\n-\n-\treturn b->selected_order - a->selected_order;\n-}\n-\n static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \t\t\t\t struct gc_config *cfg)\n {\n-\tint i, found_selected = 0;\n \tint result = 0;\n \tstruct lock_file lk;\n \tstruct repository *r = the_repository;\n@@ -1635,95 +1627,120 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \t\ttrace2_region_leave(\"maintenance\", \"detach\", the_repository);\n \t}\n \n-\tfor (i = 0; !found_selected && i < TASK__COUNT; i++)\n-\t\tfound_selected = tasks[i].selected_order >= 0;\n-\n-\tif (found_selected)\n-\t\tQSORT(tasks, TASK__COUNT, compare_tasks_by_selection);\n-\n-\tfor (i = 0; i < TASK__COUNT; i++) {\n-\t\tif (found_selected && tasks[i].selected_order < 0)\n-\t\t\tcontinue;\n-\n-\t\tif (!found_selected && !tasks[i].enabled)\n-\t\t\tcontinue;\n-\n+\tfor (size_t i = 0; i < opts->tasks_nr; i++) {\n \t\tif (opts->auto_flag &&\n-\t\t    (!tasks[i].auto_condition ||\n-\t\t     !tasks[i].auto_condition(cfg)))\n-\t\t\tcontinue;\n-\n-\t\tif (opts->schedule && tasks[i].schedule < opts->schedule)\n+\t\t    (!tasks[opts->tasks[i]].auto_condition ||\n+\t\t     !tasks[opts->tasks[i]].auto_condition(cfg)))\n \t\t\tcontinue;\n \n-\t\ttrace2_region_enter(\"maintenance\", tasks[i].name, r);\n-\t\tif (tasks[i].fn(opts, cfg)) {\n-\t\t\terror(_(\"task '%s' failed\"), tasks[i].name);\n+\t\ttrace2_region_enter(\"maintenance\", tasks[opts->tasks[i]].name, r);\n+\t\tif (tasks[opts->tasks[i]].fn(opts, cfg)) {\n+\t\t\terror(_(\"task '%s' failed\"), tasks[opts->tasks[i]].name);\n \t\t\tresult = 1;\n \t\t}\n-\t\ttrace2_region_leave(\"maintenance\", tasks[i].name, r);\n+\t\ttrace2_region_leave(\"maintenance\", tasks[opts->tasks[i]].name, r);\n \t}\n \n \trollback_lock_file(&lk);\n \treturn result;\n }\n \n-static void initialize_maintenance_strategy(void)\n+struct maintenance_strategy {\n+\tstruct {\n+\t\tint enabled;\n+\t\tenum schedule_priority schedule;\n+\t} tasks[TASK__COUNT];\n+};\n+\n+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},\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},\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+\tstruct strbuf config_name = STRBUF_INIT;\n+\tstruct maintenance_strategy strategy;\n \tconst char *config_str;\n \n-\tif (git_config_get_string_tmp(\"maintenance.strategy\", &config_str))\n-\t\treturn;\n+\t/*\n+\t * In case the user has asked us to run tasks explicitly we only use\n+\t * those specified tasks. Specifically, we do _not_ want to consult the\n+\t * config or maintenance strategy.\n+\t */\n+\tif (selected_tasks->nr) {\n+\t\tfor (size_t i = 0; i < selected_tasks->nr; i++) {\n+\t\t\tenum maintenance_task_label label = (intptr_t)selected_tasks->items[i].util;;\n+\t\t\tALLOC_GROW(opts->tasks, opts->tasks_nr + 1, opts->tasks_alloc);\n+\t\t\topts->tasks[opts->tasks_nr++] = label;\n+\t\t}\n \n-\tif (!strcasecmp(config_str, \"incremental\")) {\n-\t\ttasks[TASK_GC].schedule = SCHEDULE_NONE;\n-\t\ttasks[TASK_COMMIT_GRAPH].enabled = 1;\n-\t\ttasks[TASK_COMMIT_GRAPH].schedule = SCHEDULE_HOURLY;\n-\t\ttasks[TASK_PREFETCH].enabled = 1;\n-\t\ttasks[TASK_PREFETCH].schedule = SCHEDULE_HOURLY;\n-\t\ttasks[TASK_INCREMENTAL_REPACK].enabled = 1;\n-\t\ttasks[TASK_INCREMENTAL_REPACK].schedule = SCHEDULE_DAILY;\n-\t\ttasks[TASK_LOOSE_OBJECTS].enabled = 1;\n-\t\ttasks[TASK_LOOSE_OBJECTS].schedule = SCHEDULE_DAILY;\n-\t\ttasks[TASK_PACK_REFS].enabled = 1;\n-\t\ttasks[TASK_PACK_REFS].schedule = SCHEDULE_WEEKLY;\n+\t\treturn;\n \t}\n-}\n \n-static void initialize_task_config(const struct string_list *selected_tasks,\n-\t\t\t\t   int schedule)\n-{\n-\tstruct strbuf config_name = STRBUF_INIT;\n+\t/*\n+\t * Otherwise, the strategy depends on whether we run as part of a\n+\t * scheduled job or not:\n+\t *\n+\t *   - Scheduled maintenance does not perform any housekeeping by\n+\t *     default, but requires the user to pick a maintenance strategy.\n+\t *\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 */\n+\tif (opts->schedule) {\n+\t\tstrategy = none_strategy;\n \n-\tfor (size_t i = 0; i < TASK__COUNT; i++)\n-\t\ttasks[i].selected_order = -1;\n-\tfor (size_t i = 0; i < selected_tasks->nr; i++) {\n-\t\tstruct maintenance_task *task = selected_tasks->items[i].util;\n-\t\ttask->selected_order = i;\n+\t\tif (!git_config_get_string_tmp(\"maintenance.strategy\", &config_str)) {\n+\t\t\tif (!strcasecmp(config_str, \"incremental\"))\n+\t\t\t\tstrategy = incremental_strategy;\n+\t\t}\n+\t} else {\n+\t\tstrategy = default_strategy;\n \t}\n \n-\tif (schedule)\n-\t\tinitialize_maintenance_strategy();\n-\n \tfor (size_t i = 0; i < TASK__COUNT; i++) {\n \t\tint config_value;\n-\t\tchar *config_str;\n \n \t\tstrbuf_reset(&config_name);\n \t\tstrbuf_addf(&config_name, \"maintenance.%s.enabled\",\n \t\t\t    tasks[i].name);\n-\n \t\tif (!git_config_get_bool(config_name.buf, &config_value))\n-\t\t\ttasks[i].enabled = config_value;\n-\n-\t\tstrbuf_reset(&config_name);\n-\t\tstrbuf_addf(&config_name, \"maintenance.%s.schedule\",\n-\t\t\t    tasks[i].name);\n+\t\t\tstrategy.tasks[i].enabled = config_value;\n+\t\tif (!strategy.tasks[i].enabled)\n+\t\t\tcontinue;\n \n-\t\tif (!git_config_get_string(config_name.buf, &config_str)) {\n-\t\t\ttasks[i].schedule = parse_schedule(config_str);\n-\t\t\tfree(config_str);\n+\t\tif (opts->schedule) {\n+\t\t\tstrbuf_reset(&config_name);\n+\t\t\tstrbuf_addf(&config_name, \"maintenance.%s.schedule\",\n+\t\t\t\t    tasks[i].name);\n+\t\t\tif (!git_config_get_string_tmp(config_name.buf, &config_str))\n+\t\t\t\tstrategy.tasks[i].schedule = parse_schedule(config_str);\n+\t\t\tif (strategy.tasks[i].schedule < opts->schedule)\n+\t\t\t\tcontinue;\n \t\t}\n+\n+\t\tALLOC_GROW(opts->tasks, opts->tasks_nr + 1, opts->tasks_alloc);\n+\t\topts->tasks[opts->tasks_nr++] = i;\n \t}\n \n \tstrbuf_release(&config_name);\n@@ -1750,7 +1767,7 @@ static int task_option_parse(const struct option *opt,\n \t\treturn 1;\n \t}\n \n-\tstring_list_append(selected_tasks, arg)->util = &tasks[i];\n+\tstring_list_append(selected_tasks, arg)->util = (void *)(intptr_t)i;\n \n \treturn 0;\n }\n@@ -1791,7 +1808,7 @@ static int maintenance_run(int argc, const char **argv, const char *prefix,\n \t\t\t\t  opts.schedule, \"--schedule=\");\n \n \tgc_config(&cfg);\n-\tinitialize_task_config(&selected_tasks, opts.schedule);\n+\tinitialize_task_config(&opts, &selected_tasks);\n \n \tif (argc != 0)\n \t\tusage_with_options(builtin_maintenance_run_usage,\n@@ -1800,6 +1817,7 @@ static int maintenance_run(int argc, const char **argv, const char *prefix,\n \tret = maintenance_run_tasks(&opts, &cfg);\n \n \tstring_list_clear(&selected_tasks, 0);\n+\tmaintenance_run_opts_release(&opts);\n \tgc_config_release(&cfg);\n \treturn ret;\n }\n\n-- \n2.50.0.rc0.629.g846fc57c9e.dirty\n\n"},{"id":"519561","messageId":"20250603-b4-pks-maintenance-ref-lock-race-v4-4-52f5cf7b7e99@pks.im","threadId":"63531","inReplyTo":"20250603-b4-pks-maintenance-ref-lock-race-v4-0-52f5cf7b7e99@pks.im","subject":"[PATCH v4 04/12] builtin/maintenance: mark \"--task=\" and \"--schedule=\" as incompatible","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-03T14:01:12Z","receivedAt":"2025-06-03T14:01:25Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"--task=\" option explicitly allows the user to say which maintenance\ntasks should be run, whereas \"--schedule=\" only respects the maintenance\nstrategy configured for a specific repository. As such, it is not\nsensible to accept both options at the same time.\n\nMark them as incompatible with one another. While at it, also convert\nthe existing logic that marks \"--auto\" and \"--schedule=\" as incompatible\nto use `die_for_incompatible_opt2()`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c           | 6 ++++--\n t/t7900-maintenance.sh | 7 ++++++-\n 2 files changed, 10 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex c4af9b11287..57d7602596a 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1785,8 +1785,10 @@ static int maintenance_run(int argc, const char **argv, const char *prefix,\n \t\t\t     builtin_maintenance_run_usage,\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n \n-\tif (opts.auto_flag && opts.schedule)\n-\t\tdie(_(\"use at most one of --auto and --schedule=<frequency>\"));\n+\tdie_for_incompatible_opt2(opts.auto_flag, \"--auto\",\n+\t\t\t\t  opts.schedule, \"--schedule=\");\n+\tdie_for_incompatible_opt2(selected_tasks.nr, \"--task=\",\n+\t\t\t\t  opts.schedule, \"--schedule=\");\n \n \tgc_config(&cfg);\n \tinitialize_task_config(&selected_tasks, opts.schedule);\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 8cf89e285f4..1ada5246606 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -610,7 +610,12 @@ test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.aut\n \n test_expect_success '--auto and --schedule incompatible' '\n \ttest_must_fail git maintenance run --auto --schedule=daily 2>err &&\n-\ttest_grep \"at most one\" err\n+\ttest_grep \"cannot be used together\" err\n+'\n+\n+test_expect_success '--task and --schedule incompatible' '\n+\ttest_must_fail git maintenance run --task=pack-refs --schedule=daily 2>err &&\n+\ttest_grep \"cannot be used together\" err\n '\n \n test_expect_success 'invalid --schedule value' '\n\n-- \n2.50.0.rc0.629.g846fc57c9e.dirty\n\n"},{"id":"519562","messageId":"20250603-b4-pks-maintenance-ref-lock-race-v4-6-52f5cf7b7e99@pks.im","threadId":"63531","inReplyTo":"20250603-b4-pks-maintenance-ref-lock-race-v4-0-52f5cf7b7e99@pks.im","subject":"[PATCH v4 06/12] builtin/maintenance: extract function to run tasks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-03T14:01:14Z","receivedAt":"2025-06-03T14:01:26Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Extract the function to run maintenance tasks. This function will be\nreused in a subsequent commit where we introduce a split between\nmaintenance tasks that run before and after daemonizing the process.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 35 +++++++++++++++++++++++------------\n 1 file changed, 23 insertions(+), 12 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 4d636237cac..cfbf9d8a2b9 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1596,6 +1596,27 @@ static const struct maintenance_task tasks[] = {\n \t},\n };\n \n+static int maybe_run_task(const struct maintenance_task *task,\n+\t\t\t  struct repository *repo,\n+\t\t\t  struct maintenance_run_opts *opts,\n+\t\t\t  struct gc_config *cfg)\n+{\n+\tint ret = 0;\n+\n+\tif (opts->auto_flag &&\n+\t    (!task->auto_condition || !task->auto_condition(cfg)))\n+\t\treturn 0;\n+\n+\ttrace2_region_enter(\"maintenance\", task->name, repo);\n+\tif (task->fn(opts, cfg)) {\n+\t\terror(_(\"task '%s' failed\"), task->name);\n+\t\tret = 1;\n+\t}\n+\ttrace2_region_leave(\"maintenance\", task->name, repo);\n+\n+\treturn ret;\n+}\n+\n static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \t\t\t\t struct gc_config *cfg)\n {\n@@ -1627,19 +1648,9 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \t\ttrace2_region_leave(\"maintenance\", \"detach\", the_repository);\n \t}\n \n-\tfor (size_t i = 0; i < opts->tasks_nr; i++) {\n-\t\tif (opts->auto_flag &&\n-\t\t    (!tasks[opts->tasks[i]].auto_condition ||\n-\t\t     !tasks[opts->tasks[i]].auto_condition(cfg)))\n-\t\t\tcontinue;\n-\n-\t\ttrace2_region_enter(\"maintenance\", tasks[opts->tasks[i]].name, r);\n-\t\tif (tasks[opts->tasks[i]].fn(opts, cfg)) {\n-\t\t\terror(_(\"task '%s' failed\"), tasks[opts->tasks[i]].name);\n+\tfor (size_t i = 0; i < opts->tasks_nr; i++)\n+\t\tif (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg))\n \t\t\tresult = 1;\n-\t\t}\n-\t\ttrace2_region_leave(\"maintenance\", tasks[opts->tasks[i]].name, r);\n-\t}\n \n \trollback_lock_file(&lk);\n \treturn result;\n\n-- \n2.50.0.rc0.629.g846fc57c9e.dirty\n\n"},{"id":"519563","messageId":"20250603-b4-pks-maintenance-ref-lock-race-v4-7-52f5cf7b7e99@pks.im","threadId":"63531","inReplyTo":"20250603-b4-pks-maintenance-ref-lock-race-v4-0-52f5cf7b7e99@pks.im","subject":"[PATCH v4 07/12] builtin/maintenance: fix typedef for function pointers","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-03T14:01:15Z","receivedAt":"2025-06-03T14:01:27Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The typedefs for `maintenance_task_fn` and `maintenance_auto_fn` are\nsomewhat confusingly not true function pointers. As such, any user of\nthose typedefs needs to manually add the pointer to make use of them.\n\nFix this by making these true function pointers.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 10 +++++-----\n 1 file changed, 5 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex cfbf9d8a2b9..447e5800846 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1533,20 +1533,20 @@ static int maintenance_task_incremental_repack(struct maintenance_run_opts *opts\n \treturn 0;\n }\n \n-typedef int maintenance_task_fn(struct maintenance_run_opts *opts,\n-\t\t\t\tstruct gc_config *cfg);\n+typedef int (*maintenance_task_fn)(struct maintenance_run_opts *opts,\n+\t\t\t\t   struct gc_config *cfg);\n \n /*\n  * An auto condition function returns 1 if the task should run\n  * and 0 if the task should NOT run. See needs_to_gc() for an\n  * example.\n  */\n-typedef int maintenance_auto_fn(struct gc_config *cfg);\n+typedef int (*maintenance_auto_fn)(struct gc_config *cfg);\n \n struct maintenance_task {\n \tconst char *name;\n-\tmaintenance_task_fn *fn;\n-\tmaintenance_auto_fn *auto_condition;\n+\tmaintenance_task_fn fn;\n+\tmaintenance_auto_fn auto_condition;\n };\n \n static const struct maintenance_task tasks[] = {\n\n-- \n2.50.0.rc0.629.g846fc57c9e.dirty\n\n"},{"id":"519564","messageId":"20250603-b4-pks-maintenance-ref-lock-race-v4-8-52f5cf7b7e99@pks.im","threadId":"63531","inReplyTo":"20250603-b4-pks-maintenance-ref-lock-race-v4-0-52f5cf7b7e99@pks.im","subject":"[PATCH v4 08/12] builtin/maintenance: split into foreground and background tasks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-03T14:01:16Z","receivedAt":"2025-06-03T14:01:27Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Both git-gc(1) and git-maintenance(1) have logic to daemonize so that\nthe maintenance tasks are performed in the background. git-gc(1) has\nsome special logic though to not perform _all_ housekeeping tasks in the\nbackground: both references and reflogs are still handled synchronously\nin the foreground.\n\nThis split exists because otherwise it may easily happen that git-gc(1)\nkeeps the \"packed-refs\" file locked for an extended amount of time,\nwhere the next Git command that wants to modify any reference could now\nfail. This was especially important in the past, where git-gc(1) was\nstill executed directly as part of our automatic maintenance: git-gc(1)\nwas invoked via `git gc --auto --detach`, so we knew to handle most of\nthe maintenance tasks in the background while doing those parts that may\ncause locking issues in the foreground.\n\nWe have since moved to git-maintenance(1), which is a more flexible\nreplacement for git-gc(1). By default this command runs git-gc(1), only,\nbut it can be configured to run different tasks, as well. This command\ndoes not know about the split between maintenance tasks that should run\nbefore and after detach though, and this has led to several bug reports\nabout spurious locking errors for the \"packed-refs\" file.\n\nPrepare for a fix by introducing this split for maintenance tasks. Note\nthat this commit does not yet change any of the tasks, so there should\nnot (yet) be a change in behaviour.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 70 ++++++++++++++++++++++++++++++++++++++++++------------------\n 1 file changed, 49 insertions(+), 21 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 447e5800846..72a695853e5 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1535,84 +1535,106 @@ static int maintenance_task_incremental_repack(struct maintenance_run_opts *opts\n \n typedef int (*maintenance_task_fn)(struct maintenance_run_opts *opts,\n \t\t\t\t   struct gc_config *cfg);\n-\n-/*\n- * An auto condition function returns 1 if the task should run\n- * and 0 if the task should NOT run. See needs_to_gc() for an\n- * example.\n- */\n typedef int (*maintenance_auto_fn)(struct gc_config *cfg);\n \n struct maintenance_task {\n \tconst char *name;\n-\tmaintenance_task_fn fn;\n+\n+\t/*\n+\t * Work that will be executed before detaching. This should not include\n+\t * tasks that may run for an extended amount of time as it does cause\n+\t * auto-maintenance to block until foreground tasks have been run.\n+\t */\n+\tmaintenance_task_fn foreground;\n+\n+\t/*\n+\t * Work that will be executed after detaching. When not detaching the\n+\t * work will be run in the foreground, as well.\n+\t */\n+\tmaintenance_task_fn background;\n+\n+\t/*\n+\t * An auto condition function returns 1 if the task should run and 0 if\n+\t * the task should NOT run. See needs_to_gc() for an example.\n+\t */\n \tmaintenance_auto_fn auto_condition;\n };\n \n static const struct maintenance_task tasks[] = {\n \t[TASK_PREFETCH] = {\n \t\t.name = \"prefetch\",\n-\t\t.fn = maintenance_task_prefetch,\n+\t\t.background = maintenance_task_prefetch,\n \t},\n \t[TASK_LOOSE_OBJECTS] = {\n \t\t.name = \"loose-objects\",\n-\t\t.fn = maintenance_task_loose_objects,\n+\t\t.background = maintenance_task_loose_objects,\n \t\t.auto_condition = loose_object_auto_condition,\n \t},\n \t[TASK_INCREMENTAL_REPACK] = {\n \t\t.name = \"incremental-repack\",\n-\t\t.fn = maintenance_task_incremental_repack,\n+\t\t.background = maintenance_task_incremental_repack,\n \t\t.auto_condition = incremental_repack_auto_condition,\n \t},\n \t[TASK_GC] = {\n \t\t.name = \"gc\",\n-\t\t.fn = maintenance_task_gc,\n+\t\t.background = maintenance_task_gc,\n \t\t.auto_condition = need_to_gc,\n \t},\n \t[TASK_COMMIT_GRAPH] = {\n \t\t.name = \"commit-graph\",\n-\t\t.fn = maintenance_task_commit_graph,\n+\t\t.background = maintenance_task_commit_graph,\n \t\t.auto_condition = should_write_commit_graph,\n \t},\n \t[TASK_PACK_REFS] = {\n \t\t.name = \"pack-refs\",\n-\t\t.fn = maintenance_task_pack_refs,\n+\t\t.background = maintenance_task_pack_refs,\n \t\t.auto_condition = pack_refs_condition,\n \t},\n \t[TASK_REFLOG_EXPIRE] = {\n \t\t.name = \"reflog-expire\",\n-\t\t.fn = maintenance_task_reflog_expire,\n+\t\t.background = maintenance_task_reflog_expire,\n \t\t.auto_condition = reflog_expire_condition,\n \t},\n \t[TASK_WORKTREE_PRUNE] = {\n \t\t.name = \"worktree-prune\",\n-\t\t.fn = maintenance_task_worktree_prune,\n+\t\t.background = maintenance_task_worktree_prune,\n \t\t.auto_condition = worktree_prune_condition,\n \t},\n \t[TASK_RERERE_GC] = {\n \t\t.name = \"rerere-gc\",\n-\t\t.fn = maintenance_task_rerere_gc,\n+\t\t.background = maintenance_task_rerere_gc,\n \t\t.auto_condition = rerere_gc_condition,\n \t},\n };\n \n+enum task_phase {\n+\tTASK_PHASE_FOREGROUND,\n+\tTASK_PHASE_BACKGROUND,\n+};\n+\n static int maybe_run_task(const struct maintenance_task *task,\n \t\t\t  struct repository *repo,\n \t\t\t  struct maintenance_run_opts *opts,\n-\t\t\t  struct gc_config *cfg)\n+\t\t\t  struct gc_config *cfg,\n+\t\t\t  enum task_phase phase)\n {\n+\tint foreground = (phase == TASK_PHASE_FOREGROUND);\n+\tmaintenance_task_fn fn = foreground ? task->foreground : task->background;\n+\tconst char *region = foreground ? \"maintenance foreground\" : \"maintenance\";\n \tint ret = 0;\n \n+\tif (!fn)\n+\t\treturn 0;\n \tif (opts->auto_flag &&\n \t    (!task->auto_condition || !task->auto_condition(cfg)))\n \t\treturn 0;\n \n-\ttrace2_region_enter(\"maintenance\", task->name, repo);\n-\tif (task->fn(opts, cfg)) {\n+\ttrace2_region_enter(region, task->name, repo);\n+\tif (fn(opts, cfg)) {\n \t\terror(_(\"task '%s' failed\"), task->name);\n \t\tret = 1;\n \t}\n-\ttrace2_region_leave(\"maintenance\", task->name, repo);\n+\ttrace2_region_leave(region, task->name, repo);\n \n \treturn ret;\n }\n@@ -1641,6 +1663,11 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \t}\n \tfree(lock_path);\n \n+\tfor (size_t i = 0; i < opts->tasks_nr; i++)\n+\t\tif (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg,\n+\t\t\t\t   TASK_PHASE_FOREGROUND))\n+\t\t\tresult = 1;\n+\n \t/* Failure to daemonize is ok, we'll continue in foreground. */\n \tif (opts->detach > 0) {\n \t\ttrace2_region_enter(\"maintenance\", \"detach\", the_repository);\n@@ -1649,7 +1676,8 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \t}\n \n \tfor (size_t i = 0; i < opts->tasks_nr; i++)\n-\t\tif (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg))\n+\t\tif (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg,\n+\t\t\t\t   TASK_PHASE_BACKGROUND))\n \t\t\tresult = 1;\n \n \trollback_lock_file(&lk);\n\n-- \n2.50.0.rc0.629.g846fc57c9e.dirty\n\n"},{"id":"519565","messageId":"20250603-b4-pks-maintenance-ref-lock-race-v4-9-52f5cf7b7e99@pks.im","threadId":"63531","inReplyTo":"20250603-b4-pks-maintenance-ref-lock-race-v4-0-52f5cf7b7e99@pks.im","subject":"[PATCH v4 09/12] builtin/maintenance: fix locking race with refs and reflogs tasks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-03T14:01:17Z","receivedAt":"2025-06-03T14:01:29Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"As explained in the preceding commit, git-gc(1) knows to detach only\nafter it has already packed references and expired reflogs. This is done\nto avoid racing around their respective lockfiles.\n\nAdapt git-maintenance(1) accordingly and run the \"pack-refs\" and\n\"reflog-expire\" tasks in the foreground. Note that the \"gc\" task has the\nsame issue, but the fix is a bit more involved there and will thus be\ndone in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 72a695853e5..fdd0dd09be7 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1587,12 +1587,12 @@ static const struct maintenance_task tasks[] = {\n \t},\n \t[TASK_PACK_REFS] = {\n \t\t.name = \"pack-refs\",\n-\t\t.background = maintenance_task_pack_refs,\n+\t\t.foreground = maintenance_task_pack_refs,\n \t\t.auto_condition = pack_refs_condition,\n \t},\n \t[TASK_REFLOG_EXPIRE] = {\n \t\t.name = \"reflog-expire\",\n-\t\t.background = maintenance_task_reflog_expire,\n+\t\t.foreground = maintenance_task_reflog_expire,\n \t\t.auto_condition = reflog_expire_condition,\n \t},\n \t[TASK_WORKTREE_PRUNE] = {\n\n-- \n2.50.0.rc0.629.g846fc57c9e.dirty\n\n"},{"id":"519566","messageId":"20250603-b4-pks-maintenance-ref-lock-race-v4-11-52f5cf7b7e99@pks.im","threadId":"63531","inReplyTo":"20250603-b4-pks-maintenance-ref-lock-race-v4-0-52f5cf7b7e99@pks.im","subject":"[PATCH v4 11/12] builtin/gc: avoid global state in `gc_before_repack()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-03T14:01:19Z","receivedAt":"2025-06-03T14:01:29Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The `gc_before_repack()` should only ever run once in git-gc(1), but we\nmay end up calling it twice when the \"--detach\" flag is passed. The\nduplicated call is avoided though via a static flag in this function.\n\nThis pattern is somewhat unintuitive though. Refactor it to drop the\nstatic flag and instead guard the second call of `gc_before_repack()`\nvia `opts.detach`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 24 +++++++++---------------\n 1 file changed, 9 insertions(+), 15 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex fdd0dd09be7..4a5c4b20442 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -816,22 +816,14 @@ static int report_last_gc_error(void)\n \treturn ret;\n }\n \n-static void gc_before_repack(struct maintenance_run_opts *opts,\n-\t\t\t     struct gc_config *cfg)\n+static int gc_before_repack(struct maintenance_run_opts *opts,\n+\t\t\t    struct gc_config *cfg)\n {\n-\t/*\n-\t * We may be called twice, as both the pre- and\n-\t * post-daemonized phases will call us, but running these\n-\t * commands more than once is pointless and wasteful.\n-\t */\n-\tstatic int done = 0;\n-\tif (done++)\n-\t\treturn;\n-\n \tif (cfg->pack_refs && maintenance_task_pack_refs(opts, cfg))\n-\t\tdie(FAILED_RUN, \"pack-refs\");\n+\t\treturn error(FAILED_RUN, \"pack-refs\");\n \tif (cfg->prune_reflogs && maintenance_task_reflog_expire(opts, cfg))\n-\t\tdie(FAILED_RUN, \"reflog\");\n+\t\treturn error(FAILED_RUN, \"reflog\");\n+\treturn 0;\n }\n \n int cmd_gc(int argc,\n@@ -965,7 +957,8 @@ int cmd_gc(int argc,\n \t\t\tgoto out;\n \t\t}\n \n-\t\tgc_before_repack(&opts, &cfg); /* dies on failure */\n+\t\tif (gc_before_repack(&opts, &cfg) < 0)\n+\t\t\tdie(NULL);\n \t\tdelete_tempfile(&pidfile);\n \n \t\t/*\n@@ -995,7 +988,8 @@ int cmd_gc(int argc,\n \t\tfree(path);\n \t}\n \n-\tgc_before_repack(&opts, &cfg);\n+\tif (opts.detach <= 0)\n+\t\tgc_before_repack(&opts, &cfg);\n \n \tif (!repository_format_precious_objects) {\n \t\tstruct child_process repack_cmd = CHILD_PROCESS_INIT;\n\n-- \n2.50.0.rc0.629.g846fc57c9e.dirty\n\n"},{"id":"519567","messageId":"20250603-b4-pks-maintenance-ref-lock-race-v4-10-52f5cf7b7e99@pks.im","threadId":"63531","inReplyTo":"20250603-b4-pks-maintenance-ref-lock-race-v4-0-52f5cf7b7e99@pks.im","subject":"[PATCH v4 10/12] usage: allow dying without writing an error message","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-03T14:01:18Z","receivedAt":"2025-06-03T14:01:29Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Sometimes code wants to die in a situation where it already has written\nan error message. To use the same error code as `die()` we have to use\n`exit(128)`, which is easy to get wrong and leaves magic numbers all\nover our codebase.\n\nTeach `die_message_builtin()` to not print any error when passed a\n`NULL` pointer as error string. Like this, such users can now call\n`die(NULL)` to achieve the same result without any hardcoded error\ncodes.\n\nAdapt a couple of builtins to use this new pattern to demonstrate that\nthere is a need for such a helper.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/am.c                |  4 ++--\n builtin/checkout.c          |  4 ++--\n builtin/fetch.c             |  2 +-\n builtin/submodule--helper.c | 12 ++++++------\n usage.c                     |  2 ++\n 5 files changed, 13 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/am.c b/builtin/am.c\nindex e32a3b4c973..a800003340f 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -1000,7 +1000,7 @@ static void am_setup(struct am_state *state, enum patch_format patch_format,\n \n \tif (!patch_format) {\n \t\tfprintf_ln(stderr, _(\"Patch format detection failed.\"));\n-\t\texit(128);\n+\t\tdie(NULL);\n \t}\n \n \tif (mkdir(state->dir, 0777) < 0 && errno != EEXIST)\n@@ -1178,7 +1178,7 @@ static void NORETURN die_user_resolve(const struct am_state *state)\n \t\tstrbuf_release(&sb);\n \t}\n \n-\texit(128);\n+\tdie(NULL);\n }\n \n /**\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex d185982f3a6..536192d3456 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -838,7 +838,7 @@ static int merge_working_tree(const struct checkout_opts *opts,\n \t\tinit_tree_desc(&trees[0], &tree->object.oid,\n \t\t\t       tree->buffer, tree->size);\n \t\tif (parse_tree(new_tree) < 0)\n-\t\t\texit(128);\n+\t\t\tdie(NULL);\n \t\ttree = new_tree;\n \t\tinit_tree_desc(&trees[1], &tree->object.oid,\n \t\t\t       tree->buffer, tree->size);\n@@ -913,7 +913,7 @@ static int merge_working_tree(const struct checkout_opts *opts,\n \t\t\t\t\t\t     work,\n \t\t\t\t\t\t     old_tree);\n \t\t\tif (ret < 0)\n-\t\t\t\texit(128);\n+\t\t\t\tdie(NULL);\n \t\t\tret = reset_tree(new_tree,\n \t\t\t\t\t opts, 0,\n \t\t\t\t\t writeout_error, new_branch_info);\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex cda6eaf1fd6..b0800ea5829 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -992,7 +992,7 @@ static int update_local_ref(struct ref *ref,\n \t\tfast_forward = repo_in_merge_bases(the_repository, current,\n \t\t\t\t\t\t   updated);\n \t\tif (fast_forward < 0)\n-\t\t\texit(128);\n+\t\t\tdie(NULL);\n \t\tforced_updates_ms += (getnanotime() - t_before) / 1000000;\n \t} else {\n \t\tfast_forward = 1;\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 53da2116ddf..4255caca579 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -303,7 +303,7 @@ static void runcommand_in_submodule_cb(const struct cache_entry *list_item,\n \tchar *displaypath;\n \n \tif (validate_submodule_path(path) < 0)\n-\t\texit(128);\n+\t\tdie(NULL);\n \n \tdisplaypath = get_submodule_displaypath(path, info->prefix,\n \t\t\t\t\t\tinfo->super_prefix);\n@@ -643,7 +643,7 @@ static void status_submodule(const char *path, const struct object_id *ce_oid,\n \t};\n \n \tif (validate_submodule_path(path) < 0)\n-\t\texit(128);\n+\t\tdie(NULL);\n \n \tif (!submodule_from_path(the_repository, null_oid(the_hash_algo), path))\n \t\tdie(_(\"no submodule mapping found in .gitmodules for path '%s'\"),\n@@ -1257,7 +1257,7 @@ static void sync_submodule(const char *path, const char *prefix,\n \t\treturn;\n \n \tif (validate_submodule_path(path) < 0)\n-\t\texit(128);\n+\t\tdie(NULL);\n \n \tsub = submodule_from_path(the_repository, null_oid(the_hash_algo), path);\n \n@@ -1402,7 +1402,7 @@ static void deinit_submodule(const char *path, const char *prefix,\n \tchar *sub_git_dir = xstrfmt(\"%s/.git\", path);\n \n \tif (validate_submodule_path(path) < 0)\n-\t\texit(128);\n+\t\tdie(NULL);\n \n \tsub = submodule_from_path(the_repository, null_oid(the_hash_algo), path);\n \n@@ -1724,7 +1724,7 @@ static int clone_submodule(const struct module_clone_data *clone_data,\n \tchar *to_free = NULL;\n \n \tif (validate_submodule_path(clone_data_path) < 0)\n-\t\texit(128);\n+\t\tdie(NULL);\n \n \tif (!is_absolute_path(clone_data->path))\n \t\tclone_data_path = to_free = xstrfmt(\"%s/%s\", repo_get_work_tree(the_repository),\n@@ -3524,7 +3524,7 @@ static int module_add(int argc, const char **argv, const char *prefix,\n \tstrip_dir_trailing_slashes(add_data.sm_path);\n \n \tif (validate_submodule_path(add_data.sm_path) < 0)\n-\t\texit(128);\n+\t\tdie(NULL);\n \n \tdie_on_index_match(add_data.sm_path, force);\n \tdie_on_repo_without_commits(add_data.sm_path);\ndiff --git a/usage.c b/usage.c\nindex 38b46bbbfe7..cd7b57d6446 100644\n--- a/usage.c\n+++ b/usage.c\n@@ -67,6 +67,8 @@ static NORETURN void usage_builtin(const char *err, va_list params)\n \n static void die_message_builtin(const char *err, va_list params)\n {\n+\tif (!err)\n+\t\treturn;\n \ttrace2_cmd_error_va(err, params);\n \tvreportf(_(\"fatal: \"), err, params);\n }\n\n-- \n2.50.0.rc0.629.g846fc57c9e.dirty\n\n"},{"id":"519568","messageId":"20250603-b4-pks-maintenance-ref-lock-race-v4-12-52f5cf7b7e99@pks.im","threadId":"63531","inReplyTo":"20250603-b4-pks-maintenance-ref-lock-race-v4-0-52f5cf7b7e99@pks.im","subject":"[PATCH v4 12/12] builtin/maintenance: fix locking race when handling \"gc\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-03T14:01:20Z","receivedAt":"2025-06-03T14:01:31Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"gc\" task has a similar locking race as the one that we have fixed\nfor the \"pack-refs\" and \"reflog-expire\" tasks in preceding commits. Fix\nthis by splitting up the logic of the \"gc\" task:\n\n  - We execute `gc_before_repack()` in the foreground, which contains\n    the logic that git-gc(1) itself would execute in the foreground, as\n    well.\n\n  - We spawn git-gc(1) after detaching, but with a new hidden flag that\n    suppresses calling `gc_before_repack()`.\n\nLike this we have roughly the same logic as git-gc(1) itself and know to\nrepack refs and reflogs before detaching, thus fixing the race.\n\nNote that `gc_before_repack()` is renamed to `gc_foreground_tasks()` to\nbetter reflect what this function does.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c           | 41 +++++++++++++++++++++++++++--------------\n t/t7900-maintenance.sh | 12 ++++++------\n 2 files changed, 33 insertions(+), 20 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 4a5c4b20442..b5e6519d597 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -816,8 +816,8 @@ static int report_last_gc_error(void)\n \treturn ret;\n }\n \n-static int gc_before_repack(struct maintenance_run_opts *opts,\n-\t\t\t    struct gc_config *cfg)\n+static int gc_foreground_tasks(struct maintenance_run_opts *opts,\n+\t\t\t       struct gc_config *cfg)\n {\n \tif (cfg->pack_refs && maintenance_task_pack_refs(opts, cfg))\n \t\treturn error(FAILED_RUN, \"pack-refs\");\n@@ -837,6 +837,7 @@ int cmd_gc(int argc,\n \tpid_t pid;\n \tint daemonized = 0;\n \tint keep_largest_pack = -1;\n+\tint skip_foreground_tasks = 0;\n \ttimestamp_t dummy;\n \tstruct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT;\n \tstruct gc_config cfg = GC_CONFIG_INIT;\n@@ -869,6 +870,8 @@ int cmd_gc(int argc,\n \t\t\t N_(\"repack all other packs except the largest pack\")),\n \t\tOPT_STRING(0, \"expire-to\", &cfg.repack_expire_to, N_(\"dir\"),\n \t\t\t   N_(\"pack prefix to store a pack containing pruned objects\")),\n+\t\tOPT_HIDDEN_BOOL(0, \"skip-foreground-tasks\", &skip_foreground_tasks,\n+\t\t\t   N_(\"skip maintenance tasks typically done in the foreground\")),\n \t\tOPT_END()\n \t};\n \n@@ -952,14 +955,16 @@ int cmd_gc(int argc,\n \t\t\tgoto out;\n \t\t}\n \n-\t\tif (lock_repo_for_gc(force, &pid)) {\n-\t\t\tret = 0;\n-\t\t\tgoto out;\n-\t\t}\n+\t\tif (!skip_foreground_tasks) {\n+\t\t\tif (lock_repo_for_gc(force, &pid)) {\n+\t\t\t\tret = 0;\n+\t\t\t\tgoto out;\n+\t\t\t}\n \n-\t\tif (gc_before_repack(&opts, &cfg) < 0)\n-\t\t\tdie(NULL);\n-\t\tdelete_tempfile(&pidfile);\n+\t\t\tif (gc_foreground_tasks(&opts, &cfg) < 0)\n+\t\t\t\tdie(NULL);\n+\t\t\tdelete_tempfile(&pidfile);\n+\t\t}\n \n \t\t/*\n \t\t * failure to daemonize is ok, we'll continue\n@@ -988,8 +993,8 @@ int cmd_gc(int argc,\n \t\tfree(path);\n \t}\n \n-\tif (opts.detach <= 0)\n-\t\tgc_before_repack(&opts, &cfg);\n+\tif (opts.detach <= 0 && !skip_foreground_tasks)\n+\t\tgc_foreground_tasks(&opts, &cfg);\n \n \tif (!repository_format_precious_objects) {\n \t\tstruct child_process repack_cmd = CHILD_PROCESS_INIT;\n@@ -1225,8 +1230,14 @@ static int maintenance_task_prefetch(struct maintenance_run_opts *opts,\n \treturn 0;\n }\n \n-static int maintenance_task_gc(struct maintenance_run_opts *opts,\n-\t\t\t       struct gc_config *cfg UNUSED)\n+static int maintenance_task_gc_foreground(struct maintenance_run_opts *opts,\n+\t\t\t\t\t  struct gc_config *cfg)\n+{\n+\treturn gc_foreground_tasks(opts, cfg);\n+}\n+\n+static int maintenance_task_gc_background(struct maintenance_run_opts *opts,\n+\t\t\t\t\t  struct gc_config *cfg UNUSED)\n {\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \n@@ -1240,6 +1251,7 @@ static int maintenance_task_gc(struct maintenance_run_opts *opts,\n \telse\n \t\tstrvec_push(&child.args, \"--no-quiet\");\n \tstrvec_push(&child.args, \"--no-detach\");\n+\tstrvec_push(&child.args, \"--skip-foreground-tasks\");\n \n \treturn run_command(&child);\n }\n@@ -1571,7 +1583,8 @@ static const struct maintenance_task tasks[] = {\n \t},\n \t[TASK_GC] = {\n \t\t.name = \"gc\",\n-\t\t.background = maintenance_task_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},\n \t[TASK_COMMIT_GRAPH] = {\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 1ada5246606..ddd273d8dc2 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -49,9 +49,9 @@ test_expect_success 'run [--auto|--quiet]' '\n \t\tgit maintenance run --auto 2>/dev/null &&\n \tGIT_TRACE2_EVENT=\"$(pwd)/run-no-quiet.txt\" \\\n \t\tgit maintenance run --no-quiet 2>/dev/null &&\n-\ttest_subcommand git gc --quiet --no-detach <run-no-auto.txt &&\n-\ttest_subcommand ! git gc --auto --quiet --no-detach <run-auto.txt &&\n-\ttest_subcommand git gc --no-quiet --no-detach <run-no-quiet.txt\n+\ttest_subcommand git gc --quiet --no-detach --skip-foreground-tasks <run-no-auto.txt &&\n+\ttest_subcommand ! git gc --auto --quiet --no-detach --skip-foreground-tasks <run-auto.txt &&\n+\ttest_subcommand git gc --no-quiet --no-detach --skip-foreground-tasks <run-no-quiet.txt\n '\n \n test_expect_success 'maintenance.auto config option' '\n@@ -154,9 +154,9 @@ test_expect_success 'run --task=<task>' '\n \t\tgit maintenance run --task=commit-graph 2>/dev/null &&\n \tGIT_TRACE2_EVENT=\"$(pwd)/run-both.txt\" \\\n \t\tgit maintenance run --task=commit-graph --task=gc 2>/dev/null &&\n-\ttest_subcommand ! git gc --quiet --no-detach <run-commit-graph.txt &&\n-\ttest_subcommand git gc --quiet --no-detach <run-gc.txt &&\n-\ttest_subcommand git gc --quiet --no-detach <run-both.txt &&\n+\ttest_subcommand ! git gc --quiet --no-detach --skip-foreground-tasks <run-commit-graph.txt &&\n+\ttest_subcommand git gc --quiet --no-detach --skip-foreground-tasks <run-gc.txt &&\n+\ttest_subcommand git gc --quiet --no-detach --skip-foreground-tasks <run-both.txt &&\n \ttest_subcommand git commit-graph write --split --reachable --no-progress <run-commit-graph.txt &&\n \ttest_subcommand ! git commit-graph write --split --reachable --no-progress <run-gc.txt &&\n \ttest_subcommand git commit-graph write --split --reachable --no-progress <run-both.txt\n\n-- \n2.50.0.rc0.629.g846fc57c9e.dirty\n\n"},{"id":"519576","messageId":"xmqqmsaoq244.fsf@gitster.g","threadId":"63531","inReplyTo":"20250602-b4-pks-maintenance-ref-lock-race-v3-10-587d44252dcb@pks.im","subject":"Re: [PATCH v3 10/12] usage: allow dying without writing an error message","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-06-03T15:24:59Z","receivedAt":"2025-06-03T15:25:03Z","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> Sometimes code wants to die in a situation where it already has written\n> an error message. To use the same error code as `die()` we have to open\n> code the code with a call to `exit(128)` in such cases, which is easy to\n> get wrong and leaves magical numbers all over our codebase.\n> ...\n>  \tif (!patch_format) {\n>  \t\tfprintf_ln(stderr, _(\"Patch format detection failed.\"));\n> -\t\texit(128);\n> +\t\tdie(NULL);\n\nIt is somewhat surprising that the compiler would not complain with\n\"Hey, a NULL string as printf format string???\" given its decl.\n\nNORETURN void die(const char *err, ...) __attribute__((format (printf, 1, 2)));\n\nAs long as we are sure that compilers we care about are OK with\nthis, it is a very nice ergonomics enhancement.\n"},{"id":"519577","messageId":"28f38435-159d-426a-95e1-fc3e4fbd4118@app.fastmail.com","threadId":"63531","inReplyTo":"20250603-b4-pks-maintenance-ref-lock-race-v4-0-52f5cf7b7e99@pks.im","subject":"Re: [PATCH v4 00/12] builtin/maintenance: fix ref lock races when detaching","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-06-03T15:31:46Z","receivedAt":"2025-06-03T15:32:22Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Tue, Jun 3, 2025, at 16:01, Patrick Steinhardt wrote:\n> [snip]\n>     @@ Metadata\n>      Author: Patrick Steinhardt <ps@pks.im>\n>     \n>       ## Commit message ##\n> [snip]\n\nLooks good!\n"},{"id":"519622","messageId":"BEB56610-BFED-4DF1-A2C1-7BBE812850B7@gmail.com","threadId":"63531","inReplyTo":"20250603-b4-pks-maintenance-ref-lock-race-v4-0-52f5cf7b7e99@pks.im","subject":"Re: [PATCH v4 00/12] builtin/maintenance: fix ref lock races when detaching","fromName":"Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-06-04T00:35:36Z","receivedAt":"2025-06-04T00:35:48Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"\n> \n> Le 3 juin 2025 à 10:01, Patrick Steinhardt <ps@pks.im> a écrit :\n> \n> ﻿Hi,\n> \n> this patch series fixes races around locking the \"packed-refs\" file when\n> auto-maintenance decides to repack it. This issue has been reported e.g.\n> via [1] and [2].\n> \n> The root cause is that git-gc(1) used to know to detach _after_ having\n> repacked references. As such, callers wouldn't continue with their thing\n> until we have already packed refs, and thus the race does not exist\n> there. git-maintenance(1) didn't have the same split though, so this\n> patch series retrofits that logic.\n> \n> The series is structured as follows:\n> \n>  - Patches 1 and 2 do some light refactorings.\n> \n>  - Patches 3 to 5 refactor how we set up the list of tasks to not rely\n>    on globals anymore. Instead, we now have a single source of truth\n>    for which tasks exactly will be run.\n> \n>  - The remaining patches introduce the split of before/after-detach\n>    tasks and wire them up for \"pack-refs\", \"reflog-expire\" and \"gc\"\n>    tasks.\n> \n> Changes in v2:\n>  - A couple of commit message improvements.\n>  - Introduce `die(NULL)` to die with the correct exit code but no error\n>    message. This gets rid of some magic numbers.\n>  - Introduce an enum to discern the phases before and after detach.\n>  - Link to v1: https://lore.kernel.org/r/20250527-b4-pks-maintenance-ref-lock-race-v1-0-e1ceb2dea66e@pks.im\n> \n> Changes in v3:\n>  - Rework logic to talk about foreground/background tasks instead of\n>    before/after detach.\n>  - Link to v2: https://lore.kernel.org/r/20250530-b4-pks-maintenance-ref-lock-race-v2-0-d04e2f93e51f@pks.im\n> \n> Changes in v4:\n>  - Some more massaging of commit messages.\n>  - Link to v3: https://lore.kernel.org/r/20250602-b4-pks-maintenance-ref-lock-race-v3-0-587d44252dcb@pks.im\n> \n> Thanks!\n> \n> Patrick\n> \n> [1]: <CAJR-fbZ4X1+gN75m2dUvocR6NkowLOZ9F26cjBy8w1qd181OoQ@mail.gmail.com>\n> [2]: <CANi7bVAkNc+gY1NoXfJuDRjxjZLTgL8Lfn8_ZmWsvLAoiLPkNg@mail.gmail.com>\n> \n> ---\n> Patrick Steinhardt (12):\n>      builtin/gc: use designated field initializers for maintenance tasks\n>      builtin/gc: drop redundant local variable\n>      builtin/maintenance: centralize configuration of explicit tasks\n>      builtin/maintenance: mark \"--task=\" and \"--schedule=\" as incompatible\n>      builtin/maintenance: stop modifying global array of tasks\n>      builtin/maintenance: extract function to run tasks\n>      builtin/maintenance: fix typedef for function pointers\n>      builtin/maintenance: split into foreground and background tasks\n>      builtin/maintenance: fix locking race with refs and reflogs tasks\n>      usage: allow dying without writing an error message\n>      builtin/gc: avoid global state in `gc_before_repack()`\n>      builtin/maintenance: fix locking race when handling \"gc\" task\n> \n> builtin/am.c                |   4 +-\n> builtin/checkout.c          |   4 +-\n> builtin/fetch.c             |   2 +-\n> builtin/gc.c                | 410 +++++++++++++++++++++++++-------------------\n> builtin/submodule--helper.c |  12 +-\n> t/t7900-maintenance.sh      |  19 +-\n> usage.c                     |   2 +\n> 7 files changed, 263 insertions(+), 190 deletions(-)\n> \n> Range-diff versus v3:\n> \n> 1:  e46a65951b9 =  1:  280f13d2895 builtin/gc: use designated field initializers for maintenance tasks\n> 2:  73cd67f3e1a =  2:  16a017fb819 builtin/gc: drop redundant local variable\n> 3:  a02452a6d6f =  3:  0ab3344ddb0 builtin/maintenance: centralize configuration of explicit tasks\n> 4:  ccd7691e4d5 =  4:  69e768cb54e builtin/maintenance: mark \"--task=\" and \"--schedule=\" as incompatible\n> 5:  0e243fd81e6 =  5:  295e9e5ee9f builtin/maintenance: stop modifying global array of tasks\n> 6:  c95bd62823e =  6:  d94b0c86622 builtin/maintenance: extract function to run tasks\n> 7:  43d28434d8e =  7:  0bbba671cd0 builtin/maintenance: fix typedef for function pointers\n> 8:  d5740a5c9d9 =  8:  4ce38539bb6 builtin/maintenance: split into foreground and background tasks\n> 9:  168eb3a9372 !  9:  28092b9bed1 builtin/maintenance: fix locking race when packing refs and reflogs\n>    @@ Metadata\n>     Author: Patrick Steinhardt <ps@pks.im>\n> \n>      ## Commit message ##\n>    -    builtin/maintenance: fix locking race when packing refs and reflogs\n>    +    builtin/maintenance: fix locking race with refs and reflogs tasks\n> \n>         As explained in the preceding commit, git-gc(1) knows to detach only\n>    -    after it has already packed references and reflogs. This is done to\n>    -    avoid racing around their respective lockfiles.\n>    +    after it has already packed references and expired reflogs. This is done\n>    +    to avoid racing around their respective lockfiles.\n> \n>         Adapt git-maintenance(1) accordingly and run the \"pack-refs\" and\n>         \"reflog-expire\" tasks in the foreground. Note that the \"gc\" task has the\n> 10:  0ff01f6e2aa ! 10:  b8ed080c67d usage: allow dying without writing an error message\n>    @@ Commit message\n>         usage: allow dying without writing an error message\n> \n>         Sometimes code wants to die in a situation where it already has written\n>    -    an error message. To use the same error code as `die()` we have to open\n>    -    code the code with a call to `exit(128)` in such cases, which is easy to\n>    -    get wrong and leaves magical numbers all over our codebase.\n>    +    an error message. To use the same error code as `die()` we have to use\n>    +    `exit(128)`, which is easy to get wrong and leaves magic numbers all\n>    +    over our codebase.\n> \n>         Teach `die_message_builtin()` to not print any error when passed a\n>         `NULL` pointer as error string. Like this, such users can now call\n> 11:  93f53000e47 = 11:  5b149886263 builtin/gc: avoid global state in `gc_before_repack()`\n> 12:  01095d1bf88 = 12:  9ba01f143b3 builtin/maintenance: fix locking race when handling \"gc\" task\n> \n> ---\n> base-commit: 845c48a16a7f7b2c44d8cb137b16a4a1f0140229\n> change-id: 20250527-b4-pks-maintenance-ref-lock-race-11ae5d68e06f\n\nRange-diff vs v3 (and v2!) looks good to me; thanks for taking the time!"},{"id":"519768","messageId":"aEGVbz7fWmm3yf1L@pks.im","threadId":"63531","inReplyTo":"xmqqmsaoq244.fsf@gitster.g","subject":"Re: [PATCH v3 10/12] usage: allow dying without writing an error message","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-05T13:02:39Z","receivedAt":"2025-06-05T13:02:54Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Jun 03, 2025 at 08:24:59AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > Sometimes code wants to die in a situation where it already has written\n> > an error message. To use the same error code as `die()` we have to open\n> > code the code with a call to `exit(128)` in such cases, which is easy to\n> > get wrong and leaves magical numbers all over our codebase.\n> > ...\n> >  \tif (!patch_format) {\n> >  \t\tfprintf_ln(stderr, _(\"Patch format detection failed.\"));\n> > -\t\texit(128);\n> > +\t\tdie(NULL);\n> \n> It is somewhat surprising that the compiler would not complain with\n> \"Hey, a NULL string as printf format string???\" given its decl.\n> \n> NORETURN void die(const char *err, ...) __attribute__((format (printf, 1, 2)));\n> \n> As long as we are sure that compilers we care about are OK with\n> this, it is a very nice ergonomics enhancement.\n\nHard to say. The documentation of -Wformat doesn't explicitly mention\nwhat is expected to happen in that case. I tried compiling with\n-Wformat=2, and that didn't generate any warnings for those calls to\n`die(NULL)`. It did generate a bunch of unrelated warnings though.\n\nI haven't seen any issues in our CI, either.\n\nSo I'm inclined to just go with this variant for now. If we ever see\nthat it does cause problems with some compiler it's trivial to just\n`s/die(NULL)/die_silent()` or something similar.\n\nPatrick\n"},{"id":"519786","messageId":"45805f9d-d59b-46ba-a5d6-040ee60e189b@gmail.com","threadId":"63531","inReplyTo":"20250603-b4-pks-maintenance-ref-lock-race-v4-4-52f5cf7b7e99@pks.im","subject":"Re: [PATCH v4 04/12] builtin/maintenance: mark \"--task=\" and \"--schedule=\" as incompatible","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2025-06-05T15:46:04Z","receivedAt":"2025-06-05T15:46:07Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 6/3/2025 10:01 AM, Patrick Steinhardt wrote:\n> The \"--task=\" option explicitly allows the user to say which maintenance\n> tasks should be run, whereas \"--schedule=\" only respects the maintenance\n> strategy configured for a specific repository. As such, it is not\n> sensible to accept both options at the same time.\n> \n> Mark them as incompatible with one another. While at it, also convert\n> the existing logic that marks \"--auto\" and \"--schedule=\" as incompatible\n> to use `die_for_incompatible_opt2()`.\n\nThis is a good change. Please consider squashing in this change to the\ndocumentation to match this expectation:\n\n--- >8 ---\n\ndiff --git a/Documentation/git-maintenance.adoc b/Documentation/git-maintenance.adoc\nindex 931f3e02e85..a901c46ce0e 100644\n--- a/Documentation/git-maintenance.adoc\n+++ b/Documentation/git-maintenance.adoc\n@@ -193,6 +193,8 @@ OPTIONS\n \tconfig value. The tasks that are tested are those provided by\n \tthe `--task=<task>` option(s) or those with\n \t`maintenance.<task>.enabled` set to true.\n++\n+The `--schedule` option cannot be used with the `--task` option.\n \n --quiet::\n \tDo not report progress or other information over `stderr`.\n@@ -203,6 +205,8 @@ OPTIONS\n \targuments are specified, then only the tasks with\n \t`maintenance.<task>.enabled` configured as `true` are considered.\n \tSee the 'TASKS' section for the list of accepted `<task>` values.\n++\n+The `--task` option cannot be used with the `--schedule` option.\n \n --scheduler=auto|crontab|systemd-timer|launchctl|schtasks::\n \tWhen combined with the `start` subcommand, specify the scheduler\n\n"},{"id":"519787","messageId":"4a8b02fc-09b1-4e7a-9b44-49a526b1da48@gmail.com","threadId":"63531","inReplyTo":"20250603-b4-pks-maintenance-ref-lock-race-v4-0-52f5cf7b7e99@pks.im","subject":"Re: [PATCH v4 00/12] builtin/maintenance: fix ref lock races when detaching","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2025-06-05T15:53:32Z","receivedAt":"2025-06-05T15:53:34Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 6/3/2025 10:01 AM, Patrick Steinhardt wrote:\n> Hi,\n> \n> this patch series fixes races around locking the \"packed-refs\" file when\n> auto-maintenance decides to repack it. This issue has been reported e.g.\n> via [1] and [2].\n> \n> The root cause is that git-gc(1) used to know to detach _after_ having\n> repacked references. As such, callers wouldn't continue with their thing\n> until we have already packed refs, and thus the race does not exist\n> there. git-maintenance(1) didn't have the same split though, so this\n> patch series retrofits that logic.\n\nThanks for making these changes. I read this v4 and only found an\nopportunity to improve our docs relative to the more helpful errors\naround using --schedule and --task together. I sent a diff that could\nbe squashed in or skipped.\n\nThanks,\n-Stolee\n"},{"id":"519823","messageId":"aEJ7NW6A-FBpDTnw@pks.im","threadId":"63531","inReplyTo":"4a8b02fc-09b1-4e7a-9b44-49a526b1da48@gmail.com","subject":"Re: [PATCH v4 00/12] builtin/maintenance: fix ref lock races when detaching","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-06-06T05:23:01Z","receivedAt":"2025-06-06T05:23:08Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Jun 05, 2025 at 11:53:32AM -0400, Derrick Stolee wrote:\n> On 6/3/2025 10:01 AM, Patrick Steinhardt wrote:\n> > Hi,\n> > \n> > this patch series fixes races around locking the \"packed-refs\" file when\n> > auto-maintenance decides to repack it. This issue has been reported e.g.\n> > via [1] and [2].\n> > \n> > The root cause is that git-gc(1) used to know to detach _after_ having\n> > repacked references. As such, callers wouldn't continue with their thing\n> > until we have already packed refs, and thus the race does not exist\n> > there. git-maintenance(1) didn't have the same split though, so this\n> > patch series retrofits that logic.\n> \n> Thanks for making these changes. I read this v4 and only found an\n> opportunity to improve our docs relative to the more helpful errors\n> around using --schedule and --task together. I sent a diff that could\n> be squashed in or skipped.\n\nI see that the series got merged yesterday, so your mail overlapped with\nthat. I think it's a good addition though, so would you mind sending the\ndiff as a follow-up patch?\n\nThanks for your review!\n\nPatrick\n"}]}