git.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1)
@ 2025-04-25  7:29 Patrick Steinhardt
  2025-04-25  7:29 ` [PATCH 1/7] builtin/gc: fix indentation of `cmd_gc()` parameters Patrick Steinhardt
                   ` (11 more replies)
  0 siblings, 12 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-04-25  7:29 UTC (permalink / raw)
  To: git

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.

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/git-maintenance.adoc |   8 +++
 builtin/gc.c                       | 128 ++++++++++++++++++++++++++++---------
 builtin/worktree.c                 |  25 ++++----
 t/t7900-maintenance.sh             |  34 ++++++++++
 worktree.c                         |  30 +++++++++
 worktree.h                         |   8 +++
 6 files changed, 189 insertions(+), 44 deletions(-)


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


^ permalink raw reply	[flat|nested] 66+ messages in thread

* [PATCH 1/7] builtin/gc: fix indentation of `cmd_gc()` parameters
  2025-04-25  7:29 [PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Patrick Steinhardt
@ 2025-04-25  7:29 ` Patrick Steinhardt
  2025-04-25  7:29 ` [PATCH 2/7] builtin/gc: remove global variables where it trivial to do Patrick Steinhardt
                   ` (10 subsequent siblings)
  11 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-04-25  7:29 UTC (permalink / raw)
  To: git

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(-)

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.901.g37484f566f.dirty


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH 2/7] builtin/gc: remove global variables where it trivial to do
  2025-04-25  7:29 [PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Patrick Steinhardt
  2025-04-25  7:29 ` [PATCH 1/7] builtin/gc: fix indentation of `cmd_gc()` parameters Patrick Steinhardt
@ 2025-04-25  7:29 ` Patrick Steinhardt
  2025-04-25  7:29 ` [PATCH 3/7] builtin/gc: move pruning of worktrees into a separate function Patrick Steinhardt
                   ` (9 subsequent siblings)
  11 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-04-25  7:29 UTC (permalink / raw)
  To: git

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(-)

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.901.g37484f566f.dirty


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH 3/7] builtin/gc: move pruning of worktrees into a separate function
  2025-04-25  7:29 [PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Patrick Steinhardt
  2025-04-25  7:29 ` [PATCH 1/7] builtin/gc: fix indentation of `cmd_gc()` parameters Patrick Steinhardt
  2025-04-25  7:29 ` [PATCH 2/7] builtin/gc: remove global variables where it trivial to do Patrick Steinhardt
@ 2025-04-25  7:29 ` Patrick Steinhardt
  2025-04-25  7:29 ` [PATCH 4/7] worktree: expose function to retrieve worktree names Patrick Steinhardt
                   ` (8 subsequent siblings)
  11 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-04-25  7:29 UTC (permalink / raw)
  To: git

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(-)

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.901.g37484f566f.dirty


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH 4/7] worktree: expose function to retrieve worktree names
  2025-04-25  7:29 [PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Patrick Steinhardt
                   ` (2 preceding siblings ...)
  2025-04-25  7:29 ` [PATCH 3/7] builtin/gc: move pruning of worktrees into a separate function Patrick Steinhardt
@ 2025-04-25  7:29 ` Patrick Steinhardt
  2025-04-25  7:29 ` [PATCH 5/7] builtin/maintenance: introduce "worktree-prune" task Patrick Steinhardt
                   ` (7 subsequent siblings)
  11 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-04-25  7:29 UTC (permalink / raw)
  To: git

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(-)

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.901.g37484f566f.dirty


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH 5/7] builtin/maintenance: introduce "worktree-prune" task
  2025-04-25  7:29 [PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Patrick Steinhardt
                   ` (3 preceding siblings ...)
  2025-04-25  7:29 ` [PATCH 4/7] worktree: expose function to retrieve worktree names Patrick Steinhardt
@ 2025-04-25  7:29 ` Patrick Steinhardt
  2025-04-29 20:02   ` Derrick Stolee
  2025-04-25  7:29 ` [PATCH 6/7] builtin/gc: move rerere garbage collection into separate function Patrick Steinhardt
                   ` (6 subsequent siblings)
  11 siblings, 1 reply; 66+ messages in thread
From: Patrick Steinhardt @ 2025-04-25  7:29 UTC (permalink / raw)
  To: git

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/git-maintenance.adoc |  4 ++++
 builtin/gc.c                       | 35 +++++++++++++++++++++++++++++++++++
 t/t7900-maintenance.sh             | 17 +++++++++++++++++
 3 files changed, 56 insertions(+)

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..46c64c852dc 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,34 @@ 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;
+
+	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)) {
+			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 +1484,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 +1526,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..d21feda271f 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -493,6 +493,23 @@ test_expect_success 'reflog-expire task --auto only packs when exceeding limits'
 	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_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 &&
+	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_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.901.g37484f566f.dirty


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH 6/7] builtin/gc: move rerere garbage collection into separate function
  2025-04-25  7:29 [PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Patrick Steinhardt
                   ` (4 preceding siblings ...)
  2025-04-25  7:29 ` [PATCH 5/7] builtin/maintenance: introduce "worktree-prune" task Patrick Steinhardt
@ 2025-04-25  7:29 ` Patrick Steinhardt
  2025-04-25  7:29 ` [PATCH 7/7] builtin/maintenance: introduce "rerere-gc" task Patrick Steinhardt
                   ` (5 subsequent siblings)
  11 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-04-25  7:29 UTC (permalink / raw)
  To: git

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(-)

diff --git a/builtin/gc.c b/builtin/gc.c
index 46c64c852dc..a0816bcf302 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -375,6 +375,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)
 {
 	/*
@@ -771,7 +780,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";
@@ -948,10 +956,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.901.g37484f566f.dirty


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH 7/7] builtin/maintenance: introduce "rerere-gc" task
  2025-04-25  7:29 [PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Patrick Steinhardt
                   ` (5 preceding siblings ...)
  2025-04-25  7:29 ` [PATCH 6/7] builtin/gc: move rerere garbage collection into separate function Patrick Steinhardt
@ 2025-04-25  7:29 ` Patrick Steinhardt
  2025-04-29 20:02 ` [PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Derrick Stolee
                   ` (4 subsequent siblings)
  11 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-04-25  7:29 UTC (permalink / raw)
  To: git

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/git-maintenance.adoc |  4 ++++
 builtin/gc.c                       | 27 +++++++++++++++++++++++++++
 t/t7900-maintenance.sh             | 17 +++++++++++++++++
 3 files changed, 48 insertions(+)

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 a0816bcf302..d19449b33d4 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"
@@ -384,6 +385,26 @@ 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;
+	DIR *dir;
+
+	/* Skip garbage collecting the rerere cache in case rerere is disabled. */
+	repo_git_path_replace(the_repository, &path, "rr-cache");
+
+	dir = opendir(path.buf);
+	if (!dir)
+		goto out;
+	should_gc = !!readdir_skip_dot_and_dotdot(dir);
+
+out:
+	strbuf_release(&path);
+	closedir(dir);
+	return should_gc;
+}
+
 static int too_many_loose_objects(struct gc_config *cfg)
 {
 	/*
@@ -1491,6 +1512,7 @@ enum maintenance_task_label {
 	TASK_PACK_REFS,
 	TASK_REFLOG_EXPIRE,
 	TASK_WORKTREE_PRUNE,
+	TASK_RERERE_GC,
 
 	/* Leave as final value */
 	TASK__COUNT
@@ -1537,6 +1559,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 d21feda271f..9cc52f28ca4 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -510,6 +510,23 @@ 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
 '
 
+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
+'
+
+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 '--auto and --schedule incompatible' '
 	test_must_fail git maintenance run --auto --schedule=daily 2>err &&
 	test_grep "at most one" err

-- 
2.49.0.901.g37484f566f.dirty


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* Re: [PATCH 5/7] builtin/maintenance: introduce "worktree-prune" task
  2025-04-25  7:29 ` [PATCH 5/7] builtin/maintenance: introduce "worktree-prune" task Patrick Steinhardt
@ 2025-04-29 20:02   ` Derrick Stolee
  2025-04-30  7:08     ` Patrick Steinhardt
  0 siblings, 1 reply; 66+ messages in thread
From: Derrick Stolee @ 2025-04-29 20:02 UTC (permalink / raw)
  To: Patrick Steinhardt, git

On 4/25/2025 3:29 AM, Patrick Steinhardt wrote:
> 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.

I initially thought that this could merge down into patch 3 (move pruning
of worktrees into a separate function), but...

> +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;
> +
> +	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)) {
> +			should_prune = 1;
> +			goto out;
> +		}
> +		free(wtpath);
> +	}
> +
> +out:
> +	strvec_clear(&worktrees);
> +	strbuf_release(&reason);
> +	return should_prune;
> +}
> +

...this implementation is new and nice to have in a separate patch. I
initially wondered if this condition needed to exist in the maintenance
builtin or could be relied upon by the 'git worktree prune' command that
is called by this implementation.

If we are trying to match the behavior of 'git gc --auto', then it was
running 'git worktree prune --expire...' every time that the generic
--auto condition was satisfied. But when 'git maintenance run --auto' is
executed, each task is checked to see if it should run. If we can avoid a
child process startup, then that is very valuable (especially on Windows
where process creation is expensive).

So I think this is a good approach. Similar thoughts apply to patch 7. No
code change is needed.

> +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_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 &&
> +	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_success '--auto and --schedule incompatible' '
>  	test_must_fail git maintenance run --auto --schedule=daily 2>err &&
>  	test_grep "at most one" err

It may be good to double-check that the gc.worktreePruneExpire config value
is being used here, especially since the prune condition is operating on
that value.

Thanks,
-Stolee

^ permalink raw reply	[flat|nested] 66+ messages in thread

* Re: [PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1)
  2025-04-25  7:29 [PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Patrick Steinhardt
                   ` (6 preceding siblings ...)
  2025-04-25  7:29 ` [PATCH 7/7] builtin/maintenance: introduce "rerere-gc" task Patrick Steinhardt
@ 2025-04-29 20:02 ` Derrick Stolee
  2025-04-30  7:08   ` Patrick Steinhardt
  2025-04-30 10:25 ` [PATCH v2 0/8] " Patrick Steinhardt
                   ` (3 subsequent siblings)
  11 siblings, 1 reply; 66+ messages in thread
From: Derrick Stolee @ 2025-04-29 20:02 UTC (permalink / raw)
  To: Patrick Steinhardt, git

On 4/25/2025 3:29 AM, Patrick Steinhardt wrote:

> 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.

Thanks for putting this together. I think this is a noble goal, allowing
users and system administrators more options to fine-tune the best ways to
optimize their repos.

I wonder if any of these fine-grained steps would be valuable to add to
the default background maintenance schedule (perhaps as a follow-up)?

> Patrick Steinhardt (7):
>       builtin/gc: fix indentation of `cmd_gc()` parameters
>       builtin/gc: remove global variables where it trivial to do

These first two patches are simple cleanups. Thanks for isolating them.

>       builtin/gc: move pruning of worktrees into a separate function

This is a nice refactor with a clean method body extraction.

>       worktree: expose function to retrieve worktree names

This one is a bit messier, but still really good as it makes the
prune_worktrees() method in builtin/worktree.c less complicated. There's
just no way to make the removal of those variables look clean.

>       builtin/maintenance: introduce "worktree-prune" task

This is where we start getting into new behavior. More comments on the
patch itself.

>       builtin/gc: move rerere garbage collection into separate function
>       builtin/maintenance: introduce "rerere-gc" task

These are a nice one-two punch for this new task.

Thanks,
-Stolee

^ permalink raw reply	[flat|nested] 66+ messages in thread

* Re: [PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1)
  2025-04-29 20:02 ` [PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Derrick Stolee
@ 2025-04-30  7:08   ` Patrick Steinhardt
  0 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-04-30  7:08 UTC (permalink / raw)
  To: Derrick Stolee; +Cc: git

On Tue, Apr 29, 2025 at 04:02:18PM -0400, Derrick Stolee wrote:
> On 4/25/2025 3:29 AM, Patrick Steinhardt wrote:
> 
> > 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.
> 
> Thanks for putting this together. I think this is a noble goal, allowing
> users and system administrators more options to fine-tune the best ways to
> optimize their repos.
> 
> I wonder if any of these fine-grained steps would be valuable to add to
> the default background maintenance schedule (perhaps as a follow-up)?

I think so, yes. My immediate goal is to change `git maintenance run
--auto` to not run git-gc(1) by default anymore, but to instead have it
run the fine-grained steps. But we should probably at the same time also
adapt the background maintenance to use the same fine-grained tasks so
that the outcome matches.

My idea would be that we implement all of this via high-level strategies
that get honored by scheduled, auto and explicit maintenance alike. We
already have the "incremental" strategy, but there are other strategies
that might make sense to introduce. There should be at least one
strategy that achieves the same as git-gc(1) does right now, but there's
probably more opportunities here to have e.g. a "geometric" strategy.

Users can then pick whatever strategy works best for them, with us
providing a good default as well as rationale why we recommend one
strategy over another. Furthermore, expert users can of course tweak
these strategies even further by explicitly configuring which of the
fine-grained tasks should run.

Thinking ahead a bit, one might even be prompted to auto-select
strategies based on repository properties. A tiny repository likely does
not need multi-pack indices and cruft packs, but a huge behemoth like
the Chromium repository probably would benefit. But that is an iteration
that one can think about in the future.

Patrick

^ permalink raw reply	[flat|nested] 66+ messages in thread

* Re: [PATCH 5/7] builtin/maintenance: introduce "worktree-prune" task
  2025-04-29 20:02   ` Derrick Stolee
@ 2025-04-30  7:08     ` Patrick Steinhardt
  0 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-04-30  7:08 UTC (permalink / raw)
  To: Derrick Stolee; +Cc: git

On Tue, Apr 29, 2025 at 04:02:03PM -0400, Derrick Stolee wrote:
> On 4/25/2025 3:29 AM, Patrick Steinhardt wrote:
> > 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.
> 
> I initially thought that this could merge down into patch 3 (move pruning
> of worktrees into a separate function), but...
> 
> > +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;
> > +
> > +	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)) {
> > +			should_prune = 1;
> > +			goto out;
> > +		}
> > +		free(wtpath);
> > +	}
> > +
> > +out:
> > +	strvec_clear(&worktrees);
> > +	strbuf_release(&reason);
> > +	return should_prune;
> > +}
> > +
> 
> ...this implementation is new and nice to have in a separate patch. I
> initially wondered if this condition needed to exist in the maintenance
> builtin or could be relied upon by the 'git worktree prune' command that
> is called by this implementation.
> 
> If we are trying to match the behavior of 'git gc --auto', then it was
> running 'git worktree prune --expire...' every time that the generic
> --auto condition was satisfied. But when 'git maintenance run --auto' is
> executed, each task is checked to see if it should run. If we can avoid a
> child process startup, then that is very valuable (especially on Windows
> where process creation is expensive).

Yup, exactly. In theory, we could even make the condition configurable
via "maintenance.worktree-prune.auto" so that we treat it as a limit of
how many worktrees need to be prunable before we execute `git worktree
prune`. Maybe I'll do just that in the next iteration.

> So I think this is a good approach. Similar thoughts apply to patch 7. No
> code change is needed.
> 
> > +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_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 &&
> > +	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_success '--auto and --schedule incompatible' '
> >  	test_must_fail git maintenance run --auto --schedule=daily 2>err &&
> >  	test_grep "at most one" err
> 
> It may be good to double-check that the gc.worktreePruneExpire config value
> is being used here, especially since the prune condition is operating on
> that value.

Fair, will do.

Patrick

^ permalink raw reply	[flat|nested] 66+ messages in thread

* [PATCH v2 0/8] builtin/maintenance: implement missing tasks compared to git-gc(1)
  2025-04-25  7:29 [PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Patrick Steinhardt
                   ` (7 preceding siblings ...)
  2025-04-29 20:02 ` [PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Derrick Stolee
@ 2025-04-30 10:25 ` Patrick Steinhardt
  2025-04-30 10:25   ` [PATCH v2 1/8] builtin/gc: fix indentation of `cmd_gc()` parameters Patrick Steinhardt
                     ` (8 more replies)
  2025-05-02  8:43 ` [PATCH v3 0/7] " Patrick Steinhardt
                   ` (2 subsequent siblings)
  11 siblings, 9 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-04-30 10:25 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee

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

Thanks!

Patrick

---
Patrick Steinhardt (8):
      builtin/gc: fix indentation of `cmd_gc()` parameters
      builtin/gc: remove global variables where it trivial to do
      builtin/gc: move pruning of worktrees into a separate function
      worktree: expose function to retrieve worktree names
      builtin/maintenance: introduce "worktree-prune" task
      rerere: provide function to collect stale entries
      builtin/gc: move rerere garbage collection into separate function
      builtin/maintenance: introduce "rerere-gc" task

 Documentation/config/maintenance.adoc |  16 ++++
 Documentation/git-maintenance.adoc    |   8 ++
 builtin/gc.c                          | 153 +++++++++++++++++++++++++++-------
 builtin/worktree.c                    |  25 +++---
 rerere.c                              |  92 +++++++++++++-------
 rerere.h                              |  14 ++++
 t/t7900-maintenance.sh                | 125 +++++++++++++++++++++++++++
 worktree.c                            |  30 +++++++
 worktree.h                            |   8 ++
 9 files changed, 399 insertions(+), 72 deletions(-)

Range-diff versus v1:

1:  0304b81df0b = 1:  9c62b493297 builtin/gc: fix indentation of `cmd_gc()` parameters
2:  22c499601ee = 2:  9ae42b139fa builtin/gc: remove global variables where it trivial to do
3:  db9622a408f = 3:  50a5305b6d2 builtin/gc: move pruning of worktrees into a separate function
4:  f42205e1b6b = 4:  b71dcb0debc worktree: expose function to retrieve worktree names
5:  eade37df904 ! 5:  47d31f41c2e builtin/maintenance: introduce "worktree-prune" task
    @@ Commit message
     
         Signed-off-by: Patrick Steinhardt <ps@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


^ permalink raw reply	[flat|nested] 66+ messages in thread

* [PATCH v2 1/8] builtin/gc: fix indentation of `cmd_gc()` parameters
  2025-04-30 10:25 ` [PATCH v2 0/8] " Patrick Steinhardt
@ 2025-04-30 10:25   ` Patrick Steinhardt
  2025-04-30 10:25   ` [PATCH v2 2/8] builtin/gc: remove global variables where it trivial to do Patrick Steinhardt
                     ` (7 subsequent siblings)
  8 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-04-30 10:25 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee

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(-)

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


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH v2 2/8] builtin/gc: remove global variables where it trivial to do
  2025-04-30 10:25 ` [PATCH v2 0/8] " Patrick Steinhardt
  2025-04-30 10:25   ` [PATCH v2 1/8] builtin/gc: fix indentation of `cmd_gc()` parameters Patrick Steinhardt
@ 2025-04-30 10:25   ` Patrick Steinhardt
  2025-04-30 10:25   ` [PATCH v2 3/8] builtin/gc: move pruning of worktrees into a separate function Patrick Steinhardt
                     ` (6 subsequent siblings)
  8 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-04-30 10:25 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee

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(-)

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


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH v2 3/8] builtin/gc: move pruning of worktrees into a separate function
  2025-04-30 10:25 ` [PATCH v2 0/8] " Patrick Steinhardt
  2025-04-30 10:25   ` [PATCH v2 1/8] builtin/gc: fix indentation of `cmd_gc()` parameters Patrick Steinhardt
  2025-04-30 10:25   ` [PATCH v2 2/8] builtin/gc: remove global variables where it trivial to do Patrick Steinhardt
@ 2025-04-30 10:25   ` Patrick Steinhardt
  2025-04-30 10:25   ` [PATCH v2 4/8] worktree: expose function to retrieve worktree names Patrick Steinhardt
                     ` (5 subsequent siblings)
  8 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-04-30 10:25 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee

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(-)

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


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH v2 4/8] worktree: expose function to retrieve worktree names
  2025-04-30 10:25 ` [PATCH v2 0/8] " Patrick Steinhardt
                     ` (2 preceding siblings ...)
  2025-04-30 10:25   ` [PATCH v2 3/8] builtin/gc: move pruning of worktrees into a separate function Patrick Steinhardt
@ 2025-04-30 10:25   ` Patrick Steinhardt
  2025-04-30 10:25   ` [PATCH v2 5/8] builtin/maintenance: introduce "worktree-prune" task Patrick Steinhardt
                     ` (4 subsequent siblings)
  8 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-04-30 10:25 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee

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(-)

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


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH v2 5/8] builtin/maintenance: introduce "worktree-prune" task
  2025-04-30 10:25 ` [PATCH v2 0/8] " Patrick Steinhardt
                     ` (3 preceding siblings ...)
  2025-04-30 10:25   ` [PATCH v2 4/8] worktree: expose function to retrieve worktree names Patrick Steinhardt
@ 2025-04-30 10:25   ` Patrick Steinhardt
  2025-04-30 10:25   ` [PATCH v2 6/8] rerere: provide function to collect stale entries Patrick Steinhardt
                     ` (3 subsequent siblings)
  8 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-04-30 10:25 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee

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(+)

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


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH v2 6/8] rerere: provide function to collect stale entries
  2025-04-30 10:25 ` [PATCH v2 0/8] " Patrick Steinhardt
                     ` (4 preceding siblings ...)
  2025-04-30 10:25   ` [PATCH v2 5/8] builtin/maintenance: introduce "worktree-prune" task Patrick Steinhardt
@ 2025-04-30 10:25   ` Patrick Steinhardt
  2025-04-30 16:58     ` Junio C Hamano
  2025-04-30 10:25   ` [PATCH v2 7/8] builtin/gc: move rerere garbage collection into separate function Patrick Steinhardt
                     ` (2 subsequent siblings)
  8 siblings, 1 reply; 66+ messages in thread
From: Patrick Steinhardt @ 2025-04-30 10:25 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee

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 <ps@pks.im>
---
 rerere.c | 92 ++++++++++++++++++++++++++++++++++++++++++++--------------------
 rerere.h | 14 ++++++++++
 2 files changed, 78 insertions(+), 28 deletions(-)

diff --git a/rerere.c b/rerere.c
index 740e8ad1a0b..eb06e5f8bea 100644
--- a/rerere.c
+++ b/rerere.c
@@ -1202,8 +1202,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;
@@ -1214,11 +1214,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? */
@@ -1229,29 +1229,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;
@@ -1266,23 +1272,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;
+
+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(&to_remove, 0);
+	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 d4b5f7c9320..fd5a2388b06 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), \

