Hi, this small patch series implements the last couple of remaining tasks that are missing compared to the functionality git-gc(1) provides. Right now, git-maintenance(1) still executes git-gc(1). With these last gaps plugged though we can in theory fully replace git-gc(1) with finer grained tasks without losing any functionality. The benefit is that it becomes possible for users to have finer-grained control over what exactly the maintenance does. This patch series doesn't do that yet, but only implements whatever is needed to get there. Changes in v2: - Introduce "maintenance.worktree-prune.auto", which controls how many stale worktrees need to exist before executing `git worktree prune`. - Introduce "maintenance.rerere-gc.auto", which controls how many stale rerere entries need to exist before executing `git rerere gc`. - Add tests to verify that "gc.worktreePruneExpire" works. - Remove some fragile test logic by introducing functions that check for a given maintenance subprocess. - Link to v1: https://lore.kernel.org/r/20250425-pks-maintenance-missing-tasks-v1-0-972ed6ab2c0d@xxxxxx Thanks! Patrick --- Patrick Steinhardt (8): builtin/gc: fix indentation of `cmd_gc()` parameters builtin/gc: remove global variables where it trivial to do builtin/gc: move pruning of worktrees into a separate function worktree: expose function to retrieve worktree names builtin/maintenance: introduce "worktree-prune" task rerere: provide function to collect stale entries builtin/gc: move rerere garbage collection into separate function builtin/maintenance: introduce "rerere-gc" task Documentation/config/maintenance.adoc | 16 ++++ Documentation/git-maintenance.adoc | 8 ++ builtin/gc.c | 153 +++++++++++++++++++++++++++------- builtin/worktree.c | 25 +++--- rerere.c | 92 +++++++++++++------- rerere.h | 14 ++++ t/t7900-maintenance.sh | 125 +++++++++++++++++++++++++++ worktree.c | 30 +++++++ worktree.h | 8 ++ 9 files changed, 399 insertions(+), 72 deletions(-) Range-diff versus v1: 1: 0304b81df0b = 1: 9c62b493297 builtin/gc: fix indentation of `cmd_gc()` parameters 2: 22c499601ee = 2: 9ae42b139fa builtin/gc: remove global variables where it trivial to do 3: db9622a408f = 3: 50a5305b6d2 builtin/gc: move pruning of worktrees into a separate function 4: f42205e1b6b = 4: b71dcb0debc worktree: expose function to retrieve worktree names 5: eade37df904 ! 5: 47d31f41c2e builtin/maintenance: introduce "worktree-prune" task @@ Commit message Signed-off-by: Patrick Steinhardt <ps@xxxxxx> + ## Documentation/config/maintenance.adoc ## +@@ Documentation/config/maintenance.adoc: maintenance.reflog-expire.auto:: + positive value implies the command should run when the number of + expired reflog entries in the "HEAD" reflog is at least the value of + `maintenance.loose-objects.auto`. The default value is 100. ++ ++maintenance.worktree-prune.auto:: ++ This integer config option controls how often the `worktree-prune` task ++ should be run as part of `git maintenance run --auto`. If zero, then ++ the `worktree-prune` task will not run with the `--auto` option. A ++ negative value will force the task to run every time. Otherwise, a ++ positive value implies the command should run when the number of ++ prunable worktrees exceeds the value. The default value is 1. + ## Documentation/git-maintenance.adoc ## @@ Documentation/git-maintenance.adoc: reflog-expire:: The `reflog-expire` task deletes any entries in the reflog older than the @@ builtin/gc.c: static int maintenance_task_worktree_prune(struct maintenance_run_ + struct strbuf reason = STRBUF_INIT; + timestamp_t expiry_date; + int should_prune = 0; ++ int limit = 1; ++ ++ git_config_get_int("maintenance.worktree-prune.auto", &limit); ++ if (limit <= 0) { ++ should_prune = limit < 0; ++ goto out; ++ } + + if (parse_expiry_date(cfg->prune_worktrees_expire, &expiry_date) || + get_worktree_names(the_repository, &worktrees) < 0) @@ builtin/gc.c: static int maintenance_task_worktree_prune(struct maintenance_run_ + + strbuf_reset(&reason); + if (should_prune_worktree(worktrees.v[i], &reason, &wtpath, expiry_date)) { -+ should_prune = 1; -+ goto out; ++ limit--; ++ ++ if (!limit) { ++ should_prune = 1; ++ goto out; ++ } + } + free(wtpath); + } @@ t/t7900-maintenance.sh: test_expect_success 'reflog-expire task --auto only pack test_subcommand git reflog expire --all <reflog-expire-auto.txt ' -+test_expect_success 'worktree-prune task' ' -+ GIT_TRACE2_EVENT="$(pwd)/worktree-prune.txt" \ -+ git maintenance run --task=worktree-prune && -+ test_subcommand git worktree prune --expire 3.months.ago <worktree-prune.txt ++test_expect_worktree_prune () { ++ negate= ++ if test "$1" = "!" ++ then ++ negate="!" ++ shift ++ fi ++ ++ rm -f "worktree-prune.txt" && ++ GIT_TRACE2_EVENT="$(pwd)/worktree-prune.txt" "$@" && ++ test_subcommand $negate git worktree prune --expire 3.months.ago <worktree-prune.txt ++} ++ ++test_expect_success 'worktree-prune task without --auto always prunes' ' ++ test_expect_worktree_prune git maintenance run --task=worktree-prune +' + +test_expect_success 'worktree-prune task --auto only prunes with prunable worktree' ' -+ GIT_TRACE2_EVENT="$(pwd)/worktree-prune-auto.txt" \ -+ git maintenance run --auto --task=worktree-prune && -+ test_subcommand ! git worktree prune --expire 3.months.ago <worktree-prune-auto.txt && ++ test_expect_worktree_prune ! git maintenance run --auto --task=worktree-prune && + mkdir .git/worktrees && + : >.git/worktrees/abc && -+ GIT_TRACE2_EVENT="$(pwd)/worktree-prune-auto.txt" \ -+ git maintenance run --auto --task=worktree-prune && -+ test_subcommand git worktree prune --expire 3.months.ago <worktree-prune-auto.txt ++ test_expect_worktree_prune git maintenance run --auto --task=worktree-prune ++' ++ ++test_expect_success 'worktree-prune task with --auto honors maintenance.worktree-prune.auto' ' ++ # A negative value should always prune. ++ test_expect_worktree_prune git -c maintenance.worktree-prune.auto=-1 maintenance run --auto --task=worktree-prune && ++ ++ mkdir .git/worktrees && ++ : >.git/worktrees/first && ++ : >.git/worktrees/second && ++ : >.git/worktrees/third && ++ ++ # Zero should never prune. ++ test_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=0 maintenance run --auto --task=worktree-prune && ++ # A positive value should require at least this man prunable worktrees. ++ test_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=4 maintenance run --auto --task=worktree-prune && ++ test_expect_worktree_prune git -c maintenance.worktree-prune.auto=3 maintenance run --auto --task=worktree-prune ++' ++ ++test_expect_success 'worktree-prune task with --auto honors maintenance.worktree-prune.auto' ' ++ # A negative value should always prune. ++ test_expect_worktree_prune git -c maintenance.worktree-prune.auto=-1 maintenance run --auto --task=worktree-prune && ++ ++ mkdir .git/worktrees && ++ : >.git/worktrees/first && ++ : >.git/worktrees/second && ++ : >.git/worktrees/third && ++ ++ # Zero should never prune. ++ test_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=0 maintenance run --auto --task=worktree-prune && ++ # A positive value should require at least this many prunable worktrees. ++ test_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=4 maintenance run --auto --task=worktree-prune && ++ test_expect_worktree_prune git -c maintenance.worktree-prune.auto=3 maintenance run --auto --task=worktree-prune ++' ++ ++test_expect_success 'worktree-prune task honors gc.worktreePruneExpire' ' ++ git worktree add worktree && ++ rm -rf worktree && ++ ++ rm -f worktree-prune.txt && ++ GIT_TRACE2_EVENT="$(pwd)/worktree-prune.txt" git -c gc.worktreePruneExpire=1.week.ago maintenance run --auto --task=worktree-prune && ++ test_subcommand ! git worktree prune --expire 1.week.ago <worktree-prune.txt && ++ test_path_is_dir .git/worktrees/worktree && ++ ++ rm -f worktree-prune.txt && ++ GIT_TRACE2_EVENT="$(pwd)/worktree-prune.txt" git -c gc.worktreePruneExpire=now maintenance run --auto --task=worktree-prune && ++ test_subcommand git worktree prune --expire now <worktree-prune.txt && ++ test_path_is_missing .git/worktrees/worktree +' + test_expect_success '--auto and --schedule incompatible' ' -: ----------- > 6: 5550c115e84 rerere: provide function to collect stale entries 6: 66b2b033743 = 7: f5b234c859e builtin/gc: move rerere garbage collection into separate function 7: 9604fc4fc6b ! 8: 092e57cce01 builtin/maintenance: introduce "rerere-gc" task @@ Commit message Signed-off-by: Patrick Steinhardt <ps@xxxxxx> + ## Documentation/config/maintenance.adoc ## +@@ Documentation/config/maintenance.adoc: maintenance.reflog-expire.auto:: + expired reflog entries in the "HEAD" reflog is at least the value of + `maintenance.loose-objects.auto`. The default value is 100. + ++maintenance.rerere-gc.auto:: ++ This integer config option controls how often the `rerere-gc` task ++ should be run as part of `git maintenance run --auto`. If zero, then ++ the `rerere-gc` task will not run with the `--auto` option. A negative ++ value will force the task to run every time. Otherwise, a positive ++ value implies the command should run when the number of prunable rerere ++ entries exceeds the value. The default value is 20. ++ + maintenance.worktree-prune.auto:: + This integer config option controls how often the `worktree-prune` task + should be run as part of `git maintenance run --auto`. If zero, then + ## Documentation/git-maintenance.adoc ## @@ Documentation/git-maintenance.adoc: reflog-expire:: The `reflog-expire` task deletes any entries in the reflog older than the @@ builtin/gc.c #include "environment.h" #include "hex.h" #include "config.h" +@@ + #include "pack-objects.h" + #include "path.h" + #include "reflog.h" ++#include "rerere.h" + #include "blob.h" + #include "tree.h" + #include "promisor-remote.h" @@ builtin/gc.c: static int maintenance_task_rerere_gc(struct maintenance_run_opts *opts UNUSED, return run_command(&rerere_cmd); } @@ builtin/gc.c: static int maintenance_task_rerere_gc(struct maintenance_run_opts +static int rerere_gc_condition(struct gc_config *cfg UNUSED) +{ + struct strbuf path = STRBUF_INIT; ++ struct string_list prunable_dirs = STRING_LIST_INIT_DUP; ++ struct rerere_id *prunable_entries = NULL; ++ size_t prunable_entries_nr; + int should_gc = 0; -+ DIR *dir; ++ int limit = 20; ++ ++ git_config_get_int("maintenance.rerere-gc.auto", &limit); ++ if (limit <= 0) { ++ should_gc = limit < 0; ++ goto out; ++ } + + /* Skip garbage collecting the rerere cache in case rerere is disabled. */ + repo_git_path_replace(the_repository, &path, "rr-cache"); ++ if (!is_directory(path.buf)) ++ goto out; + -+ dir = opendir(path.buf); -+ if (!dir) ++ if (rerere_collect_stale_entries(the_repository, &prunable_dirs, ++ &prunable_entries, &prunable_entries_nr) < 0) + goto out; -+ should_gc = !!readdir_skip_dot_and_dotdot(dir); ++ ++ should_gc = prunable_entries_nr >= limit; + +out: ++ string_list_clear(&prunable_dirs, 0); ++ free(prunable_entries); + strbuf_release(&path); -+ closedir(dir); + return should_gc; +} + @@ builtin/gc.c: static struct maintenance_task tasks[] = { static int compare_tasks_by_selection(const void *a_, const void *b_) ## t/t7900-maintenance.sh ## -@@ t/t7900-maintenance.sh: test_expect_success 'worktree-prune task --auto only prunes with prunable worktr - test_subcommand git worktree prune --expire 3.months.ago <worktree-prune-auto.txt +@@ t/t7900-maintenance.sh: test_expect_success 'worktree-prune task honors gc.worktreePruneExpire' ' + test_path_is_missing .git/worktrees/worktree ' -+test_expect_success 'rerere-gc task' ' -+ GIT_TRACE2_EVENT="$(pwd)/rerere-gc.txt" \ -+ git maintenance run --task=rerere-gc && -+ test_subcommand git rerere gc <rerere-gc.txt ++setup_stale_rerere_entry () { ++ rr=.git/rr-cache/"$(printf "%0$(test_oid hexsz)d" "$1")" && ++ mkdir -p "$rr" && ++ >"$rr/preimage" && ++ >"$rr/postimage" && ++ ++ test-tool chmtime ="$((-61 * 86400))" "$rr/preimage" && ++ test-tool chmtime ="$((-61 * 86400))" "$rr/postimage" ++} ++ ++test_expect_rerere_gc () { ++ negate= ++ if test "$1" = "!" ++ then ++ negate="!" ++ shift ++ fi ++ ++ rm -f "rerere-gc.txt" && ++ GIT_TRACE2_EVENT="$(pwd)/rerere-gc.txt" "$@" && ++ test_subcommand $negate git rerere gc <rerere-gc.txt ++} ++ ++test_expect_success 'rerere-gc task without --auto always collects garbage' ' ++ test_expect_rerere_gc git maintenance run --task=rerere-gc +' + -+test_expect_success 'rerere-gc task --auto only prunes with existing rr-cache dir' ' -+ mkdir .git/rr-cache && -+ GIT_TRACE2_EVENT="$(pwd)/rerere-gc-auto.txt" \ -+ git maintenance run --auto --task=rerere-gc && -+ test_subcommand ! git rerere gc <rerere-gc-auto.txt && -+ : >.git/rr-cache/entry && -+ GIT_TRACE2_EVENT="$(pwd)/rerere-gc-auto.txt" \ -+ git maintenance run --auto --task=rerere-gc && -+ test_subcommand git rerere gc <rerere-gc-auto.txt ++test_expect_success 'rerere-gc task with --auto only prunes with prunable entries' ' ++ test_expect_rerere_gc ! git maintenance run --auto --task=rerere-gc && ++ for i in $(test_seq 19) ++ do ++ setup_stale_rerere_entry $i || return 1 ++ done && ++ test_expect_rerere_gc ! git maintenance run --auto --task=rerere-gc && ++ setup_stale_rerere_entry 20 && ++ test_expect_rerere_gc git maintenance run --auto --task=rerere-gc ++' ++ ++test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.auto' ' ++ # A negative value should always prune. ++ test_expect_rerere_gc git -c maintenance.rerere-gc.auto=-1 maintenance run --auto --task=rerere-gc && ++ ++ for i in $(test_seq 20) ++ do ++ setup_stale_rerere_entry $i || return 1 ++ done && ++ ++ # Zero should never prune. ++ test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 maintenance run --auto --task=rerere-gc && ++ # A positive value should require at least this many stale rerere entries. ++ test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=21 maintenance run --auto --task=rerere-gc && ++ test_expect_rerere_gc git -c maintenance.rerere-gc.auto=10 maintenance run --auto --task=rerere-gc +' + test_expect_success '--auto and --schedule incompatible' ' --- base-commit: a2955b34f48265d240ab8c7deb0a929ec2d65fd0 change-id: 20250424-pks-maintenance-missing-tasks-8ffcdd596b73