{"thread":{"id":"63348","subject":"[PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1)","startedAt":"2025-04-25T07:29:24Z","lastAt":"2025-05-07T07:21:52Z","messageCount":37,"participants":["Patrick Steinhardt","Derrick Stolee","Christian Couder","Eric Sunshine"],"isPatch":true,"patchVersion":1,"patchTotal":7},"messages":[{"id":"516758","messageId":"20250425-pks-maintenance-missing-tasks-v1-0-972ed6ab2c0d@pks.im","threadId":"63348","inReplyTo":null,"subject":"[PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1)","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-25T07:29:16Z","receivedAt":"2025-04-25T07:29:24Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis small patch series implements the last couple of remaining tasks\nthat are missing compared to the functionality git-gc(1) provides.\n\nRight now, git-maintenance(1) still executes git-gc(1). With these last\ngaps plugged though we can in theory fully replace git-gc(1) with finer\ngrained tasks without losing any functionality. The benefit is that it\nbecomes possible for users to have finer-grained control over what\nexactly the maintenance does.\n\nThis patch series doesn't do that yet, but only implements whatever is\nneeded to get there.\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (7):\n      builtin/gc: fix indentation of `cmd_gc()` parameters\n      builtin/gc: remove global variables where it trivial to do\n      builtin/gc: move pruning of worktrees into a separate function\n      worktree: expose function to retrieve worktree names\n      builtin/maintenance: introduce \"worktree-prune\" task\n      builtin/gc: move rerere garbage collection into separate function\n      builtin/maintenance: introduce \"rerere-gc\" task\n\n Documentation/git-maintenance.adoc |   8 +++\n builtin/gc.c                       | 128 ++++++++++++++++++++++++++++---------\n builtin/worktree.c                 |  25 ++++----\n t/t7900-maintenance.sh             |  34 ++++++++++\n worktree.c                         |  30 +++++++++\n worktree.h                         |   8 +++\n 6 files changed, 189 insertions(+), 44 deletions(-)\n\n\n---\nbase-commit: a2955b34f48265d240ab8c7deb0a929ec2d65fd0\nchange-id: 20250424-pks-maintenance-missing-tasks-8ffcdd596b73\n\n"},{"id":"516759","messageId":"20250425-pks-maintenance-missing-tasks-v1-1-972ed6ab2c0d@pks.im","threadId":"63348","inReplyTo":"20250425-pks-maintenance-missing-tasks-v1-0-972ed6ab2c0d@pks.im","subject":"[PATCH 1/7] builtin/gc: fix indentation of `cmd_gc()` parameters","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-25T07:29:17Z","receivedAt":"2025-04-25T07:29:24Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The parameters of `cmd_gc()` aren't indented properly. Fix this.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex d5c75be2522..a73ec22fb18 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -724,9 +724,9 @@ static void gc_before_repack(struct maintenance_run_opts *opts,\n }\n \n int cmd_gc(int argc,\n-const char **argv,\n-const char *prefix,\n-struct repository *repo UNUSED)\n+\t   const char **argv,\n+\t   const char *prefix,\n+\t   struct repository *repo UNUSED)\n {\n \tint aggressive = 0;\n \tint quiet = 0;\n\n-- \n2.49.0.901.g37484f566f.dirty\n\n"},{"id":"516760","messageId":"20250425-pks-maintenance-missing-tasks-v1-2-972ed6ab2c0d@pks.im","threadId":"63348","inReplyTo":"20250425-pks-maintenance-missing-tasks-v1-0-972ed6ab2c0d@pks.im","subject":"[PATCH 2/7] builtin/gc: remove global variables where it trivial to do","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-25T07:29:18Z","receivedAt":"2025-04-25T07:29:25Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We use a couple of global variables to assemble command line arguments\nfor subprocesses we execute in git-gc(1). All of these variables except\nthe one for git-repack(1) are only used in a single place though, so\nthey don't really add anything but confusion.\n\nRemove those variables.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 31 ++++++++++++-------------------\n 1 file changed, 12 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex a73ec22fb18..ada36e210f0 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -53,15 +53,9 @@ static const char * const builtin_gc_usage[] = {\n };\n \n static timestamp_t gc_log_expire_time;\n-\n static struct strvec repack = STRVEC_INIT;\n-static struct strvec prune = STRVEC_INIT;\n-static struct strvec prune_worktrees = STRVEC_INIT;\n-static struct strvec rerere = STRVEC_INIT;\n-\n static struct tempfile *pidfile;\n static struct lock_file log_lock;\n-\n static struct string_list pack_garbage = STRING_LIST_INIT_DUP;\n \n static void clean_pack_garbage(void)\n@@ -769,9 +763,6 @@ int cmd_gc(int argc,\n \t\t\t\t\t builtin_gc_usage, builtin_gc_options);\n \n \tstrvec_pushl(&repack, \"repack\", \"-d\", \"-l\", NULL);\n-\tstrvec_pushl(&prune, \"prune\", \"--expire\", NULL);\n-\tstrvec_pushl(&prune_worktrees, \"worktree\", \"prune\", \"--expire\", NULL);\n-\tstrvec_pushl(&rerere, \"rerere\", \"gc\", NULL);\n \n \tgc_config(&cfg);\n \n@@ -897,34 +888,36 @@ int cmd_gc(int argc,\n \t\tif (cfg.prune_expire) {\n \t\t\tstruct child_process prune_cmd = CHILD_PROCESS_INIT;\n \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, cfg.prune_expire);\n+\t\t\tstrvec_push(&prune_cmd.args, cfg.prune_expire);\n \t\t\tif (quiet)\n-\t\t\t\tstrvec_push(&prune, \"--no-progress\");\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,\n+\t\t\t\tstrvec_push(&prune_cmd.args,\n \t\t\t\t\t    \"--exclude-promisor-objects\");\n \t\t\tprune_cmd.git_cmd = 1;\n-\t\t\tstrvec_pushv(&prune_cmd.args, prune.v);\n+\n \t\t\tif (run_command(&prune_cmd))\n-\t\t\t\tdie(FAILED_RUN, prune.v[0]);\n+\t\t\t\tdie(FAILED_RUN, prune_cmd.args.v[0]);\n \t\t}\n \t}\n \n \tif (cfg.prune_worktrees_expire) {\n \t\tstruct child_process prune_worktrees_cmd = CHILD_PROCESS_INIT;\n \n-\t\tstrvec_push(&prune_worktrees, cfg.prune_worktrees_expire);\n \t\tprune_worktrees_cmd.git_cmd = 1;\n-\t\tstrvec_pushv(&prune_worktrees_cmd.args, prune_worktrees.v);\n+\t\tstrvec_pushl(&prune_worktrees_cmd.args, \"worktree\", \"prune\", \"--expire\", NULL);\n+\t\tstrvec_push(&prune_worktrees_cmd.args, cfg.prune_worktrees_expire);\n+\n \t\tif (run_command(&prune_worktrees_cmd))\n-\t\t\tdie(FAILED_RUN, prune_worktrees.v[0]);\n+\t\t\tdie(FAILED_RUN, prune_worktrees_cmd.args.v[0]);\n \t}\n \n \trerere_cmd.git_cmd = 1;\n-\tstrvec_pushv(&rerere_cmd.args, rerere.v);\n+\tstrvec_pushl(&rerere_cmd.args, \"rerere\", \"gc\", NULL);\n \tif (run_command(&rerere_cmd))\n-\t\tdie(FAILED_RUN, rerere.v[0]);\n+\t\tdie(FAILED_RUN, rerere_cmd.args.v[0]);\n \n \treport_garbage = report_pack_garbage;\n \treprepare_packed_git(the_repository);\n\n-- \n2.49.0.901.g37484f566f.dirty\n\n"},{"id":"516761","messageId":"20250425-pks-maintenance-missing-tasks-v1-3-972ed6ab2c0d@pks.im","threadId":"63348","inReplyTo":"20250425-pks-maintenance-missing-tasks-v1-0-972ed6ab2c0d@pks.im","subject":"[PATCH 3/7] builtin/gc: move pruning of worktrees into a separate function","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-25T07:29:19Z","receivedAt":"2025-04-25T07:29:26Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Move pruning of worktrees into a separate function. This prepares for a\nsubsequent commit where we introduce a new \"worktree-prune\" task for\ngit-maintenance(1).\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 25 +++++++++++++++----------\n 1 file changed, 15 insertions(+), 10 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex ada36e210f0..005ecc3f192 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -334,6 +334,18 @@ static int maintenance_task_reflog_expire(struct maintenance_run_opts *opts UNUS\n \treturn run_command(&cmd);\n }\n \n+static int maintenance_task_worktree_prune(struct maintenance_run_opts *opts UNUSED,\n+\t\t\t\t\t   struct gc_config *cfg)\n+{\n+\tstruct child_process prune_worktrees_cmd = CHILD_PROCESS_INIT;\n+\n+\tprune_worktrees_cmd.git_cmd = 1;\n+\tstrvec_pushl(&prune_worktrees_cmd.args, \"worktree\", \"prune\", \"--expire\", NULL);\n+\tstrvec_push(&prune_worktrees_cmd.args, cfg->prune_worktrees_expire);\n+\n+\treturn run_command(&prune_worktrees_cmd);\n+}\n+\n static int too_many_loose_objects(struct gc_config *cfg)\n {\n \t/*\n@@ -903,16 +915,9 @@ int cmd_gc(int argc,\n \t\t}\n \t}\n \n-\tif (cfg.prune_worktrees_expire) {\n-\t\tstruct child_process prune_worktrees_cmd = CHILD_PROCESS_INIT;\n-\n-\t\tprune_worktrees_cmd.git_cmd = 1;\n-\t\tstrvec_pushl(&prune_worktrees_cmd.args, \"worktree\", \"prune\", \"--expire\", NULL);\n-\t\tstrvec_push(&prune_worktrees_cmd.args, cfg.prune_worktrees_expire);\n-\n-\t\tif (run_command(&prune_worktrees_cmd))\n-\t\t\tdie(FAILED_RUN, prune_worktrees_cmd.args.v[0]);\n-\t}\n+\tif (cfg.prune_worktrees_expire &&\n+\t    maintenance_task_worktree_prune(&opts, &cfg))\n+\t\tdie(FAILED_RUN, \"worktree\");\n \n \trerere_cmd.git_cmd = 1;\n \tstrvec_pushl(&rerere_cmd.args, \"rerere\", \"gc\", NULL);\n\n-- \n2.49.0.901.g37484f566f.dirty\n\n"},{"id":"516762","messageId":"20250425-pks-maintenance-missing-tasks-v1-4-972ed6ab2c0d@pks.im","threadId":"63348","inReplyTo":"20250425-pks-maintenance-missing-tasks-v1-0-972ed6ab2c0d@pks.im","subject":"[PATCH 4/7] worktree: expose function to retrieve worktree names","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-25T07:29:20Z","receivedAt":"2025-04-25T07:29:28Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a function that retrieves worktree names as present in\n\".git/worktrees\". This function will be used in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/worktree.c | 25 ++++++++++++-------------\n worktree.c         | 30 ++++++++++++++++++++++++++++++\n worktree.h         |  8 ++++++++\n 3 files changed, 50 insertions(+), 13 deletions(-)\n\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 87ccd47794c..9b00dbf1265 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -211,27 +211,24 @@ static void prune_dups(struct string_list *l)\n \n static void prune_worktrees(void)\n {\n-\tstruct strbuf reason = STRBUF_INIT;\n \tstruct strbuf main_path = STRBUF_INIT;\n \tstruct string_list kept = STRING_LIST_INIT_DUP;\n-\tchar *path;\n-\tDIR *dir;\n-\tstruct dirent *d;\n+\tstruct strvec worktrees = STRVEC_INIT;\n+\tstruct strbuf reason = STRBUF_INIT;\n \n-\tpath = repo_git_path(the_repository, \"worktrees\");\n-\tdir = opendir(path);\n-\tfree(path);\n-\tif (!dir)\n+\tif (get_worktree_names(the_repository, &worktrees) < 0 ||\n+\t    !worktrees.nr)\n \t\treturn;\n-\twhile ((d = readdir_skip_dot_and_dotdot(dir)) != NULL) {\n+\n+\tfor (size_t i = 0; i < worktrees.nr; i++) {\n \t\tchar *path;\n+\n \t\tstrbuf_reset(&reason);\n-\t\tif (should_prune_worktree(d->d_name, &reason, &path, expire))\n-\t\t\tprune_worktree(d->d_name, reason.buf);\n+\t\tif (should_prune_worktree(worktrees.v[i], &reason, &path, expire))\n+\t\t\tprune_worktree(worktrees.v[i], reason.buf);\n \t\telse if (path)\n-\t\t\tstring_list_append_nodup(&kept, path)->util = xstrdup(d->d_name);\n+\t\t\tstring_list_append_nodup(&kept, path)->util = xstrdup(worktrees.v[i]);\n \t}\n-\tclosedir(dir);\n \n \tstrbuf_add_absolute_path(&main_path, repo_get_common_dir(the_repository));\n \t/* massage main worktree absolute path to match 'gitdir' content */\n@@ -242,6 +239,8 @@ static void prune_worktrees(void)\n \n \tif (!show_only)\n \t\tdelete_worktrees_dir_if_empty();\n+\n+\tstrvec_clear(&worktrees);\n \tstrbuf_release(&reason);\n }\n \ndiff --git a/worktree.c b/worktree.c\nindex c34b9eb74e5..947b7a82209 100644\n--- a/worktree.c\n+++ b/worktree.c\n@@ -988,6 +988,36 @@ int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath,\n \treturn rc;\n }\n \n+int get_worktree_names(struct repository *repo, struct strvec *out)\n+{\n+\tchar *worktrees_dir;\n+\tstruct dirent *d;\n+\tDIR *dir;\n+\tint ret;\n+\n+\tworktrees_dir = repo_git_path(repo, \"worktrees\");\n+\tdir = opendir(worktrees_dir);\n+\tif (!dir) {\n+\t\tif (errno == ENOENT) {\n+\t\t\tret = 0;\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\twhile ((d = readdir_skip_dot_and_dotdot(dir)) != NULL)\n+\t\tstrvec_push(out, d->d_name);\n+\n+\tret = 0;\n+out:\n+\tif (dir)\n+\t\tclosedir(dir);\n+\tfree(worktrees_dir);\n+\treturn ret;\n+}\n+\n static int move_config_setting(const char *key, const char *value,\n \t\t\t       const char *from_file, const char *to_file)\n {\ndiff --git a/worktree.h b/worktree.h\nindex e4bcccdc0ae..59825c37881 100644\n--- a/worktree.h\n+++ b/worktree.h\n@@ -38,6 +38,14 @@ struct worktree **get_worktrees(void);\n  */\n struct worktree **get_worktrees_without_reading_head(void);\n \n+/*\n+ * Retrieve all worktree names. Not all names may correspond to a fully\n+ * functional worktree. Returns 0 on success, a negative error code on failure.\n+ * Calling the function on a repository that doesn't have any worktrees is not\n+ * considered an error.\n+ */\n+int get_worktree_names(struct repository *repo, struct strvec *out);\n+\n /*\n  * Returns 1 if linked worktrees exist, 0 otherwise.\n  */\n\n-- \n2.49.0.901.g37484f566f.dirty\n\n"},{"id":"516763","messageId":"20250425-pks-maintenance-missing-tasks-v1-5-972ed6ab2c0d@pks.im","threadId":"63348","inReplyTo":"20250425-pks-maintenance-missing-tasks-v1-0-972ed6ab2c0d@pks.im","subject":"[PATCH 5/7] builtin/maintenance: introduce \"worktree-prune\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-25T07:29:21Z","receivedAt":"2025-04-25T07:29:28Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While git-gc(1) knows to prune stale worktrees, git-maintenance(1) does\nnot yet have a task for this cleanup. Introduce a new \"worktree-prune\"\ntask to plug this gap.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-maintenance.adoc |  4 ++++\n builtin/gc.c                       | 35 +++++++++++++++++++++++++++++++++++\n t/t7900-maintenance.sh             | 17 +++++++++++++++++\n 3 files changed, 56 insertions(+)\n\ndiff --git a/Documentation/git-maintenance.adoc b/Documentation/git-maintenance.adoc\nindex 3a1e2a69b6b..6f085a9cf8c 100644\n--- a/Documentation/git-maintenance.adoc\n+++ b/Documentation/git-maintenance.adoc\n@@ -166,6 +166,10 @@ reflog-expire::\n \tThe `reflog-expire` task deletes any entries in the reflog older than the\n \texpiry threshold. See linkgit:git-reflog[1] for more information.\n \n+worktree-prune::\n+\tThe `worktree-prune` task deletes stale or broken worktrees. See\n+\tlinkit:git-worktree[1] for more information.\n+\n OPTIONS\n -------\n --auto::\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 005ecc3f192..46c64c852dc 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -44,6 +44,7 @@\n #include \"hook.h\"\n #include \"setup.h\"\n #include \"trace2.h\"\n+#include \"worktree.h\"\n \n #define FAILED_RUN \"failed to run %s\"\n \n@@ -346,6 +347,34 @@ static int maintenance_task_worktree_prune(struct maintenance_run_opts *opts UNU\n \treturn run_command(&prune_worktrees_cmd);\n }\n \n+static int worktree_prune_condition(struct gc_config *cfg)\n+{\n+\tstruct strvec worktrees = STRVEC_INIT;\n+\tstruct strbuf reason = STRBUF_INIT;\n+\ttimestamp_t expiry_date;\n+\tint should_prune = 0;\n+\n+\tif (parse_expiry_date(cfg->prune_worktrees_expire, &expiry_date) ||\n+\t    get_worktree_names(the_repository, &worktrees) < 0)\n+\t\tgoto out;\n+\n+\tfor (size_t i = 0; i < worktrees.nr; i++) {\n+\t\tchar *wtpath;\n+\n+\t\tstrbuf_reset(&reason);\n+\t\tif (should_prune_worktree(worktrees.v[i], &reason, &wtpath, expiry_date)) {\n+\t\t\tshould_prune = 1;\n+\t\t\tgoto out;\n+\t\t}\n+\t\tfree(wtpath);\n+\t}\n+\n+out:\n+\tstrvec_clear(&worktrees);\n+\tstrbuf_release(&reason);\n+\treturn should_prune;\n+}\n+\n static int too_many_loose_objects(struct gc_config *cfg)\n {\n \t/*\n@@ -1455,6 +1484,7 @@ enum maintenance_task_label {\n \tTASK_COMMIT_GRAPH,\n \tTASK_PACK_REFS,\n \tTASK_REFLOG_EXPIRE,\n+\tTASK_WORKTREE_PRUNE,\n \n \t/* Leave as final value */\n \tTASK__COUNT\n@@ -1496,6 +1526,11 @@ static struct maintenance_task tasks[] = {\n \t\tmaintenance_task_reflog_expire,\n \t\treflog_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},\n };\n \n static int compare_tasks_by_selection(const void *a_, const void *b_)\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 9b82e11c100..d21feda271f 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -493,6 +493,23 @@ test_expect_success 'reflog-expire task --auto only packs when exceeding limits'\n \ttest_subcommand git reflog expire --all <reflog-expire-auto.txt\n '\n \n+test_expect_success 'worktree-prune task' '\n+\tGIT_TRACE2_EVENT=\"$(pwd)/worktree-prune.txt\" \\\n+\t\tgit maintenance run --task=worktree-prune &&\n+\ttest_subcommand git worktree prune --expire 3.months.ago <worktree-prune.txt\n+'\n+\n+test_expect_success 'worktree-prune task --auto only prunes with prunable worktree' '\n+\tGIT_TRACE2_EVENT=\"$(pwd)/worktree-prune-auto.txt\" \\\n+\t\tgit maintenance run --auto --task=worktree-prune &&\n+\ttest_subcommand ! git worktree prune --expire 3.months.ago <worktree-prune-auto.txt &&\n+\tmkdir .git/worktrees &&\n+\t: >.git/worktrees/abc &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/worktree-prune-auto.txt\" \\\n+\t\tgit maintenance run --auto --task=worktree-prune &&\n+\ttest_subcommand git worktree prune --expire 3.months.ago <worktree-prune-auto.txt\n+'\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\n-- \n2.49.0.901.g37484f566f.dirty\n\n"},{"id":"516764","messageId":"20250425-pks-maintenance-missing-tasks-v1-6-972ed6ab2c0d@pks.im","threadId":"63348","inReplyTo":"20250425-pks-maintenance-missing-tasks-v1-0-972ed6ab2c0d@pks.im","subject":"[PATCH 6/7] builtin/gc: move rerere garbage collection into separate function","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-25T07:29:22Z","receivedAt":"2025-04-25T07:29:30Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Move garbage collection of cached rerere entries into a separate\nfunction. This prepares us for a subsequent commit where we introduce a\nnew \"rerere-gc\" task for git-maintenance(1).\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 16 +++++++++++-----\n 1 file changed, 11 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 46c64c852dc..a0816bcf302 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -375,6 +375,15 @@ static int worktree_prune_condition(struct gc_config *cfg)\n \treturn should_prune;\n }\n \n+static int maintenance_task_rerere_gc(struct maintenance_run_opts *opts UNUSED,\n+\t\t\t\t      struct gc_config *cfg UNUSED)\n+{\n+\tstruct child_process rerere_cmd = CHILD_PROCESS_INIT;\n+\trerere_cmd.git_cmd = 1;\n+\tstrvec_pushl(&rerere_cmd.args, \"rerere\", \"gc\", NULL);\n+\treturn run_command(&rerere_cmd);\n+}\n+\n static int too_many_loose_objects(struct gc_config *cfg)\n {\n \t/*\n@@ -771,7 +780,6 @@ int cmd_gc(int argc,\n \tint daemonized = 0;\n \tint keep_largest_pack = -1;\n \ttimestamp_t dummy;\n-\tstruct child_process rerere_cmd = CHILD_PROCESS_INIT;\n \tstruct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT;\n \tstruct gc_config cfg = GC_CONFIG_INIT;\n \tconst char *prune_expire_sentinel = \"sentinel\";\n@@ -948,10 +956,8 @@ int cmd_gc(int argc,\n \t    maintenance_task_worktree_prune(&opts, &cfg))\n \t\tdie(FAILED_RUN, \"worktree\");\n \n-\trerere_cmd.git_cmd = 1;\n-\tstrvec_pushl(&rerere_cmd.args, \"rerere\", \"gc\", NULL);\n-\tif (run_command(&rerere_cmd))\n-\t\tdie(FAILED_RUN, rerere_cmd.args.v[0]);\n+\tif (maintenance_task_rerere_gc(&opts, &cfg))\n+\t\tdie(FAILED_RUN, \"rerere\");\n \n \treport_garbage = report_pack_garbage;\n \treprepare_packed_git(the_repository);\n\n-- \n2.49.0.901.g37484f566f.dirty\n\n"},{"id":"516765","messageId":"20250425-pks-maintenance-missing-tasks-v1-7-972ed6ab2c0d@pks.im","threadId":"63348","inReplyTo":"20250425-pks-maintenance-missing-tasks-v1-0-972ed6ab2c0d@pks.im","subject":"[PATCH 7/7] builtin/maintenance: introduce \"rerere-gc\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-25T07:29:23Z","receivedAt":"2025-04-25T07:29:31Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While git-gc(1) knows to garbage collect the rerere cache,\ngit-maintenance(1) does not yet have a task for this cleanup. Introduce\na new \"rerere-gc\" task to plug this gap.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-maintenance.adoc |  4 ++++\n builtin/gc.c                       | 27 +++++++++++++++++++++++++++\n t/t7900-maintenance.sh             | 17 +++++++++++++++++\n 3 files changed, 48 insertions(+)\n\ndiff --git a/Documentation/git-maintenance.adoc b/Documentation/git-maintenance.adoc\nindex 6f085a9cf8c..931f3e02e85 100644\n--- a/Documentation/git-maintenance.adoc\n+++ b/Documentation/git-maintenance.adoc\n@@ -166,6 +166,10 @@ reflog-expire::\n \tThe `reflog-expire` task deletes any entries in the reflog older than the\n \texpiry threshold. See linkgit:git-reflog[1] for more information.\n \n+rerere-gc::\n+\tThe `rerere-gc` task invokes garbage collection for stale entries in\n+\tthe rerere cache. See linkgit:git-rerere[1] for more information.\n+\n worktree-prune::\n \tThe `worktree-prune` task deletes stale or broken worktrees. See\n \tlinkit:git-worktree[1] for more information.\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex a0816bcf302..d19449b33d4 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -16,6 +16,7 @@\n #include \"builtin.h\"\n #include \"abspath.h\"\n #include \"date.h\"\n+#include \"dir.h\"\n #include \"environment.h\"\n #include \"hex.h\"\n #include \"config.h\"\n@@ -384,6 +385,26 @@ static int maintenance_task_rerere_gc(struct maintenance_run_opts *opts UNUSED,\n \treturn run_command(&rerere_cmd);\n }\n \n+static int rerere_gc_condition(struct gc_config *cfg UNUSED)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\tint should_gc = 0;\n+\tDIR *dir;\n+\n+\t/* Skip garbage collecting the rerere cache in case rerere is disabled. */\n+\trepo_git_path_replace(the_repository, &path, \"rr-cache\");\n+\n+\tdir = opendir(path.buf);\n+\tif (!dir)\n+\t\tgoto out;\n+\tshould_gc = !!readdir_skip_dot_and_dotdot(dir);\n+\n+out:\n+\tstrbuf_release(&path);\n+\tclosedir(dir);\n+\treturn should_gc;\n+}\n+\n static int too_many_loose_objects(struct gc_config *cfg)\n {\n \t/*\n@@ -1491,6 +1512,7 @@ enum maintenance_task_label {\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@@ -1537,6 +1559,11 @@ static struct maintenance_task tasks[] = {\n \t\tmaintenance_task_worktree_prune,\n \t\tworktree_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},\n };\n \n static int compare_tasks_by_selection(const void *a_, const void *b_)\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex d21feda271f..9cc52f28ca4 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -510,6 +510,23 @@ test_expect_success 'worktree-prune task --auto only prunes with prunable worktr\n \ttest_subcommand git worktree prune --expire 3.months.ago <worktree-prune-auto.txt\n '\n \n+test_expect_success 'rerere-gc task' '\n+\tGIT_TRACE2_EVENT=\"$(pwd)/rerere-gc.txt\" \\\n+\t\tgit maintenance run --task=rerere-gc &&\n+\ttest_subcommand git rerere gc <rerere-gc.txt\n+'\n+\n+test_expect_success 'rerere-gc task --auto only prunes with existing rr-cache dir' '\n+\tmkdir .git/rr-cache &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/rerere-gc-auto.txt\" \\\n+\t\tgit maintenance run --auto --task=rerere-gc &&\n+\ttest_subcommand ! git rerere gc <rerere-gc-auto.txt &&\n+\t: >.git/rr-cache/entry &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/rerere-gc-auto.txt\" \\\n+\t\tgit maintenance run --auto --task=rerere-gc &&\n+\ttest_subcommand git rerere gc <rerere-gc-auto.txt\n+'\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\n-- \n2.49.0.901.g37484f566f.dirty\n\n"},{"id":"516984","messageId":"c629f845-180f-4ecb-949f-a99f184f812d@gmail.com","threadId":"63348","inReplyTo":"20250425-pks-maintenance-missing-tasks-v1-5-972ed6ab2c0d@pks.im","subject":"Re: [PATCH 5/7] builtin/maintenance: introduce \"worktree-prune\" task","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2025-04-29T20:02:03Z","receivedAt":"2025-04-29T20:02:05Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 4/25/2025 3:29 AM, Patrick Steinhardt wrote:\n> While git-gc(1) knows to prune stale worktrees, git-maintenance(1) does\n> not yet have a task for this cleanup. Introduce a new \"worktree-prune\"\n> task to plug this gap.\n\nI initially thought that this could merge down into patch 3 (move pruning\nof worktrees into a separate function), but...\n\n> +static int worktree_prune_condition(struct gc_config *cfg)\n> +{\n> +\tstruct strvec worktrees = STRVEC_INIT;\n> +\tstruct strbuf reason = STRBUF_INIT;\n> +\ttimestamp_t expiry_date;\n> +\tint should_prune = 0;\n> +\n> +\tif (parse_expiry_date(cfg->prune_worktrees_expire, &expiry_date) ||\n> +\t    get_worktree_names(the_repository, &worktrees) < 0)\n> +\t\tgoto out;\n> +\n> +\tfor (size_t i = 0; i < worktrees.nr; i++) {\n> +\t\tchar *wtpath;\n> +\n> +\t\tstrbuf_reset(&reason);\n> +\t\tif (should_prune_worktree(worktrees.v[i], &reason, &wtpath, expiry_date)) {\n> +\t\t\tshould_prune = 1;\n> +\t\t\tgoto out;\n> +\t\t}\n> +\t\tfree(wtpath);\n> +\t}\n> +\n> +out:\n> +\tstrvec_clear(&worktrees);\n> +\tstrbuf_release(&reason);\n> +\treturn should_prune;\n> +}\n> +\n\n...this implementation is new and nice to have in a separate patch. I\ninitially wondered if this condition needed to exist in the maintenance\nbuiltin or could be relied upon by the 'git worktree prune' command that\nis called by this implementation.\n\nIf we are trying to match the behavior of 'git gc --auto', then it was\nrunning 'git worktree prune --expire...' every time that the generic\n--auto condition was satisfied. But when 'git maintenance run --auto' is\nexecuted, each task is checked to see if it should run. If we can avoid a\nchild process startup, then that is very valuable (especially on Windows\nwhere process creation is expensive).\n\nSo I think this is a good approach. Similar thoughts apply to patch 7. No\ncode change is needed.\n\n> +test_expect_success 'worktree-prune task' '\n> +\tGIT_TRACE2_EVENT=\"$(pwd)/worktree-prune.txt\" \\\n> +\t\tgit maintenance run --task=worktree-prune &&\n> +\ttest_subcommand git worktree prune --expire 3.months.ago <worktree-prune.txt\n> +'\n> +\n> +test_expect_success 'worktree-prune task --auto only prunes with prunable worktree' '\n> +\tGIT_TRACE2_EVENT=\"$(pwd)/worktree-prune-auto.txt\" \\\n> +\t\tgit maintenance run --auto --task=worktree-prune &&\n> +\ttest_subcommand ! git worktree prune --expire 3.months.ago <worktree-prune-auto.txt &&\n> +\tmkdir .git/worktrees &&\n> +\t: >.git/worktrees/abc &&\n> +\tGIT_TRACE2_EVENT=\"$(pwd)/worktree-prune-auto.txt\" \\\n> +\t\tgit maintenance run --auto --task=worktree-prune &&\n> +\ttest_subcommand git worktree prune --expire 3.months.ago <worktree-prune-auto.txt\n> +'\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\nIt may be good to double-check that the gc.worktreePruneExpire config value\nis being used here, especially since the prune condition is operating on\nthat value.\n\nThanks,\n-Stolee\n"},{"id":"516985","messageId":"3cc1ef7b-40d5-4802-9bf9-ff28824ce563@gmail.com","threadId":"63348","inReplyTo":"20250425-pks-maintenance-missing-tasks-v1-0-972ed6ab2c0d@pks.im","subject":"Re: [PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1)","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2025-04-29T20:02:18Z","receivedAt":"2025-04-29T20:02:21Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 4/25/2025 3:29 AM, Patrick Steinhardt wrote:\n\n> Right now, git-maintenance(1) still executes git-gc(1). With these last\n> gaps plugged though we can in theory fully replace git-gc(1) with finer\n> grained tasks without losing any functionality. The benefit is that it\n> becomes possible for users to have finer-grained control over what\n> exactly the maintenance does.\n> \n> This patch series doesn't do that yet, but only implements whatever is\n> needed to get there.\n\nThanks for putting this together. I think this is a noble goal, allowing\nusers and system administrators more options to fine-tune the best ways to\noptimize their repos.\n\nI wonder if any of these fine-grained steps would be valuable to add to\nthe default background maintenance schedule (perhaps as a follow-up)?\n\n> Patrick Steinhardt (7):\n>       builtin/gc: fix indentation of `cmd_gc()` parameters\n>       builtin/gc: remove global variables where it trivial to do\n\nThese first two patches are simple cleanups. Thanks for isolating them.\n\n>       builtin/gc: move pruning of worktrees into a separate function\n\nThis is a nice refactor with a clean method body extraction.\n\n>       worktree: expose function to retrieve worktree names\n\nThis one is a bit messier, but still really good as it makes the\nprune_worktrees() method in builtin/worktree.c less complicated. There's\njust no way to make the removal of those variables look clean.\n\n>       builtin/maintenance: introduce \"worktree-prune\" task\n\nThis is where we start getting into new behavior. More comments on the\npatch itself.\n\n>       builtin/gc: move rerere garbage collection into separate function\n>       builtin/maintenance: introduce \"rerere-gc\" task\n\nThese are a nice one-two punch for this new task.\n\nThanks,\n-Stolee\n"},{"id":"516998","messageId":"aBHMVm9QIKZwiaBX@pks.im","threadId":"63348","inReplyTo":"3cc1ef7b-40d5-4802-9bf9-ff28824ce563@gmail.com","subject":"Re: [PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1)","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-30T07:08:06Z","receivedAt":"2025-04-30T07:08:10Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Apr 29, 2025 at 04:02:18PM -0400, Derrick Stolee wrote:\n> On 4/25/2025 3:29 AM, Patrick Steinhardt wrote:\n> \n> > Right now, git-maintenance(1) still executes git-gc(1). With these last\n> > gaps plugged though we can in theory fully replace git-gc(1) with finer\n> > grained tasks without losing any functionality. The benefit is that it\n> > becomes possible for users to have finer-grained control over what\n> > exactly the maintenance does.\n> > \n> > This patch series doesn't do that yet, but only implements whatever is\n> > needed to get there.\n> \n> Thanks for putting this together. I think this is a noble goal, allowing\n> users and system administrators more options to fine-tune the best ways to\n> optimize their repos.\n> \n> I wonder if any of these fine-grained steps would be valuable to add to\n> the default background maintenance schedule (perhaps as a follow-up)?\n\nI think so, yes. My immediate goal is to change `git maintenance run\n--auto` to not run git-gc(1) by default anymore, but to instead have it\nrun the fine-grained steps. But we should probably at the same time also\nadapt the background maintenance to use the same fine-grained tasks so\nthat the outcome matches.\n\nMy idea would be that we implement all of this via high-level strategies\nthat get honored by scheduled, auto and explicit maintenance alike. We\nalready have the \"incremental\" strategy, but there are other strategies\nthat might make sense to introduce. There should be at least one\nstrategy that achieves the same as git-gc(1) does right now, but there's\nprobably more opportunities here to have e.g. a \"geometric\" strategy.\n\nUsers can then pick whatever strategy works best for them, with us\nproviding a good default as well as rationale why we recommend one\nstrategy over another. Furthermore, expert users can of course tweak\nthese strategies even further by explicitly configuring which of the\nfine-grained tasks should run.\n\nThinking ahead a bit, one might even be prompted to auto-select\nstrategies based on repository properties. A tiny repository likely does\nnot need multi-pack indices and cruft packs, but a huge behemoth like\nthe Chromium repository probably would benefit. But that is an iteration\nthat one can think about in the future.\n\nPatrick\n"},{"id":"516999","messageId":"aBHMXGNfDZQ-JVT1@pks.im","threadId":"63348","inReplyTo":"c629f845-180f-4ecb-949f-a99f184f812d@gmail.com","subject":"Re: [PATCH 5/7] builtin/maintenance: introduce \"worktree-prune\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-30T07:08:12Z","receivedAt":"2025-04-30T07:08:15Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Apr 29, 2025 at 04:02:03PM -0400, Derrick Stolee wrote:\n> On 4/25/2025 3:29 AM, Patrick Steinhardt wrote:\n> > While git-gc(1) knows to prune stale worktrees, git-maintenance(1) does\n> > not yet have a task for this cleanup. Introduce a new \"worktree-prune\"\n> > task to plug this gap.\n> \n> I initially thought that this could merge down into patch 3 (move pruning\n> of worktrees into a separate function), but...\n> \n> > +static int worktree_prune_condition(struct gc_config *cfg)\n> > +{\n> > +\tstruct strvec worktrees = STRVEC_INIT;\n> > +\tstruct strbuf reason = STRBUF_INIT;\n> > +\ttimestamp_t expiry_date;\n> > +\tint should_prune = 0;\n> > +\n> > +\tif (parse_expiry_date(cfg->prune_worktrees_expire, &expiry_date) ||\n> > +\t    get_worktree_names(the_repository, &worktrees) < 0)\n> > +\t\tgoto out;\n> > +\n> > +\tfor (size_t i = 0; i < worktrees.nr; i++) {\n> > +\t\tchar *wtpath;\n> > +\n> > +\t\tstrbuf_reset(&reason);\n> > +\t\tif (should_prune_worktree(worktrees.v[i], &reason, &wtpath, expiry_date)) {\n> > +\t\t\tshould_prune = 1;\n> > +\t\t\tgoto out;\n> > +\t\t}\n> > +\t\tfree(wtpath);\n> > +\t}\n> > +\n> > +out:\n> > +\tstrvec_clear(&worktrees);\n> > +\tstrbuf_release(&reason);\n> > +\treturn should_prune;\n> > +}\n> > +\n> \n> ...this implementation is new and nice to have in a separate patch. I\n> initially wondered if this condition needed to exist in the maintenance\n> builtin or could be relied upon by the 'git worktree prune' command that\n> is called by this implementation.\n> \n> If we are trying to match the behavior of 'git gc --auto', then it was\n> running 'git worktree prune --expire...' every time that the generic\n> --auto condition was satisfied. But when 'git maintenance run --auto' is\n> executed, each task is checked to see if it should run. If we can avoid a\n> child process startup, then that is very valuable (especially on Windows\n> where process creation is expensive).\n\nYup, exactly. In theory, we could even make the condition configurable\nvia \"maintenance.worktree-prune.auto\" so that we treat it as a limit of\nhow many worktrees need to be prunable before we execute `git worktree\nprune`. Maybe I'll do just that in the next iteration.\n\n> So I think this is a good approach. Similar thoughts apply to patch 7. No\n> code change is needed.\n> \n> > +test_expect_success 'worktree-prune task' '\n> > +\tGIT_TRACE2_EVENT=\"$(pwd)/worktree-prune.txt\" \\\n> > +\t\tgit maintenance run --task=worktree-prune &&\n> > +\ttest_subcommand git worktree prune --expire 3.months.ago <worktree-prune.txt\n> > +'\n> > +\n> > +test_expect_success 'worktree-prune task --auto only prunes with prunable worktree' '\n> > +\tGIT_TRACE2_EVENT=\"$(pwd)/worktree-prune-auto.txt\" \\\n> > +\t\tgit maintenance run --auto --task=worktree-prune &&\n> > +\ttest_subcommand ! git worktree prune --expire 3.months.ago <worktree-prune-auto.txt &&\n> > +\tmkdir .git/worktrees &&\n> > +\t: >.git/worktrees/abc &&\n> > +\tGIT_TRACE2_EVENT=\"$(pwd)/worktree-prune-auto.txt\" \\\n> > +\t\tgit maintenance run --auto --task=worktree-prune &&\n> > +\ttest_subcommand git worktree prune --expire 3.months.ago <worktree-prune-auto.txt\n> > +'\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> \n> It may be good to double-check that the gc.worktreePruneExpire config value\n> is being used here, especially since the prune condition is operating on\n> that value.\n\nFair, will do.\n\nPatrick\n"},{"id":"517222","messageId":"20250505-pks-maintenance-missing-tasks-v4-0-141f4df906a1@pks.im","threadId":"63348","inReplyTo":"20250425-pks-maintenance-missing-tasks-v1-0-972ed6ab2c0d@pks.im","subject":"[PATCH v4 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1)","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-05T08:51:40Z","receivedAt":"2025-05-05T08:51:50Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis small patch series implements the last couple of remaining tasks\nthat are missing compared to the functionality git-gc(1) provides.\n\nRight now, git-maintenance(1) still executes git-gc(1). With these last\ngaps plugged though we can in theory fully replace git-gc(1) with finer\ngrained tasks without losing any functionality. The benefit is that it\nbecomes possible for users to have finer-grained control over what\nexactly the maintenance does.\n\nThis patch series doesn't do that yet, but only implements whatever is\nneeded to get there.\n\nChanges in v2:\n  - Introduce \"maintenance.worktree-prune.auto\", which controls how many\n    stale worktrees need to exist before executing `git worktree prune`.\n  - Introduce \"maintenance.rerere-gc.auto\", which controls how many\n    stale rerere entries need to exist before executing `git rerere gc`.\n  - Add tests to verify that \"gc.worktreePruneExpire\" works.\n  - Remove some fragile test logic by introducing functions that check\n    for a given maintenance subprocess.\n  - Link to v1: https://lore.kernel.org/r/20250425-pks-maintenance-missing-tasks-v1-0-972ed6ab2c0d@pks.im\n\nChanges in v3:\n  - Simplify the heuristic for \"rerere-gc\" so that we only count the\n    number of directory entries in \".git/rr-cache\", without considering\n    staleness.\n  - Link to v2: https://lore.kernel.org/r/20250430-pks-maintenance-missing-tasks-v2-0-2580b7b8ca3a@pks.im\n\nChanges in v4:\n  - simplified the heuristic for \"rerere-gc\" even further. A positive\n    value for \"maintenance.rerere-gc.auto\" now indicates that the\n    command will run whenever there is at least one directory entry in\n    \".rr-cache\". The exact value does not matter anymore.\n  - Link to v3: https://lore.kernel.org/r/20250502-pks-maintenance-missing-tasks-v3-0-13e130d36640@pks.im\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (7):\n      builtin/gc: fix indentation of `cmd_gc()` parameters\n      builtin/gc: remove global variables where it trivial to do\n      builtin/gc: move pruning of worktrees into a separate function\n      worktree: expose function to retrieve worktree names\n      builtin/maintenance: introduce \"worktree-prune\" task\n      builtin/gc: move rerere garbage collection into separate function\n      builtin/maintenance: introduce \"rerere-gc\" task\n\n Documentation/config/maintenance.adoc |  17 ++++\n Documentation/git-maintenance.adoc    |   8 ++\n builtin/gc.c                          | 149 +++++++++++++++++++++++++++-------\n builtin/worktree.c                    |  25 +++---\n t/t7900-maintenance.sh                | 115 ++++++++++++++++++++++++++\n worktree.c                            |  30 +++++++\n worktree.h                            |   8 ++\n 7 files changed, 308 insertions(+), 44 deletions(-)\n\nRange-diff versus v3:\n\n1:  0cf3cb937c3 = 1:  6614504400b builtin/gc: fix indentation of `cmd_gc()` parameters\n2:  5982d47cc95 = 2:  6c8c66282c5 builtin/gc: remove global variables where it trivial to do\n3:  afda176acd3 = 3:  bfa8fc00434 builtin/gc: move pruning of worktrees into a separate function\n4:  b3b6a14bf1c = 4:  d87b883cf62 worktree: expose function to retrieve worktree names\n5:  e02d59bb944 = 5:  354a1985b4b builtin/maintenance: introduce \"worktree-prune\" task\n6:  cb31ec6b8bd = 6:  6f40349b53f builtin/gc: move rerere garbage collection into separate function\n7:  65e8483fe4a ! 7:  e3c9da19a2c builtin/maintenance: introduce \"rerere-gc\" task\n    @@ Documentation/config/maintenance.adoc: maintenance.reflog-expire.auto::\n     +\tThis integer config option controls how often the `rerere-gc` task\n     +\tshould be run as part of `git maintenance run --auto`. If zero, then\n     +\tthe `rerere-gc` task will not run with the `--auto` option. A negative\n    -+\tvalue will force the task to run every time. Otherwise, a positive\n    -+\tvalue implies the command should run when there are at least this many\n    -+\tdirectory entries in the \"rr-cache\" directory. The default value is 1.\n    ++\tvalue will force the task to run every time. Otherwise, any positive\n    ++\tvalue implies the command will run when the \"rr-cache\" directory exists\n    ++\tand has at least one entry, regardless of whether it is stale or not.\n    ++\tThis heuristic may be refined in the future. The default value is 1.\n     +\n      maintenance.worktree-prune.auto::\n      \tThis integer config option controls how often the `worktree-prune` task\n    @@ builtin/gc.c: static int maintenance_task_rerere_gc(struct maintenance_run_opts\n     +\n     +\t/*\n     +\t * We skip garbage collection in case we either have no \"rr-cache\"\n    -+\t * directory or when it doesn't contain at least as many directories as\n    -+\t * indicated by \"maintenance.rerere-gc.auto\".\n    ++\t * directory or when it doesn't contain at least one entry.\n     +\t */\n     +\trepo_git_path_replace(the_repository, &path, \"rr-cache\");\n     +\tdir = opendir(path.buf);\n     +\tif (!dir)\n     +\t\tgoto out;\n    -+\n    -+\twhile (readdir_skip_dot_and_dotdot(dir)) {\n    -+\t\tif (--limit)\n    -+\t\t\tcontinue;\n    -+\n    -+\t\tshould_gc = 1;\n    -+\t\tgoto out;\n    -+\t}\n    ++\tshould_gc = !!readdir_skip_dot_and_dotdot(dir);\n     +\n     +out:\n     +\tstrbuf_release(&path);\n    @@ t/t7900-maintenance.sh: test_expect_success 'worktree-prune task honors gc.workt\n     +\t# A negative value should always prune.\n     +\ttest_expect_rerere_gc git -c maintenance.rerere-gc.auto=-1 maintenance run --auto --task=rerere-gc &&\n     +\n    ++\t# A positive value prunes when there is at least one entry.\n    ++\ttest_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&\n     +\tmkdir .git/rr-cache &&\n    ++\ttest_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&\n     +\t: >.git/rr-cache/entry-1 &&\n    -+\t: >.git/rr-cache/entry-2 &&\n    ++\ttest_expect_rerere_gc git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&\n     +\n     +\t# Zero should never prune.\n    -+\ttest_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 maintenance run --auto --task=rerere-gc &&\n    -+\t# A positive value should require at least this many stale rerere entries.\n    -+\ttest_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=3 maintenance run --auto --task=rerere-gc &&\n    -+\ttest_expect_rerere_gc git -c maintenance.rerere-gc.auto=2 maintenance run --auto --task=rerere-gc\n    ++\t: >.git/rr-cache/entry-1 &&\n    ++\ttest_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 maintenance run --auto --task=rerere-gc\n     +'\n     +\n      test_expect_success '--auto and --schedule incompatible' '\n\n---\nbase-commit: a2955b34f48265d240ab8c7deb0a929ec2d65fd0\nchange-id: 20250424-pks-maintenance-missing-tasks-8ffcdd596b73\n\n"},{"id":"517223","messageId":"20250505-pks-maintenance-missing-tasks-v4-1-141f4df906a1@pks.im","threadId":"63348","inReplyTo":"20250505-pks-maintenance-missing-tasks-v4-0-141f4df906a1@pks.im","subject":"[PATCH v4 1/7] builtin/gc: fix indentation of `cmd_gc()` parameters","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-05T08:51:41Z","receivedAt":"2025-05-05T08:51:52Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The parameters of `cmd_gc()` aren't indented properly. Fix this.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex d5c75be2522..a73ec22fb18 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -724,9 +724,9 @@ static void gc_before_repack(struct maintenance_run_opts *opts,\n }\n \n int cmd_gc(int argc,\n-const char **argv,\n-const char *prefix,\n-struct repository *repo UNUSED)\n+\t   const char **argv,\n+\t   const char *prefix,\n+\t   struct repository *repo UNUSED)\n {\n \tint aggressive = 0;\n \tint quiet = 0;\n\n-- \n2.49.0.987.g0cc8ee98dc.dirty\n\n"},{"id":"517224","messageId":"20250505-pks-maintenance-missing-tasks-v4-2-141f4df906a1@pks.im","threadId":"63348","inReplyTo":"20250505-pks-maintenance-missing-tasks-v4-0-141f4df906a1@pks.im","subject":"[PATCH v4 2/7] builtin/gc: remove global variables where it trivial to do","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-05T08:51:42Z","receivedAt":"2025-05-05T08:51:52Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We use a couple of global variables to assemble command line arguments\nfor subprocesses we execute in git-gc(1). All of these variables except\nthe one for git-repack(1) are only used in a single place though, so\nthey don't really add anything but confusion.\n\nRemove those variables.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 31 ++++++++++++-------------------\n 1 file changed, 12 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex a73ec22fb18..ada36e210f0 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -53,15 +53,9 @@ static const char * const builtin_gc_usage[] = {\n };\n \n static timestamp_t gc_log_expire_time;\n-\n static struct strvec repack = STRVEC_INIT;\n-static struct strvec prune = STRVEC_INIT;\n-static struct strvec prune_worktrees = STRVEC_INIT;\n-static struct strvec rerere = STRVEC_INIT;\n-\n static struct tempfile *pidfile;\n static struct lock_file log_lock;\n-\n static struct string_list pack_garbage = STRING_LIST_INIT_DUP;\n \n static void clean_pack_garbage(void)\n@@ -769,9 +763,6 @@ int cmd_gc(int argc,\n \t\t\t\t\t builtin_gc_usage, builtin_gc_options);\n \n \tstrvec_pushl(&repack, \"repack\", \"-d\", \"-l\", NULL);\n-\tstrvec_pushl(&prune, \"prune\", \"--expire\", NULL);\n-\tstrvec_pushl(&prune_worktrees, \"worktree\", \"prune\", \"--expire\", NULL);\n-\tstrvec_pushl(&rerere, \"rerere\", \"gc\", NULL);\n \n \tgc_config(&cfg);\n \n@@ -897,34 +888,36 @@ int cmd_gc(int argc,\n \t\tif (cfg.prune_expire) {\n \t\t\tstruct child_process prune_cmd = CHILD_PROCESS_INIT;\n \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, cfg.prune_expire);\n+\t\t\tstrvec_push(&prune_cmd.args, cfg.prune_expire);\n \t\t\tif (quiet)\n-\t\t\t\tstrvec_push(&prune, \"--no-progress\");\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,\n+\t\t\t\tstrvec_push(&prune_cmd.args,\n \t\t\t\t\t    \"--exclude-promisor-objects\");\n \t\t\tprune_cmd.git_cmd = 1;\n-\t\t\tstrvec_pushv(&prune_cmd.args, prune.v);\n+\n \t\t\tif (run_command(&prune_cmd))\n-\t\t\t\tdie(FAILED_RUN, prune.v[0]);\n+\t\t\t\tdie(FAILED_RUN, prune_cmd.args.v[0]);\n \t\t}\n \t}\n \n \tif (cfg.prune_worktrees_expire) {\n \t\tstruct child_process prune_worktrees_cmd = CHILD_PROCESS_INIT;\n \n-\t\tstrvec_push(&prune_worktrees, cfg.prune_worktrees_expire);\n \t\tprune_worktrees_cmd.git_cmd = 1;\n-\t\tstrvec_pushv(&prune_worktrees_cmd.args, prune_worktrees.v);\n+\t\tstrvec_pushl(&prune_worktrees_cmd.args, \"worktree\", \"prune\", \"--expire\", NULL);\n+\t\tstrvec_push(&prune_worktrees_cmd.args, cfg.prune_worktrees_expire);\n+\n \t\tif (run_command(&prune_worktrees_cmd))\n-\t\t\tdie(FAILED_RUN, prune_worktrees.v[0]);\n+\t\t\tdie(FAILED_RUN, prune_worktrees_cmd.args.v[0]);\n \t}\n \n \trerere_cmd.git_cmd = 1;\n-\tstrvec_pushv(&rerere_cmd.args, rerere.v);\n+\tstrvec_pushl(&rerere_cmd.args, \"rerere\", \"gc\", NULL);\n \tif (run_command(&rerere_cmd))\n-\t\tdie(FAILED_RUN, rerere.v[0]);\n+\t\tdie(FAILED_RUN, rerere_cmd.args.v[0]);\n \n \treport_garbage = report_pack_garbage;\n \treprepare_packed_git(the_repository);\n\n-- \n2.49.0.987.g0cc8ee98dc.dirty\n\n"},{"id":"517225","messageId":"20250505-pks-maintenance-missing-tasks-v4-3-141f4df906a1@pks.im","threadId":"63348","inReplyTo":"20250505-pks-maintenance-missing-tasks-v4-0-141f4df906a1@pks.im","subject":"[PATCH v4 3/7] builtin/gc: move pruning of worktrees into a separate function","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-05T08:51:43Z","receivedAt":"2025-05-05T08:51:53Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Move pruning of worktrees into a separate function. This prepares for a\nsubsequent commit where we introduce a new \"worktree-prune\" task for\ngit-maintenance(1).\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 25 +++++++++++++++----------\n 1 file changed, 15 insertions(+), 10 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex ada36e210f0..005ecc3f192 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -334,6 +334,18 @@ static int maintenance_task_reflog_expire(struct maintenance_run_opts *opts UNUS\n \treturn run_command(&cmd);\n }\n \n+static int maintenance_task_worktree_prune(struct maintenance_run_opts *opts UNUSED,\n+\t\t\t\t\t   struct gc_config *cfg)\n+{\n+\tstruct child_process prune_worktrees_cmd = CHILD_PROCESS_INIT;\n+\n+\tprune_worktrees_cmd.git_cmd = 1;\n+\tstrvec_pushl(&prune_worktrees_cmd.args, \"worktree\", \"prune\", \"--expire\", NULL);\n+\tstrvec_push(&prune_worktrees_cmd.args, cfg->prune_worktrees_expire);\n+\n+\treturn run_command(&prune_worktrees_cmd);\n+}\n+\n static int too_many_loose_objects(struct gc_config *cfg)\n {\n \t/*\n@@ -903,16 +915,9 @@ int cmd_gc(int argc,\n \t\t}\n \t}\n \n-\tif (cfg.prune_worktrees_expire) {\n-\t\tstruct child_process prune_worktrees_cmd = CHILD_PROCESS_INIT;\n-\n-\t\tprune_worktrees_cmd.git_cmd = 1;\n-\t\tstrvec_pushl(&prune_worktrees_cmd.args, \"worktree\", \"prune\", \"--expire\", NULL);\n-\t\tstrvec_push(&prune_worktrees_cmd.args, cfg.prune_worktrees_expire);\n-\n-\t\tif (run_command(&prune_worktrees_cmd))\n-\t\t\tdie(FAILED_RUN, prune_worktrees_cmd.args.v[0]);\n-\t}\n+\tif (cfg.prune_worktrees_expire &&\n+\t    maintenance_task_worktree_prune(&opts, &cfg))\n+\t\tdie(FAILED_RUN, \"worktree\");\n \n \trerere_cmd.git_cmd = 1;\n \tstrvec_pushl(&rerere_cmd.args, \"rerere\", \"gc\", NULL);\n\n-- \n2.49.0.987.g0cc8ee98dc.dirty\n\n"},{"id":"517226","messageId":"20250505-pks-maintenance-missing-tasks-v4-4-141f4df906a1@pks.im","threadId":"63348","inReplyTo":"20250505-pks-maintenance-missing-tasks-v4-0-141f4df906a1@pks.im","subject":"[PATCH v4 4/7] worktree: expose function to retrieve worktree names","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-05T08:51:44Z","receivedAt":"2025-05-05T08:51:53Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a function that retrieves worktree names as present in\n\".git/worktrees\". This function will be used in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/worktree.c | 25 ++++++++++++-------------\n worktree.c         | 30 ++++++++++++++++++++++++++++++\n worktree.h         |  8 ++++++++\n 3 files changed, 50 insertions(+), 13 deletions(-)\n\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 87ccd47794c..9b00dbf1265 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -211,27 +211,24 @@ static void prune_dups(struct string_list *l)\n \n static void prune_worktrees(void)\n {\n-\tstruct strbuf reason = STRBUF_INIT;\n \tstruct strbuf main_path = STRBUF_INIT;\n \tstruct string_list kept = STRING_LIST_INIT_DUP;\n-\tchar *path;\n-\tDIR *dir;\n-\tstruct dirent *d;\n+\tstruct strvec worktrees = STRVEC_INIT;\n+\tstruct strbuf reason = STRBUF_INIT;\n \n-\tpath = repo_git_path(the_repository, \"worktrees\");\n-\tdir = opendir(path);\n-\tfree(path);\n-\tif (!dir)\n+\tif (get_worktree_names(the_repository, &worktrees) < 0 ||\n+\t    !worktrees.nr)\n \t\treturn;\n-\twhile ((d = readdir_skip_dot_and_dotdot(dir)) != NULL) {\n+\n+\tfor (size_t i = 0; i < worktrees.nr; i++) {\n \t\tchar *path;\n+\n \t\tstrbuf_reset(&reason);\n-\t\tif (should_prune_worktree(d->d_name, &reason, &path, expire))\n-\t\t\tprune_worktree(d->d_name, reason.buf);\n+\t\tif (should_prune_worktree(worktrees.v[i], &reason, &path, expire))\n+\t\t\tprune_worktree(worktrees.v[i], reason.buf);\n \t\telse if (path)\n-\t\t\tstring_list_append_nodup(&kept, path)->util = xstrdup(d->d_name);\n+\t\t\tstring_list_append_nodup(&kept, path)->util = xstrdup(worktrees.v[i]);\n \t}\n-\tclosedir(dir);\n \n \tstrbuf_add_absolute_path(&main_path, repo_get_common_dir(the_repository));\n \t/* massage main worktree absolute path to match 'gitdir' content */\n@@ -242,6 +239,8 @@ static void prune_worktrees(void)\n \n \tif (!show_only)\n \t\tdelete_worktrees_dir_if_empty();\n+\n+\tstrvec_clear(&worktrees);\n \tstrbuf_release(&reason);\n }\n \ndiff --git a/worktree.c b/worktree.c\nindex c34b9eb74e5..947b7a82209 100644\n--- a/worktree.c\n+++ b/worktree.c\n@@ -988,6 +988,36 @@ int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath,\n \treturn rc;\n }\n \n+int get_worktree_names(struct repository *repo, struct strvec *out)\n+{\n+\tchar *worktrees_dir;\n+\tstruct dirent *d;\n+\tDIR *dir;\n+\tint ret;\n+\n+\tworktrees_dir = repo_git_path(repo, \"worktrees\");\n+\tdir = opendir(worktrees_dir);\n+\tif (!dir) {\n+\t\tif (errno == ENOENT) {\n+\t\t\tret = 0;\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\twhile ((d = readdir_skip_dot_and_dotdot(dir)) != NULL)\n+\t\tstrvec_push(out, d->d_name);\n+\n+\tret = 0;\n+out:\n+\tif (dir)\n+\t\tclosedir(dir);\n+\tfree(worktrees_dir);\n+\treturn ret;\n+}\n+\n static int move_config_setting(const char *key, const char *value,\n \t\t\t       const char *from_file, const char *to_file)\n {\ndiff --git a/worktree.h b/worktree.h\nindex e4bcccdc0ae..59825c37881 100644\n--- a/worktree.h\n+++ b/worktree.h\n@@ -38,6 +38,14 @@ struct worktree **get_worktrees(void);\n  */\n struct worktree **get_worktrees_without_reading_head(void);\n \n+/*\n+ * Retrieve all worktree names. Not all names may correspond to a fully\n+ * functional worktree. Returns 0 on success, a negative error code on failure.\n+ * Calling the function on a repository that doesn't have any worktrees is not\n+ * considered an error.\n+ */\n+int get_worktree_names(struct repository *repo, struct strvec *out);\n+\n /*\n  * Returns 1 if linked worktrees exist, 0 otherwise.\n  */\n\n-- \n2.49.0.987.g0cc8ee98dc.dirty\n\n"},{"id":"517227","messageId":"20250505-pks-maintenance-missing-tasks-v4-6-141f4df906a1@pks.im","threadId":"63348","inReplyTo":"20250505-pks-maintenance-missing-tasks-v4-0-141f4df906a1@pks.im","subject":"[PATCH v4 6/7] builtin/gc: move rerere garbage collection into separate function","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-05T08:51:46Z","receivedAt":"2025-05-05T08:51:54Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Move garbage collection of cached rerere entries into a separate\nfunction. This prepares us for a subsequent commit where we introduce a\nnew \"rerere-gc\" task for git-maintenance(1).\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 16 +++++++++++-----\n 1 file changed, 11 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 93a8c856dbc..3dd1d07cca4 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -386,6 +386,15 @@ static int worktree_prune_condition(struct gc_config *cfg)\n \treturn should_prune;\n }\n \n+static int maintenance_task_rerere_gc(struct maintenance_run_opts *opts UNUSED,\n+\t\t\t\t      struct gc_config *cfg UNUSED)\n+{\n+\tstruct child_process rerere_cmd = CHILD_PROCESS_INIT;\n+\trerere_cmd.git_cmd = 1;\n+\tstrvec_pushl(&rerere_cmd.args, \"rerere\", \"gc\", NULL);\n+\treturn run_command(&rerere_cmd);\n+}\n+\n static int too_many_loose_objects(struct gc_config *cfg)\n {\n \t/*\n@@ -782,7 +791,6 @@ int cmd_gc(int argc,\n \tint daemonized = 0;\n \tint keep_largest_pack = -1;\n \ttimestamp_t dummy;\n-\tstruct child_process rerere_cmd = CHILD_PROCESS_INIT;\n \tstruct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT;\n \tstruct gc_config cfg = GC_CONFIG_INIT;\n \tconst char *prune_expire_sentinel = \"sentinel\";\n@@ -959,10 +967,8 @@ int cmd_gc(int argc,\n \t    maintenance_task_worktree_prune(&opts, &cfg))\n \t\tdie(FAILED_RUN, \"worktree\");\n \n-\trerere_cmd.git_cmd = 1;\n-\tstrvec_pushl(&rerere_cmd.args, \"rerere\", \"gc\", NULL);\n-\tif (run_command(&rerere_cmd))\n-\t\tdie(FAILED_RUN, rerere_cmd.args.v[0]);\n+\tif (maintenance_task_rerere_gc(&opts, &cfg))\n+\t\tdie(FAILED_RUN, \"rerere\");\n \n \treport_garbage = report_pack_garbage;\n \treprepare_packed_git(the_repository);\n\n-- \n2.49.0.987.g0cc8ee98dc.dirty\n\n"},{"id":"517228","messageId":"20250505-pks-maintenance-missing-tasks-v4-5-141f4df906a1@pks.im","threadId":"63348","inReplyTo":"20250505-pks-maintenance-missing-tasks-v4-0-141f4df906a1@pks.im","subject":"[PATCH v4 5/7] builtin/maintenance: introduce \"worktree-prune\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-05T08:51:45Z","receivedAt":"2025-05-05T08:51:54Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While git-gc(1) knows to prune stale worktrees, git-maintenance(1) does\nnot yet have a task for this cleanup. Introduce a new \"worktree-prune\"\ntask to plug this gap.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/maintenance.adoc |  8 ++++\n Documentation/git-maintenance.adoc    |  4 ++\n builtin/gc.c                          | 46 +++++++++++++++++++++++\n t/t7900-maintenance.sh                | 71 +++++++++++++++++++++++++++++++++++\n 4 files changed, 129 insertions(+)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex 41536162a77..b36b62c1c47 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -83,3 +83,11 @@ maintenance.reflog-expire.auto::\n \tpositive value implies the command should run when the number of\n \texpired reflog entries in the \"HEAD\" reflog is at least the value of\n \t`maintenance.loose-objects.auto`. The default value is 100.\n+\n+maintenance.worktree-prune.auto::\n+\tThis integer config option controls how often the `worktree-prune` task\n+\tshould be run as part of `git maintenance run --auto`. If zero, then\n+\tthe `worktree-prune` task will not run with the `--auto` option. A\n+\tnegative value will force the task to run every time. Otherwise, a\n+\tpositive value implies the command should run when the number of\n+\tprunable worktrees exceeds the value. The default value is 1.\ndiff --git a/Documentation/git-maintenance.adoc b/Documentation/git-maintenance.adoc\nindex 3a1e2a69b6b..6f085a9cf8c 100644\n--- a/Documentation/git-maintenance.adoc\n+++ b/Documentation/git-maintenance.adoc\n@@ -166,6 +166,10 @@ reflog-expire::\n \tThe `reflog-expire` task deletes any entries in the reflog older than the\n \texpiry threshold. See linkgit:git-reflog[1] for more information.\n \n+worktree-prune::\n+\tThe `worktree-prune` task deletes stale or broken worktrees. See\n+\tlinkit:git-worktree[1] for more information.\n+\n OPTIONS\n -------\n --auto::\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 005ecc3f192..93a8c856dbc 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -44,6 +44,7 @@\n #include \"hook.h\"\n #include \"setup.h\"\n #include \"trace2.h\"\n+#include \"worktree.h\"\n \n #define FAILED_RUN \"failed to run %s\"\n \n@@ -346,6 +347,45 @@ static int maintenance_task_worktree_prune(struct maintenance_run_opts *opts UNU\n \treturn run_command(&prune_worktrees_cmd);\n }\n \n+static int worktree_prune_condition(struct gc_config *cfg)\n+{\n+\tstruct strvec worktrees = STRVEC_INIT;\n+\tstruct strbuf reason = STRBUF_INIT;\n+\ttimestamp_t expiry_date;\n+\tint should_prune = 0;\n+\tint limit = 1;\n+\n+\tgit_config_get_int(\"maintenance.worktree-prune.auto\", &limit);\n+\tif (limit <= 0) {\n+\t\tshould_prune = limit < 0;\n+\t\tgoto out;\n+\t}\n+\n+\tif (parse_expiry_date(cfg->prune_worktrees_expire, &expiry_date) ||\n+\t    get_worktree_names(the_repository, &worktrees) < 0)\n+\t\tgoto out;\n+\n+\tfor (size_t i = 0; i < worktrees.nr; i++) {\n+\t\tchar *wtpath;\n+\n+\t\tstrbuf_reset(&reason);\n+\t\tif (should_prune_worktree(worktrees.v[i], &reason, &wtpath, expiry_date)) {\n+\t\t\tlimit--;\n+\n+\t\t\tif (!limit) {\n+\t\t\t\tshould_prune = 1;\n+\t\t\t\tgoto out;\n+\t\t\t}\n+\t\t}\n+\t\tfree(wtpath);\n+\t}\n+\n+out:\n+\tstrvec_clear(&worktrees);\n+\tstrbuf_release(&reason);\n+\treturn should_prune;\n+}\n+\n static int too_many_loose_objects(struct gc_config *cfg)\n {\n \t/*\n@@ -1455,6 +1495,7 @@ enum maintenance_task_label {\n \tTASK_COMMIT_GRAPH,\n \tTASK_PACK_REFS,\n \tTASK_REFLOG_EXPIRE,\n+\tTASK_WORKTREE_PRUNE,\n \n \t/* Leave as final value */\n \tTASK__COUNT\n@@ -1496,6 +1537,11 @@ static struct maintenance_task tasks[] = {\n \t\tmaintenance_task_reflog_expire,\n \t\treflog_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},\n };\n \n static int compare_tasks_by_selection(const void *a_, const void *b_)\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 9b82e11c100..530c56ae91e 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -493,6 +493,77 @@ test_expect_success 'reflog-expire task --auto only packs when exceeding limits'\n \ttest_subcommand git reflog expire --all <reflog-expire-auto.txt\n '\n \n+test_expect_worktree_prune () {\n+\tnegate=\n+\tif test \"$1\" = \"!\"\n+\tthen\n+\t\tnegate=\"!\"\n+\t\tshift\n+\tfi\n+\n+\trm -f \"worktree-prune.txt\" &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/worktree-prune.txt\" \"$@\" &&\n+\ttest_subcommand $negate git worktree prune --expire 3.months.ago <worktree-prune.txt\n+}\n+\n+test_expect_success 'worktree-prune task without --auto always prunes' '\n+\ttest_expect_worktree_prune git maintenance run --task=worktree-prune\n+'\n+\n+test_expect_success 'worktree-prune task --auto only prunes with prunable worktree' '\n+\ttest_expect_worktree_prune ! git maintenance run --auto --task=worktree-prune &&\n+\tmkdir .git/worktrees &&\n+\t: >.git/worktrees/abc &&\n+\ttest_expect_worktree_prune git maintenance run --auto --task=worktree-prune\n+'\n+\n+test_expect_success 'worktree-prune task with --auto honors maintenance.worktree-prune.auto' '\n+\t# A negative value should always prune.\n+\ttest_expect_worktree_prune git -c maintenance.worktree-prune.auto=-1 maintenance run --auto --task=worktree-prune &&\n+\n+\tmkdir .git/worktrees &&\n+\t: >.git/worktrees/first &&\n+\t: >.git/worktrees/second &&\n+\t: >.git/worktrees/third &&\n+\n+\t# Zero should never prune.\n+\ttest_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=0 maintenance run --auto --task=worktree-prune &&\n+\t# A positive value should require at least this man prunable worktrees.\n+\ttest_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=4 maintenance run --auto --task=worktree-prune &&\n+\ttest_expect_worktree_prune git -c maintenance.worktree-prune.auto=3 maintenance run --auto --task=worktree-prune\n+'\n+\n+test_expect_success 'worktree-prune task with --auto honors maintenance.worktree-prune.auto' '\n+\t# A negative value should always prune.\n+\ttest_expect_worktree_prune git -c maintenance.worktree-prune.auto=-1 maintenance run --auto --task=worktree-prune &&\n+\n+\tmkdir .git/worktrees &&\n+\t: >.git/worktrees/first &&\n+\t: >.git/worktrees/second &&\n+\t: >.git/worktrees/third &&\n+\n+\t# Zero should never prune.\n+\ttest_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=0 maintenance run --auto --task=worktree-prune &&\n+\t# A positive value should require at least this many prunable worktrees.\n+\ttest_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=4 maintenance run --auto --task=worktree-prune &&\n+\ttest_expect_worktree_prune git -c maintenance.worktree-prune.auto=3 maintenance run --auto --task=worktree-prune\n+'\n+\n+test_expect_success 'worktree-prune task honors gc.worktreePruneExpire' '\n+\tgit worktree add worktree &&\n+\trm -rf worktree &&\n+\n+\trm -f worktree-prune.txt &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/worktree-prune.txt\" git -c gc.worktreePruneExpire=1.week.ago maintenance run --auto --task=worktree-prune &&\n+\ttest_subcommand ! git worktree prune --expire 1.week.ago <worktree-prune.txt &&\n+\ttest_path_is_dir .git/worktrees/worktree &&\n+\n+\trm -f worktree-prune.txt &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/worktree-prune.txt\" git -c gc.worktreePruneExpire=now maintenance run --auto --task=worktree-prune &&\n+\ttest_subcommand git worktree prune --expire now <worktree-prune.txt &&\n+\ttest_path_is_missing .git/worktrees/worktree\n+'\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\n-- \n2.49.0.987.g0cc8ee98dc.dirty\n\n"},{"id":"517229","messageId":"20250505-pks-maintenance-missing-tasks-v4-7-141f4df906a1@pks.im","threadId":"63348","inReplyTo":"20250505-pks-maintenance-missing-tasks-v4-0-141f4df906a1@pks.im","subject":"[PATCH v4 7/7] builtin/maintenance: introduce \"rerere-gc\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-05T08:51:47Z","receivedAt":"2025-05-05T08:51:56Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While git-gc(1) knows to garbage collect the rerere cache,\ngit-maintenance(1) does not yet have a task for this cleanup. Introduce\na new \"rerere-gc\" task to plug this gap.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/maintenance.adoc |  9 +++++++\n Documentation/git-maintenance.adoc    |  4 ++++\n builtin/gc.c                          | 37 +++++++++++++++++++++++++++++\n t/t7900-maintenance.sh                | 44 +++++++++++++++++++++++++++++++++++\n 4 files changed, 94 insertions(+)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex b36b62c1c47..2f719342183 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -84,6 +84,15 @@ maintenance.reflog-expire.auto::\n \texpired reflog entries in the \"HEAD\" reflog is at least the value of\n \t`maintenance.loose-objects.auto`. The default value is 100.\n \n+maintenance.rerere-gc.auto::\n+\tThis integer config option controls how often the `rerere-gc` task\n+\tshould be run as part of `git maintenance run --auto`. If zero, then\n+\tthe `rerere-gc` task will not run with the `--auto` option. A negative\n+\tvalue will force the task to run every time. Otherwise, any positive\n+\tvalue implies the command will run when the \"rr-cache\" directory exists\n+\tand has at least one entry, regardless of whether it is stale or not.\n+\tThis heuristic may be refined in the future. The default value is 1.\n+\n maintenance.worktree-prune.auto::\n \tThis integer config option controls how often the `worktree-prune` task\n \tshould be run as part of `git maintenance run --auto`. If zero, then\ndiff --git a/Documentation/git-maintenance.adoc b/Documentation/git-maintenance.adoc\nindex 6f085a9cf8c..931f3e02e85 100644\n--- a/Documentation/git-maintenance.adoc\n+++ b/Documentation/git-maintenance.adoc\n@@ -166,6 +166,10 @@ reflog-expire::\n \tThe `reflog-expire` task deletes any entries in the reflog older than the\n \texpiry threshold. See linkgit:git-reflog[1] for more information.\n \n+rerere-gc::\n+\tThe `rerere-gc` task invokes garbage collection for stale entries in\n+\tthe rerere cache. See linkgit:git-rerere[1] for more information.\n+\n worktree-prune::\n \tThe `worktree-prune` task deletes stale or broken worktrees. See\n \tlinkit:git-worktree[1] for more information.\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 3dd1d07cca4..0d4a70c7804 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -16,6 +16,7 @@\n #include \"builtin.h\"\n #include \"abspath.h\"\n #include \"date.h\"\n+#include \"dir.h\"\n #include \"environment.h\"\n #include \"hex.h\"\n #include \"config.h\"\n@@ -34,6 +35,7 @@\n #include \"pack-objects.h\"\n #include \"path.h\"\n #include \"reflog.h\"\n+#include \"rerere.h\"\n #include \"blob.h\"\n #include \"tree.h\"\n #include \"promisor-remote.h\"\n@@ -395,6 +397,35 @@ static int maintenance_task_rerere_gc(struct maintenance_run_opts *opts UNUSED,\n \treturn run_command(&rerere_cmd);\n }\n \n+static int rerere_gc_condition(struct gc_config *cfg UNUSED)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\tint should_gc = 0, limit = 1;\n+\tDIR *dir = NULL;\n+\n+\tgit_config_get_int(\"maintenance.rerere-gc.auto\", &limit);\n+\tif (limit <= 0) {\n+\t\tshould_gc = limit < 0;\n+\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t * We skip garbage collection in case we either have no \"rr-cache\"\n+\t * directory or when it doesn't contain at least one entry.\n+\t */\n+\trepo_git_path_replace(the_repository, &path, \"rr-cache\");\n+\tdir = opendir(path.buf);\n+\tif (!dir)\n+\t\tgoto out;\n+\tshould_gc = !!readdir_skip_dot_and_dotdot(dir);\n+\n+out:\n+\tstrbuf_release(&path);\n+\tif (dir)\n+\t\tclosedir(dir);\n+\treturn should_gc;\n+}\n+\n static int too_many_loose_objects(struct gc_config *cfg)\n {\n \t/*\n@@ -1502,6 +1533,7 @@ enum maintenance_task_label {\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@@ -1548,6 +1580,11 @@ static struct maintenance_task tasks[] = {\n \t\tmaintenance_task_worktree_prune,\n \t\tworktree_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},\n };\n \n static int compare_tasks_by_selection(const void *a_, const void *b_)\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 530c56ae91e..af02a551869 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -564,6 +564,50 @@ test_expect_success 'worktree-prune task honors gc.worktreePruneExpire' '\n \ttest_path_is_missing .git/worktrees/worktree\n '\n \n+test_expect_rerere_gc () {\n+\tnegate=\n+\tif test \"$1\" = \"!\"\n+\tthen\n+\t\tnegate=\"!\"\n+\t\tshift\n+\tfi\n+\n+\trm -f \"rerere-gc.txt\" &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/rerere-gc.txt\" \"$@\" &&\n+\ttest_subcommand $negate git rerere gc <rerere-gc.txt\n+}\n+\n+test_expect_success 'rerere-gc task without --auto always collects garbage' '\n+\ttest_expect_rerere_gc git maintenance run --task=rerere-gc\n+'\n+\n+test_expect_success 'rerere-gc task with --auto only prunes with prunable entries' '\n+\ttest_when_finished \"rm -rf .git/rr-cache\" &&\n+\ttest_expect_rerere_gc ! git maintenance run --auto --task=rerere-gc &&\n+\tmkdir .git/rr-cache &&\n+\ttest_expect_rerere_gc ! git maintenance run --auto --task=rerere-gc &&\n+\t: >.git/rr-cache/entry &&\n+\ttest_expect_rerere_gc git maintenance run --auto --task=rerere-gc\n+'\n+\n+test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.auto' '\n+\ttest_when_finished \"rm -rf .git/rr-cache\" &&\n+\n+\t# A negative value should always prune.\n+\ttest_expect_rerere_gc git -c maintenance.rerere-gc.auto=-1 maintenance run --auto --task=rerere-gc &&\n+\n+\t# A positive value prunes when there is at least one entry.\n+\ttest_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&\n+\tmkdir .git/rr-cache &&\n+\ttest_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&\n+\t: >.git/rr-cache/entry-1 &&\n+\ttest_expect_rerere_gc git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&\n+\n+\t# Zero should never prune.\n+\t: >.git/rr-cache/entry-1 &&\n+\ttest_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 maintenance run --auto --task=rerere-gc\n+'\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\n-- \n2.49.0.987.g0cc8ee98dc.dirty\n\n"},{"id":"517294","messageId":"CAP8UFD2cuMxdpGFuBoKirw2mko6gt-djndqqZy92NypPiiFCYQ@mail.gmail.com","threadId":"63348","inReplyTo":"20250505-pks-maintenance-missing-tasks-v4-5-141f4df906a1@pks.im","subject":"Re: [PATCH v4 5/7] builtin/maintenance: introduce \"worktree-prune\" task","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-06T07:40:55Z","receivedAt":"2025-05-06T07:41:09Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Mon, May 5, 2025 at 10:52 AM Patrick Steinhardt <ps@pks.im> wrote:\n\n> +static int worktree_prune_condition(struct gc_config *cfg)\n> +{\n> +       struct strvec worktrees = STRVEC_INIT;\n> +       struct strbuf reason = STRBUF_INIT;\n> +       timestamp_t expiry_date;\n> +       int should_prune = 0;\n> +       int limit = 1;\n> +\n> +       git_config_get_int(\"maintenance.worktree-prune.auto\", &limit);\n> +       if (limit <= 0) {\n> +               should_prune = limit < 0;\n> +               goto out;\n> +       }\n> +\n> +       if (parse_expiry_date(cfg->prune_worktrees_expire, &expiry_date) ||\n> +           get_worktree_names(the_repository, &worktrees) < 0)\n> +               goto out;\n> +\n> +       for (size_t i = 0; i < worktrees.nr; i++) {\n> +               char *wtpath;\n> +\n> +               strbuf_reset(&reason);\n> +               if (should_prune_worktree(worktrees.v[i], &reason, &wtpath, expiry_date)) {\n> +                       limit--;\n> +\n> +                       if (!limit) {\n> +                               should_prune = 1;\n> +                               goto out;\n\nEric noticed in a previous round that wtpath is leaked in this `goto\nout` path, and it seems to me that it's still the case.\n\n> +                       }\n> +               }\n> +               free(wtpath);\n> +       }\n> +\n> +out:\n> +       strvec_clear(&worktrees);\n> +       strbuf_release(&reason);\n> +       return should_prune;\n> +}\n"},{"id":"517295","messageId":"CAP8UFD2BDv=kp7W6w=J00iRAj-Jqz_EjHGfH_YpNTEWC2a82QA@mail.gmail.com","threadId":"63348","inReplyTo":"20250505-pks-maintenance-missing-tasks-v4-2-141f4df906a1@pks.im","subject":"Re: [PATCH v4 2/7] builtin/gc: remove global variables where it trivial to do","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-06T07:44:03Z","receivedAt":"2025-05-06T07:44:18Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Mon, May 5, 2025 at 10:52 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> We use a couple of global variables to assemble command line arguments\n> for subprocesses we execute in git-gc(1). All of these variables except\n> the one for git-repack(1) are only used in a single place though, so\n> they don't really add anything but confusion.\n>\n> Remove those variables.\n\nAbout the commit message it seems to me that it's missing \"is\", so maybe:\n\n\"builtin/gc: remove global variables where it's trivial to do\"\n\nor just:\n\n\"builtin/gc: remove global variables where trivial to do\"\n\n?\n"},{"id":"517296","messageId":"CAP8UFD1ZtYMf0ja75AV_jGJgzAL919zpHoGy0v-G2tNV+OEDwA@mail.gmail.com","threadId":"63348","inReplyTo":"20250505-pks-maintenance-missing-tasks-v4-3-141f4df906a1@pks.im","subject":"Re: [PATCH v4 3/7] builtin/gc: move pruning of worktrees into a separate function","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-06T07:50:12Z","receivedAt":"2025-05-06T07:50:27Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Mon, May 5, 2025 at 10:52 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> Move pruning of worktrees into a separate function.\n\nRepeating the subject at the beginning of the body part of the commit\nmessage doesn't bring much value.\n\n> This prepares for a\n> subsequent commit where we introduce a new \"worktree-prune\" task for\n> git-maintenance(1).\n\nI think it might help a bit to reword in the usual \"describe the\nproblem first and then say what to do about it\" way. Maybe something\nlike:\n\n\"In a subsequent commit we are going to introduce a new\n\"worktree-prune\" task for git-maintenance(1).\n\nTo prepare for this, refactor the code that prunes worktrees into a\nseparate function.\"\n"},{"id":"517298","messageId":"CAP8UFD24jwKjP6XdD_SSv7ukgyqewCThMd-wQSRsDk00X7btiQ@mail.gmail.com","threadId":"63348","inReplyTo":"20250505-pks-maintenance-missing-tasks-v4-4-141f4df906a1@pks.im","subject":"Re: [PATCH v4 4/7] worktree: expose function to retrieve worktree names","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-06T08:20:12Z","receivedAt":"2025-05-06T08:20:26Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Mon, May 5, 2025 at 10:52 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> Introduce a function that retrieves worktree names as present in\n> \".git/worktrees\". This function will be used in a subsequent commit.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  builtin/worktree.c | 25 ++++++++++++-------------\n>  worktree.c         | 30 ++++++++++++++++++++++++++++++\n>  worktree.h         |  8 ++++++++\n>  3 files changed, 50 insertions(+), 13 deletions(-)\n>\n> diff --git a/builtin/worktree.c b/builtin/worktree.c\n> index 87ccd47794c..9b00dbf1265 100644\n> --- a/builtin/worktree.c\n> +++ b/builtin/worktree.c\n> @@ -211,27 +211,24 @@ static void prune_dups(struct string_list *l)\n>\n>  static void prune_worktrees(void)\n>  {\n> -       struct strbuf reason = STRBUF_INIT;\n>         struct strbuf main_path = STRBUF_INIT;\n>         struct string_list kept = STRING_LIST_INIT_DUP;\n> -       char *path;\n> -       DIR *dir;\n> -       struct dirent *d;\n> +       struct strvec worktrees = STRVEC_INIT;\n> +       struct strbuf reason = STRBUF_INIT;\n>\n> -       path = repo_git_path(the_repository, \"worktrees\");\n> -       dir = opendir(path);\n> -       free(path);\n> -       if (!dir)\n> +       if (get_worktree_names(the_repository, &worktrees) < 0 ||\n> +           !worktrees.nr)\n>                 return;\n> -       while ((d = readdir_skip_dot_and_dotdot(dir)) != NULL) {\n> +\n> +       for (size_t i = 0; i < worktrees.nr; i++) {\n>                 char *path;\n> +\n>                 strbuf_reset(&reason);\n> -               if (should_prune_worktree(d->d_name, &reason, &path, expire))\n> -                       prune_worktree(d->d_name, reason.buf);\n> +               if (should_prune_worktree(worktrees.v[i], &reason, &path, expire))\n> +                       prune_worktree(worktrees.v[i], reason.buf);\n>                 else if (path)\n> -                       string_list_append_nodup(&kept, path)->util = xstrdup(d->d_name);\n> +                       string_list_append_nodup(&kept, path)->util = xstrdup(worktrees.v[i]);\n>         }\n> -       closedir(dir);\n>\n>         strbuf_add_absolute_path(&main_path, repo_get_common_dir(the_repository));\n>         /* massage main worktree absolute path to match 'gitdir' content */\n> @@ -242,6 +239,8 @@ static void prune_worktrees(void)\n>\n>         if (!show_only)\n>                 delete_worktrees_dir_if_empty();\n> +\n> +       strvec_clear(&worktrees);\n>         strbuf_release(&reason);\n>  }\n>\n> diff --git a/worktree.c b/worktree.c\n> index c34b9eb74e5..947b7a82209 100644\n> --- a/worktree.c\n> +++ b/worktree.c\n> @@ -988,6 +988,36 @@ int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath,\n>         return rc;\n>  }\n>\n> +int get_worktree_names(struct repository *repo, struct strvec *out)\n> +{\n> +       char *worktrees_dir;\n> +       struct dirent *d;\n> +       DIR *dir;\n> +       int ret;\n> +\n> +       worktrees_dir = repo_git_path(repo, \"worktrees\");\n> +       dir = opendir(worktrees_dir);\n> +       if (!dir) {\n> +               if (errno == ENOENT) {\n> +                       ret = 0;\n> +                       goto out;\n> +               }\n> +\n> +               ret = -1;\n> +               goto out;\n> +       }\n> +\n> +       while ((d = readdir_skip_dot_and_dotdot(dir)) != NULL)\n> +               strvec_push(out, d->d_name);\n> +\n> +       ret = 0;\n> +out:\n> +       if (dir)\n> +               closedir(dir);\n> +       free(worktrees_dir);\n> +       return ret;\n> +}\n\nNit: this function seems to use \"goto out\" a bit too much for me. What\nabout something like:\n\nint get_worktree_names(struct repository *repo, struct strvec *out)\n{\n    int ret = 0;\n    char *worktrees_dir = repo_git_path(repo, \"worktrees\");\n    DIR *dir = opendir(worktrees_dir);\n\n    if (dir) {\n        struct dirent *d;\n        while ((d = readdir_skip_dot_and_dotdot(dir)) != NULL)\n            strvec_push(out, d->d_name);\n        closedir(dir);\n    } else {\n        if (errno != ENOENT)\n            ret = -1;\n    }\n\n    free(worktrees_dir);\n    return ret;\n}\n\n?\n"},{"id":"517302","messageId":"CAP8UFD0cKmL9e18bNGXOd0taahrK4Xx3CG+Z6BqDovAxbq0sYA@mail.gmail.com","threadId":"63348","inReplyTo":"20250505-pks-maintenance-missing-tasks-v4-6-141f4df906a1@pks.im","subject":"Re: [PATCH v4 6/7] builtin/gc: move rerere garbage collection into separate function","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-06T08:39:57Z","receivedAt":"2025-05-06T08:40:11Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Mon, May 5, 2025 at 10:52 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> Move garbage collection of cached rerere entries into a separate\n> function.\n\nNit: this is also repeating the commit message subject nearly as-is.\n\n> This prepares us for a subsequent commit where we introduce a\n> new \"rerere-gc\" task for git-maintenance(1).\n\nSo maybe something like:\n\n\"In a subsequent commit we are going to introduce a new \"rerere-gc\"\ntask for git-maintenance(1).\n\nTo prepare for this, refactor the code that garbage collects cached\nrerere entries into a separate function.\"\n"},{"id":"517304","messageId":"CAP8UFD0twBxZvEiLq8OBi2xhT+BnbL2k7swqru5CTQ67rdYM3Q@mail.gmail.com","threadId":"63348","inReplyTo":"20250505-pks-maintenance-missing-tasks-v4-0-141f4df906a1@pks.im","subject":"Re: [PATCH v4 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1)","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-06T09:05:42Z","receivedAt":"2025-05-06T09:05:55Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Mon, May 5, 2025 at 10:52 AM Patrick Steinhardt <ps@pks.im> wrote:\n\n> Changes in v4:\n>   - simplified the heuristic for \"rerere-gc\" even further. A positive\n>     value for \"maintenance.rerere-gc.auto\" now indicates that the\n>     command will run whenever there is at least one directory entry in\n>     \".rr-cache\". The exact value does not matter anymore.\n>   - Link to v3: https://lore.kernel.org/r/20250502-pks-maintenance-missing-tasks-v3-0-13e130d36640@pks.im\n\nExcept for a few nits I commented on in my replies to specific\npatches, and except that some comments from Eric Sunshine in\nhttps://lore.kernel.org/git/CAPig+cScor=E2i4w99NCrsaUd-Po=FqhkrLSyT69PSo1+h2dRw@mail.gmail.com/#t\nseems to have been overlooked (especially one about a memory leak),\nthis looks good to me.\n\nThanks!\n"},{"id":"517355","messageId":"CAPig+cSCFZArGbAXe77H3__nUAt1BTwDRsM6V2emqaMvHdxVgg@mail.gmail.com","threadId":"63348","inReplyTo":"CAP8UFD24jwKjP6XdD_SSv7ukgyqewCThMd-wQSRsDk00X7btiQ@mail.gmail.com","subject":"Re: [PATCH v4 4/7] worktree: expose function to retrieve worktree names","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2025-05-06T16:08:00Z","receivedAt":"2025-05-06T16:08:13Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Tue, May 6, 2025 at 4:23 AM Christian Couder\n<christian.couder@gmail.com> wrote:\n> On Mon, May 5, 2025 at 10:52 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > +int get_worktree_names(struct repository *repo, struct strvec *out)\n> > +{\n> > +       worktrees_dir = repo_git_path(repo, \"worktrees\");\n> > +       dir = opendir(worktrees_dir);\n> > +       if (!dir) {\n> > +               if (errno == ENOENT) {\n> > +                       ret = 0;\n> > +                       goto out;\n> > +               }\n> > +\n> > +               ret = -1;\n> > +               goto out;\n> > +       }\n> > +\n> > +       while ((d = readdir_skip_dot_and_dotdot(dir)) != NULL)\n> > +               strvec_push(out, d->d_name);\n> > +\n> > +       ret = 0;\n> > +out:\n> > +       if (dir)\n> > +               closedir(dir);\n> > +       free(worktrees_dir);\n> > +       return ret;\n> > +}\n>\n> Nit: this function seems to use \"goto out\" a bit too much for me. What\n> about something like:\n>\n> int get_worktree_names(struct repository *repo, struct strvec *out)\n> {\n>     int ret = 0;\n>     char *worktrees_dir = repo_git_path(repo, \"worktrees\");\n>     DIR *dir = opendir(worktrees_dir);\n>\n>     if (dir) {\n>         struct dirent *d;\n>         while ((d = readdir_skip_dot_and_dotdot(dir)) != NULL)\n>             strvec_push(out, d->d_name);\n>         closedir(dir);\n>     } else {\n>         if (errno != ENOENT)\n>             ret = -1;\n>     }\n>\n>     free(worktrees_dir);\n>     return ret;\n> }\n\nI had the same response and made a slightly more concise proposal[*],\nthough I'm not convinced that this function should exist at all.\n\n[*]: https://lore.kernel.org/git/CAPig+cSDDbhGrym8j=PFKBCUxBQhZPzAHXGvKy-Z6POA4Ju3sw@mail.gmail.com/\n"},{"id":"517450","messageId":"aBsGhpiLaYA8M351@pks.im","threadId":"63348","inReplyTo":"CAP8UFD2BDv=kp7W6w=J00iRAj-Jqz_EjHGfH_YpNTEWC2a82QA@mail.gmail.com","subject":"Re: [PATCH v4 2/7] builtin/gc: remove global variables where it trivial to do","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-07T07:06:46Z","receivedAt":"2025-05-07T07:06:50Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, May 06, 2025 at 09:44:03AM +0200, Christian Couder wrote:\n> On Mon, May 5, 2025 at 10:52 AM Patrick Steinhardt <ps@pks.im> wrote:\n> >\n> > We use a couple of global variables to assemble command line arguments\n> > for subprocesses we execute in git-gc(1). All of these variables except\n> > the one for git-repack(1) are only used in a single place though, so\n> > they don't really add anything but confusion.\n> >\n> > Remove those variables.\n> \n> About the commit message it seems to me that it's missing \"is\", so maybe:\n> \n> \"builtin/gc: remove global variables where it's trivial to do\"\n> \n> or just:\n> \n> \"builtin/gc: remove global variables where trivial to do\"\n> \n> ?\n\nAh, indeed.\n\nPatrick\n"},{"id":"517451","messageId":"aBsGiu3IBIUuYuYU@pks.im","threadId":"63348","inReplyTo":"CAP8UFD1ZtYMf0ja75AV_jGJgzAL919zpHoGy0v-G2tNV+OEDwA@mail.gmail.com","subject":"Re: [PATCH v4 3/7] builtin/gc: move pruning of worktrees into a separate function","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-07T07:06:50Z","receivedAt":"2025-05-07T07:06:54Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, May 06, 2025 at 09:50:12AM +0200, Christian Couder wrote:\n> On Mon, May 5, 2025 at 10:52 AM Patrick Steinhardt <ps@pks.im> wrote:\n> >\n> > Move pruning of worktrees into a separate function.\n> \n> Repeating the subject at the beginning of the body part of the commit\n> message doesn't bring much value.\n\nI disagree with that statement. The way I review commit messages the\nsubject is often detached from the body, so it always helps me quite a\nbit when the body is fully self-contained and doesn't depend on the\nsubject.\n\n> > This prepares for a\n> > subsequent commit where we introduce a new \"worktree-prune\" task for\n> > git-maintenance(1).\n> \n> I think it might help a bit to reword in the usual \"describe the\n> problem first and then say what to do about it\" way. Maybe something\n> like:\n> \n> \"In a subsequent commit we are going to introduce a new\n> \"worktree-prune\" task for git-maintenance(1).\n> \n> To prepare for this, refactor the code that prunes worktrees into a\n> separate function.\"\n\nBut this reads better regardless of that, so I'll take it. Thanks!\n\nPatrick\n"},{"id":"517452","messageId":"aBsGjVqYr2jY6oJ6@pks.im","threadId":"63348","inReplyTo":"CAP8UFD2cuMxdpGFuBoKirw2mko6gt-djndqqZy92NypPiiFCYQ@mail.gmail.com","subject":"Re: [PATCH v4 5/7] builtin/maintenance: introduce \"worktree-prune\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-07T07:06:53Z","receivedAt":"2025-05-07T07:06:57Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, May 06, 2025 at 09:40:55AM +0200, Christian Couder wrote:\n> On Mon, May 5, 2025 at 10:52 AM Patrick Steinhardt <ps@pks.im> wrote:\n> \n> > +static int worktree_prune_condition(struct gc_config *cfg)\n> > +{\n> > +       struct strvec worktrees = STRVEC_INIT;\n> > +       struct strbuf reason = STRBUF_INIT;\n> > +       timestamp_t expiry_date;\n> > +       int should_prune = 0;\n> > +       int limit = 1;\n> > +\n> > +       git_config_get_int(\"maintenance.worktree-prune.auto\", &limit);\n> > +       if (limit <= 0) {\n> > +               should_prune = limit < 0;\n> > +               goto out;\n> > +       }\n> > +\n> > +       if (parse_expiry_date(cfg->prune_worktrees_expire, &expiry_date) ||\n> > +           get_worktree_names(the_repository, &worktrees) < 0)\n> > +               goto out;\n> > +\n> > +       for (size_t i = 0; i < worktrees.nr; i++) {\n> > +               char *wtpath;\n> > +\n> > +               strbuf_reset(&reason);\n> > +               if (should_prune_worktree(worktrees.v[i], &reason, &wtpath, expiry_date)) {\n> > +                       limit--;\n> > +\n> > +                       if (!limit) {\n> > +                               should_prune = 1;\n> > +                               goto out;\n> \n> Eric noticed in a previous round that wtpath is leaked in this `goto\n> out` path, and it seems to me that it's still the case.\n\nYeah, our mails had crossed back then. Fixed now.\n\nPatrick\n"},{"id":"517453","messageId":"20250507-pks-maintenance-missing-tasks-v5-0-aa5fdfb82891@pks.im","threadId":"63348","inReplyTo":"20250425-pks-maintenance-missing-tasks-v1-0-972ed6ab2c0d@pks.im","subject":"[PATCH v5 0/6] builtin/maintenance: implement missing tasks compared to git-gc(1)","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-07T07:21:36Z","receivedAt":"2025-05-07T07:21:47Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis small patch series implements the last couple of remaining tasks\nthat are missing compared to the functionality git-gc(1) provides.\n\nRight now, git-maintenance(1) still executes git-gc(1). With these last\ngaps plugged though we can in theory fully replace git-gc(1) with finer\ngrained tasks without losing any functionality. The benefit is that it\nbecomes possible for users to have finer-grained control over what\nexactly the maintenance does.\n\nThis patch series doesn't do that yet, but only implements whatever is\nneeded to get there.\n\nChanges in v2:\n  - Introduce \"maintenance.worktree-prune.auto\", which controls how many\n    stale worktrees need to exist before executing `git worktree prune`.\n  - Introduce \"maintenance.rerere-gc.auto\", which controls how many\n    stale rerere entries need to exist before executing `git rerere gc`.\n  - Add tests to verify that \"gc.worktreePruneExpire\" works.\n  - Remove some fragile test logic by introducing functions that check\n    for a given maintenance subprocess.\n  - Link to v1: https://lore.kernel.org/r/20250425-pks-maintenance-missing-tasks-v1-0-972ed6ab2c0d@pks.im\n\nChanges in v3:\n  - Simplify the heuristic for \"rerere-gc\" so that we only count the\n    number of directory entries in \".git/rr-cache\", without considering\n    staleness.\n  - Link to v2: https://lore.kernel.org/r/20250430-pks-maintenance-missing-tasks-v2-0-2580b7b8ca3a@pks.im\n\nChanges in v4:\n  - simplified the heuristic for \"rerere-gc\" even further. A positive\n    value for \"maintenance.rerere-gc.auto\" now indicates that the\n    command will run whenever there is at least one directory entry in\n    \".rr-cache\". The exact value does not matter anymore.\n  - Link to v3: https://lore.kernel.org/r/20250502-pks-maintenance-missing-tasks-v3-0-13e130d36640@pks.im\n\nChanges in v5:\n  - Drop `get_worktree_names()` in favor of an open-coded loop.\n  - Fix a memory leak.\n  - Simplified the logic in `worktree_prune_condition()` a bit.\n  - Link to v4: https://lore.kernel.org/r/20250505-pks-maintenance-missing-tasks-v4-0-141f4df906a1@pks.im\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (6):\n      builtin/gc: fix indentation of `cmd_gc()` parameters\n      builtin/gc: remove global variables where it is trivial to do\n      builtin/gc: move pruning of worktrees into a separate function\n      builtin/maintenance: introduce \"worktree-prune\" task\n      builtin/gc: move rerere garbage collection into separate function\n      builtin/maintenance: introduce \"rerere-gc\" task\n\n Documentation/config/maintenance.adoc |  17 ++++\n Documentation/git-maintenance.adoc    |   8 ++\n builtin/gc.c                          | 148 +++++++++++++++++++++++++++-------\n t/t7900-maintenance.sh                | 115 ++++++++++++++++++++++++++\n 4 files changed, 257 insertions(+), 31 deletions(-)\n\nRange-diff versus v4:\n\n1:  59edf54e3ec = 1:  815904a68a1 builtin/gc: fix indentation of `cmd_gc()` parameters\n2:  9f02c33f5b9 ! 2:  91f4c304232 builtin/gc: remove global variables where it trivial to do\n    @@ Metadata\n     Author: Patrick Steinhardt <ps@pks.im>\n     \n      ## Commit message ##\n    -    builtin/gc: remove global variables where it trivial to do\n    +    builtin/gc: remove global variables where it is trivial to do\n     \n         We use a couple of global variables to assemble command line arguments\n         for subprocesses we execute in git-gc(1). All of these variables except\n3:  b280af7bbc4 ! 3:  9232c8aac1d builtin/gc: move pruning of worktrees into a separate function\n    @@ Metadata\n      ## Commit message ##\n         builtin/gc: move pruning of worktrees into a separate function\n     \n    -    Move pruning of worktrees into a separate function. This prepares for a\n    -    subsequent commit where we introduce a new \"worktree-prune\" task for\n    -    git-maintenance(1).\n    +    In a subsequent commit we will introduce a new \"worktree-prune\" task for\n    +    git-maintenance(1). To prepare for this, refactor the code that spawns\n    +    `git worktree prune` into a separate function.\n     \n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n4:  58ce12459c2 < -:  ----------- worktree: expose function to retrieve worktree names\n5:  efeec465db0 ! 4:  24ca70b35b9 builtin/maintenance: introduce \"worktree-prune\" task\n    @@ builtin/gc.c: static int maintenance_task_worktree_prune(struct maintenance_run_\n      \n     +static int worktree_prune_condition(struct gc_config *cfg)\n     +{\n    -+\tstruct strvec worktrees = STRVEC_INIT;\n    -+\tstruct strbuf reason = STRBUF_INIT;\n    ++\tstruct strbuf buf = STRBUF_INIT;\n    ++\tint should_prune = 0, limit = 1;\n     +\ttimestamp_t expiry_date;\n    -+\tint should_prune = 0;\n    -+\tint limit = 1;\n    ++\tstruct dirent *d;\n    ++\tDIR *dir = NULL;\n     +\n     +\tgit_config_get_int(\"maintenance.worktree-prune.auto\", &limit);\n     +\tif (limit <= 0) {\n    @@ builtin/gc.c: static int maintenance_task_worktree_prune(struct maintenance_run_\n     +\t\tgoto out;\n     +\t}\n     +\n    -+\tif (parse_expiry_date(cfg->prune_worktrees_expire, &expiry_date) ||\n    -+\t    get_worktree_names(the_repository, &worktrees) < 0)\n    ++\tif (parse_expiry_date(cfg->prune_worktrees_expire, &expiry_date))\n     +\t\tgoto out;\n     +\n    -+\tfor (size_t i = 0; i < worktrees.nr; i++) {\n    -+\t\tchar *wtpath;\n    ++\tdir = opendir(repo_git_path_replace(the_repository, &buf, \"worktrees\"));\n    ++\tif (!dir)\n    ++\t\tgoto out;\n     +\n    -+\t\tstrbuf_reset(&reason);\n    -+\t\tif (should_prune_worktree(worktrees.v[i], &reason, &wtpath, expiry_date)) {\n    ++\twhile (limit && (d = readdir_skip_dot_and_dotdot(dir))) {\n    ++\t\tchar *wtpath;\n    ++\t\tstrbuf_reset(&buf);\n    ++\t\tif (should_prune_worktree(d->d_name, &buf, &wtpath, expiry_date))\n     +\t\t\tlimit--;\n    -+\n    -+\t\t\tif (!limit) {\n    -+\t\t\t\tshould_prune = 1;\n    -+\t\t\t\tgoto out;\n    -+\t\t\t}\n    -+\t\t}\n     +\t\tfree(wtpath);\n     +\t}\n     +\n    ++\tshould_prune = !limit;\n    ++\n     +out:\n    -+\tstrvec_clear(&worktrees);\n    -+\tstrbuf_release(&reason);\n    ++\tif (dir)\n    ++\t\tclosedir(dir);\n    ++\tstrbuf_release(&buf);\n     +\treturn should_prune;\n     +}\n     +\n    @@ t/t7900-maintenance.sh: test_expect_success 'reflog-expire task --auto only pack\n     +\n     +\t# Zero should never prune.\n     +\ttest_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=0 maintenance run --auto --task=worktree-prune &&\n    -+\t# A positive value should require at least this man prunable worktrees.\n    ++\t# A positive value should require at least this many prunable worktrees.\n     +\ttest_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=4 maintenance run --auto --task=worktree-prune &&\n     +\ttest_expect_worktree_prune git -c maintenance.worktree-prune.auto=3 maintenance run --auto --task=worktree-prune\n     +'\n6:  10ed12cc737 ! 5:  67e05501fbb builtin/gc: move rerere garbage collection into separate function\n    @@ Metadata\n      ## Commit message ##\n         builtin/gc: move rerere garbage collection into separate function\n     \n    -    Move garbage collection of cached rerere entries into a separate\n    -    function. This prepares us for a subsequent commit where we introduce a\n    -    new \"rerere-gc\" task for git-maintenance(1).\n    +    In a subsequent commit we are going to introduce a new \"rerere-gc\" task\n    +    for git-maintenance(1). To prepare for this, refactor the code that\n    +    spawns `git rerere gc` into a separate function.\n     \n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n7:  be7fa13115c = 6:  818ed6b8999 builtin/maintenance: introduce \"rerere-gc\" task\n\n---\nbase-commit: a2955b34f48265d240ab8c7deb0a929ec2d65fd0\nchange-id: 20250424-pks-maintenance-missing-tasks-8ffcdd596b73\n\n"},{"id":"517454","messageId":"20250507-pks-maintenance-missing-tasks-v5-1-aa5fdfb82891@pks.im","threadId":"63348","inReplyTo":"20250507-pks-maintenance-missing-tasks-v5-0-aa5fdfb82891@pks.im","subject":"[PATCH v5 1/6] builtin/gc: fix indentation of `cmd_gc()` parameters","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-07T07:21:37Z","receivedAt":"2025-05-07T07:21:48Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The parameters of `cmd_gc()` aren't indented properly. Fix this.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex d5c75be2522..a73ec22fb18 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -724,9 +724,9 @@ static void gc_before_repack(struct maintenance_run_opts *opts,\n }\n \n int cmd_gc(int argc,\n-const char **argv,\n-const char *prefix,\n-struct repository *repo UNUSED)\n+\t   const char **argv,\n+\t   const char *prefix,\n+\t   struct repository *repo UNUSED)\n {\n \tint aggressive = 0;\n \tint quiet = 0;\n\n-- \n2.49.0.1045.g170613ef41.dirty\n\n"},{"id":"517455","messageId":"20250507-pks-maintenance-missing-tasks-v5-2-aa5fdfb82891@pks.im","threadId":"63348","inReplyTo":"20250507-pks-maintenance-missing-tasks-v5-0-aa5fdfb82891@pks.im","subject":"[PATCH v5 2/6] builtin/gc: remove global variables where it is trivial to do","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-07T07:21:38Z","receivedAt":"2025-05-07T07:21:49Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We use a couple of global variables to assemble command line arguments\nfor subprocesses we execute in git-gc(1). All of these variables except\nthe one for git-repack(1) are only used in a single place though, so\nthey don't really add anything but confusion.\n\nRemove those variables.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 31 ++++++++++++-------------------\n 1 file changed, 12 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex a73ec22fb18..ada36e210f0 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -53,15 +53,9 @@ static const char * const builtin_gc_usage[] = {\n };\n \n static timestamp_t gc_log_expire_time;\n-\n static struct strvec repack = STRVEC_INIT;\n-static struct strvec prune = STRVEC_INIT;\n-static struct strvec prune_worktrees = STRVEC_INIT;\n-static struct strvec rerere = STRVEC_INIT;\n-\n static struct tempfile *pidfile;\n static struct lock_file log_lock;\n-\n static struct string_list pack_garbage = STRING_LIST_INIT_DUP;\n \n static void clean_pack_garbage(void)\n@@ -769,9 +763,6 @@ int cmd_gc(int argc,\n \t\t\t\t\t builtin_gc_usage, builtin_gc_options);\n \n \tstrvec_pushl(&repack, \"repack\", \"-d\", \"-l\", NULL);\n-\tstrvec_pushl(&prune, \"prune\", \"--expire\", NULL);\n-\tstrvec_pushl(&prune_worktrees, \"worktree\", \"prune\", \"--expire\", NULL);\n-\tstrvec_pushl(&rerere, \"rerere\", \"gc\", NULL);\n \n \tgc_config(&cfg);\n \n@@ -897,34 +888,36 @@ int cmd_gc(int argc,\n \t\tif (cfg.prune_expire) {\n \t\t\tstruct child_process prune_cmd = CHILD_PROCESS_INIT;\n \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, cfg.prune_expire);\n+\t\t\tstrvec_push(&prune_cmd.args, cfg.prune_expire);\n \t\t\tif (quiet)\n-\t\t\t\tstrvec_push(&prune, \"--no-progress\");\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,\n+\t\t\t\tstrvec_push(&prune_cmd.args,\n \t\t\t\t\t    \"--exclude-promisor-objects\");\n \t\t\tprune_cmd.git_cmd = 1;\n-\t\t\tstrvec_pushv(&prune_cmd.args, prune.v);\n+\n \t\t\tif (run_command(&prune_cmd))\n-\t\t\t\tdie(FAILED_RUN, prune.v[0]);\n+\t\t\t\tdie(FAILED_RUN, prune_cmd.args.v[0]);\n \t\t}\n \t}\n \n \tif (cfg.prune_worktrees_expire) {\n \t\tstruct child_process prune_worktrees_cmd = CHILD_PROCESS_INIT;\n \n-\t\tstrvec_push(&prune_worktrees, cfg.prune_worktrees_expire);\n \t\tprune_worktrees_cmd.git_cmd = 1;\n-\t\tstrvec_pushv(&prune_worktrees_cmd.args, prune_worktrees.v);\n+\t\tstrvec_pushl(&prune_worktrees_cmd.args, \"worktree\", \"prune\", \"--expire\", NULL);\n+\t\tstrvec_push(&prune_worktrees_cmd.args, cfg.prune_worktrees_expire);\n+\n \t\tif (run_command(&prune_worktrees_cmd))\n-\t\t\tdie(FAILED_RUN, prune_worktrees.v[0]);\n+\t\t\tdie(FAILED_RUN, prune_worktrees_cmd.args.v[0]);\n \t}\n \n \trerere_cmd.git_cmd = 1;\n-\tstrvec_pushv(&rerere_cmd.args, rerere.v);\n+\tstrvec_pushl(&rerere_cmd.args, \"rerere\", \"gc\", NULL);\n \tif (run_command(&rerere_cmd))\n-\t\tdie(FAILED_RUN, rerere.v[0]);\n+\t\tdie(FAILED_RUN, rerere_cmd.args.v[0]);\n \n \treport_garbage = report_pack_garbage;\n \treprepare_packed_git(the_repository);\n\n-- \n2.49.0.1045.g170613ef41.dirty\n\n"},{"id":"517456","messageId":"20250507-pks-maintenance-missing-tasks-v5-3-aa5fdfb82891@pks.im","threadId":"63348","inReplyTo":"20250507-pks-maintenance-missing-tasks-v5-0-aa5fdfb82891@pks.im","subject":"[PATCH v5 3/6] builtin/gc: move pruning of worktrees into a separate function","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-07T07:21:39Z","receivedAt":"2025-05-07T07:21:50Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"In a subsequent commit we will introduce a new \"worktree-prune\" task for\ngit-maintenance(1). To prepare for this, refactor the code that spawns\n`git worktree prune` into a separate function.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 25 +++++++++++++++----------\n 1 file changed, 15 insertions(+), 10 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex ada36e210f0..005ecc3f192 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -334,6 +334,18 @@ static int maintenance_task_reflog_expire(struct maintenance_run_opts *opts UNUS\n \treturn run_command(&cmd);\n }\n \n+static int maintenance_task_worktree_prune(struct maintenance_run_opts *opts UNUSED,\n+\t\t\t\t\t   struct gc_config *cfg)\n+{\n+\tstruct child_process prune_worktrees_cmd = CHILD_PROCESS_INIT;\n+\n+\tprune_worktrees_cmd.git_cmd = 1;\n+\tstrvec_pushl(&prune_worktrees_cmd.args, \"worktree\", \"prune\", \"--expire\", NULL);\n+\tstrvec_push(&prune_worktrees_cmd.args, cfg->prune_worktrees_expire);\n+\n+\treturn run_command(&prune_worktrees_cmd);\n+}\n+\n static int too_many_loose_objects(struct gc_config *cfg)\n {\n \t/*\n@@ -903,16 +915,9 @@ int cmd_gc(int argc,\n \t\t}\n \t}\n \n-\tif (cfg.prune_worktrees_expire) {\n-\t\tstruct child_process prune_worktrees_cmd = CHILD_PROCESS_INIT;\n-\n-\t\tprune_worktrees_cmd.git_cmd = 1;\n-\t\tstrvec_pushl(&prune_worktrees_cmd.args, \"worktree\", \"prune\", \"--expire\", NULL);\n-\t\tstrvec_push(&prune_worktrees_cmd.args, cfg.prune_worktrees_expire);\n-\n-\t\tif (run_command(&prune_worktrees_cmd))\n-\t\t\tdie(FAILED_RUN, prune_worktrees_cmd.args.v[0]);\n-\t}\n+\tif (cfg.prune_worktrees_expire &&\n+\t    maintenance_task_worktree_prune(&opts, &cfg))\n+\t\tdie(FAILED_RUN, \"worktree\");\n \n \trerere_cmd.git_cmd = 1;\n \tstrvec_pushl(&rerere_cmd.args, \"rerere\", \"gc\", NULL);\n\n-- \n2.49.0.1045.g170613ef41.dirty\n\n"},{"id":"517457","messageId":"20250507-pks-maintenance-missing-tasks-v5-4-aa5fdfb82891@pks.im","threadId":"63348","inReplyTo":"20250507-pks-maintenance-missing-tasks-v5-0-aa5fdfb82891@pks.im","subject":"[PATCH v5 4/6] builtin/maintenance: introduce \"worktree-prune\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-07T07:21:40Z","receivedAt":"2025-05-07T07:21:50Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While git-gc(1) knows to prune stale worktrees, git-maintenance(1) does\nnot yet have a task for this cleanup. Introduce a new \"worktree-prune\"\ntask to plug this gap.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/maintenance.adoc |  8 ++++\n Documentation/git-maintenance.adoc    |  4 ++\n builtin/gc.c                          | 45 ++++++++++++++++++++++\n t/t7900-maintenance.sh                | 71 +++++++++++++++++++++++++++++++++++\n 4 files changed, 128 insertions(+)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex 41536162a77..b36b62c1c47 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -83,3 +83,11 @@ maintenance.reflog-expire.auto::\n \tpositive value implies the command should run when the number of\n \texpired reflog entries in the \"HEAD\" reflog is at least the value of\n \t`maintenance.loose-objects.auto`. The default value is 100.\n+\n+maintenance.worktree-prune.auto::\n+\tThis integer config option controls how often the `worktree-prune` task\n+\tshould be run as part of `git maintenance run --auto`. If zero, then\n+\tthe `worktree-prune` task will not run with the `--auto` option. A\n+\tnegative value will force the task to run every time. Otherwise, a\n+\tpositive value implies the command should run when the number of\n+\tprunable worktrees exceeds the value. The default value is 1.\ndiff --git a/Documentation/git-maintenance.adoc b/Documentation/git-maintenance.adoc\nindex 3a1e2a69b6b..6f085a9cf8c 100644\n--- a/Documentation/git-maintenance.adoc\n+++ b/Documentation/git-maintenance.adoc\n@@ -166,6 +166,10 @@ reflog-expire::\n \tThe `reflog-expire` task deletes any entries in the reflog older than the\n \texpiry threshold. See linkgit:git-reflog[1] for more information.\n \n+worktree-prune::\n+\tThe `worktree-prune` task deletes stale or broken worktrees. See\n+\tlinkit:git-worktree[1] for more information.\n+\n OPTIONS\n -------\n --auto::\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 005ecc3f192..e85e313d78f 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -44,6 +44,7 @@\n #include \"hook.h\"\n #include \"setup.h\"\n #include \"trace2.h\"\n+#include \"worktree.h\"\n \n #define FAILED_RUN \"failed to run %s\"\n \n@@ -346,6 +347,44 @@ static int maintenance_task_worktree_prune(struct maintenance_run_opts *opts UNU\n \treturn run_command(&prune_worktrees_cmd);\n }\n \n+static int worktree_prune_condition(struct gc_config *cfg)\n+{\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tint should_prune = 0, limit = 1;\n+\ttimestamp_t expiry_date;\n+\tstruct dirent *d;\n+\tDIR *dir = NULL;\n+\n+\tgit_config_get_int(\"maintenance.worktree-prune.auto\", &limit);\n+\tif (limit <= 0) {\n+\t\tshould_prune = limit < 0;\n+\t\tgoto out;\n+\t}\n+\n+\tif (parse_expiry_date(cfg->prune_worktrees_expire, &expiry_date))\n+\t\tgoto out;\n+\n+\tdir = opendir(repo_git_path_replace(the_repository, &buf, \"worktrees\"));\n+\tif (!dir)\n+\t\tgoto out;\n+\n+\twhile (limit && (d = readdir_skip_dot_and_dotdot(dir))) {\n+\t\tchar *wtpath;\n+\t\tstrbuf_reset(&buf);\n+\t\tif (should_prune_worktree(d->d_name, &buf, &wtpath, expiry_date))\n+\t\t\tlimit--;\n+\t\tfree(wtpath);\n+\t}\n+\n+\tshould_prune = !limit;\n+\n+out:\n+\tif (dir)\n+\t\tclosedir(dir);\n+\tstrbuf_release(&buf);\n+\treturn should_prune;\n+}\n+\n static int too_many_loose_objects(struct gc_config *cfg)\n {\n \t/*\n@@ -1455,6 +1494,7 @@ enum maintenance_task_label {\n \tTASK_COMMIT_GRAPH,\n \tTASK_PACK_REFS,\n \tTASK_REFLOG_EXPIRE,\n+\tTASK_WORKTREE_PRUNE,\n \n \t/* Leave as final value */\n \tTASK__COUNT\n@@ -1496,6 +1536,11 @@ static struct maintenance_task tasks[] = {\n \t\tmaintenance_task_reflog_expire,\n \t\treflog_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},\n };\n \n static int compare_tasks_by_selection(const void *a_, const void *b_)\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 9b82e11c100..8f4120a0351 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -493,6 +493,77 @@ test_expect_success 'reflog-expire task --auto only packs when exceeding limits'\n \ttest_subcommand git reflog expire --all <reflog-expire-auto.txt\n '\n \n+test_expect_worktree_prune () {\n+\tnegate=\n+\tif test \"$1\" = \"!\"\n+\tthen\n+\t\tnegate=\"!\"\n+\t\tshift\n+\tfi\n+\n+\trm -f \"worktree-prune.txt\" &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/worktree-prune.txt\" \"$@\" &&\n+\ttest_subcommand $negate git worktree prune --expire 3.months.ago <worktree-prune.txt\n+}\n+\n+test_expect_success 'worktree-prune task without --auto always prunes' '\n+\ttest_expect_worktree_prune git maintenance run --task=worktree-prune\n+'\n+\n+test_expect_success 'worktree-prune task --auto only prunes with prunable worktree' '\n+\ttest_expect_worktree_prune ! git maintenance run --auto --task=worktree-prune &&\n+\tmkdir .git/worktrees &&\n+\t: >.git/worktrees/abc &&\n+\ttest_expect_worktree_prune git maintenance run --auto --task=worktree-prune\n+'\n+\n+test_expect_success 'worktree-prune task with --auto honors maintenance.worktree-prune.auto' '\n+\t# A negative value should always prune.\n+\ttest_expect_worktree_prune git -c maintenance.worktree-prune.auto=-1 maintenance run --auto --task=worktree-prune &&\n+\n+\tmkdir .git/worktrees &&\n+\t: >.git/worktrees/first &&\n+\t: >.git/worktrees/second &&\n+\t: >.git/worktrees/third &&\n+\n+\t# Zero should never prune.\n+\ttest_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=0 maintenance run --auto --task=worktree-prune &&\n+\t# A positive value should require at least this many prunable worktrees.\n+\ttest_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=4 maintenance run --auto --task=worktree-prune &&\n+\ttest_expect_worktree_prune git -c maintenance.worktree-prune.auto=3 maintenance run --auto --task=worktree-prune\n+'\n+\n+test_expect_success 'worktree-prune task with --auto honors maintenance.worktree-prune.auto' '\n+\t# A negative value should always prune.\n+\ttest_expect_worktree_prune git -c maintenance.worktree-prune.auto=-1 maintenance run --auto --task=worktree-prune &&\n+\n+\tmkdir .git/worktrees &&\n+\t: >.git/worktrees/first &&\n+\t: >.git/worktrees/second &&\n+\t: >.git/worktrees/third &&\n+\n+\t# Zero should never prune.\n+\ttest_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=0 maintenance run --auto --task=worktree-prune &&\n+\t# A positive value should require at least this many prunable worktrees.\n+\ttest_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=4 maintenance run --auto --task=worktree-prune &&\n+\ttest_expect_worktree_prune git -c maintenance.worktree-prune.auto=3 maintenance run --auto --task=worktree-prune\n+'\n+\n+test_expect_success 'worktree-prune task honors gc.worktreePruneExpire' '\n+\tgit worktree add worktree &&\n+\trm -rf worktree &&\n+\n+\trm -f worktree-prune.txt &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/worktree-prune.txt\" git -c gc.worktreePruneExpire=1.week.ago maintenance run --auto --task=worktree-prune &&\n+\ttest_subcommand ! git worktree prune --expire 1.week.ago <worktree-prune.txt &&\n+\ttest_path_is_dir .git/worktrees/worktree &&\n+\n+\trm -f worktree-prune.txt &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/worktree-prune.txt\" git -c gc.worktreePruneExpire=now maintenance run --auto --task=worktree-prune &&\n+\ttest_subcommand git worktree prune --expire now <worktree-prune.txt &&\n+\ttest_path_is_missing .git/worktrees/worktree\n+'\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\n-- \n2.49.0.1045.g170613ef41.dirty\n\n"},{"id":"517458","messageId":"20250507-pks-maintenance-missing-tasks-v5-5-aa5fdfb82891@pks.im","threadId":"63348","inReplyTo":"20250507-pks-maintenance-missing-tasks-v5-0-aa5fdfb82891@pks.im","subject":"[PATCH v5 5/6] builtin/gc: move rerere garbage collection into separate function","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-07T07:21:41Z","receivedAt":"2025-05-07T07:21:51Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"In a subsequent commit we are going to introduce a new \"rerere-gc\" task\nfor git-maintenance(1). To prepare for this, refactor the code that\nspawns `git rerere gc` into a separate function.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 16 +++++++++++-----\n 1 file changed, 11 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e85e313d78f..0ae3071ec71 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -385,6 +385,15 @@ static int worktree_prune_condition(struct gc_config *cfg)\n \treturn should_prune;\n }\n \n+static int maintenance_task_rerere_gc(struct maintenance_run_opts *opts UNUSED,\n+\t\t\t\t      struct gc_config *cfg UNUSED)\n+{\n+\tstruct child_process rerere_cmd = CHILD_PROCESS_INIT;\n+\trerere_cmd.git_cmd = 1;\n+\tstrvec_pushl(&rerere_cmd.args, \"rerere\", \"gc\", NULL);\n+\treturn run_command(&rerere_cmd);\n+}\n+\n static int too_many_loose_objects(struct gc_config *cfg)\n {\n \t/*\n@@ -781,7 +790,6 @@ int cmd_gc(int argc,\n \tint daemonized = 0;\n \tint keep_largest_pack = -1;\n \ttimestamp_t dummy;\n-\tstruct child_process rerere_cmd = CHILD_PROCESS_INIT;\n \tstruct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT;\n \tstruct gc_config cfg = GC_CONFIG_INIT;\n \tconst char *prune_expire_sentinel = \"sentinel\";\n@@ -958,10 +966,8 @@ int cmd_gc(int argc,\n \t    maintenance_task_worktree_prune(&opts, &cfg))\n \t\tdie(FAILED_RUN, \"worktree\");\n \n-\trerere_cmd.git_cmd = 1;\n-\tstrvec_pushl(&rerere_cmd.args, \"rerere\", \"gc\", NULL);\n-\tif (run_command(&rerere_cmd))\n-\t\tdie(FAILED_RUN, rerere_cmd.args.v[0]);\n+\tif (maintenance_task_rerere_gc(&opts, &cfg))\n+\t\tdie(FAILED_RUN, \"rerere\");\n \n \treport_garbage = report_pack_garbage;\n \treprepare_packed_git(the_repository);\n\n-- \n2.49.0.1045.g170613ef41.dirty\n\n"},{"id":"517459","messageId":"20250507-pks-maintenance-missing-tasks-v5-6-aa5fdfb82891@pks.im","threadId":"63348","inReplyTo":"20250507-pks-maintenance-missing-tasks-v5-0-aa5fdfb82891@pks.im","subject":"[PATCH v5 6/6] builtin/maintenance: introduce \"rerere-gc\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-07T07:21:42Z","receivedAt":"2025-05-07T07:21:52Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While git-gc(1) knows to garbage collect the rerere cache,\ngit-maintenance(1) does not yet have a task for this cleanup. Introduce\na new \"rerere-gc\" task to plug this gap.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/maintenance.adoc |  9 +++++++\n Documentation/git-maintenance.adoc    |  4 ++++\n builtin/gc.c                          | 37 +++++++++++++++++++++++++++++\n t/t7900-maintenance.sh                | 44 +++++++++++++++++++++++++++++++++++\n 4 files changed, 94 insertions(+)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex b36b62c1c47..2f719342183 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -84,6 +84,15 @@ maintenance.reflog-expire.auto::\n \texpired reflog entries in the \"HEAD\" reflog is at least the value of\n \t`maintenance.loose-objects.auto`. The default value is 100.\n \n+maintenance.rerere-gc.auto::\n+\tThis integer config option controls how often the `rerere-gc` task\n+\tshould be run as part of `git maintenance run --auto`. If zero, then\n+\tthe `rerere-gc` task will not run with the `--auto` option. A negative\n+\tvalue will force the task to run every time. Otherwise, any positive\n+\tvalue implies the command will run when the \"rr-cache\" directory exists\n+\tand has at least one entry, regardless of whether it is stale or not.\n+\tThis heuristic may be refined in the future. The default value is 1.\n+\n maintenance.worktree-prune.auto::\n \tThis integer config option controls how often the `worktree-prune` task\n \tshould be run as part of `git maintenance run --auto`. If zero, then\ndiff --git a/Documentation/git-maintenance.adoc b/Documentation/git-maintenance.adoc\nindex 6f085a9cf8c..931f3e02e85 100644\n--- a/Documentation/git-maintenance.adoc\n+++ b/Documentation/git-maintenance.adoc\n@@ -166,6 +166,10 @@ reflog-expire::\n \tThe `reflog-expire` task deletes any entries in the reflog older than the\n \texpiry threshold. See linkgit:git-reflog[1] for more information.\n \n+rerere-gc::\n+\tThe `rerere-gc` task invokes garbage collection for stale entries in\n+\tthe rerere cache. See linkgit:git-rerere[1] for more information.\n+\n worktree-prune::\n \tThe `worktree-prune` task deletes stale or broken worktrees. See\n \tlinkit:git-worktree[1] for more information.\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 0ae3071ec71..e79082f985d 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -16,6 +16,7 @@\n #include \"builtin.h\"\n #include \"abspath.h\"\n #include \"date.h\"\n+#include \"dir.h\"\n #include \"environment.h\"\n #include \"hex.h\"\n #include \"config.h\"\n@@ -34,6 +35,7 @@\n #include \"pack-objects.h\"\n #include \"path.h\"\n #include \"reflog.h\"\n+#include \"rerere.h\"\n #include \"blob.h\"\n #include \"tree.h\"\n #include \"promisor-remote.h\"\n@@ -394,6 +396,35 @@ static int maintenance_task_rerere_gc(struct maintenance_run_opts *opts UNUSED,\n \treturn run_command(&rerere_cmd);\n }\n \n+static int rerere_gc_condition(struct gc_config *cfg UNUSED)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\tint should_gc = 0, limit = 1;\n+\tDIR *dir = NULL;\n+\n+\tgit_config_get_int(\"maintenance.rerere-gc.auto\", &limit);\n+\tif (limit <= 0) {\n+\t\tshould_gc = limit < 0;\n+\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t * We skip garbage collection in case we either have no \"rr-cache\"\n+\t * directory or when it doesn't contain at least one entry.\n+\t */\n+\trepo_git_path_replace(the_repository, &path, \"rr-cache\");\n+\tdir = opendir(path.buf);\n+\tif (!dir)\n+\t\tgoto out;\n+\tshould_gc = !!readdir_skip_dot_and_dotdot(dir);\n+\n+out:\n+\tstrbuf_release(&path);\n+\tif (dir)\n+\t\tclosedir(dir);\n+\treturn should_gc;\n+}\n+\n static int too_many_loose_objects(struct gc_config *cfg)\n {\n \t/*\n@@ -1501,6 +1532,7 @@ enum maintenance_task_label {\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@@ -1547,6 +1579,11 @@ static struct maintenance_task tasks[] = {\n \t\tmaintenance_task_worktree_prune,\n \t\tworktree_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},\n };\n \n static int compare_tasks_by_selection(const void *a_, const void *b_)\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 8f4120a0351..8cf89e285f4 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -564,6 +564,50 @@ test_expect_success 'worktree-prune task honors gc.worktreePruneExpire' '\n \ttest_path_is_missing .git/worktrees/worktree\n '\n \n+test_expect_rerere_gc () {\n+\tnegate=\n+\tif test \"$1\" = \"!\"\n+\tthen\n+\t\tnegate=\"!\"\n+\t\tshift\n+\tfi\n+\n+\trm -f \"rerere-gc.txt\" &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/rerere-gc.txt\" \"$@\" &&\n+\ttest_subcommand $negate git rerere gc <rerere-gc.txt\n+}\n+\n+test_expect_success 'rerere-gc task without --auto always collects garbage' '\n+\ttest_expect_rerere_gc git maintenance run --task=rerere-gc\n+'\n+\n+test_expect_success 'rerere-gc task with --auto only prunes with prunable entries' '\n+\ttest_when_finished \"rm -rf .git/rr-cache\" &&\n+\ttest_expect_rerere_gc ! git maintenance run --auto --task=rerere-gc &&\n+\tmkdir .git/rr-cache &&\n+\ttest_expect_rerere_gc ! git maintenance run --auto --task=rerere-gc &&\n+\t: >.git/rr-cache/entry &&\n+\ttest_expect_rerere_gc git maintenance run --auto --task=rerere-gc\n+'\n+\n+test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.auto' '\n+\ttest_when_finished \"rm -rf .git/rr-cache\" &&\n+\n+\t# A negative value should always prune.\n+\ttest_expect_rerere_gc git -c maintenance.rerere-gc.auto=-1 maintenance run --auto --task=rerere-gc &&\n+\n+\t# A positive value prunes when there is at least one entry.\n+\ttest_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&\n+\tmkdir .git/rr-cache &&\n+\ttest_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&\n+\t: >.git/rr-cache/entry-1 &&\n+\ttest_expect_rerere_gc git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&\n+\n+\t# Zero should never prune.\n+\t: >.git/rr-cache/entry-1 &&\n+\ttest_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 maintenance run --auto --task=rerere-gc\n+'\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\n-- \n2.49.0.1045.g170613ef41.dirty\n\n"}]}