-- 
2.49.0.987.g0cc8ee98dc.dirty


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH v2 7/8] builtin/gc: move rerere garbage collection into separate function
  2025-04-30 10:25 ` [PATCH v2 0/8] " Patrick Steinhardt
                     ` (5 preceding siblings ...)
  2025-04-30 10:25   ` [PATCH v2 6/8] rerere: provide function to collect stale entries Patrick Steinhardt
@ 2025-04-30 10:25   ` Patrick Steinhardt
  2025-04-30 10:25   ` [PATCH v2 8/8] builtin/maintenance: introduce "rerere-gc" task Patrick Steinhardt
  2025-04-30 10:37   ` [PATCH v2 0/8] builtin/maintenance: implement missing tasks compared to git-gc(1) Derrick Stolee
  8 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-04-30 10:25 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee

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(-)

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


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH v2 8/8] builtin/maintenance: introduce "rerere-gc" task
  2025-04-30 10:25 ` [PATCH v2 0/8] " Patrick Steinhardt
                     ` (6 preceding siblings ...)
  2025-04-30 10:25   ` [PATCH v2 7/8] builtin/gc: move rerere garbage collection into separate function Patrick Steinhardt
@ 2025-04-30 10:25   ` Patrick Steinhardt
  2025-04-30 10:37   ` [PATCH v2 0/8] builtin/maintenance: implement missing tasks compared to git-gc(1) Derrick Stolee
  8 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-04-30 10:25 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee

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                          | 41 ++++++++++++++++++++++++++
 t/t7900-maintenance.sh                | 54 +++++++++++++++++++++++++++++++++++
 4 files changed, 107 insertions(+)

diff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc
index b36b62c1c47..9c333d42b19 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 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..e56d85ea3bc 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,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)
 {
 	/*
@@ -1502,6 +1537,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 +1584,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..78da81eeb24 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 <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_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' '
 	test_must_fail git maintenance run --auto --schedule=daily 2>err &&
 	test_grep "at most one" err

-- 
2.49.0.987.g0cc8ee98dc.dirty


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* Re: [PATCH v2 0/8] builtin/maintenance: implement missing tasks compared to git-gc(1)
  2025-04-30 10:25 ` [PATCH v2 0/8] " Patrick Steinhardt
                     ` (7 preceding siblings ...)
  2025-04-30 10:25   ` [PATCH v2 8/8] builtin/maintenance: introduce "rerere-gc" task Patrick Steinhardt
