From 8ab53452417f6c14561c2d40ad0515e81411b755 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 30 Apr 2025 12:25:05 +0200 Subject: [PATCH 1/8] builtin/gc: fix indentation of `cmd_gc()` parameters The parameters of `cmd_gc()` aren't indented properly. Fix this. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/gc.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/builtin/gc.c b/builtin/gc.c index a5b86bbf16..d24cc7105b 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -728,9 +728,9 @@ static void gc_before_repack(struct maintenance_run_opts *opts, } int cmd_gc(int argc, -const char **argv, -const char *prefix, -struct repository *repo UNUSED) + const char **argv, + const char *prefix, + struct repository *repo UNUSED) { int aggressive = 0; int quiet = 0; From 0c8007206369819fc4315e26e22cbb25f0c56a92 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 30 Apr 2025 12:25:06 +0200 Subject: [PATCH 2/8] builtin/gc: remove global variables where it trivial to do We use a couple of global variables to assemble command line arguments for subprocesses we execute in git-gc(1). All of these variables except the one for git-repack(1) are only used in a single place though, so they don't really add anything but confusion. Remove those variables. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/gc.c | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/builtin/gc.c b/builtin/gc.c index d24cc7105b..ba4b30c24b 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -52,15 +52,9 @@ static const char * const builtin_gc_usage[] = { }; static timestamp_t gc_log_expire_time; - static struct strvec repack = STRVEC_INIT; -static struct strvec prune = STRVEC_INIT; -static struct strvec prune_worktrees = STRVEC_INIT; -static struct strvec rerere = STRVEC_INIT; - static struct tempfile *pidfile; static struct lock_file log_lock; - static struct string_list pack_garbage = STRING_LIST_INIT_DUP; static void clean_pack_garbage(void) @@ -779,9 +773,6 @@ int cmd_gc(int argc, builtin_gc_usage, builtin_gc_options); strvec_pushl(&repack, "repack", "-d", "-l", NULL); - strvec_pushl(&prune, "prune", "--expire", NULL); - strvec_pushl(&prune_worktrees, "worktree", "prune", "--expire", NULL); - strvec_pushl(&rerere, "rerere", "gc", NULL); gc_config(&cfg); @@ -907,34 +898,36 @@ int cmd_gc(int argc, if (cfg.prune_expire) { struct child_process prune_cmd = CHILD_PROCESS_INIT; + strvec_pushl(&prune_cmd.args, "prune", "--expire", NULL); /* run `git prune` even if using cruft packs */ - strvec_push(&prune, cfg.prune_expire); + strvec_push(&prune_cmd.args, cfg.prune_expire); if (quiet) - strvec_push(&prune, "--no-progress"); + strvec_push(&prune_cmd.args, "--no-progress"); if (repo_has_promisor_remote(the_repository)) - strvec_push(&prune, + strvec_push(&prune_cmd.args, "--exclude-promisor-objects"); prune_cmd.git_cmd = 1; - strvec_pushv(&prune_cmd.args, prune.v); + if (run_command(&prune_cmd)) - die(FAILED_RUN, prune.v[0]); + die(FAILED_RUN, prune_cmd.args.v[0]); } } if (cfg.prune_worktrees_expire) { struct child_process prune_worktrees_cmd = CHILD_PROCESS_INIT; - strvec_push(&prune_worktrees, cfg.prune_worktrees_expire); prune_worktrees_cmd.git_cmd = 1; - strvec_pushv(&prune_worktrees_cmd.args, prune_worktrees.v); + strvec_pushl(&prune_worktrees_cmd.args, "worktree", "prune", "--expire", NULL); + strvec_push(&prune_worktrees_cmd.args, cfg.prune_worktrees_expire); + if (run_command(&prune_worktrees_cmd)) - die(FAILED_RUN, prune_worktrees.v[0]); + die(FAILED_RUN, prune_worktrees_cmd.args.v[0]); } rerere_cmd.git_cmd = 1; - strvec_pushv(&rerere_cmd.args, rerere.v); + strvec_pushl(&rerere_cmd.args, "rerere", "gc", NULL); if (run_command(&rerere_cmd)) - die(FAILED_RUN, rerere.v[0]); + die(FAILED_RUN, rerere_cmd.args.v[0]); report_garbage = report_pack_garbage; reprepare_packed_git(the_repository); From 50a2249dce44c7823056fd6e87eb085cd98094d8 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 30 Apr 2025 12:25:07 +0200 Subject: [PATCH 3/8] builtin/gc: move pruning of worktrees into a separate function Move pruning of worktrees into a separate function. This prepares for a subsequent commit where we introduce a new "worktree-prune" task for git-maintenance(1). Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/gc.c | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/builtin/gc.c b/builtin/gc.c index ba4b30c24b..d91b6b7b8c 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -333,6 +333,18 @@ static int maintenance_task_reflog_expire(struct maintenance_run_opts *opts UNUS return run_command(&cmd); } +static int maintenance_task_worktree_prune(struct maintenance_run_opts *opts UNUSED, + struct gc_config *cfg) +{ + struct child_process prune_worktrees_cmd = CHILD_PROCESS_INIT; + + prune_worktrees_cmd.git_cmd = 1; + strvec_pushl(&prune_worktrees_cmd.args, "worktree", "prune", "--expire", NULL); + strvec_push(&prune_worktrees_cmd.args, cfg->prune_worktrees_expire); + + return run_command(&prune_worktrees_cmd); +} + static int too_many_loose_objects(struct gc_config *cfg) { /* @@ -913,16 +925,9 @@ int cmd_gc(int argc, } } - if (cfg.prune_worktrees_expire) { - struct child_process prune_worktrees_cmd = CHILD_PROCESS_INIT; - - prune_worktrees_cmd.git_cmd = 1; - strvec_pushl(&prune_worktrees_cmd.args, "worktree", "prune", "--expire", NULL); - strvec_push(&prune_worktrees_cmd.args, cfg.prune_worktrees_expire); - - if (run_command(&prune_worktrees_cmd)) - die(FAILED_RUN, prune_worktrees_cmd.args.v[0]); - } + if (cfg.prune_worktrees_expire && + maintenance_task_worktree_prune(&opts, &cfg)) + die(FAILED_RUN, "worktree"); rerere_cmd.git_cmd = 1; strvec_pushl(&rerere_cmd.args, "rerere", "gc", NULL); From 4167c6153b10593b753c4f73eb930b5425e091af Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 30 Apr 2025 12:25:08 +0200 Subject: [PATCH 4/8] worktree: expose function to retrieve worktree names Introduce a function that retrieves worktree names as present in ".git/worktrees". This function will be used in a subsequent commit. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/worktree.c | 25 ++++++++++++------------- worktree.c | 30 ++++++++++++++++++++++++++++++ worktree.h | 8 ++++++++ 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/builtin/worktree.c b/builtin/worktree.c index 88a36ea9f8..7b3cde99d7 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -211,27 +211,24 @@ static void prune_dups(struct string_list *l) static void prune_worktrees(void) { - struct strbuf reason = STRBUF_INIT; struct strbuf main_path = STRBUF_INIT; struct string_list kept = STRING_LIST_INIT_DUP; - char *path; - DIR *dir; - struct dirent *d; + struct strvec worktrees = STRVEC_INIT; + struct strbuf reason = STRBUF_INIT; - path = repo_git_path(the_repository, "worktrees"); - dir = opendir(path); - free(path); - if (!dir) + if (get_worktree_names(the_repository, &worktrees) < 0 || + !worktrees.nr) return; - while ((d = readdir_skip_dot_and_dotdot(dir)) != NULL) { + + for (size_t i = 0; i < worktrees.nr; i++) { char *path; + strbuf_reset(&reason); - if (should_prune_worktree(d->d_name, &reason, &path, expire)) - prune_worktree(d->d_name, reason.buf); + if (should_prune_worktree(worktrees.v[i], &reason, &path, expire)) + prune_worktree(worktrees.v[i], reason.buf); else if (path) - string_list_append_nodup(&kept, path)->util = xstrdup(d->d_name); + string_list_append_nodup(&kept, path)->util = xstrdup(worktrees.v[i]); } - closedir(dir); strbuf_add_absolute_path(&main_path, repo_get_common_dir(the_repository)); /* massage main worktree absolute path to match 'gitdir' content */ @@ -242,6 +239,8 @@ static void prune_worktrees(void) if (!show_only) delete_worktrees_dir_if_empty(); + + strvec_clear(&worktrees); strbuf_release(&reason); } diff --git a/worktree.c b/worktree.c index c34b9eb74e..947b7a8220 100644 --- a/worktree.c +++ b/worktree.c @@ -988,6 +988,36 @@ done: return rc; } +int get_worktree_names(struct repository *repo, struct strvec *out) +{ + char *worktrees_dir; + struct dirent *d; + DIR *dir; + int ret; + + worktrees_dir = repo_git_path(repo, "worktrees"); + dir = opendir(worktrees_dir); + if (!dir) { + if (errno == ENOENT) { + ret = 0; + goto out; + } + + ret = -1; + goto out; + } + + while ((d = readdir_skip_dot_and_dotdot(dir)) != NULL) + strvec_push(out, d->d_name); + + ret = 0; +out: + if (dir) + closedir(dir); + free(worktrees_dir); + return ret; +} + static int move_config_setting(const char *key, const char *value, const char *from_file, const char *to_file) { diff --git a/worktree.h b/worktree.h index e4bcccdc0a..59825c3788 100644 --- a/worktree.h +++ b/worktree.h @@ -38,6 +38,14 @@ struct worktree **get_worktrees(void); */ struct worktree **get_worktrees_without_reading_head(void); +/* + * Retrieve all worktree names. Not all names may correspond to a fully + * functional worktree. Returns 0 on success, a negative error code on failure. + * Calling the function on a repository that doesn't have any worktrees is not + * considered an error. + */ +int get_worktree_names(struct repository *repo, struct strvec *out); + /* * Returns 1 if linked worktrees exist, 0 otherwise. */ From eae6763649fe035ee30f7ae9d6f262bf0b57118f Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 30 Apr 2025 12:25:09 +0200 Subject: [PATCH 5/8] builtin/maintenance: introduce "worktree-prune" task While git-gc(1) knows to prune stale worktrees, git-maintenance(1) does not yet have a task for this cleanup. Introduce a new "worktree-prune" task to plug this gap. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- Documentation/config/maintenance.adoc | 8 +++ Documentation/git-maintenance.adoc | 4 ++ builtin/gc.c | 46 +++++++++++++++++ t/t7900-maintenance.sh | 71 +++++++++++++++++++++++++++ 4 files changed, 129 insertions(+) diff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc index 41536162a7..b36b62c1c4 100644 --- a/Documentation/config/maintenance.adoc +++ b/Documentation/config/maintenance.adoc @@ -83,3 +83,11 @@ 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. diff --git a/Documentation/git-maintenance.adoc b/Documentation/git-maintenance.adoc index 3a1e2a69b6..6f085a9cf8 100644 --- a/Documentation/git-maintenance.adoc +++ b/Documentation/git-maintenance.adoc @@ -166,6 +166,10 @@ reflog-expire:: The `reflog-expire` task deletes any entries in the reflog older than the expiry threshold. See linkgit:git-reflog[1] for more information. +worktree-prune:: + The `worktree-prune` task deletes stale or broken worktrees. See + linkit:git-worktree[1] for more information. + OPTIONS ------- --auto:: diff --git a/builtin/gc.c b/builtin/gc.c index d91b6b7b8c..a0beed8ebd 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -43,6 +43,7 @@ #include "hook.h" #include "setup.h" #include "trace2.h" +#include "worktree.h" #define FAILED_RUN "failed to run %s" @@ -345,6 +346,45 @@ static int maintenance_task_worktree_prune(struct maintenance_run_opts *opts UNU return run_command(&prune_worktrees_cmd); } +static int worktree_prune_condition(struct gc_config *cfg) +{ + struct strvec worktrees = STRVEC_INIT; + 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) + goto out; + + for (size_t i = 0; i < worktrees.nr; i++) { + char *wtpath; + + strbuf_reset(&reason); + if (should_prune_worktree(worktrees.v[i], &reason, &wtpath, expiry_date)) { + limit--; + + if (!limit) { + should_prune = 1; + goto out; + } + } + free(wtpath); + } + +out: + strvec_clear(&worktrees); + strbuf_release(&reason); + return should_prune; +} + static int too_many_loose_objects(struct gc_config *cfg) { /* @@ -1465,6 +1505,7 @@ enum maintenance_task_label { TASK_COMMIT_GRAPH, TASK_PACK_REFS, TASK_REFLOG_EXPIRE, + TASK_WORKTREE_PRUNE, /* Leave as final value */ TASK__COUNT @@ -1506,6 +1547,11 @@ static struct maintenance_task tasks[] = { maintenance_task_reflog_expire, reflog_expire_condition, }, + [TASK_WORKTREE_PRUNE] = { + "worktree-prune", + maintenance_task_worktree_prune, + worktree_prune_condition, + }, }; static int compare_tasks_by_selection(const void *a_, const void *b_) diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh index 9b82e11c10..530c56ae91 100755 --- a/t/t7900-maintenance.sh +++ b/t/t7900-maintenance.sh @@ -493,6 +493,77 @@ test_expect_success 'reflog-expire task --auto only packs when exceeding limits' test_subcommand git reflog expire --all .git/worktrees/abc && + 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 err && test_grep "at most one" err From 91b34c4529ce12d703ab147fc8f6abc55ed23dd1 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 30 Apr 2025 12:25:10 +0200 Subject: [PATCH 6/8] rerere: provide function to collect stale entries We're about to add another task for git-maintenance(1) that prunes stale rerere entries via `git rerere gc`. The condition of when to run this subcommand will be configurable so that the subcommand is only executed when a certain number of stale rerere entries exists. This requires us to know about the number of stale rerere entries in the first place, which is non-trivial to figure out. Refactor `rerere_gc()` and `prune_one()` so that garbage collection is split into three phases: 1. We collect any stale rerere entries and directories that are about to become empty. 2. Prune all stale rerere entries. 3. Remove all directories that should have become empty in (2). By splitting out the collection of stale entries we can trivially expose this function to external callers and thus reuse it in later steps. This refactoring is not expected to result in a user-visible change in behaviour. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- rerere.c | 92 +++++++++++++++++++++++++++++++++++++++----------------- rerere.h | 14 +++++++++ 2 files changed, 78 insertions(+), 28 deletions(-) diff --git a/rerere.c b/rerere.c index 3cd37c5f0a..b869370036 100644 --- a/rerere.c +++ b/rerere.c @@ -1203,8 +1203,8 @@ static void unlink_rr_item(struct rerere_id *id) strbuf_release(&buf); } -static void prune_one(struct rerere_id *id, - timestamp_t cutoff_resolve, timestamp_t cutoff_noresolve) +static int is_stale(struct rerere_id *id, + timestamp_t cutoff_resolve, timestamp_t cutoff_noresolve) { timestamp_t then; timestamp_t cutoff; @@ -1215,11 +1215,11 @@ static void prune_one(struct rerere_id *id, else { then = rerere_created_at(id); if (!then) - return; + return 0; cutoff = cutoff_noresolve; } - if (then < cutoff) - unlink_rr_item(id); + + return then < cutoff; } /* Does the basename in "path" look plausibly like an rr-cache entry? */ @@ -1230,29 +1230,35 @@ static int is_rr_cache_dirname(const char *path) return !parse_oid_hex(path, &oid, &end) && !*end; } -void rerere_gc(struct repository *r, struct string_list *rr) +int rerere_collect_stale_entries(struct repository *r, + struct string_list *prunable_dirs, + struct rerere_id **prunable_entries, + size_t *prunable_entries_nr) { - struct string_list to_remove = STRING_LIST_INIT_DUP; - DIR *dir; - struct dirent *e; - int i; timestamp_t now = time(NULL); timestamp_t cutoff_noresolve = now - 15 * 86400; timestamp_t cutoff_resolve = now - 60 * 86400; struct strbuf buf = STRBUF_INIT; + size_t prunable_entries_alloc; + struct dirent *e; + DIR *dir = NULL; + int ret; - if (setup_rerere(r, rr, 0) < 0) - return; + *prunable_entries = NULL; + *prunable_entries_nr = 0; + prunable_entries_alloc = 0; - repo_config_get_expiry_in_days(the_repository, "gc.rerereresolved", + repo_config_get_expiry_in_days(r, "gc.rerereresolved", &cutoff_resolve, now); - repo_config_get_expiry_in_days(the_repository, "gc.rerereunresolved", + repo_config_get_expiry_in_days(r, "gc.rerereunresolved", &cutoff_noresolve, now); - git_config(git_default_config, NULL); - dir = opendir(repo_git_path_replace(the_repository, &buf, "rr-cache")); - if (!dir) - die_errno(_("unable to open rr-cache directory")); - /* Collect stale conflict IDs ... */ + + dir = opendir(repo_git_path_replace(r, &buf, "rr-cache")); + if (!dir) { + ret = error_errno(_("unable to open rr-cache directory")); + goto out; + } + while ((e = readdir_skip_dot_and_dotdot(dir))) { struct rerere_dir *rr_dir; struct rerere_id id; @@ -1267,23 +1273,53 @@ void rerere_gc(struct repository *r, struct string_list *rr) for (id.variant = 0, id.collection = rr_dir; id.variant < id.collection->status_nr; id.variant++) { - prune_one(&id, cutoff_resolve, cutoff_noresolve); - if (id.collection->status[id.variant]) + if (is_stale(&id, cutoff_resolve, cutoff_noresolve)) { + ALLOC_GROW(*prunable_entries, *prunable_entries_nr + 1, + prunable_entries_alloc); + (*prunable_entries)[(*prunable_entries_nr)++] = id; + } else { now_empty = 0; + } } if (now_empty) - string_list_append(&to_remove, e->d_name); + string_list_append(prunable_dirs, e->d_name); } - closedir(dir); - /* ... and then remove the empty directories */ - for (i = 0; i < to_remove.nr; i++) - rmdir(repo_git_path_replace(the_repository, &buf, - "rr-cache/%s", to_remove.items[i].string)); + ret = 0; - string_list_clear(&to_remove, 0); +out: + strbuf_release(&buf); + if (dir) + closedir(dir); + return ret; +} + +void rerere_gc(struct repository *r, struct string_list *rr) +{ + struct string_list prunable_dirs = STRING_LIST_INIT_DUP; + struct rerere_id *prunable_entries; + struct strbuf buf = STRBUF_INIT; + size_t prunable_entries_nr; + + if (setup_rerere(r, rr, 0) < 0) + return; + + git_config(git_default_config, NULL); + + if (rerere_collect_stale_entries(r, &prunable_dirs, &prunable_entries, + &prunable_entries_nr) < 0) + exit(127); + + for (size_t i = 0; i < prunable_entries_nr; i++) + unlink_rr_item(&prunable_entries[i]); + for (size_t i = 0; i < prunable_dirs.nr; i++) + rmdir(repo_git_path_replace(r, &buf, "rr-cache/%s", + prunable_dirs.items[i].string)); + + string_list_clear(&prunable_dirs, 0); rollback_lock_file(&write_lock); strbuf_release(&buf); + free(prunable_entries); } /* diff --git a/rerere.h b/rerere.h index d4b5f7c932..fd5a2388b0 100644 --- a/rerere.h +++ b/rerere.h @@ -37,6 +37,20 @@ const char *rerere_path(struct strbuf *buf, const struct rerere_id *, int rerere_forget(struct repository *, struct pathspec *); int rerere_remaining(struct repository *, struct string_list *); void rerere_clear(struct repository *, struct string_list *); + +/* + * Collect prunable rerere entries that would be garbage collected via + * `rerere_gc()`. Whether or not an entry is prunable depends on both + * "gc.rerereResolved" and "gc.rerereUnresolved". + * + * Returns 0 on success, a negative error code in case entries could not be + * collected. + */ +int rerere_collect_stale_entries(struct repository *r, + struct string_list *prunable_dirs, + struct rerere_id **prunable_entries, + size_t *prunable_entries_nr); + void rerere_gc(struct repository *, struct string_list *); #define OPT_RERERE_AUTOUPDATE(v) OPT_UYN(0, "rerere-autoupdate", (v), \ From f7bc30f73662556c02a9dfcd36ef23fe87949c27 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 30 Apr 2025 12:25:11 +0200 Subject: [PATCH 7/8] builtin/gc: move rerere garbage collection into separate function Move garbage collection of cached rerere entries into a separate function. This prepares us for a subsequent commit where we introduce a new "rerere-gc" task for git-maintenance(1). Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/gc.c | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/builtin/gc.c b/builtin/gc.c index a0beed8ebd..83ef863f79 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -385,6 +385,15 @@ out: return should_prune; } +static int maintenance_task_rerere_gc(struct maintenance_run_opts *opts UNUSED, + struct gc_config *cfg UNUSED) +{ + struct child_process rerere_cmd = CHILD_PROCESS_INIT; + rerere_cmd.git_cmd = 1; + strvec_pushl(&rerere_cmd.args, "rerere", "gc", NULL); + return run_command(&rerere_cmd); +} + static int too_many_loose_objects(struct gc_config *cfg) { /* @@ -786,7 +795,6 @@ int cmd_gc(int argc, int daemonized = 0; int keep_largest_pack = -1; timestamp_t dummy; - struct child_process rerere_cmd = CHILD_PROCESS_INIT; struct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT; struct gc_config cfg = GC_CONFIG_INIT; const char *prune_expire_sentinel = "sentinel"; @@ -969,10 +977,8 @@ int cmd_gc(int argc, maintenance_task_worktree_prune(&opts, &cfg)) die(FAILED_RUN, "worktree"); - rerere_cmd.git_cmd = 1; - strvec_pushl(&rerere_cmd.args, "rerere", "gc", NULL); - if (run_command(&rerere_cmd)) - die(FAILED_RUN, rerere_cmd.args.v[0]); + if (maintenance_task_rerere_gc(&opts, &cfg)) + die(FAILED_RUN, "rerere"); report_garbage = report_pack_garbage; reprepare_packed_git(the_repository); From 091fc896d09f97f1dc9381fde8c3c405c61937e5 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 30 Apr 2025 12:25:12 +0200 Subject: [PATCH 8/8] builtin/maintenance: introduce "rerere-gc" task While git-gc(1) knows to garbage collect the rerere cache, git-maintenance(1) does not yet have a task for this cleanup. Introduce a new "rerere-gc" task to plug this gap. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- Documentation/config/maintenance.adoc | 8 ++++ Documentation/git-maintenance.adoc | 4 ++ builtin/gc.c | 41 ++++++++++++++++++++ t/t7900-maintenance.sh | 54 +++++++++++++++++++++++++++ 4 files changed, 107 insertions(+) diff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc index b36b62c1c4..9c333d42b1 100644 --- a/Documentation/config/maintenance.adoc +++ b/Documentation/config/maintenance.adoc @@ -84,6 +84,14 @@ 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 diff --git a/Documentation/git-maintenance.adoc b/Documentation/git-maintenance.adoc index 6f085a9cf8..931f3e02e8 100644 --- a/Documentation/git-maintenance.adoc +++ b/Documentation/git-maintenance.adoc @@ -166,6 +166,10 @@ reflog-expire:: The `reflog-expire` task deletes any entries in the reflog older than the expiry threshold. See linkgit:git-reflog[1] for more information. +rerere-gc:: + The `rerere-gc` task invokes garbage collection for stale entries in + the rerere cache. See linkgit:git-rerere[1] for more information. + worktree-prune:: The `worktree-prune` task deletes stale or broken worktrees. See linkit:git-worktree[1] for more information. diff --git a/builtin/gc.c b/builtin/gc.c index 83ef863f79..9ad225dea1 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -16,6 +16,7 @@ #include "builtin.h" #include "abspath.h" #include "date.h" +#include "dir.h" #include "environment.h" #include "hex.h" #include "config.h" @@ -33,6 +34,7 @@ #include "pack-objects.h" #include "path.h" #include "reflog.h" +#include "rerere.h" #include "blob.h" #include "tree.h" #include "promisor-remote.h" @@ -394,6 +396,39 @@ static int maintenance_task_rerere_gc(struct maintenance_run_opts *opts UNUSED, return run_command(&rerere_cmd); } +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; + 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; + + if (rerere_collect_stale_entries(the_repository, &prunable_dirs, + &prunable_entries, &prunable_entries_nr) < 0) + goto out; + + should_gc = prunable_entries_nr >= limit; + +out: + string_list_clear(&prunable_dirs, 0); + free(prunable_entries); + strbuf_release(&path); + return should_gc; +} + static int too_many_loose_objects(struct gc_config *cfg) { /* @@ -1512,6 +1547,7 @@ enum maintenance_task_label { TASK_PACK_REFS, TASK_REFLOG_EXPIRE, TASK_WORKTREE_PRUNE, + TASK_RERERE_GC, /* Leave as final value */ TASK__COUNT @@ -1558,6 +1594,11 @@ static struct maintenance_task tasks[] = { maintenance_task_worktree_prune, worktree_prune_condition, }, + [TASK_RERERE_GC] = { + "rerere-gc", + maintenance_task_rerere_gc, + rerere_gc_condition, + }, }; static int compare_tasks_by_selection(const void *a_, const void *b_) diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh index 530c56ae91..78da81eeb2 100755 --- a/t/t7900-maintenance.sh +++ b/t/t7900-maintenance.sh @@ -564,6 +564,60 @@ test_expect_success 'worktree-prune task honors gc.worktreePruneExpire' ' test_path_is_missing .git/worktrees/worktree ' +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 err && test_grep "at most one" err