{"thread":{"id":"66642","subject":"[PATCH 0/7] rerere: remove global state","startedAt":"2026-10-23T12:50:17Z","lastAt":"2026-10-26T07:28:21Z","messageCount":10,"participants":["Patrick Steinhardt","Junio C Hamano"],"isPatch":true,"patchVersion":1,"patchTotal":7},"messages":[{"id":"556009","messageId":"20261023-pks-rerere-wo-the-repository-v1-0-b6ac0c2e4b81@pks.im","threadId":"66642","inReplyTo":null,"subject":"[PATCH 0/7] rerere: remove global state","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-23T12:50:17Z","receivedAt":"2026-10-23T12:50:17Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nI got sucked down a rabbit hole once again while reviewing\ntb/rerere-wait-for-merge-rr-lock. I originally wanted to refactor the\nrerere subsystem to use subcommands, and I did. But while doing those\nrefactorings I also started getting rid of global state, and that has\nexpanded in scope more than I was originally expecting.\n\nSo I split out this refactoring into its separate patch series, which is\nthis one here. On the one hand it gets rid of `the_repository`. On the\nother hand it also refactors \"rerere.c\" to not track state via global\nvariables anymore so that it can be used multiple times for different\nrepositories.\n\nThis series is based on 7e34d8174d (The 8th batch, 2026-10-22) with\ntb/rerere-wait-for-merge-rr-lock at 5f6b6aa3ab (rerere: add \"gc\n--skip-locked\" for auto maintenance, 2026-10-13) merged into it.\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (7):\n      rerere: stop using `the_repository` in `rerere_path()`\n      rerere: stop using `the_repository` in internal helpers\n      rerere: remove redundant calls to `repo_config()`\n      rerere: cache result of `is_rerere_enabled()`\n      rerere: stop using `the_repository`\n      rerere: bundle global state into a struct\n      rerere: get rid of global rerere cache state\n\n builtin/rerere.c |  21 ++--\n hex.c            |   4 +-\n hex.h            |   3 +\n repository.c     |   1 +\n repository.h     |   6 ++\n rerere.c         | 309 ++++++++++++++++++++++++++++++-------------------------\n rerere.h         |  23 ++++-\n 7 files changed, 216 insertions(+), 151 deletions(-)\n\n\n---\nbase-commit: 13ef06d9d38088a2d97f612dca15fcd472f7b8ed\nchange-id: 20261022-pks-rerere-wo-the-repository-f6620accf4ee\n\n\n"},{"id":"556010","messageId":"20261023-pks-rerere-wo-the-repository-v1-1-b6ac0c2e4b81@pks.im","threadId":"66642","inReplyTo":"20261023-pks-rerere-wo-the-repository-v1-0-b6ac0c2e4b81@pks.im","subject":"[PATCH 1/7] rerere: stop using `the_repository` in `rerere_path()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-23T12:50:18Z","receivedAt":"2026-10-23T12:50:18Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The function `rerere_path()` computes paths of rerere cache entries\ncontained in the \"rr-cache\" directory of `the_repository`. This makes us\nimplicitly depend on global state.\n\nAdd a repository parameter so that callers can specify which repository\nto compute the path for. Adjust the internal helpers that call it to\npass down the repository they already have available in their contexts.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/rerere.c |  6 +++--\n rerere.c         | 82 +++++++++++++++++++++++++++++---------------------------\n rerere.h         |  3 ++-\n 3 files changed, 48 insertions(+), 43 deletions(-)\n\ndiff --git a/builtin/rerere.c b/builtin/rerere.c\nindex ca69505283..e900e302c3 100644\n--- a/builtin/rerere.c\n+++ b/builtin/rerere.c\n@@ -129,8 +129,10 @@ int cmd_rerere(int argc,\n \t\tfor (size_t i = 0; i < merge_rr.nr; i++) {\n \t\t\tconst char *path = merge_rr.items[i].string;\n \t\t\tconst struct rerere_id *id = merge_rr.items[i].util;\n-\t\t\tif (diff_two(rerere_path(&buf, id, \"preimage\"), path, path, path))\n-\t\t\t\tdie(_(\"unable to generate diff for '%s'\"), rerere_path(&buf, id, NULL));\n+\t\t\tif (diff_two(rerere_path(the_repository, &buf, id, \"preimage\"),\n+\t\t\t\t     path, path, path))\n+\t\t\t\tdie(_(\"unable to generate diff for '%s'\"),\n+\t\t\t\t    rerere_path(the_repository, &buf, id, NULL));\n \t\t}\n \n \t\tstrbuf_release(&buf);\ndiff --git a/rerere.c b/rerere.c\nindex 7fc32f2cf0..356111838c 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -95,17 +95,18 @@ static void assign_variant(struct rerere_id *id)\n \tid->variant = variant;\n }\n \n-const char *rerere_path(struct strbuf *buf, const struct rerere_id *id, const char *file)\n+const char *rerere_path(struct repository *repo,\n+\t\t\tstruct strbuf *buf, const struct rerere_id *id, const char *file)\n {\n \tif (!file)\n-\t\treturn repo_git_path_replace(the_repository, buf, \"rr-cache/%s\",\n+\t\treturn repo_git_path_replace(repo, buf, \"rr-cache/%s\",\n \t\t\t\t\t     rerere_id_hex(id));\n \n \tif (id->variant <= 0)\n-\t\treturn repo_git_path_replace(the_repository, buf, \"rr-cache/%s/%s\",\n+\t\treturn repo_git_path_replace(repo, buf, \"rr-cache/%s/%s\",\n \t\t\t\t\t     rerere_id_hex(id), file);\n \n-\treturn repo_git_path_replace(the_repository, buf, \"rr-cache/%s/%s.%d\",\n+\treturn repo_git_path_replace(repo, buf, \"rr-cache/%s/%s.%d\",\n \t\t\t\t     rerere_id_hex(id), file, id->variant);\n }\n \n@@ -609,8 +610,8 @@ static int try_merge(struct index_state *istate,\n \tmmfile_t base = {NULL, 0}, other = {NULL, 0};\n \tstruct strbuf buf = STRBUF_INIT;\n \n-\tif (read_mmfile(&base, rerere_path(&buf, id, \"preimage\")) ||\n-\t    read_mmfile(&other, rerere_path(&buf, id, \"postimage\"))) {\n+\tif (read_mmfile(&base, rerere_path(istate->repo, &buf, id, \"preimage\")) ||\n+\t    read_mmfile(&other, rerere_path(istate->repo, &buf, id, \"postimage\"))) {\n \t\tret = LL_MERGE_CONFLICT;\n \t} else {\n \t\t/*\n@@ -650,8 +651,8 @@ static int merge(struct index_state *istate, const struct rerere_id *id, const c\n \t * Normalize the conflicts in path and write it out to\n \t * \"thisimage\" temporary file.\n \t */\n-\tif ((handle_file(istate, path, NULL, rerere_path(&buf, id, \"thisimage\")) < 0) ||\n-\t    read_mmfile(&cur, rerere_path(&buf, id, \"thisimage\"))) {\n+\tif ((handle_file(istate, path, NULL, rerere_path(istate->repo, &buf, id, \"thisimage\")) < 0) ||\n+\t    read_mmfile(&cur, rerere_path(istate->repo, &buf, id, \"thisimage\"))) {\n \t\tret = 1;\n \t\tgoto out;\n \t}\n@@ -664,9 +665,9 @@ static int merge(struct index_state *istate, const struct rerere_id *id, const c\n \t * A successful replay of recorded resolution.\n \t * Mark that \"postimage\" was used to help gc.\n \t */\n-\tif (utime(rerere_path(&buf, id, \"postimage\"), NULL) < 0)\n+\tif (utime(rerere_path(istate->repo, &buf, id, \"postimage\"), NULL) < 0)\n \t\twarning_errno(_(\"failed utime() on '%s'\"),\n-\t\t\t      rerere_path(&buf, id, \"postimage\"));\n+\t\t\t      rerere_path(istate->repo, &buf, id, \"postimage\"));\n \n \t/* Update \"path\" with the resolution */\n \tf = fopen(path, \"w\");\n@@ -705,11 +706,11 @@ static void update_paths(struct repository *r, struct string_list *update)\n \t\tdie(_(\"unable to write new index file\"));\n }\n \n-static void remove_variant(struct rerere_id *id)\n+static void remove_variant(struct repository *r, struct rerere_id *id)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n-\tunlink_or_warn(rerere_path(&buf, id, \"postimage\"));\n-\tunlink_or_warn(rerere_path(&buf, id, \"preimage\"));\n+\tunlink_or_warn(rerere_path(r, &buf, id, \"postimage\"));\n+\tunlink_or_warn(rerere_path(r, &buf, id, \"preimage\"));\n \tid->collection->status[id->variant] = 0;\n \tstrbuf_release(&buf);\n }\n@@ -743,7 +744,7 @@ static void do_rerere_one_path(struct index_state *istate,\n \t\t\tconst int had_postimage =\n \t\t\t\tid->collection->status[variant] & RR_HAS_POSTIMAGE;\n \t\t\tconst char *postimage =\n-\t\t\t\trerere_path(&buf, id, \"postimage\");\n+\t\t\t\trerere_path(istate->repo, &buf, id, \"postimage\");\n \n \t\t\tif (copy_file(the_repository,\n \t\t\t\t      postimage,\n@@ -784,7 +785,7 @@ static void do_rerere_one_path(struct index_state *istate,\n \t\t * cleanly, there is no point maintaining our own variant.\n \t\t */\n \t\tif (0 <= id->variant && id->variant != variant)\n-\t\t\tremove_variant(id);\n+\t\t\tremove_variant(istate->repo, id);\n \n \t\tif (rerere_autoupdate)\n \t\t\tstring_list_insert(update, path);\n@@ -802,10 +803,10 @@ static void do_rerere_one_path(struct index_state *istate,\n \n \tvariant = id->variant;\n \tif (handle_file(istate, path, NULL,\n-\t\t\trerere_path(&buf, id, \"preimage\")) < 0)\n+\t\t\trerere_path(istate->repo, &buf, id, \"preimage\")) < 0)\n \t\tgoto out;\n \tif (id->collection->status[variant] & RR_HAS_POSTIMAGE) {\n-\t\tconst char *path = rerere_path(&buf, id, \"postimage\");\n+\t\tconst char *path = rerere_path(istate->repo, &buf, id, \"postimage\");\n \t\tif (unlink(path))\n \t\t\tdie_errno(_(\"cannot unlink stray '%s'\"), path);\n \t\tid->collection->status[variant] &= ~RR_HAS_POSTIMAGE;\n@@ -846,7 +847,7 @@ static int do_plain_rerere(struct repository *r,\n \t\t */\n \t\tret = handle_file(r->index, path, hash, NULL);\n \t\tif (ret != 0 && string_list_has_string(rr, path)) {\n-\t\t\tremove_variant(string_list_lookup(rr, path)->util);\n+\t\t\tremove_variant(r, string_list_lookup(rr, path)->util);\n \t\t\tstring_list_remove(rr, path, 1);\n \t\t}\n \t\tif (ret < 1)\n@@ -856,7 +857,7 @@ static int do_plain_rerere(struct repository *r,\n \t\tstring_list_insert(rr, path)->util = id;\n \n \t\t/* Ensure that the directory exists. */\n-\t\tsafe_create_dir_in_gitdir(the_repository, rerere_path(&buf, id, NULL));\n+\t\tsafe_create_dir_in_gitdir(the_repository, rerere_path(r, &buf, id, NULL));\n \t}\n \n \tfor (i = 0; i < rr->nr; i++)\n@@ -1091,8 +1092,8 @@ static int rerere_forget_one_path(struct index_state *istate,\n \t\tif (!has_rerere_resolution(id))\n \t\t\tcontinue;\n \n-\t\thandle_cache(istate, path, hash, rerere_path(&buf, id, \"thisimage\"));\n-\t\tif (read_mmfile(&cur, rerere_path(&buf, id, \"thisimage\"))) {\n+\t\thandle_cache(istate, path, hash, rerere_path(istate->repo, &buf, id, \"thisimage\"));\n+\t\tif (read_mmfile(&cur, rerere_path(istate->repo, &buf, id, \"thisimage\"))) {\n \t\t\terror(_(\"failed to update conflicted state in '%s'\"), path);\n \t\t\tgoto fail_exit;\n \t\t}\n@@ -1108,7 +1109,7 @@ static int rerere_forget_one_path(struct index_state *istate,\n \t\tgoto fail_exit;\n \t}\n \n-\tfilename = rerere_path(&buf, id, \"postimage\");\n+\tfilename = rerere_path(istate->repo, &buf, id, \"postimage\");\n \tif (unlink(filename)) {\n \t\tif (errno == ENOENT)\n \t\t\terror(_(\"no remembered resolution for '%s'\"), path);\n@@ -1122,7 +1123,7 @@ static int rerere_forget_one_path(struct index_state *istate,\n \t * conflict in the working tree, run us again to record\n \t * the postimage.\n \t */\n-\thandle_cache(istate, path, hash, rerere_path(&buf, id, \"preimage\"));\n+\thandle_cache(istate, path, hash, rerere_path(istate->repo, &buf, id, \"preimage\"));\n \tfprintf_ln(stderr, _(\"Updated preimage for '%s'\"), path);\n \n \t/*\n@@ -1181,25 +1182,25 @@ int rerere_forget(struct repository *r, struct pathspec *pathspec)\n  * Garbage collection support\n  */\n \n-static timestamp_t rerere_created_at(struct rerere_id *id)\n+static timestamp_t rerere_created_at(struct repository *r, struct rerere_id *id)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n \tstruct stat st;\n \ttimestamp_t ret;\n \n-\tret = stat(rerere_path(&buf, id, \"preimage\"), &st) ? (time_t) 0 : st.st_mtime;\n+\tret = stat(rerere_path(r, &buf, id, \"preimage\"), &st) ? (time_t) 0 : st.st_mtime;\n \n \tstrbuf_release(&buf);\n \treturn ret;\n }\n \n-static timestamp_t rerere_last_used_at(struct rerere_id *id)\n+static timestamp_t rerere_last_used_at(struct repository *r, struct rerere_id *id)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n \tstruct stat st;\n \ttimestamp_t ret;\n \n-\tret = stat(rerere_path(&buf, id, \"postimage\"), &st) ? (time_t) 0 : st.st_mtime;\n+\tret = stat(rerere_path(r, &buf, id, \"postimage\"), &st) ? (time_t) 0 : st.st_mtime;\n \n \tstrbuf_release(&buf);\n \treturn ret;\n@@ -1208,11 +1209,11 @@ static timestamp_t rerere_last_used_at(struct rerere_id *id)\n /*\n  * Remove the recorded resolution for a given conflict ID\n  */\n-static void unlink_rr_item(struct rerere_id *id)\n+static void unlink_rr_item(struct repository *r, struct rerere_id *id)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n-\tunlink_or_warn(rerere_path(&buf, id, \"thisimage\"));\n-\tremove_variant(id);\n+\tunlink_or_warn(rerere_path(r, &buf, id, \"thisimage\"));\n+\tremove_variant(r, id);\n \tid->collection->status[id->variant] = 0;\n \tstrbuf_release(&buf);\n }\n@@ -1231,18 +1232,19 @@ static void rerere_gc_cutoffs(struct repository *r,\n \t\t*cutoff_noresolve = now - 15 * 86400;\n }\n \n-static bool rerere_id_is_stale(struct rerere_id *id,\n+static bool rerere_id_is_stale(struct repository *r,\n+\t\t\t       struct rerere_id *id,\n \t\t\t       timestamp_t cutoff_resolve,\n \t\t\t       timestamp_t cutoff_noresolve)\n {\n \ttimestamp_t then;\n \ttimestamp_t cutoff;\n \n-\tthen = rerere_last_used_at(id);\n+\tthen = rerere_last_used_at(r, id);\n \tif (then) {\n \t\tcutoff = cutoff_resolve;\n \t} else {\n-\t\tthen = rerere_created_at(id);\n+\t\tthen = rerere_created_at(r, id);\n \t\tif (!then)\n \t\t\treturn false;\n \t\tcutoff = cutoff_noresolve;\n@@ -1251,11 +1253,11 @@ static bool rerere_id_is_stale(struct rerere_id *id,\n \treturn then < cutoff;\n }\n \n-static void prune_one(struct rerere_id *id,\n+static void prune_one(struct repository *r, struct rerere_id *id,\n \t\t      timestamp_t cutoff_resolve, timestamp_t cutoff_noresolve)\n {\n-\tif (rerere_id_is_stale(id, cutoff_resolve, cutoff_noresolve))\n-\t\tunlink_rr_item(id);\n+\tif (rerere_id_is_stale(r, id, cutoff_resolve, cutoff_noresolve))\n+\t\tunlink_rr_item(r, id);\n }\n \n /* Does the basename in \"path\" look plausibly like an rr-cache entry? */\n@@ -1297,7 +1299,7 @@ bool rerere_gc_needed(struct repository *r, size_t limit)\n \t\tfor (id.variant = 0;\n \t\t     id.variant < id.collection->status_nr;\n \t\t     id.variant++) {\n-\t\t\tif (rerere_id_is_stale(&id, cutoff_resolve,\n+\t\t\tif (rerere_id_is_stale(r, &id, cutoff_resolve,\n \t\t\t\t\t       cutoff_noresolve)) {\n \t\t\t\tcount += 256;\n \t\t\t\tif (count >= limit) {\n@@ -1349,7 +1351,7 @@ void rerere_gc(struct repository *r, struct string_list *rr, int flags)\n \t\tfor (id.variant = 0, id.collection = rr_dir;\n \t\t     id.variant < id.collection->status_nr;\n \t\t     id.variant++) {\n-\t\t\tprune_one(&id, cutoff_resolve, cutoff_noresolve);\n+\t\t\tprune_one(r, &id, cutoff_resolve, cutoff_noresolve);\n \t\t\tif (id.collection->status[id.variant])\n \t\t\t\tnow_empty = 0;\n \t\t}\n@@ -1387,8 +1389,8 @@ void rerere_clear(struct repository *r, struct string_list *merge_rr)\n \t\tstruct strbuf buf = STRBUF_INIT;\n \n \t\tif (!has_rerere_resolution(id)) {\n-\t\t\tunlink_rr_item(id);\n-\t\t\trmdir(rerere_path(&buf, id, NULL));\n+\t\t\tunlink_rr_item(r, id);\n+\t\t\trmdir(rerere_path(r, &buf, id, NULL));\n \t\t}\n \n \t\tstrbuf_release(&buf);\ndiff --git a/rerere.h b/rerere.h\nindex 1182ee3fb1..ee8d97917d 100644\n--- a/rerere.h\n+++ b/rerere.h\n@@ -34,7 +34,8 @@ int repo_rerere(struct repository *, int);\n  * path to that filesystem entity.  With \"file\" specified with NULL,\n  * return the path to the directory that houses these files.\n  */\n-const char *rerere_path(struct strbuf *buf, const struct rerere_id *,\n+const char *rerere_path(struct repository *,\n+\t\t\tstruct strbuf *buf, const struct rerere_id *,\n \t\t\tconst char *file);\n int rerere_forget(struct repository *, struct pathspec *);\n int rerere_remaining(struct repository *, struct string_list *);\n\n-- \n2.56.0.307.g5fe7bd680c.dirty\n\n\n"},{"id":"556011","messageId":"20261023-pks-rerere-wo-the-repository-v1-2-b6ac0c2e4b81@pks.im","threadId":"66642","inReplyTo":"20261023-pks-rerere-wo-the-repository-v1-0-b6ac0c2e4b81@pks.im","subject":"[PATCH 2/7] rerere: stop using `the_repository` in internal helpers","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-23T12:50:19Z","receivedAt":"2026-10-23T12:50:19Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Most of the static helpers in \"rerere.c\" still rely on `the_repository`\neven though their callers already have a `struct repository` available.\nThread the repository through those helpers so that we can get rid of\nreferences to that global variable.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n rerere.c | 69 ++++++++++++++++++++++++++++++++--------------------------------\n 1 file changed, 35 insertions(+), 34 deletions(-)\n\ndiff --git a/rerere.c b/rerere.c\nindex 356111838c..c568bd5e89 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -129,13 +129,13 @@ static int is_rr_file(const char *name, const char *filename, int *variant)\n \treturn 1;\n }\n \n-static void scan_rerere_dir(struct rerere_dir *rr_dir)\n+static void scan_rerere_dir(struct repository *r, struct rerere_dir *rr_dir)\n {\n \tstruct dirent *de;\n \tchar *path;\n \tDIR *dir;\n \n-\tpath = repo_git_path(the_repository, \"rr-cache/%s\", rr_dir->name);\n+\tpath = repo_git_path(r, \"rr-cache/%s\", rr_dir->name);\n \tdir = opendir(path);\n \tfree(path);\n \tif (!dir)\n@@ -154,7 +154,7 @@ static void scan_rerere_dir(struct rerere_dir *rr_dir)\n \tclosedir(dir);\n }\n \n-static struct rerere_dir *find_rerere_dir(const char *hex)\n+static struct rerere_dir *find_rerere_dir(struct repository *r, const char *hex)\n {\n \tstruct rerere_dir *rr_dir;\n \n@@ -166,7 +166,7 @@ static struct rerere_dir *find_rerere_dir(const char *hex)\n \t\trr_dir->status_alloc = 0;\n \t\tstrmap_put(&rerere_dirs, hex, rr_dir);\n \n-\t\tscan_rerere_dir(rr_dir);\n+\t\tscan_rerere_dir(r, rr_dir);\n \t}\n \treturn rr_dir;\n }\n@@ -181,17 +181,17 @@ static int has_rerere_resolution(const struct rerere_id *id)\n \treturn ((id->collection->status[variant] & both) == both);\n }\n \n-static struct rerere_id *new_rerere_id_hex(char *hex)\n+static struct rerere_id *new_rerere_id_hex(struct repository *r, char *hex)\n {\n \tstruct rerere_id *id = xmalloc(sizeof(*id));\n-\tid->collection = find_rerere_dir(hex);\n+\tid->collection = find_rerere_dir(r, hex);\n \tid->variant = -1; /* not known yet */\n \treturn id;\n }\n \n-static struct rerere_id *new_rerere_id(unsigned char *hash)\n+static struct rerere_id *new_rerere_id(struct repository *r, unsigned char *hash)\n {\n-\treturn new_rerere_id_hex(hash_to_hex(hash));\n+\treturn new_rerere_id_hex(r, hash_to_hex(hash));\n }\n \n /*\n@@ -213,7 +213,7 @@ static void read_rr(struct repository *r, struct string_list *rr)\n \t\tunsigned char hash[GIT_MAX_RAWSZ];\n \t\tstruct rerere_id *id;\n \t\tint variant;\n-\t\tconst unsigned hexsz = the_hash_algo->hexsz;\n+\t\tconst unsigned hexsz = r->hash_algo->hexsz;\n \n \t\t/* There has to be the hash, tab, path and then NUL */\n \t\tif (buf.len < hexsz + 2 || get_hash_hex(buf.buf, hash))\n@@ -231,7 +231,7 @@ static void read_rr(struct repository *r, struct string_list *rr)\n \t\tif (*(path++) != '\\t')\n \t\t\tdie(_(\"corrupt MERGE_RR\"));\n \t\tbuf.buf[hexsz] = '\\0';\n-\t\tid = new_rerere_id_hex(buf.buf);\n+\t\tid = new_rerere_id_hex(r, buf.buf);\n \t\tid->variant = variant;\n \t\t/*\n \t\t * make sure id->collection->status has enough space\n@@ -411,13 +411,14 @@ static int handle_conflict(struct strbuf *out, struct rerere_io *io,\n  * Return 1 if conflict hunks are found, 0 if there are no conflict\n  * hunks and -1 if an error occurred.\n  */\n-static int handle_path(unsigned char *hash, struct rerere_io *io, int marker_size)\n+static int handle_path(struct repository *r,\n+\t\t       unsigned char *hash, struct rerere_io *io, int marker_size)\n {\n \tstruct git_hash_ctx ctx;\n \tstruct strbuf buf = STRBUF_INIT, out = STRBUF_INIT;\n \tint has_conflicts = 0;\n \tif (hash)\n-\t\tgit_hash_init(&ctx, the_hash_algo);\n+\t\tgit_hash_init(&ctx, r->hash_algo);\n \n \twhile (!io->getline(&buf, io)) {\n \t\tif (is_conflict_marker_line(buf.buf, buf.len, marker_size) == '<') {\n@@ -466,7 +467,7 @@ static int handle_file(struct index_state *istate,\n \t\t}\n \t}\n \n-\thas_conflicts = handle_path(hash, (struct rerere_io *)&io, marker_size);\n+\thas_conflicts = handle_path(istate->repo, hash, (struct rerere_io *)&io, marker_size);\n \n \tfclose(io.input);\n \tif (io.io.wrerror)\n@@ -746,7 +747,7 @@ static void do_rerere_one_path(struct index_state *istate,\n \t\t\tconst char *postimage =\n \t\t\t\trerere_path(istate->repo, &buf, id, \"postimage\");\n \n-\t\t\tif (copy_file(the_repository,\n+\t\t\tif (copy_file(istate->repo,\n \t\t\t\t      postimage,\n \t\t\t\t      path, 0666)) {\n \t\t\t\tif (!had_postimage)\n@@ -853,11 +854,11 @@ static int do_plain_rerere(struct repository *r,\n \t\tif (ret < 1)\n \t\t\tcontinue;\n \n-\t\tid = new_rerere_id(hash);\n+\t\tid = new_rerere_id(r, hash);\n \t\tstring_list_insert(rr, path)->util = id;\n \n \t\t/* Ensure that the directory exists. */\n-\t\tsafe_create_dir_in_gitdir(the_repository, rerere_path(r, &buf, id, NULL));\n+\t\tsafe_create_dir_in_gitdir(r, rerere_path(r, &buf, id, NULL));\n \t}\n \n \tfor (i = 0; i < rr->nr; i++)\n@@ -872,18 +873,17 @@ static int do_plain_rerere(struct repository *r,\n \treturn write_rr(rr, fd);\n }\n \n-static void git_rerere_config(void)\n+static void git_rerere_config(struct repository *r)\n {\n-\trepo_config_get_bool(the_repository, \"rerere.enabled\", &rerere_enabled);\n-\trepo_config_get_bool(the_repository, \"rerere.autoupdate\", &rerere_autoupdate);\n-\trepo_config_get_int(the_repository, \"rerere.locktimeout\",\n-\t\t\t    &rerere_lock_timeout_ms);\n-\trepo_config(the_repository, git_default_config, NULL);\n+\trepo_config_get_bool(r, \"rerere.enabled\", &rerere_enabled);\n+\trepo_config_get_bool(r, \"rerere.autoupdate\", &rerere_autoupdate);\n+\trepo_config_get_int(r, \"rerere.locktimeout\", &rerere_lock_timeout_ms);\n+\trepo_config(r, git_default_config, NULL);\n }\n \n static GIT_PATH_FUNC(git_path_rr_cache, \"rr-cache\")\n \n-static int is_rerere_enabled(void)\n+static int is_rerere_enabled(struct repository *r)\n {\n \tint rr_cache_exists;\n \n@@ -895,7 +895,7 @@ static int is_rerere_enabled(void)\n \t\treturn rr_cache_exists;\n \n \tif (!rr_cache_exists &&\n-\t    safe_create_dir_in_gitdir(the_repository, git_path_rr_cache()))\n+\t    safe_create_dir_in_gitdir(r, git_path_rr_cache()))\n \t\tdie(_(\"could not create directory '%s'\"), git_path_rr_cache());\n \treturn 1;\n }\n@@ -904,8 +904,8 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n {\n \tint fd;\n \n-\tgit_rerere_config();\n-\tif (!is_rerere_enabled())\n+\tgit_rerere_config(r);\n+\tif (!is_rerere_enabled(r))\n \t\treturn -1;\n \n \tif (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))\n@@ -1017,7 +1017,7 @@ static int handle_cache(struct index_state *istate,\n \t\t\tbreak;\n \t\ti = ce_stage(ce) - 1;\n \t\tif (!mmfile[i].ptr) {\n-\t\t\tmmfile[i].ptr = odb_read_object(the_repository->objects,\n+\t\t\tmmfile[i].ptr = odb_read_object(istate->repo->objects,\n \t\t\t\t\t\t\t&ce->oid, &type, &size);\n \t\t\tif (!mmfile[i].ptr)\n \t\t\t\tdie(_(\"unable to read %s\"),\n@@ -1053,7 +1053,8 @@ static int handle_cache(struct index_state *istate,\n \t * Grab the conflict ID and optionally write the original\n \t * contents with conflict markers out.\n \t */\n-\thas_conflicts = handle_path(hash, (struct rerere_io *)&io, marker_size);\n+\thas_conflicts = handle_path(istate->repo, hash,\n+\t\t\t\t    (struct rerere_io *)&io, marker_size);\n \tstrbuf_release(&io.input);\n \tif (io.io.output)\n \t\tfclose(io.io.output);\n@@ -1080,7 +1081,7 @@ static int rerere_forget_one_path(struct index_state *istate,\n \t\treturn error(_(\"could not parse conflict hunks in '%s'\"), path);\n \n \t/* Nuke the recorded resolution for the conflict */\n-\tid = new_rerere_id(hash);\n+\tid = new_rerere_id(istate->repo, hash);\n \n \tfor (id->variant = 0;\n \t     id->variant < id->collection->status_nr;\n@@ -1295,7 +1296,7 @@ bool rerere_gc_needed(struct repository *r, size_t limit)\n \t\t    !is_rr_cache_dirname(e->d_name))\n \t\t\tcontinue;\n \n-\t\tid.collection = find_rerere_dir(e->d_name);\n+\t\tid.collection = find_rerere_dir(r, e->d_name);\n \t\tfor (id.variant = 0;\n \t\t     id.variant < id.collection->status_nr;\n \t\t     id.variant++) {\n@@ -1332,8 +1333,8 @@ void rerere_gc(struct repository *r, struct string_list *rr, int flags)\n \t\treturn;\n \n \trerere_gc_cutoffs(r, &cutoff_resolve, &cutoff_noresolve);\n-\trepo_config(the_repository, git_default_config, NULL);\n-\tdir = opendir(repo_git_path_replace(the_repository, &buf, \"rr-cache\"));\n+\trepo_config(r, git_default_config, NULL);\n+\tdir = opendir(repo_git_path_replace(r, &buf, \"rr-cache\"));\n \tif (!dir)\n \t\tdie_errno(_(\"unable to open rr-cache directory\"));\n \t/* Collect stale conflict IDs ... */\n@@ -1345,7 +1346,7 @@ void rerere_gc(struct repository *r, struct string_list *rr, int flags)\n \t\tif (!is_rr_cache_dirname(e->d_name))\n \t\t\tcontinue; /* or should we remove e->d_name? */\n \n-\t\trr_dir = find_rerere_dir(e->d_name);\n+\t\trr_dir = find_rerere_dir(r, e->d_name);\n \n \t\tnow_empty = 1;\n \t\tfor (id.variant = 0, id.collection = rr_dir;\n@@ -1362,7 +1363,7 @@ void rerere_gc(struct repository *r, struct string_list *rr, int flags)\n \n \t/* ... and then remove the empty directories */\n \tfor (i = 0; i < to_remove.nr; i++)\n-\t\trmdir(repo_git_path_replace(the_repository, &buf,\n+\t\trmdir(repo_git_path_replace(r, &buf,\n \t\t\t\t\t    \"rr-cache/%s\", to_remove.items[i].string));\n \n \tstring_list_clear(&to_remove, 0);\n\n-- \n2.56.0.307.g5fe7bd680c.dirty\n\n\n"},{"id":"556012","messageId":"20261023-pks-rerere-wo-the-repository-v1-3-b6ac0c2e4b81@pks.im","threadId":"66642","inReplyTo":"20261023-pks-rerere-wo-the-repository-v1-0-b6ac0c2e4b81@pks.im","subject":"[PATCH 3/7] rerere: remove redundant calls to `repo_config()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-23T12:50:20Z","receivedAt":"2026-10-23T12:50:20Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When setting up the rerere cache we eventually end up calling\n`repo_config()`. This call is redundant though, as all callers of\n`setup_rerere()` already called `repo_config()` beforehand.\n\nNext to being redundant though it also feels like a layering violation.\nThe intent of that function is to set up state for the rerere cache, but\nwe potentially end up modifying other global state, too. This is usually\nfine, but it is surprising to say the very least. If a caller for\nexample were to call `repo_config()` and then modifies the parsed config\nmanually then those modifications would be lost when calling\n`setup_rerere()`.\n\nDespite the layering violation though it will also create problems with\na subsequent commit, where we are about to cache whether or not the\nrerere cache is enabled. As part of that change we would stop calling\n`repo_config()` when the rerere cache is disabled, but continue to call\nit when it's enabled. So the above layering violation is essentially\nbecoming even worse, because we may or may not clobber config values.\n\nDrop the call, as well as another redundant call in `rerere_gc()`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n rerere.c | 3 +--\n 1 file changed, 1 insertion(+), 2 deletions(-)\n\ndiff --git a/rerere.c b/rerere.c\nindex c568bd5e89..56b6ef547b 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -878,7 +878,6 @@ static void git_rerere_config(struct repository *r)\n \trepo_config_get_bool(r, \"rerere.enabled\", &rerere_enabled);\n \trepo_config_get_bool(r, \"rerere.autoupdate\", &rerere_autoupdate);\n \trepo_config_get_int(r, \"rerere.locktimeout\", &rerere_lock_timeout_ms);\n-\trepo_config(r, git_default_config, NULL);\n }\n \n static GIT_PATH_FUNC(git_path_rr_cache, \"rr-cache\")\n@@ -1333,7 +1332,7 @@ void rerere_gc(struct repository *r, struct string_list *rr, int flags)\n \t\treturn;\n \n \trerere_gc_cutoffs(r, &cutoff_resolve, &cutoff_noresolve);\n-\trepo_config(r, git_default_config, NULL);\n+\n \tdir = opendir(repo_git_path_replace(r, &buf, \"rr-cache\"));\n \tif (!dir)\n \t\tdie_errno(_(\"unable to open rr-cache directory\"));\n\n-- \n2.56.0.307.g5fe7bd680c.dirty\n\n\n"},{"id":"556013","messageId":"20261023-pks-rerere-wo-the-repository-v1-4-b6ac0c2e4b81@pks.im","threadId":"66642","inReplyTo":"20261023-pks-rerere-wo-the-repository-v1-0-b6ac0c2e4b81@pks.im","subject":"[PATCH 4/7] rerere: cache result of `is_rerere_enabled()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-23T12:50:21Z","receivedAt":"2026-10-23T12:50:21Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The function `setup_rerere()` reads the rerere configuration and then\nfigures out whether the rerere cache is enabled after all, either\nbecause it has been explicitly enabled or because it's implicitly\nenabled and \".git/rr-cache\" exists. Whether the cache is enabled will\nnot change in a single process though, so it does not make sense to\nreevaluate these conditions every single time `setup_rerere()` is\ncalled.\n\nAdapt the function to instead cache the status of the rerere cache via\na new `struct repository::rerere_cache_enabled` member. This allows us\nto get rid of the `GIT_PATH_FUNC()`, which caches the path to the rerere\ncache directory. But now that we already cache the enabled state in the\nfirst place it becomes moot to cache the path, as well.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n repository.c |  1 +\n repository.h |  6 ++++++\n rerere.c     | 43 +++++++++++++++++++++++--------------------\n 3 files changed, 30 insertions(+), 20 deletions(-)\n\ndiff --git a/repository.c b/repository.c\nindex e584d6c405..f51699a6dd 100644\n--- a/repository.c\n+++ b/repository.c\n@@ -76,6 +76,7 @@ void initialize_repository(struct repository *repo)\n \trepo->bare_cfg = -1;\n \trepo->fetch_if_missing = 1;\n \trepo_config_values_init(&repo->config_values_private_);\n+\trepo->rerere_cache_enabled = -1;\n \n \t/*\n \t * When a command runs inside a repository, it learns what\ndiff --git a/repository.h b/repository.h\nindex 91b1f57db7..86795b6946 100644\n--- a/repository.h\n+++ b/repository.h\n@@ -185,6 +185,12 @@ struct repository {\n \t/* True if commit-graph has been disabled within this process. */\n \tint commit_graph_disabled;\n \n+\t/*\n+\t * Indicates whether the rerere-cache has been enabled. -1 (the default\n+\t * value) causes us to perform auto-detection via \".git/rr-cache\".\n+\t */\n+\tint rerere_cache_enabled;\n+\n \t/*\n \t * Controls whether the repository should lazily fetch missing\n \t * objects from promisor remotes. Defaults to 1.\ndiff --git a/rerere.c b/rerere.c\nindex 56b6ef547b..8fa921fc8f 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -27,9 +27,6 @@\n #define THREE_STAGED 2\n void *RERERE_RESOLVED = &RERERE_RESOLVED;\n \n-/* if rerere_enabled == -1, fall back to detection of .git/rr-cache */\n-static int rerere_enabled = -1;\n-\n /* automatically update cleanly resolved paths to the index */\n static int rerere_autoupdate;\n \n@@ -873,40 +870,46 @@ static int do_plain_rerere(struct repository *r,\n \treturn write_rr(rr, fd);\n }\n \n-static void git_rerere_config(struct repository *r)\n-{\n-\trepo_config_get_bool(r, \"rerere.enabled\", &rerere_enabled);\n-\trepo_config_get_bool(r, \"rerere.autoupdate\", &rerere_autoupdate);\n-\trepo_config_get_int(r, \"rerere.locktimeout\", &rerere_lock_timeout_ms);\n-}\n-\n-static GIT_PATH_FUNC(git_path_rr_cache, \"rr-cache\")\n-\n static int is_rerere_enabled(struct repository *r)\n {\n \tint rr_cache_exists;\n+\tchar *dir;\n \n-\tif (!rerere_enabled)\n+\tif (r->rerere_cache_enabled != -1)\n+\t\treturn r->rerere_cache_enabled;\n+\n+\trepo_config_get_bool(r, \"rerere.enabled\", &r->rerere_cache_enabled);\n+\tif (!r->rerere_cache_enabled)\n \t\treturn 0;\n \n-\trr_cache_exists = is_directory(git_path_rr_cache());\n-\tif (rerere_enabled < 0)\n-\t\treturn rr_cache_exists;\n+\tdir = repo_git_path(r, \"rr-cache\");\n+\n+\trr_cache_exists = is_directory(dir);\n+\tif (r->rerere_cache_enabled < 0) {\n+\t\tr->rerere_cache_enabled = rr_cache_exists;\n+\t\tgoto out;\n+\t}\n \n \tif (!rr_cache_exists &&\n-\t    safe_create_dir_in_gitdir(r, git_path_rr_cache()))\n-\t\tdie(_(\"could not create directory '%s'\"), git_path_rr_cache());\n-\treturn 1;\n+\t    safe_create_dir_in_gitdir(r, dir))\n+\t\tdie(_(\"could not create directory '%s'\"), dir);\n+\tr->rerere_cache_enabled = 1;\n+\n+out:\n+\tfree(dir);\n+\treturn r->rerere_cache_enabled;\n }\n \n int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n {\n \tint fd;\n \n-\tgit_rerere_config(r);\n \tif (!is_rerere_enabled(r))\n \t\treturn -1;\n \n+\trepo_config_get_bool(r, \"rerere.autoupdate\", &rerere_autoupdate);\n+\trepo_config_get_int(r, \"rerere.locktimeout\", &rerere_lock_timeout_ms);\n+\n \tif (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))\n \t\trerere_autoupdate = !!(flags & RERERE_AUTOUPDATE);\n \tif ((flags & RERERE_READONLY) && (flags & RERERE_NOWAIT))\n\n-- \n2.56.0.307.g5fe7bd680c.dirty\n\n\n"},{"id":"556014","messageId":"20261023-pks-rerere-wo-the-repository-v1-5-b6ac0c2e4b81@pks.im","threadId":"66642","inReplyTo":"20261023-pks-rerere-wo-the-repository-v1-0-b6ac0c2e4b81@pks.im","subject":"[PATCH 5/7] rerere: stop using `the_repository`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-23T12:50:22Z","receivedAt":"2026-10-23T12:50:22Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"There's only a couple of trivial function calls left that implicitly\nmake us depend on `the_repository`. Refactor those to instead use their\nvariants that allow us to make the dependency explicit.\n\nNote that this requires us to make `get_hash_hex_algop()` public.\n\nWith these changes, \"rerere.c\" no longer depends on `the_repository`, so\ndrop `USE_THE_REPOSITORY_VARIABLE`. Despite that, we still carry some\nglobal state in this file that will tie a specific rerere cache to the\nfirst repository used to populate it. This will be fixed in subsequent\ncommits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n hex.c    |  4 ++--\n hex.h    |  3 +++\n rerere.c | 14 +++++++-------\n 3 files changed, 12 insertions(+), 9 deletions(-)\n\ndiff --git a/hex.c b/hex.c\nindex f02832140d..4b7793bb8a 100644\n--- a/hex.c\n+++ b/hex.c\n@@ -5,8 +5,8 @@\n #include \"hex.h\"\n #include \"strbuf.h\"\n \n-static int get_hash_hex_algop(const char *hex, unsigned char *hash,\n-\t\t\t      const struct git_hash_algo *algop)\n+int get_hash_hex_algop(const char *hex, unsigned char *hash,\n+\t\t       const struct git_hash_algo *algop)\n {\n \tfor (size_t i = 0; i < algop->rawsz; i++) {\n \t\tint val = hex2chr(hex);\ndiff --git a/hex.h b/hex.h\nindex f15c7e2220..50a0088065 100644\n--- a/hex.h\n+++ b/hex.h\n@@ -59,6 +59,9 @@ int parse_oid_hex_algop_impl(const char *hex, struct object_id *oid, const char\n int get_oid_hex_any(const char *hex, struct object_id *oid);\n int parse_oid_hex_any(const char *hex, struct object_id *oid, const char **end);\n \n+int get_hash_hex_algop(const char *hex, unsigned char *hash,\n+\t\t       const struct git_hash_algo *algop);\n+\n #ifdef USE_THE_REPOSITORY_VARIABLE\n \n /* Like get_oid_hex_algop, but for `the_hash_algo`. */\ndiff --git a/rerere.c b/rerere.c\nindex 8fa921fc8f..366797ab7e 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -1,4 +1,3 @@\n-#define USE_THE_REPOSITORY_VARIABLE\n #define DISABLE_SIGN_COMPARE_WARNINGS\n \n #include \"git-compat-util.h\"\n@@ -188,7 +187,7 @@ static struct rerere_id *new_rerere_id_hex(struct repository *r, char *hex)\n \n static struct rerere_id *new_rerere_id(struct repository *r, unsigned char *hash)\n {\n-\treturn new_rerere_id_hex(r, hash_to_hex(hash));\n+\treturn new_rerere_id_hex(r, hash_to_hex_algop(hash, r->hash_algo));\n }\n \n /*\n@@ -213,7 +212,8 @@ static void read_rr(struct repository *r, struct string_list *rr)\n \t\tconst unsigned hexsz = r->hash_algo->hexsz;\n \n \t\t/* There has to be the hash, tab, path and then NUL */\n-\t\tif (buf.len < hexsz + 2 || get_hash_hex(buf.buf, hash))\n+\t\tif (buf.len < hexsz + 2 ||\n+\t\t    get_hash_hex_algop(buf.buf, hash, r->hash_algo))\n \t\t\tdie(_(\"corrupt MERGE_RR\"));\n \n \t\tif (buf.buf[hexsz] != '.') {\n@@ -1264,11 +1264,11 @@ static void prune_one(struct repository *r, struct rerere_id *id,\n }\n \n /* Does the basename in \"path\" look plausibly like an rr-cache entry? */\n-static int is_rr_cache_dirname(const char *path)\n+static int is_rr_cache_dirname(struct repository *r, const char *path)\n {\n \tstruct object_id oid;\n \tconst char *end;\n-\treturn !parse_oid_hex(path, &oid, &end) && !*end;\n+\treturn !parse_oid_hex_algop(path, &oid, &end, r->hash_algo) && !*end;\n }\n \n bool rerere_gc_needed(struct repository *r, size_t limit)\n@@ -1295,7 +1295,7 @@ bool rerere_gc_needed(struct repository *r, size_t limit)\n \t\t * use for estimating the number of loose objects.\n \t\t */\n \t\tif (!starts_with(e->d_name, \"17\") ||\n-\t\t    !is_rr_cache_dirname(e->d_name))\n+\t\t    !is_rr_cache_dirname(r, e->d_name))\n \t\t\tcontinue;\n \n \t\tid.collection = find_rerere_dir(r, e->d_name);\n@@ -1345,7 +1345,7 @@ void rerere_gc(struct repository *r, struct string_list *rr, int flags)\n \t\tstruct rerere_id id;\n \t\tint now_empty;\n \n-\t\tif (!is_rr_cache_dirname(e->d_name))\n+\t\tif (!is_rr_cache_dirname(r, e->d_name))\n \t\t\tcontinue; /* or should we remove e->d_name? */\n \n \t\trr_dir = find_rerere_dir(r, e->d_name);\n\n-- \n2.56.0.307.g5fe7bd680c.dirty\n\n\n"},{"id":"556015","messageId":"20261023-pks-rerere-wo-the-repository-v1-6-b6ac0c2e4b81@pks.im","threadId":"66642","inReplyTo":"20261023-pks-rerere-wo-the-repository-v1-0-b6ac0c2e4b81@pks.im","subject":"[PATCH 6/7] rerere: bundle global state into a struct","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-23T12:50:23Z","receivedAt":"2026-10-23T12:50:23Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The state of the rerere subsystem is tracked across several global\nvariables. Move them into a struct so that we can easily de-globalize\nthe state.\n\nNote that the lock timeout is only ever used in `setup_rerere()`. So\ninstead of moving it into the new struct, we just keep it as a local\nvariable.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n rerere.c | 44 +++++++++++++++++++-------------------------\n rerere.h | 11 +++++++++++\n 2 files changed, 30 insertions(+), 25 deletions(-)\n\ndiff --git a/rerere.c b/rerere.c\nindex 366797ab7e..23bcb8bc31 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -21,17 +21,15 @@\n #include \"odb.h\"\n #include \"strmap.h\"\n \n+static struct rerere_cache cache = {\n+\t.dirs = STRMAP_INIT,\n+};\n+\n #define RESOLVED 0\n #define PUNTED 1\n #define THREE_STAGED 2\n void *RERERE_RESOLVED = &RERERE_RESOLVED;\n \n-/* automatically update cleanly resolved paths to the index */\n-static int rerere_autoupdate;\n-\n-/* how long to wait for MERGE_RR.lock, in milliseconds */\n-static int rerere_lock_timeout_ms = 1000;\n-\n #define RR_HAS_POSTIMAGE 1\n #define RR_HAS_PREIMAGE 2\n struct rerere_dir {\n@@ -40,19 +38,17 @@ struct rerere_dir {\n \tchar name[FLEX_ARRAY];\n };\n \n-static struct strmap rerere_dirs = STRMAP_INIT;\n-\n static void free_rerere_dirs(void)\n {\n \tstruct hashmap_iter iter;\n \tstruct strmap_entry *ent;\n \n-\tstrmap_for_each_entry(&rerere_dirs, &iter, ent) {\n+\tstrmap_for_each_entry(&cache.dirs, &iter, ent) {\n \t\tstruct rerere_dir *rr_dir = ent->value;\n \t\tfree(rr_dir->status);\n \t\tfree(rr_dir);\n \t}\n-\tstrmap_clear(&rerere_dirs, 0);\n+\tstrmap_clear(&cache.dirs, 0);\n }\n \n static void free_rerere_id(struct string_list_item *item)\n@@ -154,13 +150,13 @@ static struct rerere_dir *find_rerere_dir(struct repository *r, const char *hex)\n {\n \tstruct rerere_dir *rr_dir;\n \n-\trr_dir = strmap_get(&rerere_dirs, hex);\n+\trr_dir = strmap_get(&cache.dirs, hex);\n \tif (!rr_dir) {\n \t\tFLEX_ALLOC_STR(rr_dir, name, hex);\n \t\trr_dir->status = NULL;\n \t\trr_dir->status_nr = 0;\n \t\trr_dir->status_alloc = 0;\n-\t\tstrmap_put(&rerere_dirs, hex, rr_dir);\n+\t\tstrmap_put(&cache.dirs, hex, rr_dir);\n \n \t\tscan_rerere_dir(r, rr_dir);\n \t}\n@@ -241,8 +237,6 @@ static void read_rr(struct repository *r, struct string_list *rr)\n \tfclose(in);\n }\n \n-static struct lock_file write_lock;\n-\n static int write_rr(struct string_list *rr, int out_fd)\n {\n \tint i;\n@@ -270,7 +264,7 @@ static int write_rr(struct string_list *rr, int out_fd)\n \n \t\tstrbuf_release(&buf);\n \t}\n-\tif (commit_lock_file(&write_lock) != 0)\n+\tif (commit_lock_file(&cache.write_lock) != 0)\n \t\tdie(_(\"unable to write rerere record\"));\n \treturn 0;\n }\n@@ -785,7 +779,7 @@ static void do_rerere_one_path(struct index_state *istate,\n \t\tif (0 <= id->variant && id->variant != variant)\n \t\t\tremove_variant(istate->repo, id);\n \n-\t\tif (rerere_autoupdate)\n+\t\tif (cache.autoupdate)\n \t\t\tstring_list_insert(update, path);\n \t\telse\n \t\t\tfprintf_ln(stderr,\n@@ -902,27 +896,27 @@ static int is_rerere_enabled(struct repository *r)\n \n int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n {\n+\tint lock_timeout_ms = 1000;\n \tint fd;\n \n \tif (!is_rerere_enabled(r))\n \t\treturn -1;\n \n-\trepo_config_get_bool(r, \"rerere.autoupdate\", &rerere_autoupdate);\n-\trepo_config_get_int(r, \"rerere.locktimeout\", &rerere_lock_timeout_ms);\n+\trepo_config_get_bool(r, \"rerere.autoupdate\", &cache.autoupdate);\n+\trepo_config_get_int(r, \"rerere.locktimeout\", &lock_timeout_ms);\n \n \tif (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))\n-\t\trerere_autoupdate = !!(flags & RERERE_AUTOUPDATE);\n+\t\tcache.autoupdate = !!(flags & RERERE_AUTOUPDATE);\n \tif ((flags & RERERE_READONLY) && (flags & RERERE_NOWAIT))\n \t\tBUG(\"RERERE_NOWAIT does not apply with RERERE_READONLY\");\n \tif (flags & RERERE_READONLY) {\n \t\tfd = 0;\n \t} else {\n \t\tint lock_flags = LOCK_DIE_ON_ERROR;\n-\t\tint timeout_ms = rerere_lock_timeout_ms;\n \n \t\tif (flags & RERERE_NOWAIT) {\n \t\t\tlock_flags = 0;\n-\t\t\ttimeout_ms = 0;\n+\t\t\tlock_timeout_ms = 0;\n \t\t}\n \n \t\t/*\n@@ -930,9 +924,9 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n \t\t * \"git rerere gc\" while it prunes rr-cache, so by default\n \t\t * wait for it instead of dying right away.\n \t\t */\n-\t\tfd = repo_hold_lock_file_for_update_timeout(r, &write_lock,\n+\t\tfd = repo_hold_lock_file_for_update_timeout(r, &cache.write_lock,\n \t\t\t\t\t\t\t    git_path_merge_rr(r),\n-\t\t\t\t\t\t\t    lock_flags, timeout_ms);\n+\t\t\t\t\t\t\t    lock_flags, lock_timeout_ms);\n \t\tif (fd < 0)\n \t\t\treturn -1;\n \t}\n@@ -1369,7 +1363,7 @@ void rerere_gc(struct repository *r, struct string_list *rr, int flags)\n \t\t\t\t\t    \"rr-cache/%s\", to_remove.items[i].string));\n \n \tstring_list_clear(&to_remove, 0);\n-\trollback_lock_file(&write_lock);\n+\trollback_lock_file(&cache.write_lock);\n \tstrbuf_release(&buf);\n }\n \n@@ -1399,5 +1393,5 @@ void rerere_clear(struct repository *r, struct string_list *merge_rr)\n \t\tstrbuf_release(&buf);\n \t}\n \tunlink_or_warn(git_path_merge_rr(r));\n-\trollback_lock_file(&write_lock);\n+\trollback_lock_file(&cache.write_lock);\n }\ndiff --git a/rerere.h b/rerere.h\nindex ee8d97917d..570667d866 100644\n--- a/rerere.h\n+++ b/rerere.h\n@@ -2,7 +2,9 @@\n #define RERERE_H\n \n #include \"gettext.h\"\n+#include \"lockfile.h\"\n #include \"string-list.h\"\n+#include \"strmap.h\"\n \n struct pathspec;\n struct repository;\n@@ -20,6 +22,15 @@ struct repository;\n  */\n extern void *RERERE_RESOLVED;\n \n+struct rerere_cache {\n+\t/* Automatically update cleanly resolved paths to the index. */\n+\tint autoupdate;\n+\n+\tstruct strmap dirs;\n+\n+\tstruct lock_file write_lock;\n+};\n+\n struct rerere_dir;\n struct rerere_id {\n \tstruct rerere_dir *collection;\n\n-- \n2.56.0.307.g5fe7bd680c.dirty\n\n\n"},{"id":"556016","messageId":"20261023-pks-rerere-wo-the-repository-v1-7-b6ac0c2e4b81@pks.im","threadId":"66642","inReplyTo":"20261023-pks-rerere-wo-the-repository-v1-0-b6ac0c2e4b81@pks.im","subject":"[PATCH 7/7] rerere: get rid of global rerere cache state","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-23T12:50:24Z","receivedAt":"2026-10-23T12:50:24Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The rerere cache state is tracked via a global variable. On the one hand\nthis ties it to one specific repository. On the other hand it has the\nconsequence that the cache may be leaking state across multiple calls\ninto the rerere subsystem.\n\nRefactor the subsystem to instead pass around the rerere cache state\nexplicitly and make callers responsible for setting it up and then\nclearing it again.\n\nFor now, all callers are updated to do so ad-hoc. This of course means\nthat state that we could carry over across multiple calls would have to\nbe set up multiple times. But that doesn't really matter much right now\nanyway, as the number of times we call those functions is bounded.\n\nNote that `rerere_gc_needed()` is the only caller where we explicitly\ndon't set up the rerere cache state. This is intentional and matches the\nprevious status quo, where we didn't honor \"rerere.enabled\", either. The\nfunction only needs access to `struct rerere_cache::dirs` anyway though,\nso it's fine to partially initialize it, only.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/rerere.c |  15 ++++--\n rerere.c         | 138 +++++++++++++++++++++++++++++++++----------------------\n rerere.h         |   9 +++-\n 3 files changed, 102 insertions(+), 60 deletions(-)\n\ndiff --git a/builtin/rerere.c b/builtin/rerere.c\nindex e900e302c3..2c17f02f56 100644\n--- a/builtin/rerere.c\n+++ b/builtin/rerere.c\n@@ -106,11 +106,15 @@ int cmd_rerere(int argc,\n \t\trerere_gc(the_repository, &merge_rr,\n \t\t\t  skip_locked ? RERERE_NOWAIT : 0);\n \telse if (!strcmp(argv[0], \"status\")) {\n-\t\tif (setup_rerere(the_repository, &merge_rr,\n-\t\t\t\t flags | RERERE_READONLY) < 0)\n+\t\tstruct rerere_cache cache;\n+\n+\t\tif (rerere_cache_setup(&cache, the_repository, &merge_rr,\n+\t\t\t\t       flags | RERERE_READONLY) < 0)\n \t\t\treturn 0;\n \t\tfor (size_t i = 0; i < merge_rr.nr; i++)\n \t\t\tprintf(\"%s\\n\", merge_rr.items[i].string);\n+\n+\t\trerere_cache_release(&cache);\n \t} else if (!strcmp(argv[0], \"remaining\")) {\n \t\trerere_remaining(the_repository, &merge_rr);\n \t\tfor (size_t i = 0; i < merge_rr.nr; i++) {\n@@ -123,8 +127,10 @@ int cmd_rerere(int argc,\n \t\t}\n \t} else if (!strcmp(argv[0], \"diff\")) {\n \t\tstruct strbuf buf = STRBUF_INIT;\n-\t\tif (setup_rerere(the_repository, &merge_rr,\n-\t\t\t\t flags | RERERE_READONLY) < 0)\n+\t\tstruct rerere_cache cache;\n+\n+\t\tif (rerere_cache_setup(&cache, the_repository, &merge_rr,\n+\t\t\t\t      flags | RERERE_READONLY) < 0)\n \t\t\treturn 0;\n \t\tfor (size_t i = 0; i < merge_rr.nr; i++) {\n \t\t\tconst char *path = merge_rr.items[i].string;\n@@ -135,6 +141,7 @@ int cmd_rerere(int argc,\n \t\t\t\t    rerere_path(the_repository, &buf, id, NULL));\n \t\t}\n \n+\t\trerere_cache_release(&cache);\n \t\tstrbuf_release(&buf);\n \t} else\n \t\tusage_with_options(rerere_usage, options);\ndiff --git a/rerere.c b/rerere.c\nindex 23bcb8bc31..e089761208 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -21,10 +21,6 @@\n #include \"odb.h\"\n #include \"strmap.h\"\n \n-static struct rerere_cache cache = {\n-\t.dirs = STRMAP_INIT,\n-};\n-\n #define RESOLVED 0\n #define PUNTED 1\n #define THREE_STAGED 2\n@@ -38,17 +34,19 @@ struct rerere_dir {\n \tchar name[FLEX_ARRAY];\n };\n \n-static void free_rerere_dirs(void)\n+void rerere_cache_release(struct rerere_cache *cache)\n {\n \tstruct hashmap_iter iter;\n \tstruct strmap_entry *ent;\n \n-\tstrmap_for_each_entry(&cache.dirs, &iter, ent) {\n+\tstrmap_for_each_entry(&cache->dirs, &iter, ent) {\n \t\tstruct rerere_dir *rr_dir = ent->value;\n \t\tfree(rr_dir->status);\n \t\tfree(rr_dir);\n \t}\n-\tstrmap_clear(&cache.dirs, 0);\n+\tstrmap_clear(&cache->dirs, 0);\n+\n+\trollback_lock_file(&cache->write_lock);\n }\n \n static void free_rerere_id(struct string_list_item *item)\n@@ -121,13 +119,13 @@ static int is_rr_file(const char *name, const char *filename, int *variant)\n \treturn 1;\n }\n \n-static void scan_rerere_dir(struct repository *r, struct rerere_dir *rr_dir)\n+static void scan_rerere_dir(struct rerere_cache *cache, struct rerere_dir *rr_dir)\n {\n \tstruct dirent *de;\n \tchar *path;\n \tDIR *dir;\n \n-\tpath = repo_git_path(r, \"rr-cache/%s\", rr_dir->name);\n+\tpath = repo_git_path(cache->repo, \"rr-cache/%s\", rr_dir->name);\n \tdir = opendir(path);\n \tfree(path);\n \tif (!dir)\n@@ -146,19 +144,20 @@ static void scan_rerere_dir(struct repository *r, struct rerere_dir *rr_dir)\n \tclosedir(dir);\n }\n \n-static struct rerere_dir *find_rerere_dir(struct repository *r, const char *hex)\n+static struct rerere_dir *find_rerere_dir(struct rerere_cache *cache,\n+\t\t\t\t\t  const char *hex)\n {\n \tstruct rerere_dir *rr_dir;\n \n-\trr_dir = strmap_get(&cache.dirs, hex);\n+\trr_dir = strmap_get(&cache->dirs, hex);\n \tif (!rr_dir) {\n \t\tFLEX_ALLOC_STR(rr_dir, name, hex);\n \t\trr_dir->status = NULL;\n \t\trr_dir->status_nr = 0;\n \t\trr_dir->status_alloc = 0;\n-\t\tstrmap_put(&cache.dirs, hex, rr_dir);\n+\t\tstrmap_put(&cache->dirs, hex, rr_dir);\n \n-\t\tscan_rerere_dir(r, rr_dir);\n+\t\tscan_rerere_dir(cache, rr_dir);\n \t}\n \treturn rr_dir;\n }\n@@ -173,17 +172,18 @@ static int has_rerere_resolution(const struct rerere_id *id)\n \treturn ((id->collection->status[variant] & both) == both);\n }\n \n-static struct rerere_id *new_rerere_id_hex(struct repository *r, char *hex)\n+static struct rerere_id *new_rerere_id_hex(struct rerere_cache *cache,\n+\t\t\t\t\t   char *hex)\n {\n \tstruct rerere_id *id = xmalloc(sizeof(*id));\n-\tid->collection = find_rerere_dir(r, hex);\n+\tid->collection = find_rerere_dir(cache, hex);\n \tid->variant = -1; /* not known yet */\n \treturn id;\n }\n \n-static struct rerere_id *new_rerere_id(struct repository *r, unsigned char *hash)\n+static struct rerere_id *new_rerere_id(struct rerere_cache *cache, unsigned char *hash)\n {\n-\treturn new_rerere_id_hex(r, hash_to_hex_algop(hash, r->hash_algo));\n+\treturn new_rerere_id_hex(cache, hash_to_hex_algop(hash, cache->repo->hash_algo));\n }\n \n /*\n@@ -193,10 +193,10 @@ static struct rerere_id *new_rerere_id(struct repository *r, unsigned char *hash\n  * work on (i.e. what is left by the previous invocation of \"git\n  * rerere\" during the current conflict resolution session).\n  */\n-static void read_rr(struct repository *r, struct string_list *rr)\n+static void read_rr(struct rerere_cache *cache, struct string_list *rr)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n-\tFILE *in = fopen_or_warn(git_path_merge_rr(r), \"r\");\n+\tFILE *in = fopen_or_warn(git_path_merge_rr(cache->repo), \"r\");\n \n \tif (!in)\n \t\treturn;\n@@ -205,11 +205,11 @@ static void read_rr(struct repository *r, struct string_list *rr)\n \t\tunsigned char hash[GIT_MAX_RAWSZ];\n \t\tstruct rerere_id *id;\n \t\tint variant;\n-\t\tconst unsigned hexsz = r->hash_algo->hexsz;\n+\t\tconst unsigned hexsz = cache->repo->hash_algo->hexsz;\n \n \t\t/* There has to be the hash, tab, path and then NUL */\n \t\tif (buf.len < hexsz + 2 ||\n-\t\t    get_hash_hex_algop(buf.buf, hash, r->hash_algo))\n+\t\t    get_hash_hex_algop(buf.buf, hash, cache->repo->hash_algo))\n \t\t\tdie(_(\"corrupt MERGE_RR\"));\n \n \t\tif (buf.buf[hexsz] != '.') {\n@@ -224,7 +224,7 @@ static void read_rr(struct repository *r, struct string_list *rr)\n \t\tif (*(path++) != '\\t')\n \t\t\tdie(_(\"corrupt MERGE_RR\"));\n \t\tbuf.buf[hexsz] = '\\0';\n-\t\tid = new_rerere_id_hex(r, buf.buf);\n+\t\tid = new_rerere_id_hex(cache, buf.buf);\n \t\tid->variant = variant;\n \t\t/*\n \t\t * make sure id->collection->status has enough space\n@@ -237,7 +237,8 @@ static void read_rr(struct repository *r, struct string_list *rr)\n \tfclose(in);\n }\n \n-static int write_rr(struct string_list *rr, int out_fd)\n+static int write_rr(struct rerere_cache *cache,\n+\t\t    struct string_list *rr, int out_fd)\n {\n \tint i;\n \tfor (i = 0; i < rr->nr; i++) {\n@@ -264,7 +265,7 @@ static int write_rr(struct string_list *rr, int out_fd)\n \n \t\tstrbuf_release(&buf);\n \t}\n-\tif (commit_lock_file(&cache.write_lock) != 0)\n+\tif (commit_lock_file(&cache->write_lock) != 0)\n \t\tdie(_(\"unable to write rerere record\"));\n \treturn 0;\n }\n@@ -564,12 +565,15 @@ static int find_conflict(struct repository *r, struct string_list *conflict)\n  */\n int rerere_remaining(struct repository *r, struct string_list *merge_rr)\n {\n-\tint i;\n+\tstruct rerere_cache cache;\n+\tint ret, i;\n \n-\tif (setup_rerere(r, merge_rr, RERERE_READONLY))\n+\tif (rerere_cache_setup(&cache, r, merge_rr, RERERE_READONLY))\n \t\treturn 0;\n-\tif (repo_read_index(r) < 0)\n-\t\treturn error(_(\"index file corrupt\"));\n+\tif (repo_read_index(r) < 0) {\n+\t\tret = error(_(\"index file corrupt\"));\n+\t\tgoto out;\n+\t}\n \n \tfor (i = 0; i < r->index->cache_nr;) {\n \t\tint conflict_type;\n@@ -586,7 +590,12 @@ int rerere_remaining(struct repository *r, struct string_list *merge_rr)\n \t\t\t}\n \t\t}\n \t}\n-\treturn 0;\n+\n+\tret = 0;\n+\n+out:\n+\trerere_cache_release(&cache);\n+\treturn ret;\n }\n \n /*\n@@ -714,10 +723,11 @@ static void remove_variant(struct repository *r, struct rerere_id *id)\n  * only have the preimage for that conflict, in which case the result\n  * needs to be recorded as a resolution in a postimage file.\n  */\n-static void do_rerere_one_path(struct index_state *istate,\n+static void do_rerere_one_path(struct rerere_cache *cache,\n \t\t\t       struct string_list_item *rr_item,\n \t\t\t       struct string_list *update)\n {\n+\tstruct index_state *istate = cache->repo->index;\n \tconst char *path = rr_item->string;\n \tstruct rerere_id *id = rr_item->util;\n \tstruct rerere_dir *rr_dir = id->collection;\n@@ -779,7 +789,7 @@ static void do_rerere_one_path(struct index_state *istate,\n \t\tif (0 <= id->variant && id->variant != variant)\n \t\t\tremove_variant(istate->repo, id);\n \n-\t\tif (cache.autoupdate)\n+\t\tif (cache->autoupdate)\n \t\t\tstring_list_insert(update, path);\n \t\telse\n \t\t\tfprintf_ln(stderr,\n@@ -810,12 +820,13 @@ static void do_rerere_one_path(struct index_state *istate,\n \tstrbuf_release(&buf);\n }\n \n-static int do_plain_rerere(struct repository *r,\n+static int do_plain_rerere(struct rerere_cache *cache,\n \t\t\t   struct string_list *rr, int fd)\n {\n \tstruct string_list conflict = STRING_LIST_INIT_DUP;\n \tstruct string_list update = STRING_LIST_INIT_DUP;\n \tstruct strbuf buf = STRBUF_INIT;\n+\tstruct repository *r = cache->repo;\n \tint i;\n \n \tfind_conflict(r, &conflict);\n@@ -845,7 +856,7 @@ static int do_plain_rerere(struct repository *r,\n \t\tif (ret < 1)\n \t\t\tcontinue;\n \n-\t\tid = new_rerere_id(r, hash);\n+\t\tid = new_rerere_id(cache, hash);\n \t\tstring_list_insert(rr, path)->util = id;\n \n \t\t/* Ensure that the directory exists. */\n@@ -853,7 +864,7 @@ static int do_plain_rerere(struct repository *r,\n \t}\n \n \tfor (i = 0; i < rr->nr; i++)\n-\t\tdo_rerere_one_path(r->index, &rr->items[i], &update);\n+\t\tdo_rerere_one_path(cache, &rr->items[i], &update);\n \n \tif (update.nr)\n \t\tupdate_paths(r, &update);\n@@ -861,7 +872,7 @@ static int do_plain_rerere(struct repository *r,\n \tstring_list_clear(&conflict, 0);\n \tstring_list_clear(&update, 0);\n \tstrbuf_release(&buf);\n-\treturn write_rr(rr, fd);\n+\treturn write_rr(cache, rr, fd);\n }\n \n static int is_rerere_enabled(struct repository *r)\n@@ -894,7 +905,10 @@ static int is_rerere_enabled(struct repository *r)\n \treturn r->rerere_cache_enabled;\n }\n \n-int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n+int rerere_cache_setup(struct rerere_cache *cache,\n+\t\t       struct repository *r,\n+\t\t       struct string_list *merge_rr,\n+\t\t       int flags)\n {\n \tint lock_timeout_ms = 1000;\n \tint fd;\n@@ -902,11 +916,14 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n \tif (!is_rerere_enabled(r))\n \t\treturn -1;\n \n-\trepo_config_get_bool(r, \"rerere.autoupdate\", &cache.autoupdate);\n+\tmemset(cache, 0, sizeof(*cache));\n+\tcache->repo = r;\n+\tstrmap_init(&cache->dirs);\n+\trepo_config_get_bool(r, \"rerere.autoupdate\", &cache->autoupdate);\n \trepo_config_get_int(r, \"rerere.locktimeout\", &lock_timeout_ms);\n \n \tif (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))\n-\t\tcache.autoupdate = !!(flags & RERERE_AUTOUPDATE);\n+\t\tcache->autoupdate = !!(flags & RERERE_AUTOUPDATE);\n \tif ((flags & RERERE_READONLY) && (flags & RERERE_NOWAIT))\n \t\tBUG(\"RERERE_NOWAIT does not apply with RERERE_READONLY\");\n \tif (flags & RERERE_READONLY) {\n@@ -924,13 +941,13 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n \t\t * \"git rerere gc\" while it prunes rr-cache, so by default\n \t\t * wait for it instead of dying right away.\n \t\t */\n-\t\tfd = repo_hold_lock_file_for_update_timeout(r, &cache.write_lock,\n+\t\tfd = repo_hold_lock_file_for_update_timeout(r, &cache->write_lock,\n \t\t\t\t\t\t\t    git_path_merge_rr(r),\n \t\t\t\t\t\t\t    lock_flags, lock_timeout_ms);\n \t\tif (fd < 0)\n \t\t\treturn -1;\n \t}\n-\tread_rr(r, merge_rr);\n+\tread_rr(cache, merge_rr);\n \treturn fd;\n }\n \n@@ -942,13 +959,15 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n int repo_rerere(struct repository *r, int flags)\n {\n \tstruct string_list merge_rr = STRING_LIST_INIT_DUP;\n+\tstruct rerere_cache cache;\n \tint fd, status;\n \n-\tfd = setup_rerere(r, &merge_rr, flags);\n+\tfd = rerere_cache_setup(&cache, r, &merge_rr, flags);\n \tif (fd < 0)\n \t\treturn 0;\n-\tstatus = do_plain_rerere(r, &merge_rr, fd);\n-\tfree_rerere_dirs();\n+\tstatus = do_plain_rerere(&cache, &merge_rr, fd);\n+\n+\trerere_cache_release(&cache);\n \tstring_list_clear(&merge_rr, 1);\n \treturn status;\n }\n@@ -1057,10 +1076,11 @@ static int handle_cache(struct index_state *istate,\n \treturn has_conflicts;\n }\n \n-static int rerere_forget_one_path(struct index_state *istate,\n+static int rerere_forget_one_path(struct rerere_cache *cache,\n \t\t\t\t  const char *path,\n \t\t\t\t  struct string_list *rr)\n {\n+\tstruct index_state *istate = cache->repo->index;\n \tconst char *filename;\n \tstruct rerere_id *id;\n \tunsigned char hash[GIT_MAX_RAWSZ];\n@@ -1077,7 +1097,7 @@ static int rerere_forget_one_path(struct index_state *istate,\n \t\treturn error(_(\"could not parse conflict hunks in '%s'\"), path);\n \n \t/* Nuke the recorded resolution for the conflict */\n-\tid = new_rerere_id(istate->repo, hash);\n+\tid = new_rerere_id(cache, hash);\n \n \tfor (id->variant = 0;\n \t     id->variant < id->collection->status_nr;\n@@ -1145,11 +1165,12 @@ int rerere_forget(struct repository *r, struct pathspec *pathspec)\n \tint i, fd, ret;\n \tstruct string_list conflict = STRING_LIST_INIT_DUP;\n \tstruct string_list merge_rr = STRING_LIST_INIT_DUP;\n+\tstruct rerere_cache cache;\n \n \tif (repo_read_index(r) < 0)\n \t\treturn error(_(\"index file corrupt\"));\n \n-\tfd = setup_rerere(r, &merge_rr, RERERE_NOAUTOUPDATE);\n+\tfd = rerere_cache_setup(&cache, r, &merge_rr, RERERE_NOAUTOUPDATE);\n \tif (fd < 0)\n \t\treturn 0;\n \n@@ -1165,13 +1186,14 @@ int rerere_forget(struct repository *r, struct pathspec *pathspec)\n \t\tif (!match_pathspec(r->index, pathspec, it->string,\n \t\t\t\t    strlen(it->string), 0, NULL, 0))\n \t\t\tcontinue;\n-\t\trerere_forget_one_path(r->index, it->string, &merge_rr);\n+\t\trerere_forget_one_path(&cache, it->string, &merge_rr);\n \t}\n \n-\tret = write_rr(&merge_rr, fd);\n+\tret = write_rr(&cache, &merge_rr, fd);\n \n \tstring_list_clear(&conflict, 0);\n \tstring_list_clear(&merge_rr, 1);\n+\trerere_cache_release(&cache);\n \treturn ret;\n }\n \n@@ -1267,6 +1289,10 @@ static int is_rr_cache_dirname(struct repository *r, const char *path)\n \n bool rerere_gc_needed(struct repository *r, size_t limit)\n {\n+\tstruct rerere_cache cache = {\n+\t\t.repo = r,\n+\t\t.dirs = STRMAP_INIT,\n+\t};\n \ttimestamp_t cutoff_resolve, cutoff_noresolve;\n \tstruct strbuf buf = STRBUF_INIT;\n \tbool needed = false;\n@@ -1292,7 +1318,7 @@ bool rerere_gc_needed(struct repository *r, size_t limit)\n \t\t    !is_rr_cache_dirname(r, e->d_name))\n \t\t\tcontinue;\n \n-\t\tid.collection = find_rerere_dir(r, e->d_name);\n+\t\tid.collection = find_rerere_dir(&cache, e->d_name);\n \t\tfor (id.variant = 0;\n \t\t     id.variant < id.collection->status_nr;\n \t\t     id.variant++) {\n@@ -1310,13 +1336,14 @@ bool rerere_gc_needed(struct repository *r, size_t limit)\n out:\n \tif (dir)\n \t\tclosedir(dir);\n-\tfree_rerere_dirs();\n+\trerere_cache_release(&cache);\n \tstrbuf_release(&buf);\n \treturn needed;\n }\n \n void rerere_gc(struct repository *r, struct string_list *rr, int flags)\n {\n+\tstruct rerere_cache cache;\n \tstruct string_list to_remove = STRING_LIST_INIT_DUP;\n \tDIR *dir;\n \tstruct dirent *e;\n@@ -1325,7 +1352,7 @@ void rerere_gc(struct repository *r, struct string_list *rr, int flags)\n \ttimestamp_t cutoff_resolve;\n \tstruct strbuf buf = STRBUF_INIT;\n \n-\tif (setup_rerere(r, rr, flags) < 0)\n+\tif (rerere_cache_setup(&cache, r, rr, flags) < 0)\n \t\treturn;\n \n \trerere_gc_cutoffs(r, &cutoff_resolve, &cutoff_noresolve);\n@@ -1342,7 +1369,7 @@ void rerere_gc(struct repository *r, struct string_list *rr, int flags)\n \t\tif (!is_rr_cache_dirname(r, e->d_name))\n \t\t\tcontinue; /* or should we remove e->d_name? */\n \n-\t\trr_dir = find_rerere_dir(r, e->d_name);\n+\t\trr_dir = find_rerere_dir(&cache, e->d_name);\n \n \t\tnow_empty = 1;\n \t\tfor (id.variant = 0, id.collection = rr_dir;\n@@ -1363,7 +1390,7 @@ void rerere_gc(struct repository *r, struct string_list *rr, int flags)\n \t\t\t\t\t    \"rr-cache/%s\", to_remove.items[i].string));\n \n \tstring_list_clear(&to_remove, 0);\n-\trollback_lock_file(&cache.write_lock);\n+\trerere_cache_release(&cache);\n \tstrbuf_release(&buf);\n }\n \n@@ -1376,9 +1403,10 @@ void rerere_gc(struct repository *r, struct string_list *rr, int flags)\n  */\n void rerere_clear(struct repository *r, struct string_list *merge_rr)\n {\n+\tstruct rerere_cache cache;\n \tint i;\n \n-\tif (setup_rerere(r, merge_rr, 0) < 0)\n+\tif (rerere_cache_setup(&cache, r, merge_rr, 0) < 0)\n \t\treturn;\n \n \tfor (i = 0; i < merge_rr->nr; i++) {\n@@ -1393,5 +1421,5 @@ void rerere_clear(struct repository *r, struct string_list *merge_rr)\n \t\tstrbuf_release(&buf);\n \t}\n \tunlink_or_warn(git_path_merge_rr(r));\n-\trollback_lock_file(&cache.write_lock);\n+\trerere_cache_release(&cache);\n }\ndiff --git a/rerere.h b/rerere.h\nindex 570667d866..d3cbd10e13 100644\n--- a/rerere.h\n+++ b/rerere.h\n@@ -23,6 +23,8 @@ struct repository;\n extern void *RERERE_RESOLVED;\n \n struct rerere_cache {\n+\tstruct repository *repo;\n+\n \t/* Automatically update cleanly resolved paths to the index. */\n \tint autoupdate;\n \n@@ -37,7 +39,12 @@ struct rerere_id {\n \tint variant;\n };\n \n-int setup_rerere(struct repository *,struct string_list *, int);\n+int rerere_cache_setup(struct rerere_cache *cache,\n+\t\t       struct repository *r,\n+\t\t       struct string_list *merge_rr,\n+\t\t       int flags);\n+void rerere_cache_release(struct rerere_cache *cache);\n+\n int repo_rerere(struct repository *, int);\n /*\n  * Given the conflict ID and the name of a \"file\" used for replaying\n\n-- \n2.56.0.307.g5fe7bd680c.dirty\n\n\n"},{"id":"556080","messageId":"xmqqv76s8i12.fsf@gitster.g","threadId":"66642","inReplyTo":"20261023-pks-rerere-wo-the-repository-v1-7-b6ac0c2e4b81@pks.im","subject":"Re: [PATCH 7/7] rerere: get rid of global rerere cache state","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-10-23T17:04:57Z","receivedAt":"2026-10-23T17:04:57Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> The rerere cache state is tracked via a global variable. On the one hand\n> this ties it to one specific repository. On the other hand it has the\n> consequence that the cache may be leaking state across multiple calls\n> into the rerere subsystem.\n>\n> Refactor the subsystem to instead pass around the rerere cache state\n> explicitly and make callers responsible for setting it up and then\n> clearing it again.\n\nThis step is a logical conclusion of what was done in the previous\nstep.  I very much like it.\n\nI was somehow hoping to see that an earlier addition of \"enabled\"\nbit to the \"struct repository\" would be undone and replaced with\nthis structure, but that may not be feasible, I guess.\n\nQueued.\n\n"},{"id":"556258","messageId":"at8BFdzlcaiDl_sL@pks.im","threadId":"66642","inReplyTo":"xmqqv76s8i12.fsf@gitster.g","subject":"Re: [PATCH 7/7] rerere: get rid of global rerere cache state","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-26T07:28:21Z","receivedAt":"2026-10-26T07:28:21Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Oct 23, 2026 at 10:04:57AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > The rerere cache state is tracked via a global variable. On the one hand\n> > this ties it to one specific repository. On the other hand it has the\n> > consequence that the cache may be leaking state across multiple calls\n> > into the rerere subsystem.\n> >\n> > Refactor the subsystem to instead pass around the rerere cache state\n> > explicitly and make callers responsible for setting it up and then\n> > clearing it again.\n> \n> This step is a logical conclusion of what was done in the previous\n> step.  I very much like it.\n> \n> I was somehow hoping to see that an earlier addition of \"enabled\"\n> bit to the \"struct repository\" would be undone and replaced with\n> this structure, but that may not be feasible, I guess.\n\nI guess you could in theory, but it felt a bit cleaner to me to cache\nthis value in the repository itself. Caching whether or not there even\nis a rerere cache in the `struct rerere_cache` itself just didn't feel\nright to me.\n\nWe could also just not bother caching this value at all. As I mention in\nthe commit message, we don't really have any code paths where this logic\ngets called in a hot loop or something like that. So we could instead\njust look up \"rerere.enabled\" every single time you call\n`rerere_cache_setup()`, and nobody would really care. But we also check\nfor the existence of \".git/rr-cache\", adding another stat(3p) call every\ntime this function is called, and that just feels wrong to me.\n\nIt's probably premature optimization though. So if you agree with that\nconclusion we could adapt similar to the below patch.\n\nPatrick\n\ndiff --git a/repository.c b/repository.c\nindex f51699a6dd9..e584d6c4054 100644\n--- a/repository.c\n+++ b/repository.c\n@@ -76,7 +76,6 @@ void initialize_repository(struct repository *repo)\n \trepo->bare_cfg = -1;\n \trepo->fetch_if_missing = 1;\n \trepo_config_values_init(&repo->config_values_private_);\n-\trepo->rerere_cache_enabled = -1;\n \n \t/*\n \t * When a command runs inside a repository, it learns what\ndiff --git a/repository.h b/repository.h\nindex 86795b6946c..91b1f57db7b 100644\n--- a/repository.h\n+++ b/repository.h\n@@ -185,12 +185,6 @@ struct repository {\n \t/* True if commit-graph has been disabled within this process. */\n \tint commit_graph_disabled;\n \n-\t/*\n-\t * Indicates whether the rerere-cache has been enabled. -1 (the default\n-\t * value) causes us to perform auto-detection via \".git/rr-cache\".\n-\t */\n-\tint rerere_cache_enabled;\n-\n \t/*\n \t * Controls whether the repository should lazily fetch missing\n \t * objects from promisor remotes. Defaults to 1.\ndiff --git a/rerere.c b/rerere.c\nindex e0897612085..58bf298ae00 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -878,31 +878,29 @@ static int do_plain_rerere(struct rerere_cache *cache,\n static int is_rerere_enabled(struct repository *r)\n {\n \tint rr_cache_exists;\n+\tint enabled = -1;\n \tchar *dir;\n \n-\tif (r->rerere_cache_enabled != -1)\n-\t\treturn r->rerere_cache_enabled;\n-\n-\trepo_config_get_bool(r, \"rerere.enabled\", &r->rerere_cache_enabled);\n-\tif (!r->rerere_cache_enabled)\n+\trepo_config_get_bool(r, \"rerere.enabled\", &enabled);\n+\tif (!enabled)\n \t\treturn 0;\n \n \tdir = repo_git_path(r, \"rr-cache\");\n \n \trr_cache_exists = is_directory(dir);\n-\tif (r->rerere_cache_enabled < 0) {\n-\t\tr->rerere_cache_enabled = rr_cache_exists;\n+\tif (enabled < 0) {\n+\t\tenabled = rr_cache_exists;\n \t\tgoto out;\n \t}\n \n \tif (!rr_cache_exists &&\n \t    safe_create_dir_in_gitdir(r, dir))\n \t\tdie(_(\"could not create directory '%s'\"), dir);\n-\tr->rerere_cache_enabled = 1;\n+\tenabled = 1;\n \n out:\n \tfree(dir);\n-\treturn r->rerere_cache_enabled;\n+\treturn enabled;\n }\n \n int rerere_cache_setup(struct rerere_cache *cache,\n\n"}]}