@ 2025-04-30 10:37   ` Derrick Stolee
  8 siblings, 0 replies; 66+ messages in thread
From: Derrick Stolee @ 2025-04-30 10:37 UTC (permalink / raw)
  To: Patrick Steinhardt, git

On 4/30/2025 6:25 AM, Patrick Steinhardt wrote:
> 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

This version satisfies my comments from v1 and builds further by including
the maintenance.<task>.auto config options.

LGTM. Thanks,
-Stolee



^ permalink raw reply	[flat|nested] 66+ messages in thread

* Re: [PATCH v2 6/8] rerere: provide function to collect stale entries
  2025-04-30 10:25   ` [PATCH v2 6/8] rerere: provide function to collect stale entries Patrick Steinhardt
@ 2025-04-30 16:58     ` Junio C Hamano
  2025-05-02  8:07       ` Patrick Steinhardt
  0 siblings, 1 reply; 66+ messages in thread
From: Junio C Hamano @ 2025-04-30 16:58 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git, Derrick Stolee

Patrick Steinhardt <ps@pks.im> writes:

> 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.

I have no objection against the goal of allowing "git maintenance"
drive "git rerere gc", and as the primary author of this code path I
do not see anything wrong, in the "correctness" sense, in the
updated code.

I however am not sure if "count what we would prune, and remove only
when there are too many" would work well for this subsystem, because
I expect that the cost to enumerate existing rerere entries and
check each of them for staleness would be the dominant part,
relative to actual "rm -fr <rerere-id>", of the cost you are paying
when you run "git rerere gc".

And if my suspicion is correct, all this change does to the plain
vanilla user of "git rerere gc" is to have them pay the extra cost
of allocating and deallocating the list of names of paths in string
lists.

We need to see some performance measurement to show that the we pay
for collection and counting is a lot smaller compared to the whole
pruning operation to justify the "auto" thing.

> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
>  rerere.c | 92 ++++++++++++++++++++++++++++++++++++++++++++--------------------
>  rerere.h | 14 ++++++++++
>  2 files changed, 78 insertions(+), 28 deletions(-)
>
> diff --git a/rerere.c b/rerere.c
> index 740e8ad1a0b..eb06e5f8bea 100644
> --- a/rerere.c
> +++ b/rerere.c
> @@ -1202,8 +1202,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;
> @@ -1214,11 +1214,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? */
> @@ -1229,29 +1229,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;
> @@ -1266,23 +1272,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;
> +
> +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(&to_remove, 0);
> +	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 d4b5f7c9320..fd5a2388b06 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), \

^ permalink raw reply	[flat|nested] 66+ messages in thread

* Re: [PATCH v2 6/8] rerere: provide function to collect stale entries
  2025-04-30 16:58     ` Junio C Hamano
@ 2025-05-02  8:07       ` Patrick Steinhardt
  2025-05-02 16:35         ` Junio C Hamano
  0 siblings, 1 reply; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-02  8:07 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git, Derrick Stolee

On Wed, Apr 30, 2025 at 09:58:13AM -0700, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
> 
> > 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.
> 
> I have no objection against the goal of allowing "git maintenance"
> drive "git rerere gc", and as the primary author of this code path I
> do not see anything wrong, in the "correctness" sense, in the
> updated code.
> 
> I however am not sure if "count what we would prune, and remove only
> when there are too many" would work well for this subsystem, because
> I expect that the cost to enumerate existing rerere entries and
> check each of them for staleness would be the dominant part,
> relative to actual "rm -fr <rerere-id>", of the cost you are paying
> when you run "git rerere gc".
> 
> And if my suspicion is correct, all this change does to the plain
> vanilla user of "git rerere gc" is to have them pay the extra cost
> of allocating and deallocating the list of names of paths in string
> lists.

Yeah, I think this concern makes sense indeed. I was a bit sceptical
myself whether this is going too far. Maybe a simpler solution would be
to just count the number of directories in ".git/rr-cache", without
checking whether those actually are prunable?

We could also adapt this to be closer to the original version, where we
only verified that ".git/rr-cache" exists and contains at least one
subdirectory. This can even be combined with the above approach if we
set "maintenance.rerere-gc.auto=1" by default.

> We need to see some performance measurement to show that the we pay
> for collection and counting is a lot smaller compared to the whole
> pruning operation to justify the "auto" thing.

Hm. I guess ultimately the answer is going to be "it depends". The
performance implication on Windows is going to be quite different
compared to the performance on Linux/macOS.

In any case, let's go with the simpler model for now. We can still
iterate as needed if we eventually see that the heuristic is too dumb to
be useful.

Patrick

^ permalink raw reply	[flat|nested] 66+ messages in thread

* [PATCH v3 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1)
  2025-04-25  7:29 [PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Patrick Steinhardt
                   ` (8 preceding siblings ...)
  2025-04-30 10:25 ` [PATCH v2 0/8] " Patrick Steinhardt
@ 2025-05-02  8:43 ` Patrick Steinhardt
  2025-05-02  8:43   ` [PATCH v3 1/7] builtin/gc: fix indentation of `cmd_gc()` parameters Patrick Steinhardt
                     ` (7 more replies)
  2025-05-05  8:51 ` [PATCH v4 " Patrick Steinhardt
  2025-05-07  7:21 ` [PATCH v5 0/6] " Patrick Steinhardt
  11 siblings, 8 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-02  8:43 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee, Junio C Hamano

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


^ permalink raw reply	[flat|nested] 66+ messages in thread

* [PATCH v3 1/7] builtin/gc: fix indentation of `cmd_gc()` parameters
  2025-05-02  8:43 ` [PATCH v3 0/7] " Patrick Steinhardt
@ 2025-05-02  8:43   ` Patrick Steinhardt
  2025-05-02  8:43   ` [PATCH v3 2/7] builtin/gc: remove global variables where it trivial to do Patrick Steinhardt
                     ` (6 subsequent siblings)
  7 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-02  8:43 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee, Junio C Hamano

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(-)

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


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH v3 2/7] builtin/gc: remove global variables where it trivial to do
  2025-05-02  8:43 ` [PATCH v3 0/7] " Patrick Steinhardt
  2025-05-02  8:43   ` [PATCH v3 1/7] builtin/gc: fix indentation of `cmd_gc()` parameters Patrick Steinhardt
@ 2025-05-02  8:43   ` Patrick Steinhardt
  2025-05-02  8:44   ` [PATCH v3 3/7] builtin/gc: move pruning of worktrees into a separate function Patrick Steinhardt
                     ` (5 subsequent siblings)
  7 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-02  8:43 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee, Junio C Hamano

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(-)

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


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH v3 3/7] builtin/gc: move pruning of worktrees into a separate function
  2025-05-02  8:43 ` [PATCH v3 0/7] " Patrick Steinhardt
  2025-05-02  8:43   ` [PATCH v3 1/7] builtin/gc: fix indentation of `cmd_gc()` parameters Patrick Steinhardt
  2025-05-02  8:43   ` [PATCH v3 2/7] builtin/gc: remove global variables where it trivial to do Patrick Steinhardt
