threads / patch / 63382

v3, 7 partsbuiltin/gc: fix indentation of `cmd_gc()` parameters

Subject: [PATCH v3 1/7] builtin/gc: fix indentation of `cmd_gc()` parameters

## tl;dr

15 messages between May 2, 2025 and May 7, 2025. Diffs are folded; open one to read it.

replies: 14people: 4as markdown or json

Patrick Steinhardt· May 2, 2025, 08:43 UTC · lore

[PATCH v3 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1)

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.im
Changes in v3:
  - Simplify the heuristic for "rerere-gc" so that we only count the
    number of directory entries in ".git/rr-cache", without considering
    staleness.
  - Link to v2: https://lore.kernel.org/r/20250430-pks-maintenance-missing-tasks-v2-0-2580b7b8ca3a@pks.im
Thanks!
Patrick
---
Patrick Steinhardt (7):
      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
      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                          | 157 +++++++++++++++++++++++++++-------
 builtin/worktree.c                    |  25 +++---
 t/t7900-maintenance.sh                | 114 ++++++++++++++++++++++++
 worktree.c                            |  30 +++++++
 worktree.h                            |   8 ++
 7 files changed, 314 insertions(+), 44 deletions(-)
Range-diff versus v2:
1:  fe67cebba69 = 1:  ac3cd73438b builtin/gc: fix indentation of `cmd_gc()` parameters
2:  623c53de24c = 2:  662dee455b6 builtin/gc: remove global variables where it trivial to do
3:  7c6b33075ca = 3:  522d40d8da7 builtin/gc: move pruning of worktrees into a separate function
4:  423963c589e = 4:  737df0a7dcc worktree: expose function to retrieve worktree names
5:  a031d9aa9fc = 5:  8a24fb6547b builtin/maintenance: introduce "worktree-prune" task
6:  6610b72a0f5 < -:  ----------- rerere: provide function to collect stale entries
7:  4e6c461e0b6 = 6:  3ce62afb54f builtin/gc: move rerere garbage collection into separate function
8:  4763c2aa09e ! 7:  90e86a52fcc builtin/maintenance: introduce "rerere-gc" task
    @@ Documentation/config/maintenance.adoc: maintenance.reflog-expire.auto::
     +	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.
    ++	value implies the command should run when there are at least this many
    ++	directory entries in the "rr-cache" directory. The default value is 1.
     +
      maintenance.worktree-prune.auto::
      	This integer config option controls how often the `worktree-prune` task
    @@ 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;
    -+	int limit = 20;
    ++	int should_gc = 0, limit = 1;
    ++	DIR *dir = NULL;
     +
     +	git_config_get_int("maintenance.rerere-gc.auto", &limit);
     +	if (limit <= 0) {
    @@ builtin/gc.c: static int maintenance_task_rerere_gc(struct maintenance_run_opts
     +		goto out;
     +	}
     +
    -+	/* Skip garbage collecting the rerere cache in case rerere is disabled. */
    ++	/*
    ++	 * We skip garbage collection in case we either have no "rr-cache"
    ++	 * directory or when it doesn't contain at least as many directories as
    ++	 * indicated by "maintenance.rerere-gc.auto".
    ++	 */
     +	repo_git_path_replace(the_repository, &path, "rr-cache");
    -+	if (!is_directory(path.buf))
    ++	dir = opendir(path.buf);
    ++	if (!dir)
     +		goto out;
     +
    -+	if (rerere_collect_stale_entries(the_repository, &prunable_dirs,
    -+					 &prunable_entries, &prunable_entries_nr) < 0)
    -+		goto out;
    ++	while (readdir_skip_dot_and_dotdot(dir)) {
    ++		if (--limit)
    ++			continue;
     +
    -+	should_gc = prunable_entries_nr >= limit;
    ++		should_gc = 1;
    ++		goto out;
    ++	}
     +
     +out:
    -+	string_list_clear(&prunable_dirs, 0);
    -+	free(prunable_entries);
     +	strbuf_release(&path);
    ++	if (dir)
    ++		closedir(dir);
     +	return should_gc;
     +}
     +
    @@ t/t7900-maintenance.sh: test_expect_success 'worktree-prune task honors gc.workt
      	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" = "!"
    @@ t/t7900-maintenance.sh: test_expect_success 'worktree-prune task honors gc.workt
     +'
     +
     +test_expect_success 'rerere-gc task with --auto only prunes with prunable entries' '
    ++	test_when_finished "rm -rf .git/rr-cache" &&
     +	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 &&
    ++	mkdir .git/rr-cache &&
     +	test_expect_rerere_gc ! git maintenance run --auto --task=rerere-gc &&
    -+	setup_stale_rerere_entry 20 &&
    ++	: >.git/rr-cache/entry &&
     +	test_expect_rerere_gc git maintenance run --auto --task=rerere-gc
     +'
     +
     +test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.auto' '
    ++	test_when_finished "rm -rf .git/rr-cache" &&
    ++
     +	# 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 &&
    ++	mkdir .git/rr-cache &&
    ++	: >.git/rr-cache/entry-1 &&
    ++	: >.git/rr-cache/entry-2 &&
     +
     +	# 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_rerere_gc ! git -c maintenance.rerere-gc.auto=3 maintenance run --auto --task=rerere-gc &&
    ++	test_expect_rerere_gc git -c maintenance.rerere-gc.auto=2 maintenance run --auto --task=rerere-gc
     +'
     +
      test_expect_success '--auto and --schedule incompatible' '

