git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH v2 6/8] rerere: provide function to collect stale entries

From
Patrick Steinhardt <ps@pks.im>
Date
Apr 30, 2025, 10:25 UTC
Message-ID
<20250430-pks-maintenance-missing-tasks-v2-6-2580b7b8ca3a@pks.im>
In-Reply-To
<20250430-pks-maintenance-missing-tasks-v2-0-2580b7b8ca3a@pks.im>

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
Previous: Patrick SteinhardtNext: Junio C Hamano
Message 7 of 14 in “builtin/maintenance: implement missing tasks compared to git-gc(1)”
  1. 0/8 builtin/maintenance: implement missing tasks compared to git-gc(1)Patrick Steinhardt, Apr 30, 2025
  2. 1/8 builtin/gc: fix indentation of `cmd_gc()` parametersPatrick Steinhardt, Apr 30, 2025
  3. 2/8 builtin/gc: remove global variables where it trivial to doPatrick Steinhardt, Apr 30, 2025
  4. 3/8 builtin/gc: move pruning of worktrees into a separate functionPatrick Steinhardt, Apr 30, 2025
  5. 4/8 worktree: expose function to retrieve worktree namesPatrick Steinhardt, Apr 30, 2025
  6. 5/8 builtin/maintenance: introduce "worktree-prune" taskPatrick Steinhardt, Apr 30, 2025
  7. 6/8 rerere: provide function to collect stale entriesPatrick Steinhardt, Apr 30, 2025
  8. Junio C HamanoApr 30, 2025
  9. Patrick SteinhardtMay 2, 2025
  10. Junio C HamanoMay 2, 2025
  11. Patrick SteinhardtMay 5, 2025
  12. 7/8 builtin/gc: move rerere garbage collection into separate functionPatrick Steinhardt, Apr 30, 2025
  13. 8/8 builtin/maintenance: introduce "rerere-gc" taskPatrick Steinhardt, Apr 30, 2025
  14. Derrick StoleeApr 30, 2025

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.