@ 2025-05-02  8:44   ` Patrick Steinhardt
  2025-05-02  8:44   ` [PATCH v3 4/7] worktree: expose function to retrieve worktree names Patrick Steinhardt
                     ` (4 subsequent siblings)
  7 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-02  8:44 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee, Junio C Hamano

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(-)

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


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH v3 4/7] worktree: expose function to retrieve worktree names
  2025-05-02  8:43 ` [PATCH v3 0/7] " Patrick Steinhardt
                     ` (2 preceding siblings ...)
  2025-05-02  8:44   ` [PATCH v3 3/7] builtin/gc: move pruning of worktrees into a separate function Patrick Steinhardt
@ 2025-05-02  8:44   ` Patrick Steinhardt
  2025-05-05  8:42     ` Eric Sunshine
  2025-05-02  8:44   ` [PATCH v3 5/7] builtin/maintenance: introduce "worktree-prune" task Patrick Steinhardt
                     ` (3 subsequent siblings)
  7 siblings, 1 reply; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-02  8:44 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee, Junio C Hamano

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(-)

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


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH v3 5/7] builtin/maintenance: introduce "worktree-prune" task
  2025-05-02  8:43 ` [PATCH v3 0/7] " Patrick Steinhardt
                     ` (3 preceding siblings ...)
  2025-05-02  8:44   ` [PATCH v3 4/7] worktree: expose function to retrieve worktree names Patrick Steinhardt
@ 2025-05-02  8:44   ` Patrick Steinhardt
  2025-05-05  8:59     ` Eric Sunshine
  2025-05-02  8:44   ` [PATCH v3 6/7] builtin/gc: move rerere garbage collection into separate function Patrick Steinhardt
                     ` (2 subsequent siblings)
  7 siblings, 1 reply; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-02  8:44 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee, Junio C Hamano

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(+)

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


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH v3 6/7] builtin/gc: move rerere garbage collection into separate function
  2025-05-02  8:43 ` [PATCH v3 0/7] " Patrick Steinhardt
                     ` (4 preceding siblings ...)
  2025-05-02  8:44   ` [PATCH v3 5/7] builtin/maintenance: introduce "worktree-prune" task Patrick Steinhardt
@ 2025-05-02  8:44   ` Patrick Steinhardt
  2025-05-02  8:44   ` [PATCH v3 7/7] builtin/maintenance: introduce "rerere-gc" task Patrick Steinhardt
  2025-05-02 14:57   ` [PATCH v3 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Derrick Stolee
  7 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-02  8:44 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee, Junio C Hamano

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(-)

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


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH v3 7/7] builtin/maintenance: introduce "rerere-gc" task
  2025-05-02  8:43 ` [PATCH v3 0/7] " Patrick Steinhardt
                     ` (5 preceding siblings ...)
  2025-05-02  8:44   ` [PATCH v3 6/7] builtin/gc: move rerere garbage collection into separate function Patrick Steinhardt
@ 2025-05-02  8:44   ` Patrick Steinhardt
  2025-05-02 14:57   ` [PATCH v3 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Derrick Stolee
  7 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-02  8:44 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee, Junio C Hamano

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(+)

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


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* Re: [PATCH v3 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1)
  2025-05-02  8:43 ` [PATCH v3 0/7] " Patrick Steinhardt
                     ` (6 preceding siblings ...)
  2025-05-02  8:44   ` [PATCH v3 7/7] builtin/maintenance: introduce "rerere-gc" task Patrick Steinhardt
@ 2025-05-02 14:57   ` Derrick Stolee
  2025-05-02 21:07     ` Junio C Hamano
  7 siblings, 1 reply; 66+ messages in thread
From: Derrick Stolee @ 2025-05-02 14:57 UTC (permalink / raw)
  To: Patrick Steinhardt, git; +Cc: Junio C Hamano

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


^ permalink raw reply	[flat|nested] 66+ messages in thread

* Re: [PATCH v2 6/8] rerere: provide function to collect stale entries
  2025-05-02  8:07       ` Patrick Steinhardt
@ 2025-05-02 16:35         ` Junio C Hamano
  2025-05-05  7:22           ` Patrick Steinhardt
  0 siblings, 1 reply; 66+ messages in thread
From: Junio C Hamano @ 2025-05-02 16:35 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git, Derrick Stolee

Patrick Steinhardt <ps@pks.im> writes:

>> And if my suspicion is correct, all this change does to the plain
>> vanilla user of "git rerere gc" is to have them pay the extra cost
>> of allocating and deallocating the list of names of paths in string
>> lists.
>
> Yeah, I think this concern makes sense indeed. I was a bit sceptical
> myself whether this is going too far. Maybe a simpler solution would be
> to just count the number of directories in ".git/rr-cache", without
> checking whether those actually are prunable?

I dunno.  It is not like you are guestimating files under
.git/objects/??/ without couting all of them, just to see if you
have too many of them and reduce them by repacking and/or pruning.

Having too many existing rerere entries does not correleate you
would have many prunable ones.  If your development is nearly linear
and you do not rebuild the same merges over and over every day,
majority of rerere entries may be created and recorded without ever
getting used even once.  On the other hand, in my tree, where the
same merges along master..jch first-parent history are recreated
multiple times every day, the picture would be different.

> We could also adapt this to be closer to the original version, where we
> only verified that ".git/rr-cache" exists and contains at least one
> subdirectory. This can even be combined with the above approach if we
> set "maintenance.rerere-gc.auto=1" by default.

But wouldn't that be like always running "rerere gc"?  What would
you save by checking the existence of at least one rerere entry?
Cost to spawn "rerere gc" as a subprocess?  Wouldn't it be a better
use of the engineering effort to libify the logic of that process
and make it internally callable in the longer term (or are you
already doing that in this series)?

>> We need to see some performance measurement to show that the we pay
>> for collection and counting is a lot smaller compared to the whole
>> pruning operation to justify the "auto" thing.
>
> Hm. I guess ultimately the answer is going to be "it depends". The
> performance implication on Windows is going to be quite different
> compared to the performance on Linux/macOS.

Yes, but we need to start somewhere ;-)

^ permalink raw reply	[flat|nested] 66+ messages in thread

* Re: [PATCH v3 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1)
  2025-05-02 14:57   ` [PATCH v3 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Derrick Stolee
@ 2025-05-02 21:07     ` Junio C Hamano
  2025-05-05  7:32       ` Patrick Steinhardt
  0 siblings, 1 reply; 66+ messages in thread
From: Junio C Hamano @ 2025-05-02 21:07 UTC (permalink / raw)
  To: Derrick Stolee; +Cc: Patrick Steinhardt, git

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.

Thanks, all.



^ permalink raw reply	[flat|nested] 66+ messages in thread

* Re: [PATCH v2 6/8] rerere: provide function to collect stale entries
  2025-05-02 16:35         ` Junio C Hamano
@ 2025-05-05  7:22           ` Patrick Steinhardt
  0 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-05  7:22 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git, Derrick Stolee

On Fri, May 02, 2025 at 09:35:57AM -0700, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
> > We could also adapt this to be closer to the original version, where we
> > only verified that ".git/rr-cache" exists and contains at least one
> > subdirectory. This can even be combined with the above approach if we
> > set "maintenance.rerere-gc.auto=1" by default.
> 
> But wouldn't that be like always running "rerere gc"?  What would
> you save by checking the existence of at least one rerere entry?
> Cost to spawn "rerere gc" as a subprocess?

What you save is executing the rerere logic in case the feature isn't
enabled at all, but also in the case where you have eventually disabled
the rerere cache but still have a now-empty ".git/rr-cache" directory.

> Wouldn't it be a better use of the engineering effort to libify the
> logic of that process and make it internally callable in the longer
> term (or are you already doing that in this series)?

The logic itself already is quite encapsulated, but I decided to not use
it for now because we don't do so for any of the other tasks, either.
But I do agree that we should eventually refactor all of our tasks so
that they don't need to spawn an external process at all.

Patrick

^ permalink raw reply	[flat|nested] 66+ messages in thread

* Re: [PATCH v3 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1)
  2025-05-02 21:07     ` Junio C Hamano
@ 2025-05-05  7:32       ` Patrick Steinhardt
  0 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-05  7:32 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Derrick Stolee, git

On Fri, May 02, 2025 at 02:07:28PM -0700, Junio C Hamano wrote:
> 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

^ permalink raw reply	[flat|nested] 66+ messages in thread

* Re: [PATCH v3 4/7] worktree: expose function to retrieve worktree names
  2025-05-02  8:44   ` [PATCH v3 4/7] worktree: expose function to retrieve worktree names Patrick Steinhardt
@ 2025-05-05  8:42     ` Eric Sunshine
  2025-05-07  7:06       ` Patrick Steinhardt
  0 siblings, 1 reply; 66+ messages in thread
From: Eric Sunshine @ 2025-05-05  8:42 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git, Derrick Stolee, Junio C Hamano

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?

> 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

> 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.

^ permalink raw reply	[flat|nested] 66+ messages in thread