--- base-commit: a2955b34f48265d240ab8c7deb0a929ec2d65fd0 change-id: 20250424-pks-maintenance-missing-tasks-8ffcdd596b73

Patrick Steinhardt· May 2, 2025, 08:43 UTC · re: Patrick Steinhardt · lore
The parameters of `cmd_gc()` aren't indented properly. Fix this.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/gc.c | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)
Show changes to builtin/gc.c +3 −3
diff --git a/builtin/gc.c b/builtin/gc.c
index d5c75be2522..a73ec22fb18 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -724,9 +724,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;
-- 
2.49.0.987.g0cc8ee98dc.dirty
Patrick Steinhardt· May 2, 2025, 08:43 UTC · re: Patrick Steinhardt · lore

[PATCH v3 2/7] 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 <ps@pks.im>
---
 builtin/gc.c | 31 ++++++++++++-------------------
 1 file changed, 12 insertions(+), 19 deletions(-)
Show changes to builtin/gc.c +12 −19
diff --git a/builtin/gc.c b/builtin/gc.c
index a73ec22fb18..ada36e210f0 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -53,15 +53,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)
@@ -769,9 +763,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);
 
@@ -897,34 +888,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);
-- 
2.49.0.987.g0cc8ee98dc.dirty
Patrick Steinhardt· May 2, 2025, 08:44 UTC · re: Patrick Steinhardt · lore

[PATCH v3 3/7] 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 <ps@pks.im>
---
 builtin/gc.c | 25 +++++++++++++++----------
 1 file changed, 15 insertions(+), 10 deletions(-)
