rerere: provide function to collect stale entries

We're about to add another task for git-maintenance(1) that prunes stale
rerere entries via `git rerere gc`. The condition of when to run this
subcommand will be configurable so that the subcommand is only executed
when a certain number of stale rerere entries exists. This requires us
to know about the number of stale rerere entries in the first place,
which is non-trivial to figure out.

Refactor `rerere_gc()` and `prune_one()` so that garbage collection is
split into three phases:

  1. We collect any stale rerere entries and directories that are about
     to become empty.

  2. Prune all stale rerere entries.

  3. Remove all directories that should have become empty in (2).

By splitting out the collection of stale entries we can trivially expose
this function to external callers and thus reuse it in later steps.

This refactoring is not expected to result in a user-visible change in
behaviour.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Patrick Steinhardt 2025-04-30 12:25:10 +02:00 committed by Junio C Hamano
parent eae6763649
commit 91b34c4529
2 changed files with 78 additions and 28 deletions

View File

@ -1203,8 +1203,8 @@ static void unlink_rr_item(struct rerere_id *id)
strbuf_release(&buf);
}

static void prune_one(struct rerere_id *id,
timestamp_t cutoff_resolve, timestamp_t cutoff_noresolve)
static int is_stale(struct rerere_id *id,
timestamp_t cutoff_resolve, timestamp_t cutoff_noresolve)
{
timestamp_t then;
timestamp_t cutoff;
@ -1215,11 +1215,11 @@ static void prune_one(struct rerere_id *id,
else {
then = rerere_created_at(id);
if (!then)
return;
return 0;
cutoff = cutoff_noresolve;
}
if (then < cutoff)
unlink_rr_item(id);

return then < cutoff;
}

/* Does the basename in "path" look plausibly like an rr-cache entry? */
@ -1230,29 +1230,35 @@ static int is_rr_cache_dirname(const char *path)
return !parse_oid_hex(path, &oid, &end) && !*end;
}

void rerere_gc(struct repository *r, struct string_list *rr)
int rerere_collect_stale_entries(struct repository *r,
struct string_list *prunable_dirs,
struct rerere_id **prunable_entries,
size_t *prunable_entries_nr)
{
struct string_list to_remove = STRING_LIST_INIT_DUP;
DIR *dir;
struct dirent *e;
int i;
timestamp_t now = time(NULL);
timestamp_t cutoff_noresolve = now - 15 * 86400;
timestamp_t cutoff_resolve = now - 60 * 86400;
struct strbuf buf = STRBUF_INIT;
size_t prunable_entries_alloc;
struct dirent *e;
DIR *dir = NULL;
int ret;

if (setup_rerere(r, rr, 0) < 0)
return;
*prunable_entries = NULL;
*prunable_entries_nr = 0;
prunable_entries_alloc = 0;

repo_config_get_expiry_in_days(the_repository, "gc.rerereresolved",
repo_config_get_expiry_in_days(r, "gc.rerereresolved",
&cutoff_resolve, now);
repo_config_get_expiry_in_days(the_repository, "gc.rerereunresolved",
repo_config_get_expiry_in_days(r, "gc.rerereunresolved",
&cutoff_noresolve, now);
git_config(git_default_config, NULL);
dir = opendir(repo_git_path_replace(the_repository, &buf, "rr-cache"));
if (!dir)
die_errno(_("unable to open rr-cache directory"));
/* Collect stale conflict IDs ... */

dir = opendir(repo_git_path_replace(r, &buf, "rr-cache"));
if (!dir) {
ret = error_errno(_("unable to open rr-cache directory"));
goto out;
}

while ((e = readdir_skip_dot_and_dotdot(dir))) {
struct rerere_dir *rr_dir;
struct rerere_id id;
@ -1267,23 +1273,53 @@ void rerere_gc(struct repository *r, struct string_list *rr)
for (id.variant = 0, id.collection = rr_dir;
id.variant < id.collection->status_nr;
id.variant++) {
prune_one(&id, cutoff_resolve, cutoff_noresolve);
if (id.collection->status[id.variant])
if (is_stale(&id, cutoff_resolve, cutoff_noresolve)) {
ALLOC_GROW(*prunable_entries, *prunable_entries_nr + 1,
prunable_entries_alloc);
(*prunable_entries)[(*prunable_entries_nr)++] = id;
} else {
now_empty = 0;
}
}
if (now_empty)
string_list_append(&to_remove, e->d_name);
string_list_append(prunable_dirs, e->d_name);
}
closedir(dir);

/* ... and then remove the empty directories */
for (i = 0; i < to_remove.nr; i++)
rmdir(repo_git_path_replace(the_repository, &buf,
"rr-cache/%s", to_remove.items[i].string));
ret = 0;

string_list_clear(&to_remove, 0);
out:
strbuf_release(&buf);
if (dir)
closedir(dir);
return ret;
}

void rerere_gc(struct repository *r, struct string_list *rr)
{
struct string_list prunable_dirs = STRING_LIST_INIT_DUP;
struct rerere_id *prunable_entries;
struct strbuf buf = STRBUF_INIT;
size_t prunable_entries_nr;

if (setup_rerere(r, rr, 0) < 0)
return;

git_config(git_default_config, NULL);

if (rerere_collect_stale_entries(r, &prunable_dirs, &prunable_entries,
&prunable_entries_nr) < 0)
exit(127);

for (size_t i = 0; i < prunable_entries_nr; i++)
unlink_rr_item(&prunable_entries[i]);
for (size_t i = 0; i < prunable_dirs.nr; i++)
rmdir(repo_git_path_replace(r, &buf, "rr-cache/%s",
prunable_dirs.items[i].string));

string_list_clear(&prunable_dirs, 0);
rollback_lock_file(&write_lock);
strbuf_release(&buf);
free(prunable_entries);
}

/*

View File

@ -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), \