* [PATCH v4 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1)
  2025-04-25  7:29 [PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Patrick Steinhardt
                   ` (9 preceding siblings ...)
  2025-05-02  8:43 ` [PATCH v3 0/7] " Patrick Steinhardt
@ 2025-05-05  8:51 ` Patrick Steinhardt
  2025-05-05  8:51   ` [PATCH v4 1/7] builtin/gc: fix indentation of `cmd_gc()` parameters Patrick Steinhardt
                     ` (7 more replies)
  2025-05-07  7:21 ` [PATCH v5 0/6] " Patrick Steinhardt
  11 siblings, 8 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-05  8:51 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee, Junio C Hamano

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

Changes in v4:
  - simplified the heuristic for "rerere-gc" even further. A positive
    value for "maintenance.rerere-gc.auto" now indicates that the
    command will run whenever there is at least one directory entry in
    ".rr-cache". The exact value does not matter anymore.
  - Link to v3: https://lore.kernel.org/r/20250502-pks-maintenance-missing-tasks-v3-0-13e130d36640@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 |  17 ++++
 Documentation/git-maintenance.adoc    |   8 ++
 builtin/gc.c                          | 149 +++++++++++++++++++++++++++-------
 builtin/worktree.c                    |  25 +++---
 t/t7900-maintenance.sh                | 115 ++++++++++++++++++++++++++
 worktree.c                            |  30 +++++++
 worktree.h                            |   8 ++
 7 files changed, 308 insertions(+), 44 deletions(-)

Range-diff versus v3:

1:  0cf3cb937c3 = 1:  6614504400b builtin/gc: fix indentation of `cmd_gc()` parameters
2:  5982d47cc95 = 2:  6c8c66282c5 builtin/gc: remove global variables where it trivial to do
3:  afda176acd3 = 3:  bfa8fc00434 builtin/gc: move pruning of worktrees into a separate function
4:  b3b6a14bf1c = 4:  d87b883cf62 worktree: expose function to retrieve worktree names
5:  e02d59bb944 = 5:  354a1985b4b builtin/maintenance: introduce "worktree-prune" task
6:  cb31ec6b8bd = 6:  6f40349b53f builtin/gc: move rerere garbage collection into separate function
7:  65e8483fe4a ! 7:  e3c9da19a2c builtin/maintenance: introduce "rerere-gc" task
    @@ Documentation/config/maintenance.adoc: maintenance.reflog-expire.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.
    ++	value will force the task to run every time. Otherwise, any positive
    ++	value implies the command will run when the "rr-cache" directory exists
    ++	and has at least one entry, regardless of whether it is stale or not.
    ++	This heuristic may be refined in the future. 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
     +
     +	/*
     +	 * 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".
    ++	 * directory or when it doesn't contain at least one entry.
     +	 */
     +	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;
    -+	}
    ++	should_gc = !!readdir_skip_dot_and_dotdot(dir);
     +
     +out:
     +	strbuf_release(&path);
    @@ t/t7900-maintenance.sh: test_expect_success 'worktree-prune task honors gc.workt
     +	# A negative value should always prune.
     +	test_expect_rerere_gc git -c maintenance.rerere-gc.auto=-1 maintenance run --auto --task=rerere-gc &&
     +
    ++	# A positive value prunes when there is at least one entry.
    ++	test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&
     +	mkdir .git/rr-cache &&
    ++	test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&
     +	: >.git/rr-cache/entry-1 &&
    -+	: >.git/rr-cache/entry-2 &&
    ++	test_expect_rerere_gc git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&
     +
     +	# 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
    ++	: >.git/rr-cache/entry-1 &&
    ++	test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 maintenance run --auto --task=rerere-gc
     +'
     +
      test_expect_success '--auto and --schedule incompatible' '

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


^ permalink raw reply	[flat|nested] 66+ messages in thread

* [PATCH v4 1/7] builtin/gc: fix indentation of `cmd_gc()` parameters
  2025-05-05  8:51 ` [PATCH v4 " Patrick Steinhardt
@ 2025-05-05  8:51   ` Patrick Steinhardt
  2025-05-05  8:51   ` [PATCH v4 2/7] builtin/gc: remove global variables where it trivial to do Patrick Steinhardt
                     ` (6 subsequent siblings)
  7 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-05  8:51 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee, Junio C Hamano

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(-)

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


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH v4 2/7] builtin/gc: remove global variables where it trivial to do
  2025-05-05  8:51 ` [PATCH v4 " Patrick Steinhardt
  2025-05-05  8:51   ` [PATCH v4 1/7] builtin/gc: fix indentation of `cmd_gc()` parameters Patrick Steinhardt
@ 2025-05-05  8:51   ` Patrick Steinhardt
  2025-05-06  7:44     ` Christian Couder
  2025-05-05  8:51   ` [PATCH v4 3/7] builtin/gc: move pruning of worktrees into a separate function Patrick Steinhardt
                     ` (5 subsequent siblings)
  7 siblings, 1 reply; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-05  8:51 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee, Junio C Hamano

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(-)

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


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH v4 3/7] builtin/gc: move pruning of worktrees into a separate function
  2025-05-05  8:51 ` [PATCH v4 " Patrick Steinhardt
  2025-05-05  8:51   ` [PATCH v4 1/7] builtin/gc: fix indentation of `cmd_gc()` parameters Patrick Steinhardt
  2025-05-05  8:51   ` [PATCH v4 2/7] builtin/gc: remove global variables where it trivial to do Patrick Steinhardt
@ 2025-05-05  8:51   ` Patrick Steinhardt
  2025-05-06  7:50     ` Christian Couder
  2025-05-05  8:51   ` [PATCH v4 4/7] worktree: expose function to retrieve worktree names Patrick Steinhardt
                     ` (4 subsequent siblings)
  7 siblings, 1 reply; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-05  8:51 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee, Junio C Hamano

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(-)

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


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH v4 4/7] worktree: expose function to retrieve worktree names
  2025-05-05  8:51 ` [PATCH v4 " Patrick Steinhardt
                     ` (2 preceding siblings ...)
  2025-05-05  8:51   ` [PATCH v4 3/7] builtin/gc: move pruning of worktrees into a separate function Patrick Steinhardt
@ 2025-05-05  8:51   ` Patrick Steinhardt
  2025-05-06  8:20     ` Christian Couder
  2025-05-05  8:51   ` [PATCH v4 5/7] builtin/maintenance: introduce "worktree-prune" task Patrick Steinhardt
                     ` (3 subsequent siblings)
  7 siblings, 1 reply; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-05  8:51 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee, Junio C Hamano

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(-)

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


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH v4 5/7] builtin/maintenance: introduce "worktree-prune" task
  2025-05-05  8:51 ` [PATCH v4 " Patrick Steinhardt
                     ` (3 preceding siblings ...)
  2025-05-05  8:51   ` [PATCH v4 4/7] worktree: expose function to retrieve worktree names Patrick Steinhardt
@ 2025-05-05  8:51   ` Patrick Steinhardt
  2025-05-06  7:40     ` Christian Couder
  2025-05-05  8:51   ` [PATCH v4 6/7] builtin/gc: move rerere garbage collection into separate function Patrick Steinhardt
                     ` (2 subsequent siblings)
  7 siblings, 1 reply; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-05  8:51 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee, Junio C Hamano

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(+)

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


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH v4 6/7] builtin/gc: move rerere garbage collection into separate function
  2025-05-05  8:51 ` [PATCH v4 " Patrick Steinhardt
                     ` (4 preceding siblings ...)
  2025-05-05  8:51   ` [PATCH v4 5/7] builtin/maintenance: introduce "worktree-prune" task Patrick Steinhardt
@ 2025-05-05  8:51   ` Patrick Steinhardt
  2025-05-06  8:39     ` Christian Couder
  2025-05-05  8:51   ` [PATCH v4 7/7] builtin/maintenance: introduce "rerere-gc" task Patrick Steinhardt
  2025-05-06  9:05   ` [PATCH v4 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Christian Couder
  7 siblings, 1 reply; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-05  8:51 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee, Junio C Hamano

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(-)

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


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH v4 7/7] builtin/maintenance: introduce "rerere-gc" task
  2025-05-05  8:51 ` [PATCH v4 " Patrick Steinhardt
                     ` (5 preceding siblings ...)
  2025-05-05  8:51   ` [PATCH v4 6/7] builtin/gc: move rerere garbage collection into separate function Patrick Steinhardt
@ 2025-05-05  8:51   ` Patrick Steinhardt
  2025-05-06  9:05   ` [PATCH v4 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Christian Couder
  7 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-05  8:51 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee, Junio C Hamano

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 |  9 +++++++
 Documentation/git-maintenance.adoc    |  4 ++++
 builtin/gc.c                          | 37 +++++++++++++++++++++++++++++
 t/t7900-maintenance.sh                | 44 +++++++++++++++++++++++++++++++++++
 4 files changed, 94 insertions(+)

diff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc
index b36b62c1c47..2f719342183 100644
--- a/Documentation/config/maintenance.adoc
+++ b/Documentation/config/maintenance.adoc
@@ -84,6 +84,15 @@ 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, any positive
+	value implies the command will run when the "rr-cache" directory exists
+	and has at least one entry, regardless of whether it is stale or not.
+	This heuristic may be refined in the future. 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..0d4a70c7804 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,35 @@ 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 one entry.
+	 */
+	repo_git_path_replace(the_repository, &path, "rr-cache");
+	dir = opendir(path.buf);
+	if (!dir)
+		goto out;
+	should_gc = !!readdir_skip_dot_and_dotdot(dir);
+
+out:
+	strbuf_release(&path);
+	if (dir)
+		closedir(dir);
+	return should_gc;
+}
+
 static int too_many_loose_objects(struct gc_config *cfg)
 {
 	/*
@@ -1502,6 +1533,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 +1580,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..af02a551869 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -564,6 +564,50 @@ 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 &&
+
+	# A positive value prunes when there is at least one entry.
+	test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&
+	mkdir .git/rr-cache &&
+	test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&
+	: >.git/rr-cache/entry-1 &&
+	test_expect_rerere_gc git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&
+
+	# Zero should never prune.
+	: >.git/rr-cache/entry-1 &&
+	test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 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


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* Re: [PATCH v3 5/7] builtin/maintenance: introduce "worktree-prune" task
  2025-05-02  8:44   ` [PATCH v3 5/7] builtin/maintenance: introduce "worktree-prune" task Patrick Steinhardt
@ 2025-05-05  8:59     ` Eric Sunshine
  2025-05-07  7:06       ` Patrick Steinhardt
  0 siblings, 1 reply; 66+ messages in thread
From: Eric Sunshine @ 2025-05-05  8:59 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git, Derrick Stolee, Junio C Hamano

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

> +                       limit--;
> +
> +                       if (!limit) {
> +                               should_prune = 1;
> +                               goto out;
> +                       }
> +               }
> +               free(wtpath);

This leaks `wtpath` when it takes the `goto out` arm.

> +       }
> +
> +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/

^ permalink raw reply	[flat|nested] 66+ messages in thread

* Re: [PATCH v4 5/7] builtin/maintenance: introduce "worktree-prune" task
  2025-05-05  8:51   ` [PATCH v4 5/7] builtin/maintenance: introduce "worktree-prune" task Patrick Steinhardt
@ 2025-05-06  7:40     ` Christian Couder
  2025-05-07  7:06       ` Patrick Steinhardt
  0 siblings, 1 reply; 66+ messages in thread
From: Christian Couder @ 2025-05-06  7:40 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git, Derrick Stolee, Junio C Hamano

On Mon, May 5, 2025 at 10:52 AM Patrick Steinhardt <ps@pks.im> wrote:

> +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;

Eric noticed in a previous round that wtpath is leaked in this `goto
out` path, and it seems to me that it's still the case.

> +                       }
> +               }
> +               free(wtpath);
> +       }
> +
> +out:
> +       strvec_clear(&worktrees);
> +       strbuf_release(&reason);
> +       return should_prune;
> +}

^ permalink raw reply	[flat|nested] 66+ messages in thread

* Re: [PATCH v4 2/7] builtin/gc: remove global variables where it trivial to do
  2025-05-05  8:51   ` [PATCH v4 2/7] builtin/gc: remove global variables where it trivial to do Patrick Steinhardt