Show changes to builtin/gc.c +15 −10
diff --git a/builtin/gc.c b/builtin/gc.c
index ada36e210f0..005ecc3f192 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -334,6 +334,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)
 {
 	/*
@@ -903,16 +915,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);
-- 
2.49.0.987.g0cc8ee98dc.dirty
Patrick Steinhardt· May 2, 2025, 08:44 UTC · re: Patrick Steinhardt · lore

[PATCH v3 4/7] 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 <ps@pks.im>
---
 builtin/worktree.c | 25 ++++++++++++-------------
 worktree.c         | 30 ++++++++++++++++++++++++++++++
 worktree.h         |  8 ++++++++
 3 files changed, 50 insertions(+), 13 deletions(-)
Show changes to 3 files +50 −13

builtin/worktree.c, worktree.c, worktree.h

diff --git a/builtin/worktree.c b/builtin/worktree.c
index 87ccd47794c..9b00dbf1265 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 c34b9eb74e5..947b7a82209 100644
--- a/worktree.c
+++ b/worktree.c
@@ -988,6 +988,36 @@ int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath,
 	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 e4bcccdc0ae..59825c37881 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.
  */
-- 
2.49.0.987.g0cc8ee98dc.dirty
Eric Sunshine· May 5, 2025, 08:42 UTC · re: Patrick Steinhardt · lore

Re: [PATCH v3 4/7] worktree: expose function to retrieve worktree names

On Fri, May 2, 2025 at 4:44 AM Patrick Steinhardt <ps@pks.im> wrote:
> 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 <ps@pks.im>

I'm not convinced that this patch or the get_worktree_names() function which it adds to worktree.[hc] adds value. Aside from the mere act of consulting the directory at repo_git_path(r, "worktrees"), there is nothing about the function at all related to worktrees. It doesn't make any guarantees, such as only returning entries which at least superficially look like worktree-metadata directories, or perform any sort of validation. I don't see how this is any better than the caller just implementing its own bog-standard opendir() / readdir()-loop / closedir() over repo_git_path(r, "worktrees"). Or, if you don't want the caller to implement its own readdir()-loop, I wouldn't be surprised if we already have a function which does exactly this for a provided path, though I haven't checked. If there isn't such a generic function, perhaps it makes more sense to add one and call it with repo_git_path(r, "worktrees") as its input?

Show 26 quoted lines
> diff --git a/worktree.c b/worktree.c
> @@ -988,6 +988,36 @@ int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath,
> +int get_worktree_names(struct repository *repo, struct strvec *out)
> +{
> +       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;
> +}

It's subjective, but although we often recommend the `goto` approach on this project to ensure proper cleanup, the above seems to place an unnecessarily high cognitive load on the reader. I would think that the following straightforward `goto`-free approach would suffice:

    worktrees_dir = repo_git_path(repo, "worktrees");
    dir = opendir(worktrees_dir);
    FREE_AND_NULL(worktrees_dir);
    if (!dir)
        return errno == ENOENT ? 0 : -1;
    while ((d = readdir_skip_dot_and_dotdot(dir)) != NULL)
        strvec_push(out, d->d_name);
    closedir(dir);
    return 0
Show 9 quoted lines
> diff --git a/worktree.h b/worktree.h
> @@ -38,6 +38,14 @@ struct worktree **get_worktrees(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);

As I was reading through the patch, I was worried that you might have overlooked the fact that the names returned by the function might not be fully functional worktrees or that the caller of this function might not realize that, so I'm glad to see that you documented this potential downside.

In fact, there is no guarantee that the returned entries are even directories; they could be anything that someone happened to create in the .git/worktrees directory, such as plain files, special files, etc. So, aside from the objections I wrote above, the term "names" here is potentially unclear and misleading, as used both in the documentation and the function name itself, and the fact that I can't come up with a better term further leads me to believe that this is not a function we really want to be publishing.

Patrick Steinhardt· May 7, 2025, 07:06 UTC · re: Eric Sunshine · lore

Re: [PATCH v3 4/7] worktree: expose function to retrieve worktree names

On Mon, May 05, 2025 at 04:42:40AM -0400, Eric Sunshine wrote:
Show 20 quoted lines
> On Fri, May 2, 2025 at 4:44 AM Patrick Steinhardt <ps@pks.im> wrote:
> > 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 <ps@pks.im>
> 
> I'm not convinced that this patch or the get_worktree_names() function
> which it adds to worktree.[hc] adds value. Aside from the mere act of
> consulting the directory at repo_git_path(r, "worktrees"), there is
> nothing about the function at all related to worktrees. It doesn't
> make any guarantees, such as only returning entries which at least
> superficially look like worktree-metadata directories, or perform any
> sort of validation. I don't see how this is any better than the caller
> just implementing its own bog-standard opendir() / readdir()-loop /
> closedir() over repo_git_path(r, "worktrees"). Or, if you don't want
> the caller to implement its own readdir()-loop, I wouldn't be
> surprised if we already have a function which does exactly this for a
> provided path, though I haven't checked. If there isn't such a generic
> function, perhaps it makes more sense to add one and call it with
> repo_git_path(r, "worktrees") as its input?

I think this is fair criticism indeed, and open-coding this isn't even any more complex, either, as shown by the below patch. I'll drop this patch.

Patrick
Show changes to builtin/gc.c +15 −11
diff --git a/builtin/gc.c b/builtin/gc.c
index 82f0fac81a4..eb4469b7858 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -351,11 +351,11 @@ static int maintenance_task_worktree_prune(struct maintenance_run_opts *opts UNU
 
 static int worktree_prune_condition(struct gc_config *cfg)
 {
-	struct strvec worktrees = STRVEC_INIT;
-	struct strbuf reason = STRBUF_INIT;
+	struct strbuf buf = STRBUF_INIT;
+	int should_prune = 0, limit = 1;
 	timestamp_t expiry_date;
-	int should_prune = 0;
-	int limit = 1;
+	struct dirent *d;
+	DIR *dir = NULL;
 
 	git_config_get_int("maintenance.worktree-prune.auto", &limit);
 	if (limit <= 0) {
@@ -363,15 +363,18 @@ static int worktree_prune_condition(struct gc_config *cfg)
 		goto out;
 	}
 
-	if (parse_expiry_date(cfg->prune_worktrees_expire, &expiry_date) ||
-	    get_worktree_names(the_repository, &worktrees) < 0)
+	if (parse_expiry_date(cfg->prune_worktrees_expire, &expiry_date))
+		goto out;
+
+	dir = opendir(repo_git_path_replace(the_repository, &buf, "worktrees"));
+	if (!dir)
 		goto out;
 
-	for (size_t i = 0; i < worktrees.nr; i++) {
+	while ((d = readdir_skip_dot_and_dotdot(dir))) {
 		char *wtpath;
 
-		strbuf_reset(&reason);
-		if (should_prune_worktree(worktrees.v[i], &reason, &wtpath, expiry_date)) {
+		strbuf_reset(&buf);
+		if (should_prune_worktree(d->d_name, &buf, &wtpath, expiry_date)) {
 			limit--;
 
 			if (!limit) {
@@ -384,8 +387,9 @@ static int worktree_prune_condition(struct gc_config *cfg)
 	}
 
 out:
-	strvec_clear(&worktrees);
-	strbuf_release(&reason);
+	if (dir)
+		closedir(dir);
+	strbuf_release(&buf);
 	return should_prune;
 }
Patrick Steinhardt· May 2, 2025, 08:44 UTC · re: Patrick Steinhardt · lore

[PATCH v3 5/7] 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 <ps@pks.im>
---
 Documentation/config/maintenance.adoc |  8 ++++
 Documentation/git-maintenance.adoc    |  4 ++
 builtin/gc.c                          | 46 +++++++++++++++++++++++
 t/t7900-maintenance.sh                | 71 +++++++++++++++++++++++++++++++++++
 4 files changed, 129 insertions(+)
Show changes to 4 files +129 −0

Documentation/config/maintenance.adoc, Documentation/git-maintenance.adoc, builtin/gc.c, t/t7900-maintenance.sh

diff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc
index 41536162a77..b36b62c1c47 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 3a1e2a69b6b..6f085a9cf8c 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 005ecc3f192..93a8c856dbc 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -44,6 +44,7 @@
 #include "hook.h"
 #include "setup.h"
 #include "trace2.h"
+#include "worktree.h"
 
 #define FAILED_RUN "failed to run %s"
 
@@ -346,6 +347,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)
 {
 	/*
@@ -1455,6 +1495,7 @@ enum maintenance_task_label {
 	TASK_COMMIT_GRAPH,
 	TASK_PACK_REFS,
 	TASK_REFLOG_EXPIRE,
+	TASK_WORKTREE_PRUNE,
 
 	/* Leave as final value */
 	TASK__COUNT
@@ -1496,6 +1537,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 9b82e11c100..530c56ae91e 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 <reflog-expire-auto.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' '
+	test_expect_worktree_prune ! git maintenance run --auto --task=worktree-prune &&
+	mkdir .git/worktrees &&
+	: >.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 <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' '
 	test_must_fail git maintenance run --auto --schedule=daily 2>err &&
 	test_grep "at most one" err
-- 
2.49.0.987.g0cc8ee98dc.dirty
Eric Sunshine· May 5, 2025, 08:59 UTC · re: Patrick Steinhardt · lore

Re: [PATCH v3 5/7] builtin/maintenance: introduce "worktree-prune" task

On Fri, May 2, 2025 at 4:44 AM Patrick Steinhardt <ps@pks.im> wrote:
Show 25 quoted lines
> 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 <ps@pks.im>
> ---
> diff --git a/builtin/gc.c b/builtin/gc.c
> @@ -346,6 +347,45 @@ static int maintenance_task_worktree_prune(struct maintenance_run_opts *opts UNU
> +static int worktree_prune_condition(struct gc_config *cfg)
> +{
> + [...]
> +       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)) {

As I was reading the commit message, I had concerns that if this implementation was rolling its own pruning logic, it might overlook conditions such as a worktree being locked, so I'm glad to see that you're using should_prune_worktree() here rather than reinventing the wheel.

Show 8 quoted lines
> +                       limit--;
> +
> +                       if (!limit) {
> +                               should_prune = 1;
> +                               goto out;
> +                       }
> +               }
> +               free(wtpath);
This leaks `wtpath` when it takes the `goto out` arm.
Show 21 quoted lines
> +       }
> +
> +out:
> +       strvec_clear(&worktrees);
> +       strbuf_release(&reason);
> +       return should_prune;
> +}
> diff --git 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_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.
s/man/many/
Patrick Steinhardt· May 7, 2025, 07:06 UTC · re: Eric Sunshine · lore

Re: [PATCH v3 5/7] builtin/maintenance: introduce "worktree-prune" task

On Mon, May 05, 2025 at 04:59:14AM -0400, Eric Sunshine wrote:
Show 11 quoted lines
> On Fri, May 2, 2025 at 4:44 AM Patrick Steinhardt <ps@pks.im> wrote:
> > +                       limit--;
> > +
> > +                       if (!limit) {
> > +                               should_prune = 1;
> > +                               goto out;
> > +                       }
> > +               }
> > +               free(wtpath);
> 
> This leaks `wtpath` when it takes the `goto out` arm.
Good catch, fixed now.
Show 23 quoted lines
> > +       }
> > +
> > +out:
> > +       strvec_clear(&worktrees);
> > +       strbuf_release(&reason);
> > +       return should_prune;
> > +}
> > diff --git 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_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.
> 
> s/man/many/
Fixed, as well.
Patrick
Patrick Steinhardt· May 2, 2025, 08:44 UTC · re: Patrick Steinhardt · lore

[PATCH v3 6/7] 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 <ps@pks.im>
---
 builtin/gc.c | 16 +++++++++++-----
 1 file changed, 11 insertions(+), 5 deletions(-)
Show changes to builtin/gc.c +11 −5
diff --git a/builtin/gc.c b/builtin/gc.c
index 93a8c856dbc..3dd1d07cca4 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -386,6 +386,15 @@ static int worktree_prune_condition(struct gc_config *cfg)
 	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)
 {
 	/*
@@ -782,7 +791,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";
@@ -959,10 +967,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);
-- 
2.49.0.987.g0cc8ee98dc.dirty
Patrick Steinhardt· May 2, 2025, 08:44 UTC · re: Patrick Steinhardt · lore

[PATCH v3 7/7] 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 <ps@pks.im>
---
 Documentation/config/maintenance.adoc |  8 +++++++
 Documentation/git-maintenance.adoc    |  4 ++++
 builtin/gc.c                          | 45 +++++++++++++++++++++++++++++++++++
 t/t7900-maintenance.sh                | 43 +++++++++++++++++++++++++++++++++
 4 files changed, 100 insertions(+)
Show changes to 4 files +100 −0

Documentation/config/maintenance.adoc, Documentation/git-maintenance.adoc, builtin/gc.c, t/t7900-maintenance.sh

diff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc
index b36b62c1c47..2e8299dd15a 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 there are at least this many
+	directory entries in the "rr-cache" directory. The default value is 1.
+
 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 6f085a9cf8c..931f3e02e85 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 3dd1d07cca4..7bba826c496 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"
@@ -34,6 +35,7 @@
 #include "pack-objects.h"
 #include "path.h"
 #include "reflog.h"
+#include "rerere.h"
 #include "blob.h"
 #include "tree.h"
 #include "promisor-remote.h"
@@ -395,6 +397,43 @@ 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;
+	int should_gc = 0, limit = 1;
+	DIR *dir = NULL;
+
+	git_config_get_int("maintenance.rerere-gc.auto", &limit);
+	if (limit <= 0) {
+		should_gc = limit < 0;
+		goto out;
+	}
+
+	/*
+	 * We skip garbage collection in case we either have no "rr-cache"
+	 * directory or when it doesn't contain at least as many directories as
+	 * indicated by "maintenance.rerere-gc.auto".
+	 */
+	repo_git_path_replace(the_repository, &path, "rr-cache");
+	dir = opendir(path.buf);
+	if (!dir)
+		goto out;
+
+	while (readdir_skip_dot_and_dotdot(dir)) {
+		if (--limit)
+			continue;
+
+		should_gc = 1;
+		goto out;
+	}
+
+out:
+	strbuf_release(&path);
+	if (dir)
+		closedir(dir);
+	return should_gc;
+}
+
 static int too_many_loose_objects(struct gc_config *cfg)
 {
 	/*
@@ -1502,6 +1541,7 @@ enum maintenance_task_label {
 	TASK_PACK_REFS,
 	TASK_REFLOG_EXPIRE,
 	TASK_WORKTREE_PRUNE,
+	TASK_RERERE_GC,
 
 	/* Leave as final value */
 	TASK__COUNT
@@ -1548,6 +1588,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 530c56ae91e..f34322de320 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -564,6 +564,49 @@ test_expect_success 'worktree-prune task honors gc.worktreePruneExpire' '
 	test_path_is_missing .git/worktrees/worktree
 '
 
+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 with --auto only prunes with prunable entries' '
+	test_when_finished "rm -rf .git/rr-cache" &&
+	test_expect_rerere_gc ! git maintenance run --auto --task=rerere-gc &&
+	mkdir .git/rr-cache &&
+	test_expect_rerere_gc ! git maintenance run --auto --task=rerere-gc &&
+	: >.git/rr-cache/entry &&
+	test_expect_rerere_gc git maintenance run --auto --task=rerere-gc
+'
+
+test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.auto' '
+	test_when_finished "rm -rf .git/rr-cache" &&
+
+	# A negative value should always prune.
+	test_expect_rerere_gc git -c maintenance.rerere-gc.auto=-1 maintenance run --auto --task=rerere-gc &&
+
+	mkdir .git/rr-cache &&
+	: >.git/rr-cache/entry-1 &&
+	: >.git/rr-cache/entry-2 &&
+
+	# 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=3 maintenance run --auto --task=rerere-gc &&
+	test_expect_rerere_gc git -c maintenance.rerere-gc.auto=2 maintenance run --auto --task=rerere-gc
+'
+
 test_expect_success '--auto and --schedule incompatible' '
 	test_must_fail git maintenance run --auto --schedule=daily 2>err &&
 	test_grep "at most one" err
-- 
2.49.0.987.g0cc8ee98dc.dirty
Derrick Stolee· May 2, 2025, 14:57 UTC · re: Patrick Steinhardt · lore

Re: [PATCH v3 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1)

On 5/2/2025 4:43 AM, Patrick Steinhardt wrote:
> Changes in v3:
>   - Simplify the heuristic for "rerere-gc" so that we only count the
>     number of directory entries in ".git/rr-cache", without considering
>     staleness.

The range-diff was harder to read than just re-reviewing patch 7, but I think this v3 is ready to go.

Thanks, -Stolee

Junio C Hamano· May 2, 2025, 21:07 UTC · re: Derrick Stolee · lore

Re: [PATCH v3 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1)

Derrick Stolee <stolee@gmail.com> writes:
Show 9 quoted lines
> On 5/2/2025 4:43 AM, Patrick Steinhardt wrote:
>
>> Changes in v3:
>>   - Simplify the heuristic for "rerere-gc" so that we only count the
>>     number of directory entries in ".git/rr-cache", without considering
>>     staleness.
>
> The range-diff was harder to read than just re-reviewing patch 7, but I
> think this v3 is ready to go.

I still do not think "configurable 'rerere gc' basing the decision on the number of existing rerere entries" adds negative value to the system. If there is truly more than N active rerere entries currently in your repository with your workflow, such a configuration essentially decides to run 'rerere gc' every time (and without pruning enough entries to make the next 'rerere gc' unnecessary), and never otherwise. It is not like pruning unneeded loose object files and packing the rest into a packfile, where running it once (even if it resulted in miniscule packfile due to misidentification) would remove all the loose object files, which makes 'repack' unneeded for some time until we accumulate more of them. After 'rerere gc', you still will have rerere entries kept.

Until we can implement a meaningful automation (which may require changing the way rerere entries are stored on disk to help us cheaply tell if there truly are excessive number of no longer relevant rerere entries; code that we can readily borrow from "rerere gc" is enough, as I said), I do not think we should add extra code to make such a useless decision. Instead, I would prefer to see a single "do we or do we not run `rerere gc`?" Boolean, until that happens.

Other than that, I think the series is a good addition to the system. Giving finer grained control is great, and 'git maintenance' is a much better framework than 'git gc' to do so.

Thanks, all.
Patrick Steinhardt· May 5, 2025, 07:32 UTC · re: Junio C Hamano · lore

Re: [PATCH v3 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1)

On Fri, May 02, 2025 at 02:07:28PM -0700, Junio C Hamano wrote:
Show 37 quoted lines
> Derrick Stolee <stolee@gmail.com> writes:
> 
> > On 5/2/2025 4:43 AM, Patrick Steinhardt wrote:
> >
> >> Changes in v3:
> >>   - Simplify the heuristic for "rerere-gc" so that we only count the
> >>     number of directory entries in ".git/rr-cache", without considering
> >>     staleness.
> >
> > The range-diff was harder to read than just re-reviewing patch 7, but I
> > think this v3 is ready to go.
> 
> I still do not think "configurable 'rerere gc' basing the decision
> on the number of existing rerere entries" adds negative value to the
> system.  If there is truly more than N active rerere entries
> currently in your repository with your workflow, such a
> configuration essentially decides to run 'rerere gc' every time (and
> without pruning enough entries to make the next 'rerere gc'
> unnecessary), and never otherwise.  It is not like pruning unneeded
> loose object files and packing the rest into a packfile, where
> running it once (even if it resulted in miniscule packfile due to
> misidentification) would remove all the loose object files, which
> makes 'repack' unneeded for some time until we accumulate more of
> them.  After 'rerere gc', you still will have rerere entries kept.
> 
> Until we can implement a meaningful automation (which may require
> changing the way rerere entries are stored on disk to help us
> cheaply tell if there truly are excessive number of no longer
> relevant rerere entries; code that we can readily borrow from
> "rerere gc" is enough, as I said), I do not think we should add
> extra code to make such a useless decision.  Instead, I would prefer
> to see a single "do we or do we not run `rerere gc`?" Boolean, until
> that happens.
> 
> Other than that, I think the series is a good addition to the
> system.  Giving finer grained control is great, and 'git maintenance'
> is a much better framework than 'git gc' to do so.

Ok, fair enough. I'll stick with the "maintenance.rerere-gc.auto" config as integer, but will adapt it so that any positive value will cause us to invoke `git rerere gc` when the "rr-cache" directory exists and has at least one entry.

The reason I want to keep it as an integer is mostly to stay consistent with all the other "maintenance.*.auto" settings, even though it does not add a lot of value right now.

Patrick

← back to recent threads