{"thread":{"id":"63373","subject":"[PATCH v2 1/8] builtin/gc: fix indentation of `cmd_gc()` parameters","startedAt":"2025-04-30T10:25:15Z","lastAt":"2025-05-05T07:22:43Z","messageCount":14,"participants":["Patrick Steinhardt","Derrick Stolee","Junio C Hamano"],"isPatch":true,"patchVersion":2,"patchTotal":8},"messages":[{"id":"517007","messageId":"20250430-pks-maintenance-missing-tasks-v2-1-2580b7b8ca3a@pks.im","threadId":"63373","inReplyTo":"20250430-pks-maintenance-missing-tasks-v2-0-2580b7b8ca3a@pks.im","subject":"[PATCH v2 1/8] builtin/gc: fix indentation of `cmd_gc()` parameters","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-30T10:25:05Z","receivedAt":"2025-04-30T10:25:15Z","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":"517008","messageId":"20250430-pks-maintenance-missing-tasks-v2-0-2580b7b8ca3a@pks.im","threadId":"63373","inReplyTo":"20250425-pks-maintenance-missing-tasks-v1-0-972ed6ab2c0d@pks.im","subject":"[PATCH v2 0/8] builtin/maintenance: implement missing tasks compared to git-gc(1)","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-30T10:25:04Z","receivedAt":"2025-04-30T10:25:15Z","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\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (8):\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      rerere: provide function to collect stale entries\n      builtin/gc: move rerere garbage collection into separate function\n      builtin/maintenance: introduce \"rerere-gc\" task\n\n Documentation/config/maintenance.adoc |  16 ++++\n Documentation/git-maintenance.adoc    |   8 ++\n builtin/gc.c                          | 153 +++++++++++++++++++++++++++-------\n builtin/worktree.c                    |  25 +++---\n rerere.c                              |  92 +++++++++++++-------\n rerere.h                              |  14 ++++\n t/t7900-maintenance.sh                | 125 +++++++++++++++++++++++++++\n worktree.c                            |  30 +++++++\n worktree.h                            |   8 ++\n 9 files changed, 399 insertions(+), 72 deletions(-)\n\nRange-diff versus v1:\n\n1:  0304b81df0b = 1:  9c62b493297 builtin/gc: fix indentation of `cmd_gc()` parameters\n2:  22c499601ee = 2:  9ae42b139fa builtin/gc: remove global variables where it trivial to do\n3:  db9622a408f = 3:  50a5305b6d2 builtin/gc: move pruning of worktrees into a separate function\n4:  f42205e1b6b = 4:  b71dcb0debc worktree: expose function to retrieve worktree names\n5:  eade37df904 ! 5:  47d31f41c2e builtin/maintenance: introduce \"worktree-prune\" task\n    @@ Commit message\n     \n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n    + ## Documentation/config/maintenance.adoc ##\n    +@@ Documentation/config/maintenance.adoc: 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.\n    +\n      ## Documentation/git-maintenance.adoc ##\n     @@ Documentation/git-maintenance.adoc: reflog-expire::\n      \tThe `reflog-expire` task deletes any entries in the reflog older than the\n    @@ builtin/gc.c: static int maintenance_task_worktree_prune(struct maintenance_run_\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    @@ builtin/gc.c: static int maintenance_task_worktree_prune(struct maintenance_run_\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\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    @@ t/t7900-maintenance.sh: test_expect_success 'reflog-expire task --auto only pack\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    ++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    -+\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    ++\ttest_expect_worktree_prune ! git maintenance run --auto --task=worktree-prune &&\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    ++\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-:  ----------- > 6:  5550c115e84 rerere: provide function to collect stale entries\n6:  66b2b033743 = 7:  f5b234c859e builtin/gc: move rerere garbage collection into separate function\n7:  9604fc4fc6b ! 8:  092e57cce01 builtin/maintenance: introduce \"rerere-gc\" task\n    @@ Commit message\n     \n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n    + ## Documentation/config/maintenance.adoc ##\n    +@@ Documentation/config/maintenance.adoc: 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, a positive\n    ++\tvalue implies the command should run when the number of prunable rerere\n    ++\tentries exceeds the value. The default value is 20.\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    +\n      ## Documentation/git-maintenance.adoc ##\n     @@ Documentation/git-maintenance.adoc: reflog-expire::\n      \tThe `reflog-expire` task deletes any entries in the reflog older than the\n    @@ builtin/gc.c\n      #include \"environment.h\"\n      #include \"hex.h\"\n      #include \"config.h\"\n    +@@\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     @@ builtin/gc.c: static int maintenance_task_rerere_gc(struct maintenance_run_opts *opts UNUSED,\n      \treturn run_command(&rerere_cmd);\n      }\n    @@ builtin/gc.c: static int maintenance_task_rerere_gc(struct maintenance_run_opts\n     +static int rerere_gc_condition(struct gc_config *cfg UNUSED)\n     +{\n     +\tstruct strbuf path = STRBUF_INIT;\n    ++\tstruct string_list prunable_dirs = STRING_LIST_INIT_DUP;\n    ++\tstruct rerere_id *prunable_entries = NULL;\n    ++\tsize_t prunable_entries_nr;\n     +\tint should_gc = 0;\n    -+\tDIR *dir;\n    ++\tint limit = 20;\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/* Skip garbage collecting the rerere cache in case rerere is disabled. */\n     +\trepo_git_path_replace(the_repository, &path, \"rr-cache\");\n    ++\tif (!is_directory(path.buf))\n    ++\t\tgoto out;\n     +\n    -+\tdir = opendir(path.buf);\n    -+\tif (!dir)\n    ++\tif (rerere_collect_stale_entries(the_repository, &prunable_dirs,\n    ++\t\t\t\t\t &prunable_entries, &prunable_entries_nr) < 0)\n     +\t\tgoto out;\n    -+\tshould_gc = !!readdir_skip_dot_and_dotdot(dir);\n    ++\n    ++\tshould_gc = prunable_entries_nr >= limit;\n     +\n     +out:\n    ++\tstring_list_clear(&prunable_dirs, 0);\n    ++\tfree(prunable_entries);\n     +\tstrbuf_release(&path);\n    -+\tclosedir(dir);\n     +\treturn should_gc;\n     +}\n     +\n    @@ builtin/gc.c: static struct maintenance_task tasks[] = {\n      static int compare_tasks_by_selection(const void *a_, const void *b_)\n     \n      ## t/t7900-maintenance.sh ##\n    -@@ t/t7900-maintenance.sh: 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    +@@ t/t7900-maintenance.sh: test_expect_success 'worktree-prune task honors gc.worktreePruneExpire' '\n    + \ttest_path_is_missing .git/worktrees/worktree\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    ++setup_stale_rerere_entry () {\n    ++\trr=.git/rr-cache/\"$(printf \"%0$(test_oid hexsz)d\" \"$1\")\" &&\n    ++\tmkdir -p \"$rr\" &&\n    ++\t>\"$rr/preimage\" &&\n    ++\t>\"$rr/postimage\" &&\n    ++\n    ++\ttest-tool chmtime =\"$((-61 * 86400))\" \"$rr/preimage\" &&\n    ++\ttest-tool chmtime =\"$((-61 * 86400))\" \"$rr/postimage\"\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 --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    ++test_expect_success 'rerere-gc task with --auto only prunes with prunable entries' '\n    ++\ttest_expect_rerere_gc ! git maintenance run --auto --task=rerere-gc &&\n    ++\tfor i in $(test_seq 19)\n    ++\tdo\n    ++\t\tsetup_stale_rerere_entry $i || return 1\n    ++\tdone &&\n    ++\ttest_expect_rerere_gc ! git maintenance run --auto --task=rerere-gc &&\n    ++\tsetup_stale_rerere_entry 20 &&\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    ++\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    ++\tfor i in $(test_seq 20)\n    ++\tdo\n    ++\t\tsetup_stale_rerere_entry $i || return 1\n    ++\tdone &&\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=21 maintenance run --auto --task=rerere-gc &&\n    ++\ttest_expect_rerere_gc git -c maintenance.rerere-gc.auto=10 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":"517010","messageId":"20250430-pks-maintenance-missing-tasks-v2-2-2580b7b8ca3a@pks.im","threadId":"63373","inReplyTo":"20250430-pks-maintenance-missing-tasks-v2-0-2580b7b8ca3a@pks.im","subject":"[PATCH v2 2/8] builtin/gc: remove global variables where it trivial to do","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-30T10:25:06Z","receivedAt":"2025-04-30T10:25:16Z","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":"517009","messageId":"20250430-pks-maintenance-missing-tasks-v2-3-2580b7b8ca3a@pks.im","threadId":"63373","inReplyTo":"20250430-pks-maintenance-missing-tasks-v2-0-2580b7b8ca3a@pks.im","subject":"[PATCH v2 3/8] builtin/gc: move pruning of worktrees into a separate function","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-30T10:25:07Z","receivedAt":"2025-04-30T10:25:17Z","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":"517011","messageId":"20250430-pks-maintenance-missing-tasks-v2-4-2580b7b8ca3a@pks.im","threadId":"63373","inReplyTo":"20250430-pks-maintenance-missing-tasks-v2-0-2580b7b8ca3a@pks.im","subject":"[PATCH v2 4/8] worktree: expose function to retrieve worktree names","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-30T10:25:08Z","receivedAt":"2025-04-30T10:25:17Z","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":"517013","messageId":"20250430-pks-maintenance-missing-tasks-v2-5-2580b7b8ca3a@pks.im","threadId":"63373","inReplyTo":"20250430-pks-maintenance-missing-tasks-v2-0-2580b7b8ca3a@pks.im","subject":"[PATCH v2 5/8] builtin/maintenance: introduce \"worktree-prune\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-30T10:25:09Z","receivedAt":"2025-04-30T10:25:17Z","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":"517014","messageId":"20250430-pks-maintenance-missing-tasks-v2-6-2580b7b8ca3a@pks.im","threadId":"63373","inReplyTo":"20250430-pks-maintenance-missing-tasks-v2-0-2580b7b8ca3a@pks.im","subject":"[PATCH v2 6/8] rerere: provide function to collect stale entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-30T10:25:10Z","receivedAt":"2025-04-30T10:25:18Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We're about to add another task for git-maintenance(1) that prunes stale\nrerere entries via `git rerere gc`. The condition of when to run this\nsubcommand will be configurable so that the subcommand is only executed\nwhen a certain number of stale rerere entries exists. This requires us\nto know about the number of stale rerere entries in the first place,\nwhich is non-trivial to figure out.\n\nRefactor `rerere_gc()` and `prune_one()` so that garbage collection is\nsplit into three phases:\n\n  1. We collect any stale rerere entries and directories that are about\n     to become empty.\n\n  2. Prune all stale rerere entries.\n\n  3. Remove all directories that should have become empty in (2).\n\nBy splitting out the collection of stale entries we can trivially expose\nthis function to external callers and thus reuse it in later steps.\n\nThis refactoring is not expected to result in a user-visible change in\nbehaviour.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n rerere.c | 92 ++++++++++++++++++++++++++++++++++++++++++++--------------------\n rerere.h | 14 ++++++++++\n 2 files changed, 78 insertions(+), 28 deletions(-)\n\ndiff --git a/rerere.c b/rerere.c\nindex 740e8ad1a0b..eb06e5f8bea 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -1202,8 +1202,8 @@ static void unlink_rr_item(struct rerere_id *id)\n \tstrbuf_release(&buf);\n }\n \n-static void prune_one(struct rerere_id *id,\n-\t\t      timestamp_t cutoff_resolve, timestamp_t cutoff_noresolve)\n+static int is_stale(struct rerere_id *id,\n+\t\t    timestamp_t cutoff_resolve, timestamp_t cutoff_noresolve)\n {\n \ttimestamp_t then;\n \ttimestamp_t cutoff;\n@@ -1214,11 +1214,11 @@ static void prune_one(struct rerere_id *id,\n \telse {\n \t\tthen = rerere_created_at(id);\n \t\tif (!then)\n-\t\t\treturn;\n+\t\t\treturn 0;\n \t\tcutoff = cutoff_noresolve;\n \t}\n-\tif (then < cutoff)\n-\t\tunlink_rr_item(id);\n+\n+\treturn then < cutoff;\n }\n \n /* Does the basename in \"path\" look plausibly like an rr-cache entry? */\n@@ -1229,29 +1229,35 @@ static int is_rr_cache_dirname(const char *path)\n \treturn !parse_oid_hex(path, &oid, &end) && !*end;\n }\n \n-void rerere_gc(struct repository *r, struct string_list *rr)\n+int rerere_collect_stale_entries(struct repository *r,\n+\t\t\t\t struct string_list *prunable_dirs,\n+\t\t\t\t struct rerere_id **prunable_entries,\n+\t\t\t\t size_t *prunable_entries_nr)\n {\n-\tstruct string_list to_remove = STRING_LIST_INIT_DUP;\n-\tDIR *dir;\n-\tstruct dirent *e;\n-\tint i;\n \ttimestamp_t now = time(NULL);\n \ttimestamp_t cutoff_noresolve = now - 15 * 86400;\n \ttimestamp_t cutoff_resolve = now - 60 * 86400;\n \tstruct strbuf buf = STRBUF_INIT;\n+\tsize_t prunable_entries_alloc;\n+\tstruct dirent *e;\n+\tDIR *dir = NULL;\n+\tint ret;\n \n-\tif (setup_rerere(r, rr, 0) < 0)\n-\t\treturn;\n+\t*prunable_entries = NULL;\n+\t*prunable_entries_nr = 0;\n+\tprunable_entries_alloc = 0;\n \n-\trepo_config_get_expiry_in_days(the_repository, \"gc.rerereresolved\",\n+\trepo_config_get_expiry_in_days(r, \"gc.rerereresolved\",\n \t\t\t\t       &cutoff_resolve, now);\n-\trepo_config_get_expiry_in_days(the_repository, \"gc.rerereunresolved\",\n+\trepo_config_get_expiry_in_days(r, \"gc.rerereunresolved\",\n \t\t\t\t       &cutoff_noresolve, now);\n-\tgit_config(git_default_config, NULL);\n-\tdir = opendir(repo_git_path_replace(the_repository, &buf, \"rr-cache\"));\n-\tif (!dir)\n-\t\tdie_errno(_(\"unable to open rr-cache directory\"));\n-\t/* Collect stale conflict IDs ... */\n+\n+\tdir = opendir(repo_git_path_replace(r, &buf, \"rr-cache\"));\n+\tif (!dir) {\n+\t\tret = error_errno(_(\"unable to open rr-cache directory\"));\n+\t\tgoto out;\n+\t}\n+\n \twhile ((e = readdir_skip_dot_and_dotdot(dir))) {\n \t\tstruct rerere_dir *rr_dir;\n \t\tstruct rerere_id id;\n@@ -1266,23 +1272,53 @@ void rerere_gc(struct repository *r, struct string_list *rr)\n \t\tfor (id.variant = 0, id.collection = rr_dir;\n \t\t     id.variant < id.collection->status_nr;\n \t\t     id.variant++) {\n-\t\t\tprune_one(&id, cutoff_resolve, cutoff_noresolve);\n-\t\t\tif (id.collection->status[id.variant])\n+\t\t\tif (is_stale(&id, cutoff_resolve, cutoff_noresolve)) {\n+\t\t\t\tALLOC_GROW(*prunable_entries, *prunable_entries_nr + 1,\n+\t\t\t\t\t   prunable_entries_alloc);\n+\t\t\t\t(*prunable_entries)[(*prunable_entries_nr)++] = id;\n+\t\t\t} else {\n \t\t\t\tnow_empty = 0;\n+\t\t\t}\n \t\t}\n \t\tif (now_empty)\n-\t\t\tstring_list_append(&to_remove, e->d_name);\n+\t\t\tstring_list_append(prunable_dirs, e->d_name);\n \t}\n-\tclosedir(dir);\n \n-\t/* ... and then remove the empty directories */\n-\tfor (i = 0; i < to_remove.nr; i++)\n-\t\trmdir(repo_git_path_replace(the_repository, &buf,\n-\t\t\t\t\t    \"rr-cache/%s\", to_remove.items[i].string));\n+\tret = 0;\n+\n+out:\n+\tstrbuf_release(&buf);\n+\tif (dir)\n+\t\tclosedir(dir);\n+\treturn ret;\n+}\n+\n+void rerere_gc(struct repository *r, struct string_list *rr)\n+{\n+\tstruct string_list prunable_dirs = STRING_LIST_INIT_DUP;\n+\tstruct rerere_id *prunable_entries;\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tsize_t prunable_entries_nr;\n+\n+\tif (setup_rerere(r, rr, 0) < 0)\n+\t\treturn;\n+\n+\tgit_config(git_default_config, NULL);\n+\n+\tif (rerere_collect_stale_entries(r, &prunable_dirs, &prunable_entries,\n+\t\t\t\t\t &prunable_entries_nr) < 0)\n+\t\texit(127);\n+\n+\tfor (size_t i = 0; i < prunable_entries_nr; i++)\n+\t\tunlink_rr_item(&prunable_entries[i]);\n+\tfor (size_t i = 0; i < prunable_dirs.nr; i++)\n+\t\trmdir(repo_git_path_replace(r, &buf, \"rr-cache/%s\",\n+\t\t\t\t\t    prunable_dirs.items[i].string));\n \n-\tstring_list_clear(&to_remove, 0);\n+\tstring_list_clear(&prunable_dirs, 0);\n \trollback_lock_file(&write_lock);\n \tstrbuf_release(&buf);\n+\tfree(prunable_entries);\n }\n \n /*\ndiff --git a/rerere.h b/rerere.h\nindex d4b5f7c9320..fd5a2388b06 100644\n--- a/rerere.h\n+++ b/rerere.h\n@@ -37,6 +37,20 @@ const char *rerere_path(struct strbuf *buf, const struct rerere_id *,\n int rerere_forget(struct repository *, struct pathspec *);\n int rerere_remaining(struct repository *, struct string_list *);\n void rerere_clear(struct repository *, struct string_list *);\n+\n+/*\n+ * Collect prunable rerere entries that would be garbage collected via\n+ * `rerere_gc()`. Whether or not an entry is prunable depends on both\n+ * \"gc.rerereResolved\" and \"gc.rerereUnresolved\".\n+ *\n+ * Returns 0 on success, a negative error code in case entries could not be\n+ * collected.\n+ */\n+int rerere_collect_stale_entries(struct repository *r,\n+\t\t\t\t struct string_list *prunable_dirs,\n+\t\t\t\t struct rerere_id **prunable_entries,\n+\t\t\t\t size_t *prunable_entries_nr);\n+\n void rerere_gc(struct repository *, struct string_list *);\n \n #define OPT_RERERE_AUTOUPDATE(v) OPT_UYN(0, \"rerere-autoupdate\", (v), \\\n\n-- \n2.49.0.987.g0cc8ee98dc.dirty\n\n"},{"id":"517012","messageId":"20250430-pks-maintenance-missing-tasks-v2-7-2580b7b8ca3a@pks.im","threadId":"63373","inReplyTo":"20250430-pks-maintenance-missing-tasks-v2-0-2580b7b8ca3a@pks.im","subject":"[PATCH v2 7/8] builtin/gc: move rerere garbage collection into separate function","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-30T10:25:11Z","receivedAt":"2025-04-30T10:25:19Z","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":"517015","messageId":"20250430-pks-maintenance-missing-tasks-v2-8-2580b7b8ca3a@pks.im","threadId":"63373","inReplyTo":"20250430-pks-maintenance-missing-tasks-v2-0-2580b7b8ca3a@pks.im","subject":"[PATCH v2 8/8] builtin/maintenance: introduce \"rerere-gc\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-30T10:25:12Z","receivedAt":"2025-04-30T10:25:19Z","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 |  8 ++++++\n Documentation/git-maintenance.adoc    |  4 +++\n builtin/gc.c                          | 41 ++++++++++++++++++++++++++\n t/t7900-maintenance.sh                | 54 +++++++++++++++++++++++++++++++++++\n 4 files changed, 107 insertions(+)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex b36b62c1c47..9c333d42b19 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -84,6 +84,14 @@ 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, a positive\n+\tvalue implies the command should run when the number of prunable rerere\n+\tentries exceeds the value. The default value is 20.\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..e56d85ea3bc 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,39 @@ 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+\tstruct string_list prunable_dirs = STRING_LIST_INIT_DUP;\n+\tstruct rerere_id *prunable_entries = NULL;\n+\tsize_t prunable_entries_nr;\n+\tint should_gc = 0;\n+\tint limit = 20;\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/* Skip garbage collecting the rerere cache in case rerere is disabled. */\n+\trepo_git_path_replace(the_repository, &path, \"rr-cache\");\n+\tif (!is_directory(path.buf))\n+\t\tgoto out;\n+\n+\tif (rerere_collect_stale_entries(the_repository, &prunable_dirs,\n+\t\t\t\t\t &prunable_entries, &prunable_entries_nr) < 0)\n+\t\tgoto out;\n+\n+\tshould_gc = prunable_entries_nr >= limit;\n+\n+out:\n+\tstring_list_clear(&prunable_dirs, 0);\n+\tfree(prunable_entries);\n+\tstrbuf_release(&path);\n+\treturn should_gc;\n+}\n+\n static int too_many_loose_objects(struct gc_config *cfg)\n {\n \t/*\n@@ -1502,6 +1537,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 +1584,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..78da81eeb24 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -564,6 +564,60 @@ test_expect_success 'worktree-prune task honors gc.worktreePruneExpire' '\n \ttest_path_is_missing .git/worktrees/worktree\n '\n \n+setup_stale_rerere_entry () {\n+\trr=.git/rr-cache/\"$(printf \"%0$(test_oid hexsz)d\" \"$1\")\" &&\n+\tmkdir -p \"$rr\" &&\n+\t>\"$rr/preimage\" &&\n+\t>\"$rr/postimage\" &&\n+\n+\ttest-tool chmtime =\"$((-61 * 86400))\" \"$rr/preimage\" &&\n+\ttest-tool chmtime =\"$((-61 * 86400))\" \"$rr/postimage\"\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_expect_rerere_gc ! git maintenance run --auto --task=rerere-gc &&\n+\tfor i in $(test_seq 19)\n+\tdo\n+\t\tsetup_stale_rerere_entry $i || return 1\n+\tdone &&\n+\ttest_expect_rerere_gc ! git maintenance run --auto --task=rerere-gc &&\n+\tsetup_stale_rerere_entry 20 &&\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+\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+\tfor i in $(test_seq 20)\n+\tdo\n+\t\tsetup_stale_rerere_entry $i || return 1\n+\tdone &&\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=21 maintenance run --auto --task=rerere-gc &&\n+\ttest_expect_rerere_gc git -c maintenance.rerere-gc.auto=10 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":"517016","messageId":"732db882-f27c-4619-91cb-2cc291a7c7d5@gmail.com","threadId":"63373","inReplyTo":"20250430-pks-maintenance-missing-tasks-v2-0-2580b7b8ca3a@pks.im","subject":"Re: [PATCH v2 0/8] builtin/maintenance: implement missing tasks compared to git-gc(1)","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2025-04-30T10:37:08Z","receivedAt":"2025-04-30T10:37:09Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 4/30/2025 6:25 AM, Patrick Steinhardt wrote:\n> Hi,\n> \n> this small patch series implements the last couple of remaining tasks\n> that are missing compared to the functionality git-gc(1) provides.\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> Changes 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\nThis version satisfies my comments from v1 and builds further by including\nthe maintenance.<task>.auto config options.\n\nLGTM. Thanks,\n-Stolee\n\n\n"},{"id":"517028","messageId":"xmqqy0vh1t96.fsf@gitster.g","threadId":"63373","inReplyTo":"20250430-pks-maintenance-missing-tasks-v2-6-2580b7b8ca3a@pks.im","subject":"Re: [PATCH v2 6/8] rerere: provide function to collect stale entries","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-04-30T16:58:13Z","receivedAt":"2025-04-30T16:58:16Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> We're about to add another task for git-maintenance(1) that prunes stale\n> rerere entries via `git rerere gc`. The condition of when to run this\n> subcommand will be configurable so that the subcommand is only executed\n> when a certain number of stale rerere entries exists. This requires us\n> to know about the number of stale rerere entries in the first place,\n> which is non-trivial to figure out.\n>\n> Refactor `rerere_gc()` and `prune_one()` so that garbage collection is\n> split into three phases:\n>\n>   1. We collect any stale rerere entries and directories that are about\n>      to become empty.\n>\n>   2. Prune all stale rerere entries.\n>\n>   3. Remove all directories that should have become empty in (2).\n>\n> By splitting out the collection of stale entries we can trivially expose\n> this function to external callers and thus reuse it in later steps.\n>\n> This refactoring is not expected to result in a user-visible change in\n> behaviour.\n\nI have no objection against the goal of allowing \"git maintenance\"\ndrive \"git rerere gc\", and as the primary author of this code path I\ndo not see anything wrong, in the \"correctness\" sense, in the\nupdated code.\n\nI however am not sure if \"count what we would prune, and remove only\nwhen there are too many\" would work well for this subsystem, because\nI expect that the cost to enumerate existing rerere entries and\ncheck each of them for staleness would be the dominant part,\nrelative to actual \"rm -fr <rerere-id>\", of the cost you are paying\nwhen you run \"git rerere gc\".\n\nAnd if my suspicion is correct, all this change does to the plain\nvanilla user of \"git rerere gc\" is to have them pay the extra cost\nof allocating and deallocating the list of names of paths in string\nlists.\n\nWe need to see some performance measurement to show that the we pay\nfor collection and counting is a lot smaller compared to the whole\npruning operation to justify the \"auto\" thing.\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  rerere.c | 92 ++++++++++++++++++++++++++++++++++++++++++++--------------------\n>  rerere.h | 14 ++++++++++\n>  2 files changed, 78 insertions(+), 28 deletions(-)\n>\n> diff --git a/rerere.c b/rerere.c\n> index 740e8ad1a0b..eb06e5f8bea 100644\n> --- a/rerere.c\n> +++ b/rerere.c\n> @@ -1202,8 +1202,8 @@ static void unlink_rr_item(struct rerere_id *id)\n>  \tstrbuf_release(&buf);\n>  }\n>  \n> -static void prune_one(struct rerere_id *id,\n> -\t\t      timestamp_t cutoff_resolve, timestamp_t cutoff_noresolve)\n> +static int is_stale(struct rerere_id *id,\n> +\t\t    timestamp_t cutoff_resolve, timestamp_t cutoff_noresolve)\n>  {\n>  \ttimestamp_t then;\n>  \ttimestamp_t cutoff;\n> @@ -1214,11 +1214,11 @@ static void prune_one(struct rerere_id *id,\n>  \telse {\n>  \t\tthen = rerere_created_at(id);\n>  \t\tif (!then)\n> -\t\t\treturn;\n> +\t\t\treturn 0;\n>  \t\tcutoff = cutoff_noresolve;\n>  \t}\n> -\tif (then < cutoff)\n> -\t\tunlink_rr_item(id);\n> +\n> +\treturn then < cutoff;\n>  }\n>  \n>  /* Does the basename in \"path\" look plausibly like an rr-cache entry? */\n> @@ -1229,29 +1229,35 @@ static int is_rr_cache_dirname(const char *path)\n>  \treturn !parse_oid_hex(path, &oid, &end) && !*end;\n>  }\n>  \n> -void rerere_gc(struct repository *r, struct string_list *rr)\n> +int rerere_collect_stale_entries(struct repository *r,\n> +\t\t\t\t struct string_list *prunable_dirs,\n> +\t\t\t\t struct rerere_id **prunable_entries,\n> +\t\t\t\t size_t *prunable_entries_nr)\n>  {\n> -\tstruct string_list to_remove = STRING_LIST_INIT_DUP;\n> -\tDIR *dir;\n> -\tstruct dirent *e;\n> -\tint i;\n>  \ttimestamp_t now = time(NULL);\n>  \ttimestamp_t cutoff_noresolve = now - 15 * 86400;\n>  \ttimestamp_t cutoff_resolve = now - 60 * 86400;\n>  \tstruct strbuf buf = STRBUF_INIT;\n> +\tsize_t prunable_entries_alloc;\n> +\tstruct dirent *e;\n> +\tDIR *dir = NULL;\n> +\tint ret;\n>  \n> -\tif (setup_rerere(r, rr, 0) < 0)\n> -\t\treturn;\n> +\t*prunable_entries = NULL;\n> +\t*prunable_entries_nr = 0;\n> +\tprunable_entries_alloc = 0;\n>  \n> -\trepo_config_get_expiry_in_days(the_repository, \"gc.rerereresolved\",\n> +\trepo_config_get_expiry_in_days(r, \"gc.rerereresolved\",\n>  \t\t\t\t       &cutoff_resolve, now);\n> -\trepo_config_get_expiry_in_days(the_repository, \"gc.rerereunresolved\",\n> +\trepo_config_get_expiry_in_days(r, \"gc.rerereunresolved\",\n>  \t\t\t\t       &cutoff_noresolve, now);\n> -\tgit_config(git_default_config, NULL);\n> -\tdir = opendir(repo_git_path_replace(the_repository, &buf, \"rr-cache\"));\n> -\tif (!dir)\n> -\t\tdie_errno(_(\"unable to open rr-cache directory\"));\n> -\t/* Collect stale conflict IDs ... */\n> +\n> +\tdir = opendir(repo_git_path_replace(r, &buf, \"rr-cache\"));\n> +\tif (!dir) {\n> +\t\tret = error_errno(_(\"unable to open rr-cache directory\"));\n> +\t\tgoto out;\n> +\t}\n> +\n>  \twhile ((e = readdir_skip_dot_and_dotdot(dir))) {\n>  \t\tstruct rerere_dir *rr_dir;\n>  \t\tstruct rerere_id id;\n> @@ -1266,23 +1272,53 @@ void rerere_gc(struct repository *r, struct string_list *rr)\n>  \t\tfor (id.variant = 0, id.collection = rr_dir;\n>  \t\t     id.variant < id.collection->status_nr;\n>  \t\t     id.variant++) {\n> -\t\t\tprune_one(&id, cutoff_resolve, cutoff_noresolve);\n> -\t\t\tif (id.collection->status[id.variant])\n> +\t\t\tif (is_stale(&id, cutoff_resolve, cutoff_noresolve)) {\n> +\t\t\t\tALLOC_GROW(*prunable_entries, *prunable_entries_nr + 1,\n> +\t\t\t\t\t   prunable_entries_alloc);\n> +\t\t\t\t(*prunable_entries)[(*prunable_entries_nr)++] = id;\n> +\t\t\t} else {\n>  \t\t\t\tnow_empty = 0;\n> +\t\t\t}\n>  \t\t}\n>  \t\tif (now_empty)\n> -\t\t\tstring_list_append(&to_remove, e->d_name);\n> +\t\t\tstring_list_append(prunable_dirs, e->d_name);\n>  \t}\n> -\tclosedir(dir);\n>  \n> -\t/* ... and then remove the empty directories */\n> -\tfor (i = 0; i < to_remove.nr; i++)\n> -\t\trmdir(repo_git_path_replace(the_repository, &buf,\n> -\t\t\t\t\t    \"rr-cache/%s\", to_remove.items[i].string));\n> +\tret = 0;\n> +\n> +out:\n> +\tstrbuf_release(&buf);\n> +\tif (dir)\n> +\t\tclosedir(dir);\n> +\treturn ret;\n> +}\n> +\n> +void rerere_gc(struct repository *r, struct string_list *rr)\n> +{\n> +\tstruct string_list prunable_dirs = STRING_LIST_INIT_DUP;\n> +\tstruct rerere_id *prunable_entries;\n> +\tstruct strbuf buf = STRBUF_INIT;\n> +\tsize_t prunable_entries_nr;\n> +\n> +\tif (setup_rerere(r, rr, 0) < 0)\n> +\t\treturn;\n> +\n> +\tgit_config(git_default_config, NULL);\n> +\n> +\tif (rerere_collect_stale_entries(r, &prunable_dirs, &prunable_entries,\n> +\t\t\t\t\t &prunable_entries_nr) < 0)\n> +\t\texit(127);\n> +\n> +\tfor (size_t i = 0; i < prunable_entries_nr; i++)\n> +\t\tunlink_rr_item(&prunable_entries[i]);\n> +\tfor (size_t i = 0; i < prunable_dirs.nr; i++)\n> +\t\trmdir(repo_git_path_replace(r, &buf, \"rr-cache/%s\",\n> +\t\t\t\t\t    prunable_dirs.items[i].string));\n>  \n> -\tstring_list_clear(&to_remove, 0);\n> +\tstring_list_clear(&prunable_dirs, 0);\n>  \trollback_lock_file(&write_lock);\n>  \tstrbuf_release(&buf);\n> +\tfree(prunable_entries);\n>  }\n>  \n>  /*\n> diff --git a/rerere.h b/rerere.h\n> index d4b5f7c9320..fd5a2388b06 100644\n> --- a/rerere.h\n> +++ b/rerere.h\n> @@ -37,6 +37,20 @@ const char *rerere_path(struct strbuf *buf, const struct rerere_id *,\n>  int rerere_forget(struct repository *, struct pathspec *);\n>  int rerere_remaining(struct repository *, struct string_list *);\n>  void rerere_clear(struct repository *, struct string_list *);\n> +\n> +/*\n> + * Collect prunable rerere entries that would be garbage collected via\n> + * `rerere_gc()`. Whether or not an entry is prunable depends on both\n> + * \"gc.rerereResolved\" and \"gc.rerereUnresolved\".\n> + *\n> + * Returns 0 on success, a negative error code in case entries could not be\n> + * collected.\n> + */\n> +int rerere_collect_stale_entries(struct repository *r,\n> +\t\t\t\t struct string_list *prunable_dirs,\n> +\t\t\t\t struct rerere_id **prunable_entries,\n> +\t\t\t\t size_t *prunable_entries_nr);\n> +\n>  void rerere_gc(struct repository *, struct string_list *);\n>  \n>  #define OPT_RERERE_AUTOUPDATE(v) OPT_UYN(0, \"rerere-autoupdate\", (v), \\\n"},{"id":"517087","messageId":"aBR9R3PxSCuLON6G@pks.im","threadId":"63373","inReplyTo":"xmqqy0vh1t96.fsf@gitster.g","subject":"Re: [PATCH v2 6/8] rerere: provide function to collect stale entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-02T08:07:35Z","receivedAt":"2025-05-02T08:07:39Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Apr 30, 2025 at 09:58:13AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > We're about to add another task for git-maintenance(1) that prunes stale\n> > rerere entries via `git rerere gc`. The condition of when to run this\n> > subcommand will be configurable so that the subcommand is only executed\n> > when a certain number of stale rerere entries exists. This requires us\n> > to know about the number of stale rerere entries in the first place,\n> > which is non-trivial to figure out.\n> >\n> > Refactor `rerere_gc()` and `prune_one()` so that garbage collection is\n> > split into three phases:\n> >\n> >   1. We collect any stale rerere entries and directories that are about\n> >      to become empty.\n> >\n> >   2. Prune all stale rerere entries.\n> >\n> >   3. Remove all directories that should have become empty in (2).\n> >\n> > By splitting out the collection of stale entries we can trivially expose\n> > this function to external callers and thus reuse it in later steps.\n> >\n> > This refactoring is not expected to result in a user-visible change in\n> > behaviour.\n> \n> I have no objection against the goal of allowing \"git maintenance\"\n> drive \"git rerere gc\", and as the primary author of this code path I\n> do not see anything wrong, in the \"correctness\" sense, in the\n> updated code.\n> \n> I however am not sure if \"count what we would prune, and remove only\n> when there are too many\" would work well for this subsystem, because\n> I expect that the cost to enumerate existing rerere entries and\n> check each of them for staleness would be the dominant part,\n> relative to actual \"rm -fr <rerere-id>\", of the cost you are paying\n> when you run \"git rerere gc\".\n> \n> And if my suspicion is correct, all this change does to the plain\n> vanilla user of \"git rerere gc\" is to have them pay the extra cost\n> of allocating and deallocating the list of names of paths in string\n> lists.\n\nYeah, I think this concern makes sense indeed. I was a bit sceptical\nmyself whether this is going too far. Maybe a simpler solution would be\nto just count the number of directories in \".git/rr-cache\", without\nchecking whether those actually are prunable?\n\nWe could also adapt this to be closer to the original version, where we\nonly verified that \".git/rr-cache\" exists and contains at least one\nsubdirectory. This can even be combined with the above approach if we\nset \"maintenance.rerere-gc.auto=1\" by default.\n\n> We need to see some performance measurement to show that the we pay\n> for collection and counting is a lot smaller compared to the whole\n> pruning operation to justify the \"auto\" thing.\n\nHm. I guess ultimately the answer is going to be \"it depends\". The\nperformance implication on Windows is going to be quite different\ncompared to the performance on Linux/macOS.\n\nIn any case, let's go with the simpler model for now. We can still\niterate as needed if we eventually see that the heuristic is too dumb to\nbe useful.\n\nPatrick\n"},{"id":"517121","messageId":"xmqqwmazug0i.fsf@gitster.g","threadId":"63373","inReplyTo":"aBR9R3PxSCuLON6G@pks.im","subject":"Re: [PATCH v2 6/8] rerere: provide function to collect stale entries","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-05-02T16:35:57Z","receivedAt":"2025-05-02T16:36:00Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n>> And if my suspicion is correct, all this change does to the plain\n>> vanilla user of \"git rerere gc\" is to have them pay the extra cost\n>> of allocating and deallocating the list of names of paths in string\n>> lists.\n>\n> Yeah, I think this concern makes sense indeed. I was a bit sceptical\n> myself whether this is going too far. Maybe a simpler solution would be\n> to just count the number of directories in \".git/rr-cache\", without\n> checking whether those actually are prunable?\n\nI dunno.  It is not like you are guestimating files under\n.git/objects/??/ without couting all of them, just to see if you\nhave too many of them and reduce them by repacking and/or pruning.\n\nHaving too many existing rerere entries does not correleate you\nwould have many prunable ones.  If your development is nearly linear\nand you do not rebuild the same merges over and over every day,\nmajority of rerere entries may be created and recorded without ever\ngetting used even once.  On the other hand, in my tree, where the\nsame merges along master..jch first-parent history are recreated\nmultiple times every day, the picture would be different.\n\n> We could also adapt this to be closer to the original version, where we\n> only verified that \".git/rr-cache\" exists and contains at least one\n> subdirectory. This can even be combined with the above approach if we\n> set \"maintenance.rerere-gc.auto=1\" by default.\n\nBut wouldn't that be like always running \"rerere gc\"?  What would\nyou save by checking the existence of at least one rerere entry?\nCost to spawn \"rerere gc\" as a subprocess?  Wouldn't it be a better\nuse of the engineering effort to libify the logic of that process\nand make it internally callable in the longer term (or are you\nalready doing that in this series)?\n\n>> We need to see some performance measurement to show that the we pay\n>> for collection and counting is a lot smaller compared to the whole\n>> pruning operation to justify the \"auto\" thing.\n>\n> Hm. I guess ultimately the answer is going to be \"it depends\". The\n> performance implication on Windows is going to be quite different\n> compared to the performance on Linux/macOS.\n\nYes, but we need to start somewhere ;-)\n"},{"id":"517211","messageId":"aBhnOiHgfycVJhHQ@pks.im","threadId":"63373","inReplyTo":"xmqqwmazug0i.fsf@gitster.g","subject":"Re: [PATCH v2 6/8] rerere: provide function to collect stale entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-05-05T07:22:34Z","receivedAt":"2025-05-05T07:22:43Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, May 02, 2025 at 09:35:57AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> > We could also adapt this to be closer to the original version, where we\n> > only verified that \".git/rr-cache\" exists and contains at least one\n> > subdirectory. This can even be combined with the above approach if we\n> > set \"maintenance.rerere-gc.auto=1\" by default.\n> \n> But wouldn't that be like always running \"rerere gc\"?  What would\n> you save by checking the existence of at least one rerere entry?\n> Cost to spawn \"rerere gc\" as a subprocess?\n\nWhat you save is executing the rerere logic in case the feature isn't\nenabled at all, but also in the case where you have eventually disabled\nthe rerere cache but still have a now-empty \".git/rr-cache\" directory.\n\n> Wouldn't it be a better use of the engineering effort to libify the\n> logic of that process and make it internally callable in the longer\n> term (or are you already doing that in this series)?\n\nThe logic itself already is quite encapsulated, but I decided to not use\nit for now because we don't do so for any of the other tasks, either.\nBut I do agree that we should eventually refactor all of our tasks so\nthat they don't need to spawn an external process at all.\n\nPatrick\n"}]}