@ 2025-05-06  7:44     ` Christian Couder
  2025-05-07  7:06       ` Patrick Steinhardt
  0 siblings, 1 reply; 66+ messages in thread
From: Christian Couder @ 2025-05-06  7:44 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git, Derrick Stolee, Junio C Hamano

On Mon, May 5, 2025 at 10:52 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> 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.

About the commit message it seems to me that it's missing "is", so maybe:

"builtin/gc: remove global variables where it's trivial to do"

or just:

"builtin/gc: remove global variables where trivial to do"

?

^ permalink raw reply	[flat|nested] 66+ messages in thread

* Re: [PATCH v4 3/7] builtin/gc: move pruning of worktrees into a separate function
  2025-05-05  8:51   ` [PATCH v4 3/7] builtin/gc: move pruning of worktrees into a separate function Patrick Steinhardt
@ 2025-05-06  7:50     ` Christian Couder
  2025-05-07  7:06       ` Patrick Steinhardt
  0 siblings, 1 reply; 66+ messages in thread
From: Christian Couder @ 2025-05-06  7:50 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git, Derrick Stolee, Junio C Hamano

On Mon, May 5, 2025 at 10:52 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> Move pruning of worktrees into a separate function.

Repeating the subject at the beginning of the body part of the commit
message doesn't bring much value.

> This prepares for a
> subsequent commit where we introduce a new "worktree-prune" task for
> git-maintenance(1).

I think it might help a bit to reword in the usual "describe the
problem first and then say what to do about it" way. Maybe something
like:

"In a subsequent commit we are going to introduce a new
"worktree-prune" task for git-maintenance(1).

To prepare for this, refactor the code that prunes worktrees into a
separate function."

^ permalink raw reply	[flat|nested] 66+ messages in thread

* Re: [PATCH v4 4/7] worktree: expose function to retrieve worktree names
  2025-05-05  8:51   ` [PATCH v4 4/7] worktree: expose function to retrieve worktree names Patrick Steinhardt
@ 2025-05-06  8:20     ` Christian Couder
  2025-05-06 16:08       ` Eric Sunshine
  0 siblings, 1 reply; 66+ messages in thread
From: Christian Couder @ 2025-05-06  8:20 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git, Derrick Stolee, Junio C Hamano

On Mon, May 5, 2025 at 10:52 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>
> ---
>  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 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;
> +}

Nit: this function seems to use "goto out" a bit too much for me. What
about something like:

int get_worktree_names(struct repository *repo, struct strvec *out)
{
    int ret = 0;
    char *worktrees_dir = repo_git_path(repo, "worktrees");
    DIR *dir = opendir(worktrees_dir);

    if (dir) {
        struct dirent *d;
        while ((d = readdir_skip_dot_and_dotdot(dir)) != NULL)
            strvec_push(out, d->d_name);
        closedir(dir);
    } else {
        if (errno != ENOENT)
            ret = -1;
    }

    free(worktrees_dir);
    return ret;
}

?

^ permalink raw reply	[flat|nested] 66+ messages in thread

* Re: [PATCH v4 6/7] builtin/gc: move rerere garbage collection into separate function
  2025-05-05  8:51   ` [PATCH v4 6/7] builtin/gc: move rerere garbage collection into separate function Patrick Steinhardt
@ 2025-05-06  8:39     ` Christian Couder
  0 siblings, 0 replies; 66+ messages in thread
From: Christian Couder @ 2025-05-06  8:39 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git, Derrick Stolee, Junio C Hamano

On Mon, May 5, 2025 at 10:52 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> Move garbage collection of cached rerere entries into a separate
> function.

Nit: this is also repeating the commit message subject nearly as-is.

> This prepares us for a subsequent commit where we introduce a
> new "rerere-gc" task for git-maintenance(1).

So maybe something like:

"In a subsequent commit we are going to introduce a new "rerere-gc"
task for git-maintenance(1).

To prepare for this, refactor the code that garbage collects cached
rerere entries into a separate function."

^ permalink raw reply	[flat|nested] 66+ messages in thread

* Re: [PATCH v4 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1)
  2025-05-05  8:51 ` [PATCH v4 " Patrick Steinhardt
                     ` (6 preceding siblings ...)
  2025-05-05  8:51   ` [PATCH v4 7/7] builtin/maintenance: introduce "rerere-gc" task Patrick Steinhardt
@ 2025-05-06  9:05   ` Christian Couder
  7 siblings, 0 replies; 66+ messages in thread
From: Christian Couder @ 2025-05-06  9:05 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git, Derrick Stolee, Junio C Hamano

On Mon, May 5, 2025 at 10:52 AM Patrick Steinhardt <ps@pks.im> wrote:

> Changes in v4:
>   - simplified the heuristic for "rerere-gc" even further. A positive
>     value for "maintenance.rerere-gc.auto" now indicates that the
>     command will run whenever there is at least one directory entry in
>     ".rr-cache". The exact value does not matter anymore.
>   - Link to v3: https://lore.kernel.org/r/20250502-pks-maintenance-missing-tasks-v3-0-13e130d36640@pks.im

Except for a few nits I commented on in my replies to specific
patches, and except that some comments from Eric Sunshine in
https://lore.kernel.org/git/CAPig+cScor=E2i4w99NCrsaUd-Po=FqhkrLSyT69PSo1+h2dRw@mail.gmail.com/#t
seems to have been overlooked (especially one about a memory leak),
this looks good to me.

Thanks!

^ permalink raw reply	[flat|nested] 66+ messages in thread

* Re: [PATCH v4 4/7] worktree: expose function to retrieve worktree names
  2025-05-06  8:20     ` Christian Couder
@ 2025-05-06 16:08       ` Eric Sunshine
  0 siblings, 0 replies; 66+ messages in thread
From: Eric Sunshine @ 2025-05-06 16:08 UTC (permalink / raw)
  To: Christian Couder; +Cc: Patrick Steinhardt, git, Derrick Stolee, Junio C Hamano

On Tue, May 6, 2025 at 4:23 AM Christian Couder
<christian.couder@gmail.com> wrote:
> On Mon, May 5, 2025 at 10:52 AM Patrick Steinhardt <ps@pks.im> wrote:
> > +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;
> > +}
>
> Nit: this function seems to use "goto out" a bit too much for me. What
> about something like:
>
> int get_worktree_names(struct repository *repo, struct strvec *out)
> {
>     int ret = 0;
>     char *worktrees_dir = repo_git_path(repo, "worktrees");
>     DIR *dir = opendir(worktrees_dir);
>
>     if (dir) {
>         struct dirent *d;
>         while ((d = readdir_skip_dot_and_dotdot(dir)) != NULL)
>             strvec_push(out, d->d_name);
>         closedir(dir);
>     } else {
>         if (errno != ENOENT)
>             ret = -1;
>     }
>
>     free(worktrees_dir);
>     return ret;
> }

I had the same response and made a slightly more concise proposal[*],
though I'm not convinced that this function should exist at all.

[*]: https://lore.kernel.org/git/CAPig+cSDDbhGrym8j=PFKBCUxBQhZPzAHXGvKy-Z6POA4Ju3sw@mail.gmail.com/

^ permalink raw reply	[flat|nested] 66+ messages in thread

* Re: [PATCH v3 4/7] worktree: expose function to retrieve worktree names
  2025-05-05  8:42     ` Eric Sunshine
@ 2025-05-07  7:06       ` Patrick Steinhardt
  0 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-07  7:06 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: git, Derrick Stolee, Junio C Hamano

On Mon, May 05, 2025 at 04:42:40AM -0400, Eric Sunshine wrote:
> 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

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;
 }

^ permalink raw reply related	[flat|nested] 66+ messages in thread

* Re: [PATCH v3 5/7] builtin/maintenance: introduce "worktree-prune" task
  2025-05-05  8:59     ` Eric Sunshine
@ 2025-05-07  7:06       ` Patrick Steinhardt
  0 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-07  7:06 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: git, Derrick Stolee, Junio C Hamano

On Mon, May 05, 2025 at 04:59:14AM -0400, Eric Sunshine wrote:
> 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.

> > +       }
> > +
> > +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

^ permalink raw reply	[flat|nested] 66+ messages in thread

* Re: [PATCH v4 2/7] builtin/gc: remove global variables where it trivial to do
  2025-05-06  7:44     ` Christian Couder
@ 2025-05-07  7:06       ` Patrick Steinhardt
  0 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-07  7:06 UTC (permalink / raw)
  To: Christian Couder; +Cc: git, Derrick Stolee, Junio C Hamano

On Tue, May 06, 2025 at 09:44:03AM +0200, Christian Couder wrote:
> On Mon, May 5, 2025 at 10:52 AM Patrick Steinhardt <ps@pks.im> wrote:
> >
> > 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.
> 
> About the commit message it seems to me that it's missing "is", so maybe:
> 
> "builtin/gc: remove global variables where it's trivial to do"
> 
> or just:
> 
> "builtin/gc: remove global variables where trivial to do"
> 
> ?

Ah, indeed.

Patrick

^ permalink raw reply	[flat|nested] 66+ messages in thread

* Re: [PATCH v4 3/7] builtin/gc: move pruning of worktrees into a separate function
  2025-05-06  7:50     ` Christian Couder
@ 2025-05-07  7:06       ` Patrick Steinhardt
  0 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-07  7:06 UTC (permalink / raw)
  To: Christian Couder; +Cc: git, Derrick Stolee, Junio C Hamano

On Tue, May 06, 2025 at 09:50:12AM +0200, Christian Couder wrote:
> On Mon, May 5, 2025 at 10:52 AM Patrick Steinhardt <ps@pks.im> wrote:
> >
> > Move pruning of worktrees into a separate function.
> 
> Repeating the subject at the beginning of the body part of the commit
> message doesn't bring much value.

I disagree with that statement. The way I review commit messages the
subject is often detached from the body, so it always helps me quite a
bit when the body is fully self-contained and doesn't depend on the
subject.

> > This prepares for a
> > subsequent commit where we introduce a new "worktree-prune" task for
> > git-maintenance(1).
> 
> I think it might help a bit to reword in the usual "describe the
> problem first and then say what to do about it" way. Maybe something
> like:
> 
> "In a subsequent commit we are going to introduce a new
> "worktree-prune" task for git-maintenance(1).
> 
> To prepare for this, refactor the code that prunes worktrees into a
> separate function."

But this reads better regardless of that, so I'll take it. Thanks!

Patrick

^ permalink raw reply	[flat|nested] 66+ messages in thread

