[PATCH v2 0/8] builtin/maintenance: implement missing tasks compared to git-gc(1)
- From
Patrick Steinhardt <ps@pks.im>
- Date
- Apr 30, 2025, 10:25 UTC
- Message-ID
- <20250430-pks-maintenance-missing-tasks-v2-0-2580b7b8ca3a@pks.im>
- In-Reply-To
- <20250425-pks-maintenance-missing-tasks-v1-0-972ed6ab2c0d@pks.im>
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@pks.imThanks!
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" taskDocumentation/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@pks.im>
+ ## 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@pks.im>
+ ## 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