* Re: [PATCH v4 5/7] builtin/maintenance: introduce "worktree-prune" task
  2025-05-06  7:40     ` Christian Couder
@ 2025-05-07  7:06       ` Patrick Steinhardt
  0 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-07  7:06 UTC (permalink / raw)
  To: Christian Couder; +Cc: git, Derrick Stolee, Junio C Hamano

On Tue, May 06, 2025 at 09:40:55AM +0200, Christian Couder wrote:
> On Mon, May 5, 2025 at 10:52 AM Patrick Steinhardt <ps@pks.im> wrote:
> 
> > +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;
> 
> Eric noticed in a previous round that wtpath is leaked in this `goto
> out` path, and it seems to me that it's still the case.

Yeah, our mails had crossed back then. Fixed now.

Patrick

^ permalink raw reply	[flat|nested] 66+ messages in thread

* [PATCH v5 0/6] builtin/maintenance: implement missing tasks compared to git-gc(1)
  2025-04-25  7:29 [PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Patrick Steinhardt
                   ` (10 preceding siblings ...)
  2025-05-05  8:51 ` [PATCH v4 " Patrick Steinhardt
@ 2025-05-07  7:21 ` Patrick Steinhardt
  2025-05-07  7:21   ` [PATCH v5 1/6] builtin/gc: fix indentation of `cmd_gc()` parameters Patrick Steinhardt
                     ` (5 more replies)
  11 siblings, 6 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-07  7:21 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee, Junio C Hamano, Eric Sunshine, Christian Couder

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

Changes in v4:
  - simplified the heuristic for "rerere-gc" even further. A positive
    value for "maintenance.rerere-gc.auto" now indicates that the
    command will run whenever there is at least one directory entry in
    ".rr-cache". The exact value does not matter anymore.
  - Link to v3: https://lore.kernel.org/r/20250502-pks-maintenance-missing-tasks-v3-0-13e130d36640@pks.im

Changes in v5:
  - Drop `get_worktree_names()` in favor of an open-coded loop.
  - Fix a memory leak.
  - Simplified the logic in `worktree_prune_condition()` a bit.
  - Link to v4: https://lore.kernel.org/r/20250505-pks-maintenance-missing-tasks-v4-0-141f4df906a1@pks.im

Thanks!

Patrick

---
Patrick Steinhardt (6):
      builtin/gc: fix indentation of `cmd_gc()` parameters
      builtin/gc: remove global variables where it is trivial to do
      builtin/gc: move pruning of worktrees into a separate function
      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 |  17 ++++
 Documentation/git-maintenance.adoc    |   8 ++
 builtin/gc.c                          | 148 +++++++++++++++++++++++++++-------
 t/t7900-maintenance.sh                | 115 ++++++++++++++++++++++++++
 4 files changed, 257 insertions(+), 31 deletions(-)

Range-diff versus v4:

1:  59edf54e3ec = 1:  815904a68a1 builtin/gc: fix indentation of `cmd_gc()` parameters
2:  9f02c33f5b9 ! 2:  91f4c304232 builtin/gc: remove global variables where it trivial to do
    @@ Metadata
     Author: Patrick Steinhardt <ps@pks.im>
     
      ## Commit message ##
    -    builtin/gc: remove global variables where it trivial to do
    +    builtin/gc: remove global variables where it is 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
3:  b280af7bbc4 ! 3:  9232c8aac1d builtin/gc: move pruning of worktrees into a separate function
    @@ Metadata
      ## Commit message ##
         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).
    +    In a subsequent commit we will introduce a new "worktree-prune" task for
    +    git-maintenance(1). To prepare for this, refactor the code that spawns
    +    `git worktree prune` into a separate function.
     
         Signed-off-by: Patrick Steinhardt <ps@pks.im>
     
4:  58ce12459c2 < -:  ----------- worktree: expose function to retrieve worktree names
5:  efeec465db0 ! 4:  24ca70b35b9 builtin/maintenance: introduce "worktree-prune" task
    @@ builtin/gc.c: static int maintenance_task_worktree_prune(struct maintenance_run_
      
     +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) {
    @@ builtin/gc.c: static int maintenance_task_worktree_prune(struct maintenance_run_
     +		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;
     +
    -+	for (size_t i = 0; i < worktrees.nr; i++) {
    -+		char *wtpath;
    ++	dir = opendir(repo_git_path_replace(the_repository, &buf, "worktrees"));
    ++	if (!dir)
    ++		goto out;
     +
    -+		strbuf_reset(&reason);
    -+		if (should_prune_worktree(worktrees.v[i], &reason, &wtpath, expiry_date)) {
    ++	while (limit && (d = readdir_skip_dot_and_dotdot(dir))) {
    ++		char *wtpath;
    ++		strbuf_reset(&buf);
    ++		if (should_prune_worktree(d->d_name, &buf, &wtpath, expiry_date))
     +			limit--;
    -+
    -+			if (!limit) {
    -+				should_prune = 1;
    -+				goto out;
    -+			}
    -+		}
     +		free(wtpath);
     +	}
     +
    ++	should_prune = !limit;
    ++
     +out:
    -+	strvec_clear(&worktrees);
    -+	strbuf_release(&reason);
    ++	if (dir)
    ++		closedir(dir);
    ++	strbuf_release(&buf);
     +	return should_prune;
     +}
     +
    @@ t/t7900-maintenance.sh: test_expect_success 'reflog-expire task --auto only pack
     +
     +	# 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.
    ++	# 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
     +'
6:  10ed12cc737 ! 5:  67e05501fbb builtin/gc: move rerere garbage collection into separate function
    @@ Metadata
      ## Commit message ##
         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).
    +    In a subsequent commit we are going to introduce a new "rerere-gc" task
    +    for git-maintenance(1). To prepare for this, refactor the code that
    +    spawns `git rerere gc` into a separate function.
     
         Signed-off-by: Patrick Steinhardt <ps@pks.im>
     
7:  be7fa13115c = 6:  818ed6b8999 builtin/maintenance: introduce "rerere-gc" task

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


^ permalink raw reply	[flat|nested] 66+ messages in thread

* [PATCH v5 1/6] builtin/gc: fix indentation of `cmd_gc()` parameters
  2025-05-07  7:21 ` [PATCH v5 0/6] " Patrick Steinhardt
@ 2025-05-07  7:21   ` Patrick Steinhardt
  2025-05-07  7:21   ` [PATCH v5 2/6] builtin/gc: remove global variables where it is trivial to do Patrick Steinhardt
                     ` (4 subsequent siblings)
  5 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-07  7:21 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee, Junio C Hamano, Eric Sunshine, Christian Couder

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(-)

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.1045.g170613ef41.dirty


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH v5 2/6] builtin/gc: remove global variables where it is trivial to do
  2025-05-07  7:21 ` [PATCH v5 0/6] " Patrick Steinhardt
  2025-05-07  7:21   ` [PATCH v5 1/6] builtin/gc: fix indentation of `cmd_gc()` parameters Patrick Steinhardt
@ 2025-05-07  7:21   ` Patrick Steinhardt
  2025-05-07  7:21   ` [PATCH v5 3/6] builtin/gc: move pruning of worktrees into a separate function Patrick Steinhardt
                     ` (3 subsequent siblings)
  5 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-07  7:21 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee, Junio C Hamano, Eric Sunshine, Christian Couder

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(-)

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.1045.g170613ef41.dirty


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH v5 3/6] builtin/gc: move pruning of worktrees into a separate function
  2025-05-07  7:21 ` [PATCH v5 0/6] " Patrick Steinhardt
  2025-05-07  7:21   ` [PATCH v5 1/6] builtin/gc: fix indentation of `cmd_gc()` parameters Patrick Steinhardt
  2025-05-07  7:21   ` [PATCH v5 2/6] builtin/gc: remove global variables where it is trivial to do Patrick Steinhardt
@ 2025-05-07  7:21   ` Patrick Steinhardt
  2025-05-07  7:21   ` [PATCH v5 4/6] builtin/maintenance: introduce "worktree-prune" task Patrick Steinhardt
                     ` (2 subsequent siblings)
  5 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-07  7:21 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee, Junio C Hamano, Eric Sunshine, Christian Couder

In a subsequent commit we will introduce a new "worktree-prune" task for
git-maintenance(1). To prepare for this, refactor the code that spawns
`git worktree prune` into a separate function.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/gc.c | 25 +++++++++++++++----------
 1 file changed, 15 insertions(+), 10 deletions(-)

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.1045.g170613ef41.dirty


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH v5 4/6] builtin/maintenance: introduce "worktree-prune" task
  2025-05-07  7:21 ` [PATCH v5 0/6] " Patrick Steinhardt
                     ` (2 preceding siblings ...)
  2025-05-07  7:21   ` [PATCH v5 3/6] builtin/gc: move pruning of worktrees into a separate function Patrick Steinhardt
@ 2025-05-07  7:21   ` Patrick Steinhardt
  2025-05-07  7:21   ` [PATCH v5 5/6] builtin/gc: move rerere garbage collection into separate function Patrick Steinhardt
  2025-05-07  7:21   ` [PATCH v5 6/6] builtin/maintenance: introduce "rerere-gc" task Patrick Steinhardt
  5 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-07  7:21 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee, Junio C Hamano, Eric Sunshine, Christian Couder

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                          | 45 ++++++++++++++++++++++
 t/t7900-maintenance.sh                | 71 +++++++++++++++++++++++++++++++++++
 4 files changed, 128 insertions(+)

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..e85e313d78f 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,44 @@ 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 strbuf buf = STRBUF_INIT;
+	int should_prune = 0, limit = 1;
+	timestamp_t expiry_date;
+	struct dirent *d;
+	DIR *dir = NULL;
+
+	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))
+		goto out;
+
+	dir = opendir(repo_git_path_replace(the_repository, &buf, "worktrees"));
+	if (!dir)
+		goto out;
+
+	while (limit && (d = readdir_skip_dot_and_dotdot(dir))) {
+		char *wtpath;
+		strbuf_reset(&buf);
+		if (should_prune_worktree(d->d_name, &buf, &wtpath, expiry_date))
+			limit--;
+		free(wtpath);
+	}
+
+	should_prune = !limit;
+
+out:
+	if (dir)
+		closedir(dir);
+	strbuf_release(&buf);
+	return should_prune;
+}
+
 static int too_many_loose_objects(struct gc_config *cfg)
 {
 	/*
@@ -1455,6 +1494,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 +1536,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..8f4120a0351 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 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 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.1045.g170613ef41.dirty


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH v5 5/6] builtin/gc: move rerere garbage collection into separate function
  2025-05-07  7:21 ` [PATCH v5 0/6] " Patrick Steinhardt
                     ` (3 preceding siblings ...)
  2025-05-07  7:21   ` [PATCH v5 4/6] builtin/maintenance: introduce "worktree-prune" task Patrick Steinhardt
@ 2025-05-07  7:21   ` Patrick Steinhardt
  2025-05-07  7:21   ` [PATCH v5 6/6] builtin/maintenance: introduce "rerere-gc" task Patrick Steinhardt
  5 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-07  7:21 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee, Junio C Hamano, Eric Sunshine, Christian Couder

In a subsequent commit we are going to introduce a new "rerere-gc" task
for git-maintenance(1). To prepare for this, refactor the code that
spawns `git rerere gc` into a separate function.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/gc.c | 16 +++++++++++-----
 1 file changed, 11 insertions(+), 5 deletions(-)

diff --git a/builtin/gc.c b/builtin/gc.c
index e85e313d78f..0ae3071ec71 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -385,6 +385,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)
 {
 	/*
@@ -781,7 +790,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";
@@ -958,10 +966,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.1045.g170613ef41.dirty


^ permalink raw reply related	[flat|nested] 66+ messages in thread

* [PATCH v5 6/6] builtin/maintenance: introduce "rerere-gc" task
  2025-05-07  7:21 ` [PATCH v5 0/6] " Patrick Steinhardt
                     ` (4 preceding siblings ...)
  2025-05-07  7:21   ` [PATCH v5 5/6] builtin/gc: move rerere garbage collection into separate function Patrick Steinhardt
@ 2025-05-07  7:21   ` Patrick Steinhardt
  5 siblings, 0 replies; 66+ messages in thread
From: Patrick Steinhardt @ 2025-05-07  7:21 UTC (permalink / raw)
  To: git; +Cc: Derrick Stolee, Junio C Hamano, Eric Sunshine, Christian Couder

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 |  9 +++++++
 Documentation/git-maintenance.adoc    |  4 ++++
 builtin/gc.c                          | 37 +++++++++++++++++++++++++++++
 t/t7900-maintenance.sh                | 44 +++++++++++++++++++++++++++++++++++
 4 files changed, 94 insertions(+)

diff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc
index b36b62c1c47..2f719342183 100644
--- a/Documentation/config/maintenance.adoc
+++ b/Documentation/config/maintenance.adoc
@@ -84,6 +84,15 @@ 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, any positive
+	value implies the command will run when the "rr-cache" directory exists
+	and has at least one entry, regardless of whether it is stale or not.
+	This heuristic may be refined in the future. 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 0ae3071ec71..e79082f985d 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"
@@ -394,6 +396,35 @@ 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 one entry.
+	 */
+	repo_git_path_replace(the_repository, &path, "rr-cache");
+	dir = opendir(path.buf);
+	if (!dir)
+		goto out;
+	should_gc = !!readdir_skip_dot_and_dotdot(dir);
+
+out:
+	strbuf_release(&path);
+	if (dir)
+		closedir(dir);
+	return should_gc;
+}
+
 static int too_many_loose_objects(struct gc_config *cfg)
 {
 	/*
@@ -1501,6 +1532,7 @@ enum maintenance_task_label {
 	TASK_PACK_REFS,
 	TASK_REFLOG_EXPIRE,
 	TASK_WORKTREE_PRUNE,
+	TASK_RERERE_GC,
 
 	/* Leave as final value */
 	TASK__COUNT
@@ -1547,6 +1579,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 8f4120a0351..8cf89e285f4 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -564,6 +564,50 @@ 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 &&
+
+	# A positive value prunes when there is at least one entry.
+	test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&
+	mkdir .git/rr-cache &&
+	test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&
+	: >.git/rr-cache/entry-1 &&
+	test_expect_rerere_gc git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&
+
+	# Zero should never prune.
+	: >.git/rr-cache/entry-1 &&
+	test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 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.1045.g170613ef41.dirty


^ permalink raw reply related	[flat|nested] 66+ messages in thread

end of thread, other threads:[~2025-05-07  7:21 UTC | newest]

Thread overview: 66+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-04-25  7:29 [PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Patrick Steinhardt
2025-04-25  7:29 ` [PATCH 1/7] builtin/gc: fix indentation of `cmd_gc()` parameters Patrick Steinhardt
2025-04-25  7:29 ` [PATCH 2/7] builtin/gc: remove global variables where it trivial to do Patrick Steinhardt
2025-04-25  7:29 ` [PATCH 3/7] builtin/gc: move pruning of worktrees into a separate function Patrick Steinhardt
2025-04-25  7:29 ` [PATCH 4/7] worktree: expose function to retrieve worktree names Patrick Steinhardt
2025-04-25  7:29 ` [PATCH 5/7] builtin/maintenance: introduce "worktree-prune" task Patrick Steinhardt
2025-04-29 20:02   ` Derrick Stolee
2025-04-30  7:08     ` Patrick Steinhardt
2025-04-25  7:29 ` [PATCH 6/7] builtin/gc: move rerere garbage collection into separate function Patrick Steinhardt
2025-04-25  7:29 ` [PATCH 7/7] builtin/maintenance: introduce "rerere-gc" task Patrick Steinhardt
2025-04-29 20:02 ` [PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Derrick Stolee
2025-04-30  7:08   ` Patrick Steinhardt
2025-04-30 10:25 ` [PATCH v2 0/8] " Patrick Steinhardt
2025-04-30 10:25   ` [PATCH v2 1/8] builtin/gc: fix indentation of `cmd_gc()` parameters Patrick Steinhardt
2025-04-30 10:25   ` [PATCH v2 2/8] builtin/gc: remove global variables where it trivial to do Patrick Steinhardt
2025-04-30 10:25   ` [PATCH v2 3/8] builtin/gc: move pruning of worktrees into a separate function Patrick Steinhardt
2025-04-30 10:25   ` [PATCH v2 4/8] worktree: expose function to retrieve worktree names Patrick Steinhardt
2025-04-30 10:25   ` [PATCH v2 5/8] builtin/maintenance: introduce "worktree-prune" task Patrick Steinhardt
2025-04-30 10:25   ` [PATCH v2 6/8] rerere: provide function to collect stale entries Patrick Steinhardt
2025-04-30 16:58     ` Junio C Hamano
2025-05-02  8:07       ` Patrick Steinhardt
2025-05-02 16:35         ` Junio C Hamano
2025-05-05  7:22           ` Patrick Steinhardt
2025-04-30 10:25   ` [PATCH v2 7/8] builtin/gc: move rerere garbage collection into separate function Patrick Steinhardt
2025-04-30 10:25   ` [PATCH v2 8/8] builtin/maintenance: introduce "rerere-gc" task Patrick Steinhardt
2025-04-30 10:37   ` [PATCH v2 0/8] builtin/maintenance: implement missing tasks compared to git-gc(1) Derrick Stolee
2025-05-02  8:43 ` [PATCH v3 0/7] " Patrick Steinhardt
2025-05-02  8:43   ` [PATCH v3 1/7] builtin/gc: fix indentation of `cmd_gc()` parameters Patrick Steinhardt
2025-05-02  8:43   ` [PATCH v3 2/7] builtin/gc: remove global variables where it trivial to do Patrick Steinhardt
2025-05-02  8:44   ` [PATCH v3 3/7] builtin/gc: move pruning of worktrees into a separate function Patrick Steinhardt
2025-05-02  8:44   ` [PATCH v3 4/7] worktree: expose function to retrieve worktree names Patrick Steinhardt
2025-05-05  8:42     ` Eric Sunshine
2025-05-07  7:06       ` Patrick Steinhardt
2025-05-02  8:44   ` [PATCH v3 5/7] builtin/maintenance: introduce "worktree-prune" task Patrick Steinhardt
2025-05-05  8:59     ` Eric Sunshine
2025-05-07  7:06       ` Patrick Steinhardt
2025-05-02  8:44   ` [PATCH v3 6/7] builtin/gc: move rerere garbage collection into separate function Patrick Steinhardt
2025-05-02  8:44   ` [PATCH v3 7/7] builtin/maintenance: introduce "rerere-gc" task Patrick Steinhardt
2025-05-02 14:57   ` [PATCH v3 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Derrick Stolee
2025-05-02 21:07     ` Junio C Hamano
2025-05-05  7:32       ` Patrick Steinhardt
2025-05-05  8:51 ` [PATCH v4 " Patrick Steinhardt
2025-05-05  8:51   ` [PATCH v4 1/7] builtin/gc: fix indentation of `cmd_gc()` parameters Patrick Steinhardt
2025-05-05  8:51   ` [PATCH v4 2/7] builtin/gc: remove global variables where it trivial to do Patrick Steinhardt
2025-05-06  7:44     ` Christian Couder
2025-05-07  7:06       ` Patrick Steinhardt
2025-05-05  8:51   ` [PATCH v4 3/7] builtin/gc: move pruning of worktrees into a separate function Patrick Steinhardt
2025-05-06  7:50     ` Christian Couder
2025-05-07  7:06       ` Patrick Steinhardt
2025-05-05  8:51   ` [PATCH v4 4/7] worktree: expose function to retrieve worktree names Patrick Steinhardt
2025-05-06  8:20     ` Christian Couder
2025-05-06 16:08       ` Eric Sunshine
2025-05-05  8:51   ` [PATCH v4 5/7] builtin/maintenance: introduce "worktree-prune" task Patrick Steinhardt
2025-05-06  7:40     ` Christian Couder
2025-05-07  7:06       ` Patrick Steinhardt
2025-05-05  8:51   ` [PATCH v4 6/7] builtin/gc: move rerere garbage collection into separate function Patrick Steinhardt
2025-05-06  8:39     ` Christian Couder
2025-05-05  8:51   ` [PATCH v4 7/7] builtin/maintenance: introduce "rerere-gc" task Patrick Steinhardt
2025-05-06  9:05   ` [PATCH v4 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Christian Couder
2025-05-07  7:21 ` [PATCH v5 0/6] " Patrick Steinhardt
2025-05-07  7:21   ` [PATCH v5 1/6] builtin/gc: fix indentation of `cmd_gc()` parameters Patrick Steinhardt
2025-05-07  7:21   ` [PATCH v5 2/6] builtin/gc: remove global variables where it is trivial to do Patrick Steinhardt
2025-05-07  7:21   ` [PATCH v5 3/6] builtin/gc: move pruning of worktrees into a separate function Patrick Steinhardt
2025-05-07  7:21   ` [PATCH v5 4/6] builtin/maintenance: introduce "worktree-prune" task Patrick Steinhardt
2025-05-07  7:21   ` [PATCH v5 5/6] builtin/gc: move rerere garbage collection into separate function Patrick Steinhardt
2025-05-07  7:21   ` [PATCH v5 6/6] builtin/maintenance: introduce "rerere-gc" task Patrick Steinhardt

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).