{"thread":{"id":"66250","subject":"[PATCH] rerere: keep a background gc from killing a rebase","startedAt":"2026-09-02T08:31:39Z","lastAt":"2026-10-02T11:11:39Z","messageCount":37,"participants":["Thomas Bachem via GitGitGadget","Phillip Wood","Thomas Bachem","Patrick Steinhardt","Junio C Hamano"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"551734","messageId":"pull.2214.git.1788337897490.gitgitgadget@gmail.com","threadId":"66250","inReplyTo":null,"subject":"[PATCH] rerere: keep a background gc from killing a rebase","fromName":"Thomas Bachem via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-02T08:31:37Z","receivedAt":"2026-09-02T08:31:39Z","isPatch":true,"body":"From: Thomas Bachem <mail@thomasbachem.com>\n\nSince 2.54 unscheduled maintenance uses the \"geometric\" strategy, so\nthe \"git maintenance run --auto --detach\" behind every \"git commit\"\nruns \"git rerere gc\" in the background whenever rr-cache has an entry.\nThat includes the \"git commit\" the sequencer runs for a resolved pick\non \"git rebase --continue\".\n\nrerere_gc() takes MERGE_RR.lock through setup_rerere(), which uses\nLOCK_DIE_ON_ERROR, and so does the sequencer's repo_rerere() at the\nnext conflict a few milliseconds later. Whichever comes second dies.\nWhen it is the rebase, it dies in do_pick_commit() with the index\nwritten but before make_patch() writes rebase-merge/{message,patch,\nstopped-sha}, and every later \"git rebase --continue\" refuses with\n\"you have staged changes in your working tree\". When it is the \"git\ncommit\" of a later continue, that one dies in its post-commit\nrepo_rerere() after the commit was made. Before 2.54 the same\ncollision needed an auto gc to actually run, since gc runs\n\"rerere gc\" at its end.\n\nA rebase with two conflicts in a row shows it. The filler makes the\npick slower than the ~5 ms the background task needs to take the\nlock, and keeps the lock held for about 0.4 s. It hit 6 of 6 runs\nhere on 2.55.0, and a test suite driving rebases on toy repositories\nwith a single rr-cache entry hit it in both runs that were traced:\n\n    git init -q -b main r && cd r\n    git config rerere.enabled true\n    git config maintenance.auto false\n    mkdir pad && seq 20000 | (cd pad && split -l 1 -a 5)\n    echo base >f && git add -A && git commit -qm base\n    git checkout -q -b topic\n    echo b >f && git commit -qam B\n    echo c >f && git commit -qam C\n    git checkout -q main\n    echo a >f && git commit -qam A\n    git repack -adq\n    seq 20000 | awk '{printf \".git/rr-cache/%040x\\n\", $1}' \\\n        | xargs mkdir -p\n    for d in .git/rr-cache/*/; do echo x >$d/preimage; done\n    git config --unset maintenance.auto\n    git checkout -q topic\n    git rebase main\n    echo ab >f && git add f\n    GIT_EDITOR=true git rebase --continue\n\nThe second continue dies with \"Unable to create '.git/MERGE_RR.lock':\nFile exists\" while the gc spawned by its own commit holds the lock,\nand after resolving C every further continue refuses. Maintenance\nstays off during the setup so that no repack is pending: a repack due\nat that commit runs ahead of rerere-gc in the task list and would\nspend the window.\n\nThe gc needs the lock: it removes every rr-cache directory it finds\nempty, and a rerere that has just created its directory but not yet\nwritten the preimage looks exactly like that. So keep the lock and fix\nboth orders. When the gc finds the lock busy, let it warn and do\nnothing this time, the way \"maintenance run\" treats its own lock, so a\nmanual \"git rerere gc\" sees the warning and the maintenance task and\n\"git gc\" see a clean exit. When the gc holds the lock, let every other\ncaller wait it out instead of dying at once, for rerere.lockTimeout\nmilliseconds with the semantics of core.packedRefsTimeout: 1000 by\ndefault, 0 for the old behaviour, -1 for an unbounded wait. Walking a\n20000-entry rr-cache takes about 0.4 s here.\n\nThat rebase now completes. The tests cover the gc under a held lock,\ndirectly and through the maintenance task, a merge that waits a lock\nout within a five second rerere.lockTimeout, and one that fails at\nonce with a timeout of 0.\n\nAssisted-by: Claude Fable 5.1\nSigned-off-by: Thomas Bachem <mail@thomasbachem.com>\n---\n    rerere: keep a background gc from killing a rebase\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2214%2Fthomasbachem%2Frerere-gc-lock-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2214/thomasbachem/rerere-gc-lock-v1\nPull-Request: https://github.com/gitgitgadget/git/pull/2214\n\n Documentation/config/rerere.adoc |  8 +++++++\n Documentation/git-rerere.adoc    |  4 +++-\n rerere.c                         | 27 +++++++++++++++++----\n rerere.h                         |  1 +\n t/t4200-rerere.sh                | 40 ++++++++++++++++++++++++++++++++\n t/t7900-maintenance.sh           |  8 +++++++\n 6 files changed, 82 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/config/rerere.adoc b/Documentation/config/rerere.adoc\nindex 3a78b5ebb1..8041a1587b 100644\n--- a/Documentation/config/rerere.adoc\n+++ b/Documentation/config/rerere.adoc\n@@ -10,3 +10,11 @@ rerere.enabled::\n \tenabled if there is an `rr-cache` directory under the\n \t`$GIT_DIR`, e.g. if \"rerere\" was previously used in the\n \trepository.\n+\n+rerere.lockTimeout::\n+\tThe length of time, in milliseconds, to retry when trying to\n+\ttake the rerere lock while another process holds it, typically\n+\ta background `git rerere gc`.  Value 0 means not to retry at\n+\tall; -1 means to try indefinitely.  Default is 1000 (i.e.,\n+\tretry for 1 second).  `git rerere gc` itself does not wait and\n+\tskips its run instead.\ndiff --git a/Documentation/git-rerere.adoc b/Documentation/git-rerere.adoc\nindex 4e6ab9a27c..05935b0603 100644\n--- a/Documentation/git-rerere.adoc\n+++ b/Documentation/git-rerere.adoc\n@@ -70,7 +70,9 @@ occurred a long time ago.  By default, unresolved conflicts older\n than 15 days and resolved conflicts older than 60\n days are pruned.  These defaults are controlled via the\n `gc.rerereUnresolved` and `gc.rerereResolved` configuration\n-variables respectively.\n+variables respectively.  If another process holds the lock on the\n+recorded resolutions, for example a merge or rebase that is recording\n+a conflict, `gc` does nothing and reports so.\n \n \n DISCUSSION\ndiff --git a/rerere.c b/rerere.c\nindex 8232542585..22d114262b 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -32,6 +32,7 @@ static int rerere_enabled = -1;\n \n /* automatically update cleanly resolved paths to the index */\n static int rerere_autoupdate;\n+static int rerere_lock_timeout_ms = 1000;\n \n #define RR_HAS_POSTIMAGE 1\n #define RR_HAS_PREIMAGE 2\n@@ -876,6 +877,8 @@ static void git_rerere_config(void)\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 }\n \n@@ -908,12 +911,26 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n \n \tif (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))\n \t\trerere_autoupdate = !!(flags & RERERE_AUTOUPDATE);\n-\tif (flags & RERERE_READONLY)\n+\tif (flags & RERERE_READONLY) {\n \t\tfd = 0;\n-\telse\n+\t} else if (flags & RERERE_SKIP_LOCKED) {\n \t\tfd = hold_lock_file_for_update(&write_lock,\n-\t\t\t\t\t       git_path_merge_rr(r),\n-\t\t\t\t\t       LOCK_DIE_ON_ERROR);\n+\t\t\t\t\t       git_path_merge_rr(r), 0);\n+\t\tif (fd < 0) {\n+\t\t\twarning_errno(_(\"unable to lock '%s', skipping\"),\n+\t\t\t\t      git_path_merge_rr(r));\n+\t\t\treturn -1;\n+\t\t}\n+\t} else {\n+\t\t/*\n+\t\t * A background \"rerere gc\" holds the lock for as long as it\n+\t\t * takes to walk rr-cache, so wait it out rather than die.\n+\t\t */\n+\t\tfd = hold_lock_file_for_update_timeout(&write_lock,\n+\t\t\t\t\t\t       git_path_merge_rr(r),\n+\t\t\t\t\t\t       LOCK_DIE_ON_ERROR,\n+\t\t\t\t\t\t       rerere_lock_timeout_ms);\n+\t}\n \tread_rr(r, merge_rr);\n \treturn fd;\n }\n@@ -1237,7 +1254,7 @@ void rerere_gc(struct repository *r, struct string_list *rr)\n \ttimestamp_t cutoff_resolve = now - 60 * 86400;\n \tstruct strbuf buf = STRBUF_INIT;\n \n-\tif (setup_rerere(r, rr, 0) < 0)\n+\tif (setup_rerere(r, rr, RERERE_SKIP_LOCKED) < 0)\n \t\treturn;\n \n \trepo_config_get_expiry_in_days(the_repository, \"gc.rerereresolved\",\ndiff --git a/rerere.h b/rerere.h\nindex d4b5f7c932..87964bb3c5 100644\n--- a/rerere.h\n+++ b/rerere.h\n@@ -10,6 +10,7 @@ struct repository;\n #define RERERE_AUTOUPDATE   01\n #define RERERE_NOAUTOUPDATE 02\n #define RERERE_READONLY     04\n+#define RERERE_SKIP_LOCKED  010\n \n /*\n  * Marks paths that have been hand-resolved and added to the\ndiff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh\nindex 1717f407c8..6b90294435 100755\n--- a/t/t4200-rerere.sh\n+++ b/t/t4200-rerere.sh\n@@ -242,6 +242,46 @@ test_expect_success 'old records rest in peace' '\n \ttest_path_is_missing $rr2/preimage\n '\n \n+test_expect_success 'gc does nothing while MERGE_RR is locked' '\n+\tmkdir -p $rr2 &&\n+\techo Hello >$rr2/preimage &&\n+\ttest-tool chmtime =$just_over_15_days_ago $rr2/preimage &&\n+\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\tgit rerere gc 2>err &&\n+\ttest_grep \"MERGE_RR\" err &&\n+\ttest_path_is_file $rr2/preimage &&\n+\n+\trm .git/MERGE_RR.lock &&\n+\tgit rerere gc &&\n+\ttest_path_is_missing $rr2/preimage\n+'\n+\n+test_expect_success 'a held lock is waited out within rerere.lockTimeout' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\t{\n+\t\t(sleep 1 && rm -f .git/MERGE_RR.lock) &\n+\t} &&\n+\ttest_must_fail git -c rerere.lockTimeout=5000 merge first 2>err &&\n+\twait &&\n+\ttest_grep ! \"Unable to create\" err &&\n+\tgrep \"^=======\\$\" $rr/preimage\n+'\n+\n+test_expect_success 'rerere.lockTimeout=0 fails at once on a held lock' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 merge first 2>err &&\n+\ttest_grep \"Unable to create\" err &&\n+\ttest_path_is_missing $rr/preimage\n+'\n+\n rerere_gc_custom_expiry_test () {\n \tfive_days=\"$1\" right_now=\"$2\"\n \ttest_expect_success \"rerere gc with custom expiry ($five_days, $right_now)\" '\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex d7f82e1bec..a55ca2e829 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -885,6 +885,14 @@ test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.aut\n \ttest_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 maintenance run --auto --task=rerere-gc\n '\n \n+test_expect_success 'rerere-gc task succeeds while MERGE_RR is locked' '\n+\ttest_when_finished \"rm -rf .git/rr-cache .git/MERGE_RR.lock\" &&\n+\tmkdir .git/rr-cache &&\n+\t: >.git/rr-cache/entry &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_expect_rerere_gc git maintenance run --task=rerere-gc\n+'\n+\n test_expect_success '--auto and --schedule incompatible' '\n \ttest_must_fail git maintenance run --auto --schedule=daily 2>err &&\n \ttest_grep \"cannot be used together\" err\n\nbase-commit: e9019fcafe0040228b8631c30f97ae1adb61bcdc\n-- \ngitgitgadget\n"},{"id":"551754","messageId":"fc8b288c-3d77-4cf6-adff-f981e6a7a7d2@gmail.com","threadId":"66250","inReplyTo":"pull.2214.git.1788337897490.gitgitgadget@gmail.com","subject":"Re: [PATCH] rerere: keep a background gc from killing a rebase","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-02T13:27:40Z","receivedAt":"2026-09-02T13:27:45Z","isPatch":true,"body":"Hi Thomas\n\nOn 02/09/2026 09:31, Thomas Bachem via GitGitGadget wrote:\n> From: Thomas Bachem <mail@thomasbachem.com>\n> \n> Since 2.54 unscheduled maintenance uses the \"geometric\" strategy, so\n\nThat change really is the gift that keeps on giving\n\n> the \"git maintenance run --auto --detach\" behind every \"git commit\"\n> runs \"git rerere gc\" in the background whenever rr-cache has an entry.\n> That includes the \"git commit\" the sequencer runs for a resolved pick\n> on \"git rebase --continue\".\n> \n> rerere_gc() takes MERGE_RR.lock through setup_rerere(), which uses\n> LOCK_DIE_ON_ERROR, and so does the sequencer's repo_rerere() at the\n> next conflict a few milliseconds later. Whichever comes second dies.\n\nTo me this is another reason why we should disable gc.auto while \nrebasing. To do that we need to pass \"-c gc.auto=false -c \nmaintenance.auto=false\" when running \"git commit\" in run_git_commit() \nand also when running \"git merge\" in do_merge(). We should also pass \nthose settings via GIT_CONFIG_PARAMETERS when running a exec command in \ndo_exec(). That is largly papering over the cracks but until we have a \nsystematic solution it does at least stop exposing users to this bug.\n\n> When it is the rebase, it dies in do_pick_commit() \n\nThat's a bug us well - we should be returning errors, not dying \n-rerere_setup() should be returning an error, so we can clean up and \nreschedule the pick.\n\nThere is a lot of detail here about what causes the problem which is \nhelpful, but there is very little discussion about the fix. As I \nunderstand it we now block the sequencer until the background \nmaintenance has completed, or continue to die in an inconvenient state \nwe timeout before the background maintenance finishes. That seems rather \nunfortunate as the idea of running the maintenance in the background is \nto prevent it from interfering with other commands.\n\nI think my preferred solution is to disable gc while rebasing. Returning \nan error from rerere_setup() would also help in the case where the user \nruns \"git commit\" and then continues the rebase. I'd be interested to \nhear what Junio and Patrick think about that. I'm also not clear why \ngc.auto has to fork a separate process just to check if it needs to run \nor not, I've not been following closely but my impression is that that \nis the cause of quite a lot of the lock contention bugs we've seen.\n\nThanks\n\nPhillip\n\n> with the index\n> written but before make_patch() writes rebase-merge/{message,patch,\n> stopped-sha}, and every later \"git rebase --continue\" refuses with\n> \"you have staged changes in your working tree\". When it is the \"git\n> commit\" of a later continue, that one dies in its post-commit\n> repo_rerere() after the commit was made. Before 2.54 the same\n> collision needed an auto gc to actually run, since gc runs\n> \"rerere gc\" at its end.\n> \n> A rebase with two conflicts in a row shows it. The filler makes the\n> pick slower than the ~5 ms the background task needs to take the\n> lock, and keeps the lock held for about 0.4 s. It hit 6 of 6 runs\n> here on 2.55.0, and a test suite driving rebases on toy repositories\n> with a single rr-cache entry hit it in both runs that were traced:\n> \n>      git init -q -b main r && cd r\n>      git config rerere.enabled true\n>      git config maintenance.auto false\n>      mkdir pad && seq 20000 | (cd pad && split -l 1 -a 5)\n>      echo base >f && git add -A && git commit -qm base\n>      git checkout -q -b topic\n>      echo b >f && git commit -qam B\n>      echo c >f && git commit -qam C\n>      git checkout -q main\n>      echo a >f && git commit -qam A\n>      git repack -adq\n>      seq 20000 | awk '{printf \".git/rr-cache/%040x\\n\", $1}' \\\n>          | xargs mkdir -p\n>      for d in .git/rr-cache/*/; do echo x >$d/preimage; done\n>      git config --unset maintenance.auto\n>      git checkout -q topic\n>      git rebase main\n>      echo ab >f && git add f\n>      GIT_EDITOR=true git rebase --continue\n> \n> The second continue dies with \"Unable to create '.git/MERGE_RR.lock':\n> File exists\" while the gc spawned by its own commit holds the lock,\n> and after resolving C every further continue refuses. Maintenance\n> stays off during the setup so that no repack is pending: a repack due\n> at that commit runs ahead of rerere-gc in the task list and would\n> spend the window.\n> \n> The gc needs the lock: it removes every rr-cache directory it finds\n> empty, and a rerere that has just created its directory but not yet\n> written the preimage looks exactly like that. So keep the lock and fix\n> both orders. When the gc finds the lock busy, let it warn and do\n> nothing this time, the way \"maintenance run\" treats its own lock, so a\n> manual \"git rerere gc\" sees the warning and the maintenance task and\n> \"git gc\" see a clean exit. When the gc holds the lock, let every other\n> caller wait it out instead of dying at once, for rerere.lockTimeout\n> milliseconds with the semantics of core.packedRefsTimeout: 1000 by\n> default, 0 for the old behaviour, -1 for an unbounded wait. Walking a\n> 20000-entry rr-cache takes about 0.4 s here.\n> \n> That rebase now completes. The tests cover the gc under a held lock,\n> directly and through the maintenance task, a merge that waits a lock\n> out within a five second rerere.lockTimeout, and one that fails at\n> once with a timeout of 0.\n> \n> Assisted-by: Claude Fable 5.1\n> Signed-off-by: Thomas Bachem <mail@thomasbachem.com>\n> ---\n>      rerere: keep a background gc from killing a rebase\n> \n> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2214%2Fthomasbachem%2Frerere-gc-lock-v1\n> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2214/thomasbachem/rerere-gc-lock-v1\n> Pull-Request: https://github.com/gitgitgadget/git/pull/2214\n> \n>   Documentation/config/rerere.adoc |  8 +++++++\n>   Documentation/git-rerere.adoc    |  4 +++-\n>   rerere.c                         | 27 +++++++++++++++++----\n>   rerere.h                         |  1 +\n>   t/t4200-rerere.sh                | 40 ++++++++++++++++++++++++++++++++\n>   t/t7900-maintenance.sh           |  8 +++++++\n>   6 files changed, 82 insertions(+), 6 deletions(-)\n> \n> diff --git a/Documentation/config/rerere.adoc b/Documentation/config/rerere.adoc\n> index 3a78b5ebb1..8041a1587b 100644\n> --- a/Documentation/config/rerere.adoc\n> +++ b/Documentation/config/rerere.adoc\n> @@ -10,3 +10,11 @@ rerere.enabled::\n>   \tenabled if there is an `rr-cache` directory under the\n>   \t`$GIT_DIR`, e.g. if \"rerere\" was previously used in the\n>   \trepository.\n> +\n> +rerere.lockTimeout::\n> +\tThe length of time, in milliseconds, to retry when trying to\n> +\ttake the rerere lock while another process holds it, typically\n> +\ta background `git rerere gc`.  Value 0 means not to retry at\n> +\tall; -1 means to try indefinitely.  Default is 1000 (i.e.,\n> +\tretry for 1 second).  `git rerere gc` itself does not wait and\n> +\tskips its run instead.\n> diff --git a/Documentation/git-rerere.adoc b/Documentation/git-rerere.adoc\n> index 4e6ab9a27c..05935b0603 100644\n> --- a/Documentation/git-rerere.adoc\n> +++ b/Documentation/git-rerere.adoc\n> @@ -70,7 +70,9 @@ occurred a long time ago.  By default, unresolved conflicts older\n>   than 15 days and resolved conflicts older than 60\n>   days are pruned.  These defaults are controlled via the\n>   `gc.rerereUnresolved` and `gc.rerereResolved` configuration\n> -variables respectively.\n> +variables respectively.  If another process holds the lock on the\n> +recorded resolutions, for example a merge or rebase that is recording\n> +a conflict, `gc` does nothing and reports so.\n>   \n>   \n>   DISCUSSION\n> diff --git a/rerere.c b/rerere.c\n> index 8232542585..22d114262b 100644\n> --- a/rerere.c\n> +++ b/rerere.c\n> @@ -32,6 +32,7 @@ static int rerere_enabled = -1;\n>   \n>   /* automatically update cleanly resolved paths to the index */\n>   static int rerere_autoupdate;\n> +static int rerere_lock_timeout_ms = 1000;\n>   \n>   #define RR_HAS_POSTIMAGE 1\n>   #define RR_HAS_PREIMAGE 2\n> @@ -876,6 +877,8 @@ static void git_rerere_config(void)\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>   }\n>   \n> @@ -908,12 +911,26 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n>   \n>   \tif (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))\n>   \t\trerere_autoupdate = !!(flags & RERERE_AUTOUPDATE);\n> -\tif (flags & RERERE_READONLY)\n> +\tif (flags & RERERE_READONLY) {\n>   \t\tfd = 0;\n> -\telse\n> +\t} else if (flags & RERERE_SKIP_LOCKED) {\n>   \t\tfd = hold_lock_file_for_update(&write_lock,\n> -\t\t\t\t\t       git_path_merge_rr(r),\n> -\t\t\t\t\t       LOCK_DIE_ON_ERROR);\n> +\t\t\t\t\t       git_path_merge_rr(r), 0);\n> +\t\tif (fd < 0) {\n> +\t\t\twarning_errno(_(\"unable to lock '%s', skipping\"),\n> +\t\t\t\t      git_path_merge_rr(r));\n> +\t\t\treturn -1;\n> +\t\t}\n> +\t} else {\n> +\t\t/*\n> +\t\t * A background \"rerere gc\" holds the lock for as long as it\n> +\t\t * takes to walk rr-cache, so wait it out rather than die.\n> +\t\t */\n> +\t\tfd = hold_lock_file_for_update_timeout(&write_lock,\n> +\t\t\t\t\t\t       git_path_merge_rr(r),\n> +\t\t\t\t\t\t       LOCK_DIE_ON_ERROR,\n> +\t\t\t\t\t\t       rerere_lock_timeout_ms);\n> +\t}\n>   \tread_rr(r, merge_rr);\n>   \treturn fd;\n>   }\n> @@ -1237,7 +1254,7 @@ void rerere_gc(struct repository *r, struct string_list *rr)\n>   \ttimestamp_t cutoff_resolve = now - 60 * 86400;\n>   \tstruct strbuf buf = STRBUF_INIT;\n>   \n> -\tif (setup_rerere(r, rr, 0) < 0)\n> +\tif (setup_rerere(r, rr, RERERE_SKIP_LOCKED) < 0)\n>   \t\treturn;\n>   \n>   \trepo_config_get_expiry_in_days(the_repository, \"gc.rerereresolved\",\n> diff --git a/rerere.h b/rerere.h\n> index d4b5f7c932..87964bb3c5 100644\n> --- a/rerere.h\n> +++ b/rerere.h\n> @@ -10,6 +10,7 @@ struct repository;\n>   #define RERERE_AUTOUPDATE   01\n>   #define RERERE_NOAUTOUPDATE 02\n>   #define RERERE_READONLY     04\n> +#define RERERE_SKIP_LOCKED  010\n>   \n>   /*\n>    * Marks paths that have been hand-resolved and added to the\n> diff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh\n> index 1717f407c8..6b90294435 100755\n> --- a/t/t4200-rerere.sh\n> +++ b/t/t4200-rerere.sh\n> @@ -242,6 +242,46 @@ test_expect_success 'old records rest in peace' '\n>   \ttest_path_is_missing $rr2/preimage\n>   '\n>   \n> +test_expect_success 'gc does nothing while MERGE_RR is locked' '\n> +\tmkdir -p $rr2 &&\n> +\techo Hello >$rr2/preimage &&\n> +\ttest-tool chmtime =$just_over_15_days_ago $rr2/preimage &&\n> +\n> +\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n> +\t>.git/MERGE_RR.lock &&\n> +\tgit rerere gc 2>err &&\n> +\ttest_grep \"MERGE_RR\" err &&\n> +\ttest_path_is_file $rr2/preimage &&\n> +\n> +\trm .git/MERGE_RR.lock &&\n> +\tgit rerere gc &&\n> +\ttest_path_is_missing $rr2/preimage\n> +'\n> +\n> +test_expect_success 'a held lock is waited out within rerere.lockTimeout' '\n> +\tgit reset --hard &&\n> +\trm -rf $rr &&\n> +\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n> +\t>.git/MERGE_RR.lock &&\n> +\t{\n> +\t\t(sleep 1 && rm -f .git/MERGE_RR.lock) &\n> +\t} &&\n> +\ttest_must_fail git -c rerere.lockTimeout=5000 merge first 2>err &&\n> +\twait &&\n> +\ttest_grep ! \"Unable to create\" err &&\n> +\tgrep \"^=======\\$\" $rr/preimage\n> +'\n> +\n> +test_expect_success 'rerere.lockTimeout=0 fails at once on a held lock' '\n> +\tgit reset --hard &&\n> +\trm -rf $rr &&\n> +\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n> +\t>.git/MERGE_RR.lock &&\n> +\ttest_must_fail git -c rerere.lockTimeout=0 merge first 2>err &&\n> +\ttest_grep \"Unable to create\" err &&\n> +\ttest_path_is_missing $rr/preimage\n> +'\n> +\n>   rerere_gc_custom_expiry_test () {\n>   \tfive_days=\"$1\" right_now=\"$2\"\n>   \ttest_expect_success \"rerere gc with custom expiry ($five_days, $right_now)\" '\n> diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\n> index d7f82e1bec..a55ca2e829 100755\n> --- a/t/t7900-maintenance.sh\n> +++ b/t/t7900-maintenance.sh\n> @@ -885,6 +885,14 @@ test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.aut\n>   \ttest_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 maintenance run --auto --task=rerere-gc\n>   '\n>   \n> +test_expect_success 'rerere-gc task succeeds while MERGE_RR is locked' '\n> +\ttest_when_finished \"rm -rf .git/rr-cache .git/MERGE_RR.lock\" &&\n> +\tmkdir .git/rr-cache &&\n> +\t: >.git/rr-cache/entry &&\n> +\t>.git/MERGE_RR.lock &&\n> +\ttest_expect_rerere_gc git maintenance run --task=rerere-gc\n> +'\n> +\n>   test_expect_success '--auto and --schedule incompatible' '\n>   \ttest_must_fail git maintenance run --auto --schedule=daily 2>err &&\n>   \ttest_grep \"cannot be used together\" err\n> \n> base-commit: e9019fcafe0040228b8631c30f97ae1adb61bcdc\n\n"},{"id":"551773","messageId":"CAA0xjtpYDGODpC9gJCZ_8KUvvtW53tTDed0iyDSZJCQchTWuAw@mail.gmail.com","threadId":"66250","inReplyTo":"fc8b288c-3d77-4cf6-adff-f981e6a7a7d2@gmail.com","subject":"Re: [PATCH] rerere: keep a background gc from killing a rebase","fromName":"Thomas Bachem","fromEmail":"mail@thomasbachem.com","sentAt":"2026-09-02T15:07:23Z","receivedAt":"2026-09-02T15:07:36Z","isPatch":true,"body":"Hi Phillip,\n\nOn 02/09/2026 15:27, Phillip Wood wrote:\n> To me this is another reason why we should disable gc.auto while\n> rebasing. To do that we need to pass \"-c gc.auto=false -c\n> maintenance.auto=false\" when running \"git commit\" in run_git_commit()\n> and also when running \"git merge\" in do_merge(). We should also pass\n> those settings via GIT_CONFIG_PARAMETERS when running a exec command in\n> do_exec(). That is largly papering over the cracks but until we have a\n> systematic solution it does at least stop exposing users to this bug.\n\nOK, I'll do that. It is also more consistent than it looks: the\ncommits the sequencer creates in-process via try_to_commit() don't run\nauto maintenance at all, only the \"git commit\" child does (for a\nresolved, reworded or squashed commit). What surprised me is that a\nrebase with the merge backend then never runs maintenance, not even at\nthe end, because it doesn't go through finish_rebase() where the apply\nbackend runs it. Do you want a single run at the end of the sequence\nin that patch, or keep it minimal?\n\nFWIW, the tool I hit this with has been setting both for its whole\nprocess tree since, and the failures stopped.\n\n>> When it is the rebase, it dies in do_pick_commit()\n>\n> That's a bug us well - we should be returning errors, not dying\n> -rerere_setup() should be returning an error, so we can clean up and\n> reschedule the pick.\n\nYes. I don't think we even need to reschedule: when repo_rerere() is\ncalled there, the merge result is already in the index and worktree,\nthe error and advice have been printed, and the return value is\nignored. If setup_rerere() reports the lock and returns -1, the pick\njust stops at the conflict like any other, minus rerere's recording\nand replay, and --continue works. I went through the callers of\nsetup_rerere(): all of them handle a negative return, because that is\nwhat a disabled rerere returns, so this is close to a one-branch\nchange. It also fixes the stale-lock case (crashed process), which\ndisabling gc can't.\n\n> As I understand it we now block the sequencer until the background\n> maintenance has completed, or continue to die in an inconvenient state\n> we timeout before the background maintenance finishes. That seems rather\n> unfortunate as the idea of running the maintenance in the background is\n> to prevent it from interfering with other commands.\n\nRight, that's what it does. I copied the timeout from\ncore.packedRefsTimeout, but a ref update can't be skipped and a rerere\ncan, so the wait buys little. I'll drop rerere.lockTimeout.\n\nWhat it did buy: the gc spawned by the continue's own commit needs\n~5ms to take the lock, the next pick usually longer to reach its\nrerere, so the gc is normally holding it by then. With only the\ngc-side skip my repro still died 3 of 3 times; with the error return\nthose runs would survive but lose rerere at that stop. Tolerable, but\nit is why I'd rather have the sequencer patch in the same series than\nleave it for later.\n\n> I think my preferred solution is to disable gc while rebasing. Returning\n> an error from rerere_setup() would also help in the case where the user\n> runs \"git commit\" and then continues the rebase. I'd be interested to\n> hear what Junio and Patrick think about that.\n\nSo v2 would be two patches: rerere returning an error on a busy lock\n(with \"rerere gc\" still warning and skipping as in v1, and a commit\nmessage that talks about the fix instead of the trace), and the\nsequencer disabling gc.auto/maintenance.auto for \"git commit\", \"git\nmerge\" and exec. I'll wait for Junio and Patrick before rerolling in\ncase they see it differently.\n\nPatrick, one thing I noticed on the way: since 452b12c2e0\n(builtin/maintenance: use \"geometric\" strategy by default, 2026-02-24)\nevery \"maintenance run --auto\" runs rerere-gc as soon as rr-cache has\neven a single entry, stale or not. The doc for\nmaintenance.rerere-gc.auto says the heuristic may be refined; that\nwould make this rare for every command, not only the sequencer. Not\ntouching it in this series, just mentioning it.\n\nThanks,\nTom\n\n\nAm Mi., 2. Sept. 2026 um 15:27 Uhr schrieb Phillip Wood\n<phillip.wood123@gmail.com>:\n>\n> Hi Thomas\n>\n> On 02/09/2026 09:31, Thomas Bachem via GitGitGadget wrote:\n> > From: Thomas Bachem <mail@thomasbachem.com>\n> >\n> > Since 2.54 unscheduled maintenance uses the \"geometric\" strategy, so\n>\n> That change really is the gift that keeps on giving\n>\n> > the \"git maintenance run --auto --detach\" behind every \"git commit\"\n> > runs \"git rerere gc\" in the background whenever rr-cache has an entry.\n> > That includes the \"git commit\" the sequencer runs for a resolved pick\n> > on \"git rebase --continue\".\n> >\n> > rerere_gc() takes MERGE_RR.lock through setup_rerere(), which uses\n> > LOCK_DIE_ON_ERROR, and so does the sequencer's repo_rerere() at the\n> > next conflict a few milliseconds later. Whichever comes second dies.\n>\n> To me this is another reason why we should disable gc.auto while\n> rebasing. To do that we need to pass \"-c gc.auto=false -c\n> maintenance.auto=false\" when running \"git commit\" in run_git_commit()\n> and also when running \"git merge\" in do_merge(). We should also pass\n> those settings via GIT_CONFIG_PARAMETERS when running a exec command in\n> do_exec(). That is largly papering over the cracks but until we have a\n> systematic solution it does at least stop exposing users to this bug.\n>\n> > When it is the rebase, it dies in do_pick_commit()\n>\n> That's a bug us well - we should be returning errors, not dying\n> -rerere_setup() should be returning an error, so we can clean up and\n> reschedule the pick.\n>\n> There is a lot of detail here about what causes the problem which is\n> helpful, but there is very little discussion about the fix. As I\n> understand it we now block the sequencer until the background\n> maintenance has completed, or continue to die in an inconvenient state\n> we timeout before the background maintenance finishes. That seems rather\n> unfortunate as the idea of running the maintenance in the background is\n> to prevent it from interfering with other commands.\n>\n> I think my preferred solution is to disable gc while rebasing. Returning\n> an error from rerere_setup() would also help in the case where the user\n> runs \"git commit\" and then continues the rebase. I'd be interested to\n> hear what Junio and Patrick think about that. I'm also not clear why\n> gc.auto has to fork a separate process just to check if it needs to run\n> or not, I've not been following closely but my impression is that that\n> is the cause of quite a lot of the lock contention bugs we've seen.\n>\n> Thanks\n>\n> Phillip\n>\n> > with the index\n> > written but before make_patch() writes rebase-merge/{message,patch,\n> > stopped-sha}, and every later \"git rebase --continue\" refuses with\n> > \"you have staged changes in your working tree\". When it is the \"git\n> > commit\" of a later continue, that one dies in its post-commit\n> > repo_rerere() after the commit was made. Before 2.54 the same\n> > collision needed an auto gc to actually run, since gc runs\n> > \"rerere gc\" at its end.\n> >\n> > A rebase with two conflicts in a row shows it. The filler makes the\n> > pick slower than the ~5 ms the background task needs to take the\n> > lock, and keeps the lock held for about 0.4 s. It hit 6 of 6 runs\n> > here on 2.55.0, and a test suite driving rebases on toy repositories\n> > with a single rr-cache entry hit it in both runs that were traced:\n> >\n> >      git init -q -b main r && cd r\n> >      git config rerere.enabled true\n> >      git config maintenance.auto false\n> >      mkdir pad && seq 20000 | (cd pad && split -l 1 -a 5)\n> >      echo base >f && git add -A && git commit -qm base\n> >      git checkout -q -b topic\n> >      echo b >f && git commit -qam B\n> >      echo c >f && git commit -qam C\n> >      git checkout -q main\n> >      echo a >f && git commit -qam A\n> >      git repack -adq\n> >      seq 20000 | awk '{printf \".git/rr-cache/%040x\\n\", $1}' \\\n> >          | xargs mkdir -p\n> >      for d in .git/rr-cache/*/; do echo x >$d/preimage; done\n> >      git config --unset maintenance.auto\n> >      git checkout -q topic\n> >      git rebase main\n> >      echo ab >f && git add f\n> >      GIT_EDITOR=true git rebase --continue\n> >\n> > The second continue dies with \"Unable to create '.git/MERGE_RR.lock':\n> > File exists\" while the gc spawned by its own commit holds the lock,\n> > and after resolving C every further continue refuses. Maintenance\n> > stays off during the setup so that no repack is pending: a repack due\n> > at that commit runs ahead of rerere-gc in the task list and would\n> > spend the window.\n> >\n> > The gc needs the lock: it removes every rr-cache directory it finds\n> > empty, and a rerere that has just created its directory but not yet\n> > written the preimage looks exactly like that. So keep the lock and fix\n> > both orders. When the gc finds the lock busy, let it warn and do\n> > nothing this time, the way \"maintenance run\" treats its own lock, so a\n> > manual \"git rerere gc\" sees the warning and the maintenance task and\n> > \"git gc\" see a clean exit. When the gc holds the lock, let every other\n> > caller wait it out instead of dying at once, for rerere.lockTimeout\n> > milliseconds with the semantics of core.packedRefsTimeout: 1000 by\n> > default, 0 for the old behaviour, -1 for an unbounded wait. Walking a\n> > 20000-entry rr-cache takes about 0.4 s here.\n> >\n> > That rebase now completes. The tests cover the gc under a held lock,\n> > directly and through the maintenance task, a merge that waits a lock\n> > out within a five second rerere.lockTimeout, and one that fails at\n> > once with a timeout of 0.\n> >\n> > Assisted-by: Claude Fable 5.1\n> > Signed-off-by: Thomas Bachem <mail@thomasbachem.com>\n> > ---\n> >      rerere: keep a background gc from killing a rebase\n> >\n> > Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2214%2Fthomasbachem%2Frerere-gc-lock-v1\n> > Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2214/thomasbachem/rerere-gc-lock-v1\n> > Pull-Request: https://github.com/gitgitgadget/git/pull/2214\n> >\n> >   Documentation/config/rerere.adoc |  8 +++++++\n> >   Documentation/git-rerere.adoc    |  4 +++-\n> >   rerere.c                         | 27 +++++++++++++++++----\n> >   rerere.h                         |  1 +\n> >   t/t4200-rerere.sh                | 40 ++++++++++++++++++++++++++++++++\n> >   t/t7900-maintenance.sh           |  8 +++++++\n> >   6 files changed, 82 insertions(+), 6 deletions(-)\n> >\n> > diff --git a/Documentation/config/rerere.adoc b/Documentation/config/rerere.adoc\n> > index 3a78b5ebb1..8041a1587b 100644\n> > --- a/Documentation/config/rerere.adoc\n> > +++ b/Documentation/config/rerere.adoc\n> > @@ -10,3 +10,11 @@ rerere.enabled::\n> >       enabled if there is an `rr-cache` directory under the\n> >       `$GIT_DIR`, e.g. if \"rerere\" was previously used in the\n> >       repository.\n> > +\n> > +rerere.lockTimeout::\n> > +     The length of time, in milliseconds, to retry when trying to\n> > +     take the rerere lock while another process holds it, typically\n> > +     a background `git rerere gc`.  Value 0 means not to retry at\n> > +     all; -1 means to try indefinitely.  Default is 1000 (i.e.,\n> > +     retry for 1 second).  `git rerere gc` itself does not wait and\n> > +     skips its run instead.\n> > diff --git a/Documentation/git-rerere.adoc b/Documentation/git-rerere.adoc\n> > index 4e6ab9a27c..05935b0603 100644\n> > --- a/Documentation/git-rerere.adoc\n> > +++ b/Documentation/git-rerere.adoc\n> > @@ -70,7 +70,9 @@ occurred a long time ago.  By default, unresolved conflicts older\n> >   than 15 days and resolved conflicts older than 60\n> >   days are pruned.  These defaults are controlled via the\n> >   `gc.rerereUnresolved` and `gc.rerereResolved` configuration\n> > -variables respectively.\n> > +variables respectively.  If another process holds the lock on the\n> > +recorded resolutions, for example a merge or rebase that is recording\n> > +a conflict, `gc` does nothing and reports so.\n> >\n> >\n> >   DISCUSSION\n> > diff --git a/rerere.c b/rerere.c\n> > index 8232542585..22d114262b 100644\n> > --- a/rerere.c\n> > +++ b/rerere.c\n> > @@ -32,6 +32,7 @@ static int rerere_enabled = -1;\n> >\n> >   /* automatically update cleanly resolved paths to the index */\n> >   static int rerere_autoupdate;\n> > +static int rerere_lock_timeout_ms = 1000;\n> >\n> >   #define RR_HAS_POSTIMAGE 1\n> >   #define RR_HAS_PREIMAGE 2\n> > @@ -876,6 +877,8 @@ static void git_rerere_config(void)\n> >   {\n> >       repo_config_get_bool(the_repository, \"rerere.enabled\", &rerere_enabled);\n> >       repo_config_get_bool(the_repository, \"rerere.autoupdate\", &rerere_autoupdate);\n> > +     repo_config_get_int(the_repository, \"rerere.locktimeout\",\n> > +                         &rerere_lock_timeout_ms);\n> >       repo_config(the_repository, git_default_config, NULL);\n> >   }\n> >\n> > @@ -908,12 +911,26 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n> >\n> >       if (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))\n> >               rerere_autoupdate = !!(flags & RERERE_AUTOUPDATE);\n> > -     if (flags & RERERE_READONLY)\n> > +     if (flags & RERERE_READONLY) {\n> >               fd = 0;\n> > -     else\n> > +     } else if (flags & RERERE_SKIP_LOCKED) {\n> >               fd = hold_lock_file_for_update(&write_lock,\n> > -                                            git_path_merge_rr(r),\n> > -                                            LOCK_DIE_ON_ERROR);\n> > +                                            git_path_merge_rr(r), 0);\n> > +             if (fd < 0) {\n> > +                     warning_errno(_(\"unable to lock '%s', skipping\"),\n> > +                                   git_path_merge_rr(r));\n> > +                     return -1;\n> > +             }\n> > +     } else {\n> > +             /*\n> > +              * A background \"rerere gc\" holds the lock for as long as it\n> > +              * takes to walk rr-cache, so wait it out rather than die.\n> > +              */\n> > +             fd = hold_lock_file_for_update_timeout(&write_lock,\n> > +                                                    git_path_merge_rr(r),\n> > +                                                    LOCK_DIE_ON_ERROR,\n> > +                                                    rerere_lock_timeout_ms);\n> > +     }\n> >       read_rr(r, merge_rr);\n> >       return fd;\n> >   }\n> > @@ -1237,7 +1254,7 @@ void rerere_gc(struct repository *r, struct string_list *rr)\n> >       timestamp_t cutoff_resolve = now - 60 * 86400;\n> >       struct strbuf buf = STRBUF_INIT;\n> >\n> > -     if (setup_rerere(r, rr, 0) < 0)\n> > +     if (setup_rerere(r, rr, RERERE_SKIP_LOCKED) < 0)\n> >               return;\n> >\n> >       repo_config_get_expiry_in_days(the_repository, \"gc.rerereresolved\",\n> > diff --git a/rerere.h b/rerere.h\n> > index d4b5f7c932..87964bb3c5 100644\n> > --- a/rerere.h\n> > +++ b/rerere.h\n> > @@ -10,6 +10,7 @@ struct repository;\n> >   #define RERERE_AUTOUPDATE   01\n> >   #define RERERE_NOAUTOUPDATE 02\n> >   #define RERERE_READONLY     04\n> > +#define RERERE_SKIP_LOCKED  010\n> >\n> >   /*\n> >    * Marks paths that have been hand-resolved and added to the\n> > diff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh\n> > index 1717f407c8..6b90294435 100755\n> > --- a/t/t4200-rerere.sh\n> > +++ b/t/t4200-rerere.sh\n> > @@ -242,6 +242,46 @@ test_expect_success 'old records rest in peace' '\n> >       test_path_is_missing $rr2/preimage\n> >   '\n> >\n> > +test_expect_success 'gc does nothing while MERGE_RR is locked' '\n> > +     mkdir -p $rr2 &&\n> > +     echo Hello >$rr2/preimage &&\n> > +     test-tool chmtime =$just_over_15_days_ago $rr2/preimage &&\n> > +\n> > +     test_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n> > +     >.git/MERGE_RR.lock &&\n> > +     git rerere gc 2>err &&\n> > +     test_grep \"MERGE_RR\" err &&\n> > +     test_path_is_file $rr2/preimage &&\n> > +\n> > +     rm .git/MERGE_RR.lock &&\n> > +     git rerere gc &&\n> > +     test_path_is_missing $rr2/preimage\n> > +'\n> > +\n> > +test_expect_success 'a held lock is waited out within rerere.lockTimeout' '\n> > +     git reset --hard &&\n> > +     rm -rf $rr &&\n> > +     test_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n> > +     >.git/MERGE_RR.lock &&\n> > +     {\n> > +             (sleep 1 && rm -f .git/MERGE_RR.lock) &\n> > +     } &&\n> > +     test_must_fail git -c rerere.lockTimeout=5000 merge first 2>err &&\n> > +     wait &&\n> > +     test_grep ! \"Unable to create\" err &&\n> > +     grep \"^=======\\$\" $rr/preimage\n> > +'\n> > +\n> > +test_expect_success 'rerere.lockTimeout=0 fails at once on a held lock' '\n> > +     git reset --hard &&\n> > +     rm -rf $rr &&\n> > +     test_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n> > +     >.git/MERGE_RR.lock &&\n> > +     test_must_fail git -c rerere.lockTimeout=0 merge first 2>err &&\n> > +     test_grep \"Unable to create\" err &&\n> > +     test_path_is_missing $rr/preimage\n> > +'\n> > +\n> >   rerere_gc_custom_expiry_test () {\n> >       five_days=\"$1\" right_now=\"$2\"\n> >       test_expect_success \"rerere gc with custom expiry ($five_days, $right_now)\" '\n> > diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\n> > index d7f82e1bec..a55ca2e829 100755\n> > --- a/t/t7900-maintenance.sh\n> > +++ b/t/t7900-maintenance.sh\n> > @@ -885,6 +885,14 @@ test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.aut\n> >       test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 maintenance run --auto --task=rerere-gc\n> >   '\n> >\n> > +test_expect_success 'rerere-gc task succeeds while MERGE_RR is locked' '\n> > +     test_when_finished \"rm -rf .git/rr-cache .git/MERGE_RR.lock\" &&\n> > +     mkdir .git/rr-cache &&\n> > +     : >.git/rr-cache/entry &&\n> > +     >.git/MERGE_RR.lock &&\n> > +     test_expect_rerere_gc git maintenance run --task=rerere-gc\n> > +'\n> > +\n> >   test_expect_success '--auto and --schedule incompatible' '\n> >       test_must_fail git maintenance run --auto --schedule=daily 2>err &&\n> >       test_grep \"cannot be used together\" err\n> >\n> > base-commit: e9019fcafe0040228b8631c30f97ae1adb61bcdc\n>\n"},{"id":"551832","messageId":"apkkVAYOqjfAsp9-@pks.im","threadId":"66250","inReplyTo":"pull.2214.git.1788337897490.gitgitgadget@gmail.com","subject":"Re: [PATCH] rerere: keep a background gc from killing a rebase","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-09-03T07:40:04Z","receivedAt":"2026-09-03T07:40:22Z","isPatch":true,"body":"On Wed, Sep 02, 2026 at 08:31:37AM +0000, Thomas Bachem via GitGitGadget wrote:\n> From: Thomas Bachem <mail@thomasbachem.com>\n> \n> Since 2.54 unscheduled maintenance uses the \"geometric\" strategy, so\n> the \"git maintenance run --auto --detach\" behind every \"git commit\"\n> runs \"git rerere gc\" in the background whenever rr-cache has an entry.\n> That includes the \"git commit\" the sequencer runs for a resolved pick\n> on \"git rebase --continue\".\n\nI think this hints that we should tweak the default value of\n\"maintenance.rerere-gc.auto\". The way it's currently written we indeed\nare quite aggressive with spawning `git rerere gc`, and I agree that we\nshould tweak it. And in the best case we'd not only respect whether we\nhave a specific number of entries, but we should also respect whether\nthose would be garbage collected in the first place.\n\nI'll send a patch series later today to do this.\n\n[snip]\n> The gc needs the lock: it removes every rr-cache directory it finds\n> empty, and a rerere that has just created its directory but not yet\n> written the preimage looks exactly like that. So keep the lock and fix\n> both orders. When the gc finds the lock busy, let it warn and do\n> nothing this time, the way \"maintenance run\" treats its own lock, so a\n> manual \"git rerere gc\" sees the warning and the maintenance task and\n> \"git gc\" see a clean exit. When the gc holds the lock, let every other\n> caller wait it out instead of dying at once, for rerere.lockTimeout\n> milliseconds with the semantics of core.packedRefsTimeout: 1000 by\n> default, 0 for the old behaviour, -1 for an unbounded wait. Walking a\n> 20000-entry rr-cache takes about 0.4 s here.\n\nHaving a locking timeout is sensible anyway, I think. It does not only\nsolve races with a concurrent maintenance run, but also with concurrent\nwriters.\n\n> diff --git a/rerere.c b/rerere.c\n> index 8232542585..22d114262b 100644\n> --- a/rerere.c\n> +++ b/rerere.c\n> @@ -32,6 +32,7 @@ static int rerere_enabled = -1;\n>  \n>  /* automatically update cleanly resolved paths to the index */\n>  static int rerere_autoupdate;\n> +static int rerere_lock_timeout_ms = 1000;\n>  \n>  #define RR_HAS_POSTIMAGE 1\n>  #define RR_HAS_PREIMAGE 2\n> @@ -876,6 +877,8 @@ static void git_rerere_config(void)\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>  }\n>  \n> @@ -908,12 +911,26 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n>  \n>  \tif (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))\n>  \t\trerere_autoupdate = !!(flags & RERERE_AUTOUPDATE);\n> -\tif (flags & RERERE_READONLY)\n> +\tif (flags & RERERE_READONLY) {\n>  \t\tfd = 0;\n> -\telse\n> +\t} else if (flags & RERERE_SKIP_LOCKED) {\n>  \t\tfd = hold_lock_file_for_update(&write_lock,\n> -\t\t\t\t\t       git_path_merge_rr(r),\n> -\t\t\t\t\t       LOCK_DIE_ON_ERROR);\n> +\t\t\t\t\t       git_path_merge_rr(r), 0);\n> +\t\tif (fd < 0) {\n> +\t\t\twarning_errno(_(\"unable to lock '%s', skipping\"),\n> +\t\t\t\t      git_path_merge_rr(r));\n> +\t\t\treturn -1;\n> +\t\t}\n\nWe should instead pass `LOCK_REPORT_ON_ERROR`, as the lockfile machinery\nknows better why exactly locking has failed.\n\n> +\t} else {\n> +\t\t/*\n> +\t\t * A background \"rerere gc\" holds the lock for as long as it\n> +\t\t * takes to walk rr-cache, so wait it out rather than die.\n> +\t\t */\n> +\t\tfd = hold_lock_file_for_update_timeout(&write_lock,\n> +\t\t\t\t\t\t       git_path_merge_rr(r),\n> +\t\t\t\t\t\t       LOCK_DIE_ON_ERROR,\n> +\t\t\t\t\t\t       rerere_lock_timeout_ms);\n> +\t}\n\nI think we can easily combine those two branches and simply set the\ntimeout value to 0 in case we see the flag.\n\nPatrick\n"},{"id":"551834","messageId":"CAA0xjtp+Og_k7BYZfwX-LRW_8TAiCyp846+Mhk+hERM_GmRYkA@mail.gmail.com","threadId":"66250","inReplyTo":"apkkVAYOqjfAsp9-@pks.im","subject":"Re: [PATCH] rerere: keep a background gc from killing a rebase","fromName":"Thomas Bachem","fromEmail":"mail@thomasbachem.com","sentAt":"2026-09-03T08:11:05Z","receivedAt":"2026-09-03T08:11:18Z","isPatch":true,"body":"Hi Patrick,\n\nOn Thu, Sep 03, 2026 at 09:40:04AM +0200, Patrick Steinhardt wrote:\n> I think this hints that we should tweak the default value of\n> \"maintenance.rerere-gc.auto\". The way it's currently written we indeed\n> are quite aggressive with spawning `git rerere gc`, and I agree that we\n> should tweak it. And in the best case we'd not only respect whether we\n> have a specific number of entries, but we should also respect whether\n> those would be garbage collected in the first place.\n>\n> I'll send a patch series later today to do this.\n\nThanks. Checking whether anything would actually be pruned sounds\nright to me. It takes the frequency away, not the race, so I'd still\ndo the sequencer part Phillip asked for.\n\n> Having a locking timeout is sensible anyway, I think. It does not only\n> solve races with a concurrent maintenance run, but also with concurrent\n> writers.\n\nPhillip found the wait unfortunate and I offered to drop it. You would\nkeep it. I think the two fit together: wait up to rerere.lockTimeout,\nthen warn and return -1 instead of dying, so the caller goes on\nwithout rerere this once. The gc passes 0 and does not wait. That\ntakes the die out, which is what broke the rebase. The wait stays,\nbounded to a second, but skipping rerere is not free either: it can\nmean resolving a conflict again that rerere had already recorded, and\na second is cheap next to that. With the sequencer no longer spawning\nthe gc and your heuristic change, it should rarely come to either.\nPhillip, would that work for you?\n\n> We should instead pass `LOCK_REPORT_ON_ERROR`, as the lockfile machinery\n> knows better why exactly locking has failed.\n\nAgreed on the text, which also names a stale lock. But the callers\nthat go on without rerere then exit as if it were disabled, \"git\ncommit\" with 0, so for them I'd print it as a warning through\nunable_to_lock_message() rather than let LOCK_REPORT_ON_ERROR call it\nan error. An explicit \"git rerere forget\" or \"clear\" fails as before.\n\n> I think we can easily combine those two branches and simply set the\n> timeout value to 0 in case we see the flag.\n\nYes, that folds into one call.\n\nSo v2: setup_rerere() waits up to rerere.lockTimeout, 0 for the gc,\nthen warns and returns -1 where the caller can go on, with the\nsequencer patch on top. I'll reroll once Phillip has had a look.\n\nThanks,\nTom\n"},{"id":"551835","messageId":"apkwpKTGaMwTf0Hz@pks.im","threadId":"66250","inReplyTo":"CAA0xjtp+Og_k7BYZfwX-LRW_8TAiCyp846+Mhk+hERM_GmRYkA@mail.gmail.com","subject":"Re: [PATCH] rerere: keep a background gc from killing a rebase","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-09-03T08:32:36Z","receivedAt":"2026-09-03T08:32:49Z","isPatch":true,"body":"On Thu, Sep 03, 2026 at 10:11:05AM +0200, Thomas Bachem wrote:\n> Hi Patrick,\n> \n> On Thu, Sep 03, 2026 at 09:40:04AM +0200, Patrick Steinhardt wrote:\n> > I think this hints that we should tweak the default value of\n> > \"maintenance.rerere-gc.auto\". The way it's currently written we indeed\n> > are quite aggressive with spawning `git rerere gc`, and I agree that we\n> > should tweak it. And in the best case we'd not only respect whether we\n> > have a specific number of entries, but we should also respect whether\n> > those would be garbage collected in the first place.\n> >\n> > I'll send a patch series later today to do this.\n> \n> Thanks. Checking whether anything would actually be pruned sounds\n> right to me. It takes the frequency away, not the race, so I'd still\n> do the sequencer part Phillip asked for.\n\nYes. Ideally, I'd think that we should both introduce the grace period\nfor locking the file and adapting the heuristic used by the maintenance\nstrategy. Whether we should completely disable auto-maintenance when in\nthe sequencer... I dunno. In any case, that feels like another separate\ntopic that should probably be discussed in its own series.\n\n> > Having a locking timeout is sensible anyway, I think. It does not only\n> > solve races with a concurrent maintenance run, but also with concurrent\n> > writers.\n> \n> Phillip found the wait unfortunate and I offered to drop it. You would\n> keep it. I think the two fit together: wait up to rerere.lockTimeout,\n> then warn and return -1 instead of dying, so the caller goes on\n> without rerere this once. The gc passes 0 and does not wait. That\n> takes the die out, which is what broke the rebase. The wait stays,\n> bounded to a second, but skipping rerere is not free either: it can\n> mean resolving a conflict again that rerere had already recorded, and\n> a second is cheap next to that. With the sequencer no longer spawning\n> the gc and your heuristic change, it should rarely come to either.\n> Phillip, would that work for you?\n\nI think that having the wait is a sensible thing to do, as the race was\na preexisting one that was only uncovered by the change to the default\nmaintenance strategy. It can also happen with two concurrent processes\nthat both happen to write rerere entries. You wouldn't normally see the\nwait anyway, so in the happy path nobody will really care. And in the\ncases where you would see it the user is probably more happy to wait a\nbit than having Git die (or just not write a rerere entry at all).\n\nPatrick\n"},{"id":"551857","messageId":"CAA0xjtpLtWqoJ+unHZn+Okcy=6_9EozuSKt3ZLMM-j+VGyJfjA@mail.gmail.com","threadId":"66250","inReplyTo":"apkwpKTGaMwTf0Hz@pks.im","subject":"Re: [PATCH] rerere: keep a background gc from killing a rebase","fromName":"Thomas Bachem","fromEmail":"mail@thomasbachem.com","sentAt":"2026-09-03T12:12:07Z","receivedAt":"2026-09-03T12:12:20Z","isPatch":true,"body":"Hi Patrick,\n\nOn Thu, Sep 03, 2026 at 10:32:36AM +0200, Patrick Steinhardt wrote:\n> Yes. Ideally, I'd think that we should both introduce the grace period\n> for locking the file and adapting the heuristic used by the maintenance\n> strategy. Whether we should completely disable auto-maintenance when in\n> the sequencer... I dunno. In any case, that feels like another separate\n> topic that should probably be discussed in its own series.\n\nPhillip, this is the part I said I'd do in this series, so I'd\nrather answer it here than just drop it. I think Patrick is right\nthat it's a topic of its own. My reason for wanting it in the same\nseries was the recording lost at a stop while the gc holds the lock,\nand that was for the variant without the wait. With the wait kept,\nthe next pick waits the gc out and records as before, so the\nsequencer patch no longer buys the rebase anything the rerere patch\ndoesn't, short of a prune that outlasts the timeout.\n\nWhat it would still decide is whether a rebase with the merge backend\nruns maintenance at all, the question from my last mail, and that is\na discussion of its own. So I'd make v2 the rerere patch alone and\nsend the sequencer change separately if you still want it. Say if\nyou would rather keep them together.\n\n> I think that having the wait is a sensible thing to do, as the race was\n> a preexisting one that was only uncovered by the change to the default\n> maintenance strategy. It can also happen with two concurrent processes\n> that both happen to write rerere entries. You wouldn't normally see the\n> wait anyway, so in the happy path nobody will really care. And in the\n> cases where you would see it the user is probably more happy to wait a\n> bit than having Git die (or just not write a rerere entry at all).\n\nAgreed, and that is the order v2 keeps: wait first, skip only once\nthe wait has run out. Since your series means the gc now only runs\nwhen there is something to prune, I measured how long that wait can\nget: pruning 20000 stale entries holds the lock for 2.7 s here,\nwalking 20000 fresh ones takes 0.4 s, so the one second default\ncovers a prune of roughly 7000 entries if it scales. I'd keep the\ndefault. A backlog that size is a one-off, and where it does hit,\nthe timeout now skips one recording where it used to kill the\nrebase.\n\nMy patch is based on maint since the bug is there, and I'd keep it\nthat way unless Junio would rather have it on master. Merged up it\nconflicts with d43f701d32 (lockfile: add\nrepo_hold_lock_file_for_update{,_timeout}{,_mode}(), 2026-07-14) in\nsetup_rerere(). The resolution is to take the repo-scoped helper, and\nwith that t4200 and t7900 pass on top of your series. I'll wait a\nday or two for Phillip before rerolling.\n\nThanks,\nTom\n"},{"id":"551866","messageId":"ca3b91b6-254c-4b86-adb8-da3217e9f6e7@gmail.com","threadId":"66250","inReplyTo":"apkwpKTGaMwTf0Hz@pks.im","subject":"Re: [PATCH] rerere: keep a background gc from killing a rebase","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-03T13:50:39Z","receivedAt":"2026-09-03T13:50:45Z","isPatch":true,"body":"Hi Patrick and Thomas\n\nOn 03/09/2026 09:32, Patrick Steinhardt wrote:\n> On Thu, Sep 03, 2026 at 10:11:05AM +0200, Thomas Bachem wrote:\n>> Hi Patrick,\n>>\n>> On Thu, Sep 03, 2026 at 09:40:04AM +0200, Patrick Steinhardt wrote:\n>>> I think this hints that we should tweak the default value of\n>>> \"maintenance.rerere-gc.auto\". The way it's currently written we indeed\n>>> are quite aggressive with spawning `git rerere gc`, and I agree that we\n>>> should tweak it. And in the best case we'd not only respect whether we\n>>> have a specific number of entries, but we should also respect whether\n>>> those would be garbage collected in the first place.\n>>>\n>>> I'll send a patch series later today to do this.\n>>\n>> Thanks. Checking whether anything would actually be pruned sounds\n>> right to me. It takes the frequency away, not the race, so I'd still\n>> do the sequencer part Phillip asked for.\n> \n> Yes. Ideally, I'd think that we should both introduce the grace period\n> for locking the file and adapting the heuristic used by the maintenance\n> strategy. \n\nI agree\n\n> Whether we should completely disable auto-maintenance when in\n> the sequencer... I dunno. In any case, that feels like another separate\n> topic that should probably be discussed in its own series.\n\nWe've seen other bugs reported related to auto-maintenance triggered \nduring a rebase such as the one dscho fixed recently. While I can see \nrepacking might be helpful during a very large rebase, I do not think \ngarbage collection is useful - all the objects and rerere entries that \nare created during the rebase are going to be too fresh to be collected. \nSo I think it would be a good idea to disable auto maintenance in a \nrebase and see if anyone complains. If it turns out to be a problem we \ncan figure out how to make it repack incrementally.\n\n>>> Having a locking timeout is sensible anyway, I think. It does not only\n>>> solve races with a concurrent maintenance run, but also with concurrent\n>>> writers.\n>>\n>> Phillip found the wait unfortunate and I offered to drop it. You would\n>> keep it. I think the two fit together: wait up to rerere.lockTimeout,\n>> then warn and return -1 instead of dying, so the caller goes on\n>> without rerere this once. The gc passes 0 and does not wait. That\n>> takes the die out, which is what broke the rebase. The wait stays,\n>> bounded to a second, but skipping rerere is not free either: it can\n>> mean resolving a conflict again that rerere had already recorded, and\n>> a second is cheap next to that. With the sequencer no longer spawning\n>> the gc and your heuristic change, it should rarely come to either.\n>> Phillip, would that work for you?\n> \n> I think that having the wait is a sensible thing to do, as the race was\n> a preexisting one that was only uncovered by the change to the default\n> maintenance strategy. It can also happen with two concurrent processes\n> that both happen to write rerere entries. You wouldn't normally see the\n> wait anyway, so in the happy path nobody will really care. And in the\n> cases where you would see it the user is probably more happy to wait a\n> bit than having Git die (or just not write a rerere entry at all).\n\nI don't object to the timeout as part of the solution. My objection was \nbased on it being the only solution as it is inconvenient to the user if \nthey have to wait for background maintenance jobs and it does not stop \nthe rebase from failing if the timeout is too short.\n\nThanks\n\nPhillip\n"},{"id":"551867","messageId":"86efb07c-a0ce-49b0-b4eb-7d6b4bbaeccc@gmail.com","threadId":"66250","inReplyTo":"CAA0xjtpYDGODpC9gJCZ_8KUvvtW53tTDed0iyDSZJCQchTWuAw@mail.gmail.com","subject":"Re: [PATCH] rerere: keep a background gc from killing a rebase","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-03T13:50:59Z","receivedAt":"2026-09-03T13:51:08Z","isPatch":true,"body":"Hi Thomas\n\nOn 02/09/2026 16:07, Thomas Bachem wrote:\n> On 02/09/2026 15:27, Phillip Wood wrote:\n>> To me this is another reason why we should disable gc.auto while\n>> rebasing. To do that we need to pass \"-c gc.auto=false -c\n>> maintenance.auto=false\" when running \"git commit\" in run_git_commit()\n>> and also when running \"git merge\" in do_merge(). We should also pass\n>> those settings via GIT_CONFIG_PARAMETERS when running a exec command in\n>> do_exec(). That is largly papering over the cracks but until we have a\n>> systematic solution it does at least stop exposing users to this bug.\n> \n> OK, I'll do that. It is also more consistent than it looks: the\n> commits the sequencer creates in-process via try_to_commit() don't run\n> auto maintenance at all, only the \"git commit\" child does (for a\n> resolved, reworded or squashed commit). What surprised me is that a\n> rebase with the merge backend then never runs maintenance, not even at\n> the end, because it doesn't go through finish_rebase() where the apply\n> backend runs it. Do you want a single run at the end of the sequence\n> in that patch, or keep it minimal?\n\nWe should be consistent between the backends, so yes we should be \ncalling run_auto_maintenance() at the end of a rebase with the merge \nbackend.\n\n> FWIW, the tool I hit this with has been setting both for its whole\n> process tree since, and the failures stopped.\n> \n>>> When it is the rebase, it dies in do_pick_commit()\n>>\n>> That's a bug us well - we should be returning errors, not dying\n>> -rerere_setup() should be returning an error, so we can clean up and\n>> reschedule the pick.\n> \n> Yes. I don't think we even need to reschedule: when repo_rerere() is\n> called there, the merge result is already in the index and worktree,\n> the error and advice have been printed, and the return value is\n> ignored. If setup_rerere() reports the lock and returns -1, the pick\n> just stops at the conflict like any other, minus rerere's recording\n> and replay, and --continue works. \n\nOh good point, if we get an error then we'll write the files to get \"git \nrebase --continue\" to commit the conflict resolution so we don't need to \nreschedule.\n\nThanks\n\nPhillip\n\n>> I think my preferred solution is to disable gc while rebasing. Returning\n>> an error from rerere_setup() would also help in the case where the user\n>> runs \"git commit\" and then continues the rebase. I'd be interested to\n>> hear what Junio and Patrick think about that.\n> \n> So v2 would be two patches: rerere returning an error on a busy lock\n> (with \"rerere gc\" still warning and skipping as in v1, and a commit\n> message that talks about the fix instead of the trace), and the\n> sequencer disabling gc.auto/maintenance.auto for \"git commit\", \"git\n> merge\" and exec. I'll wait for Junio and Patrick before rerolling in\n> case they see it differently.\n> \n> Patrick, one thing I noticed on the way: since 452b12c2e0\n> (builtin/maintenance: use \"geometric\" strategy by default, 2026-02-24)\n> every \"maintenance run --auto\" runs rerere-gc as soon as rr-cache has\n> even a single entry, stale or not. The doc for\n> maintenance.rerere-gc.auto says the heuristic may be refined; that\n> would make this rare for every command, not only the sequencer. Not\n> touching it in this series, just mentioning it.\n> \n> Thanks,\n> Tom\n> \n> \n> Am Mi., 2. Sept. 2026 um 15:27 Uhr schrieb Phillip Wood\n> <phillip.wood123@gmail.com>:\n>>\n>> Hi Thomas\n>>\n>> On 02/09/2026 09:31, Thomas Bachem via GitGitGadget wrote:\n>>> From: Thomas Bachem <mail@thomasbachem.com>\n>>>\n>>> Since 2.54 unscheduled maintenance uses the \"geometric\" strategy, so\n>>\n>> That change really is the gift that keeps on giving\n>>\n>>> the \"git maintenance run --auto --detach\" behind every \"git commit\"\n>>> runs \"git rerere gc\" in the background whenever rr-cache has an entry.\n>>> That includes the \"git commit\" the sequencer runs for a resolved pick\n>>> on \"git rebase --continue\".\n>>>\n>>> rerere_gc() takes MERGE_RR.lock through setup_rerere(), which uses\n>>> LOCK_DIE_ON_ERROR, and so does the sequencer's repo_rerere() at the\n>>> next conflict a few milliseconds later. Whichever comes second dies.\n>>\n>> To me this is another reason why we should disable gc.auto while\n>> rebasing. To do that we need to pass \"-c gc.auto=false -c\n>> maintenance.auto=false\" when running \"git commit\" in run_git_commit()\n>> and also when running \"git merge\" in do_merge(). We should also pass\n>> those settings via GIT_CONFIG_PARAMETERS when running a exec command in\n>> do_exec(). That is largly papering over the cracks but until we have a\n>> systematic solution it does at least stop exposing users to this bug.\n>>\n>>> When it is the rebase, it dies in do_pick_commit()\n>>\n>> That's a bug us well - we should be returning errors, not dying\n>> -rerere_setup() should be returning an error, so we can clean up and\n>> reschedule the pick.\n>>\n>> There is a lot of detail here about what causes the problem which is\n>> helpful, but there is very little discussion about the fix. As I\n>> understand it we now block the sequencer until the background\n>> maintenance has completed, or continue to die in an inconvenient state\n>> we timeout before the background maintenance finishes. That seems rather\n>> unfortunate as the idea of running the maintenance in the background is\n>> to prevent it from interfering with other commands.\n>>\n>> I think my preferred solution is to disable gc while rebasing. Returning\n>> an error from rerere_setup() would also help in the case where the user\n>> runs \"git commit\" and then continues the rebase. I'd be interested to\n>> hear what Junio and Patrick think about that. I'm also not clear why\n>> gc.auto has to fork a separate process just to check if it needs to run\n>> or not, I've not been following closely but my impression is that that\n>> is the cause of quite a lot of the lock contention bugs we've seen.\n>>\n>> Thanks\n>>\n>> Phillip\n>>\n>>> with the index\n>>> written but before make_patch() writes rebase-merge/{message,patch,\n>>> stopped-sha}, and every later \"git rebase --continue\" refuses with\n>>> \"you have staged changes in your working tree\". When it is the \"git\n>>> commit\" of a later continue, that one dies in its post-commit\n>>> repo_rerere() after the commit was made. Before 2.54 the same\n>>> collision needed an auto gc to actually run, since gc runs\n>>> \"rerere gc\" at its end.\n>>>\n>>> A rebase with two conflicts in a row shows it. The filler makes the\n>>> pick slower than the ~5 ms the background task needs to take the\n>>> lock, and keeps the lock held for about 0.4 s. It hit 6 of 6 runs\n>>> here on 2.55.0, and a test suite driving rebases on toy repositories\n>>> with a single rr-cache entry hit it in both runs that were traced:\n>>>\n>>>       git init -q -b main r && cd r\n>>>       git config rerere.enabled true\n>>>       git config maintenance.auto false\n>>>       mkdir pad && seq 20000 | (cd pad && split -l 1 -a 5)\n>>>       echo base >f && git add -A && git commit -qm base\n>>>       git checkout -q -b topic\n>>>       echo b >f && git commit -qam B\n>>>       echo c >f && git commit -qam C\n>>>       git checkout -q main\n>>>       echo a >f && git commit -qam A\n>>>       git repack -adq\n>>>       seq 20000 | awk '{printf \".git/rr-cache/%040x\\n\", $1}' \\\n>>>           | xargs mkdir -p\n>>>       for d in .git/rr-cache/*/; do echo x >$d/preimage; done\n>>>       git config --unset maintenance.auto\n>>>       git checkout -q topic\n>>>       git rebase main\n>>>       echo ab >f && git add f\n>>>       GIT_EDITOR=true git rebase --continue\n>>>\n>>> The second continue dies with \"Unable to create '.git/MERGE_RR.lock':\n>>> File exists\" while the gc spawned by its own commit holds the lock,\n>>> and after resolving C every further continue refuses. Maintenance\n>>> stays off during the setup so that no repack is pending: a repack due\n>>> at that commit runs ahead of rerere-gc in the task list and would\n>>> spend the window.\n>>>\n>>> The gc needs the lock: it removes every rr-cache directory it finds\n>>> empty, and a rerere that has just created its directory but not yet\n>>> written the preimage looks exactly like that. So keep the lock and fix\n>>> both orders. When the gc finds the lock busy, let it warn and do\n>>> nothing this time, the way \"maintenance run\" treats its own lock, so a\n>>> manual \"git rerere gc\" sees the warning and the maintenance task and\n>>> \"git gc\" see a clean exit. When the gc holds the lock, let every other\n>>> caller wait it out instead of dying at once, for rerere.lockTimeout\n>>> milliseconds with the semantics of core.packedRefsTimeout: 1000 by\n>>> default, 0 for the old behaviour, -1 for an unbounded wait. Walking a\n>>> 20000-entry rr-cache takes about 0.4 s here.\n>>>\n>>> That rebase now completes. The tests cover the gc under a held lock,\n>>> directly and through the maintenance task, a merge that waits a lock\n>>> out within a five second rerere.lockTimeout, and one that fails at\n>>> once with a timeout of 0.\n>>>\n>>> Assisted-by: Claude Fable 5.1\n>>> Signed-off-by: Thomas Bachem <mail@thomasbachem.com>\n>>> ---\n>>>       rerere: keep a background gc from killing a rebase\n>>>\n>>> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2214%2Fthomasbachem%2Frerere-gc-lock-v1\n>>> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2214/thomasbachem/rerere-gc-lock-v1\n>>> Pull-Request: https://github.com/gitgitgadget/git/pull/2214\n>>>\n>>>    Documentation/config/rerere.adoc |  8 +++++++\n>>>    Documentation/git-rerere.adoc    |  4 +++-\n>>>    rerere.c                         | 27 +++++++++++++++++----\n>>>    rerere.h                         |  1 +\n>>>    t/t4200-rerere.sh                | 40 ++++++++++++++++++++++++++++++++\n>>>    t/t7900-maintenance.sh           |  8 +++++++\n>>>    6 files changed, 82 insertions(+), 6 deletions(-)\n>>>\n>>> diff --git a/Documentation/config/rerere.adoc b/Documentation/config/rerere.adoc\n>>> index 3a78b5ebb1..8041a1587b 100644\n>>> --- a/Documentation/config/rerere.adoc\n>>> +++ b/Documentation/config/rerere.adoc\n>>> @@ -10,3 +10,11 @@ rerere.enabled::\n>>>        enabled if there is an `rr-cache` directory under the\n>>>        `$GIT_DIR`, e.g. if \"rerere\" was previously used in the\n>>>        repository.\n>>> +\n>>> +rerere.lockTimeout::\n>>> +     The length of time, in milliseconds, to retry when trying to\n>>> +     take the rerere lock while another process holds it, typically\n>>> +     a background `git rerere gc`.  Value 0 means not to retry at\n>>> +     all; -1 means to try indefinitely.  Default is 1000 (i.e.,\n>>> +     retry for 1 second).  `git rerere gc` itself does not wait and\n>>> +     skips its run instead.\n>>> diff --git a/Documentation/git-rerere.adoc b/Documentation/git-rerere.adoc\n>>> index 4e6ab9a27c..05935b0603 100644\n>>> --- a/Documentation/git-rerere.adoc\n>>> +++ b/Documentation/git-rerere.adoc\n>>> @@ -70,7 +70,9 @@ occurred a long time ago.  By default, unresolved conflicts older\n>>>    than 15 days and resolved conflicts older than 60\n>>>    days are pruned.  These defaults are controlled via the\n>>>    `gc.rerereUnresolved` and `gc.rerereResolved` configuration\n>>> -variables respectively.\n>>> +variables respectively.  If another process holds the lock on the\n>>> +recorded resolutions, for example a merge or rebase that is recording\n>>> +a conflict, `gc` does nothing and reports so.\n>>>\n>>>\n>>>    DISCUSSION\n>>> diff --git a/rerere.c b/rerere.c\n>>> index 8232542585..22d114262b 100644\n>>> --- a/rerere.c\n>>> +++ b/rerere.c\n>>> @@ -32,6 +32,7 @@ static int rerere_enabled = -1;\n>>>\n>>>    /* automatically update cleanly resolved paths to the index */\n>>>    static int rerere_autoupdate;\n>>> +static int rerere_lock_timeout_ms = 1000;\n>>>\n>>>    #define RR_HAS_POSTIMAGE 1\n>>>    #define RR_HAS_PREIMAGE 2\n>>> @@ -876,6 +877,8 @@ static void git_rerere_config(void)\n>>>    {\n>>>        repo_config_get_bool(the_repository, \"rerere.enabled\", &rerere_enabled);\n>>>        repo_config_get_bool(the_repository, \"rerere.autoupdate\", &rerere_autoupdate);\n>>> +     repo_config_get_int(the_repository, \"rerere.locktimeout\",\n>>> +                         &rerere_lock_timeout_ms);\n>>>        repo_config(the_repository, git_default_config, NULL);\n>>>    }\n>>>\n>>> @@ -908,12 +911,26 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n>>>\n>>>        if (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))\n>>>                rerere_autoupdate = !!(flags & RERERE_AUTOUPDATE);\n>>> -     if (flags & RERERE_READONLY)\n>>> +     if (flags & RERERE_READONLY) {\n>>>                fd = 0;\n>>> -     else\n>>> +     } else if (flags & RERERE_SKIP_LOCKED) {\n>>>                fd = hold_lock_file_for_update(&write_lock,\n>>> -                                            git_path_merge_rr(r),\n>>> -                                            LOCK_DIE_ON_ERROR);\n>>> +                                            git_path_merge_rr(r), 0);\n>>> +             if (fd < 0) {\n>>> +                     warning_errno(_(\"unable to lock '%s', skipping\"),\n>>> +                                   git_path_merge_rr(r));\n>>> +                     return -1;\n>>> +             }\n>>> +     } else {\n>>> +             /*\n>>> +              * A background \"rerere gc\" holds the lock for as long as it\n>>> +              * takes to walk rr-cache, so wait it out rather than die.\n>>> +              */\n>>> +             fd = hold_lock_file_for_update_timeout(&write_lock,\n>>> +                                                    git_path_merge_rr(r),\n>>> +                                                    LOCK_DIE_ON_ERROR,\n>>> +                                                    rerere_lock_timeout_ms);\n>>> +     }\n>>>        read_rr(r, merge_rr);\n>>>        return fd;\n>>>    }\n>>> @@ -1237,7 +1254,7 @@ void rerere_gc(struct repository *r, struct string_list *rr)\n>>>        timestamp_t cutoff_resolve = now - 60 * 86400;\n>>>        struct strbuf buf = STRBUF_INIT;\n>>>\n>>> -     if (setup_rerere(r, rr, 0) < 0)\n>>> +     if (setup_rerere(r, rr, RERERE_SKIP_LOCKED) < 0)\n>>>                return;\n>>>\n>>>        repo_config_get_expiry_in_days(the_repository, \"gc.rerereresolved\",\n>>> diff --git a/rerere.h b/rerere.h\n>>> index d4b5f7c932..87964bb3c5 100644\n>>> --- a/rerere.h\n>>> +++ b/rerere.h\n>>> @@ -10,6 +10,7 @@ struct repository;\n>>>    #define RERERE_AUTOUPDATE   01\n>>>    #define RERERE_NOAUTOUPDATE 02\n>>>    #define RERERE_READONLY     04\n>>> +#define RERERE_SKIP_LOCKED  010\n>>>\n>>>    /*\n>>>     * Marks paths that have been hand-resolved and added to the\n>>> diff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh\n>>> index 1717f407c8..6b90294435 100755\n>>> --- a/t/t4200-rerere.sh\n>>> +++ b/t/t4200-rerere.sh\n>>> @@ -242,6 +242,46 @@ test_expect_success 'old records rest in peace' '\n>>>        test_path_is_missing $rr2/preimage\n>>>    '\n>>>\n>>> +test_expect_success 'gc does nothing while MERGE_RR is locked' '\n>>> +     mkdir -p $rr2 &&\n>>> +     echo Hello >$rr2/preimage &&\n>>> +     test-tool chmtime =$just_over_15_days_ago $rr2/preimage &&\n>>> +\n>>> +     test_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n>>> +     >.git/MERGE_RR.lock &&\n>>> +     git rerere gc 2>err &&\n>>> +     test_grep \"MERGE_RR\" err &&\n>>> +     test_path_is_file $rr2/preimage &&\n>>> +\n>>> +     rm .git/MERGE_RR.lock &&\n>>> +     git rerere gc &&\n>>> +     test_path_is_missing $rr2/preimage\n>>> +'\n>>> +\n>>> +test_expect_success 'a held lock is waited out within rerere.lockTimeout' '\n>>> +     git reset --hard &&\n>>> +     rm -rf $rr &&\n>>> +     test_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n>>> +     >.git/MERGE_RR.lock &&\n>>> +     {\n>>> +             (sleep 1 && rm -f .git/MERGE_RR.lock) &\n>>> +     } &&\n>>> +     test_must_fail git -c rerere.lockTimeout=5000 merge first 2>err &&\n>>> +     wait &&\n>>> +     test_grep ! \"Unable to create\" err &&\n>>> +     grep \"^=======\\$\" $rr/preimage\n>>> +'\n>>> +\n>>> +test_expect_success 'rerere.lockTimeout=0 fails at once on a held lock' '\n>>> +     git reset --hard &&\n>>> +     rm -rf $rr &&\n>>> +     test_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n>>> +     >.git/MERGE_RR.lock &&\n>>> +     test_must_fail git -c rerere.lockTimeout=0 merge first 2>err &&\n>>> +     test_grep \"Unable to create\" err &&\n>>> +     test_path_is_missing $rr/preimage\n>>> +'\n>>> +\n>>>    rerere_gc_custom_expiry_test () {\n>>>        five_days=\"$1\" right_now=\"$2\"\n>>>        test_expect_success \"rerere gc with custom expiry ($five_days, $right_now)\" '\n>>> diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\n>>> index d7f82e1bec..a55ca2e829 100755\n>>> --- a/t/t7900-maintenance.sh\n>>> +++ b/t/t7900-maintenance.sh\n>>> @@ -885,6 +885,14 @@ test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.aut\n>>>        test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 maintenance run --auto --task=rerere-gc\n>>>    '\n>>>\n>>> +test_expect_success 'rerere-gc task succeeds while MERGE_RR is locked' '\n>>> +     test_when_finished \"rm -rf .git/rr-cache .git/MERGE_RR.lock\" &&\n>>> +     mkdir .git/rr-cache &&\n>>> +     : >.git/rr-cache/entry &&\n>>> +     >.git/MERGE_RR.lock &&\n>>> +     test_expect_rerere_gc git maintenance run --task=rerere-gc\n>>> +'\n>>> +\n>>>    test_expect_success '--auto and --schedule incompatible' '\n>>>        test_must_fail git maintenance run --auto --schedule=daily 2>err &&\n>>>        test_grep \"cannot be used together\" err\n>>>\n>>> base-commit: e9019fcafe0040228b8631c30f97ae1adb61bcdc\n>>\n\n"},{"id":"551932","messageId":"pull.2214.v2.git.1788507876543.gitgitgadget@gmail.com","threadId":"66250","inReplyTo":"pull.2214.git.1788337897490.gitgitgadget@gmail.com","subject":"[PATCH v2] rerere: keep a background gc from killing a rebase","fromName":"Thomas Bachem via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-04T07:44:36Z","receivedAt":"2026-09-04T07:44:39Z","isPatch":true,"body":"From: Thomas Bachem <mail@thomasbachem.com>\n\nSince 2.54 unscheduled maintenance uses the \"geometric\" strategy, so\nthe \"git maintenance run --auto --detach\" behind every \"git commit\"\nruns \"git rerere gc\" in the background whenever rr-cache has an entry.\nThat includes the \"git commit\" the sequencer runs for a resolved pick\non \"git rebase --continue\".\n\nrerere_gc() takes MERGE_RR.lock through setup_rerere(), which uses\nLOCK_DIE_ON_ERROR, and so does the sequencer's repo_rerere() at the\nnext conflict a few milliseconds later. Whichever comes second dies.\nWhen it is the rebase, it dies in do_pick_commit() with the index\nwritten but before make_patch() writes rebase-merge/{message,patch,\nstopped-sha}, and every later \"git rebase --continue\" refuses with\n\"you have staged changes in your working tree\". When it is the \"git\ncommit\" of a later continue, that one dies in its post-commit\nrepo_rerere() after the commit was made. Before 2.54 the same\ncollision needed an auto gc to actually run, since gc runs\n\"rerere gc\" at its end.\n\nA rebase with two conflicts in a row shows it. The filler makes the\npick slower than the ~5 ms the background task needs to take the\nlock, and the stale entries give the gc something to prune, which\nkeeps the lock held for about half a second. It hit 6 of 6 runs here\non 2.55.0, and a test suite driving rebases on toy repositories with\na single rr-cache entry hit it in both runs that were traced:\n\n    git init -q -b main r && cd r\n    git config rerere.enabled true\n    git config maintenance.auto false\n    mkdir pad && seq 20000 | (cd pad && split -l 1 -a 5)\n    echo base >f && git add -A && git commit -qm base\n    git checkout -q -b topic\n    echo b >f && git commit -qam B\n    echo c >f && git commit -qam C\n    git checkout -q main\n    echo a >f && git commit -qam A\n    git repack -adq\n    git ls-files -s pad | head -n 5000 |\n        awk '{print \".git/rr-cache/\" $2}' | xargs mkdir -p\n    for d in .git/rr-cache/*/; do echo x >$d/preimage; done\n    touch -t 202001010000 .git/rr-cache/*/preimage\n    git config --unset maintenance.auto\n    git checkout -q topic\n    git rebase main\n    echo ab >f && git add f\n    GIT_EDITOR=true git rebase --continue\n\nThe continue dies with \"Unable to create '.git/MERGE_RR.lock': File\nexists\" while the gc spawned by its own commit holds the lock, and\nafter resolving C every further continue refuses. Maintenance stays\noff during the setup so that no repack is pending: a repack due at\nthat commit runs ahead of rerere-gc in the task list and would spend\nthe window.\n\nThe gc needs the lock: it removes every rr-cache directory it finds\nempty, and a rerere that has just created its directory but not yet\nwritten the preimage looks exactly like that. So keep the lock and\nstop dying over it. A caller that finds the lock held now waits for\nrerere.lockTimeout milliseconds, with the semantics of\ncore.packedRefsTimeout and the same default of 1000, and then warns\nand goes on without rerere: a merge, a commit or a pick loses one\nrecording or replay, which is nothing next to a rebase that cannot\ncontinue. The gc itself never waits, since it has nothing to lose\nfrom a skipped run, and \"git rerere\", \"git rerere forget\" and \"git\nrerere clear\" keep dying, since they exist for nothing but the state\nbehind the lock. The clearing \"git am\" and \"git rebase\" do on --abort\nand --skip goes on without it: the cleanup that follows removes\nMERGE_RR anyway, and the unresolved entries it would have dropped are\nleft for the gc. A stale MERGE_RR.lock, which used to stop every\nmerge, now costs each command a second and a warning until it is\nremoved.\n\nThat rebase now stops at C the normal way, with its preimage recorded\nonce the prune is over, and continues once C is resolved. The tests\ncover the gc under a held lock, directly and through the maintenance\ntask, a merge that waits a lock out within rerere.lockTimeout, a\nmerge, a commit and a rebase that go on without rerere once it is\nup, \"git rebase --abort\" doing the same, and the three explicit\ncommands failing.\n\nAssisted-by: Claude Fable 5.1\nSigned-off-by: Thomas Bachem <mail@thomasbachem.com>\n---\n    rerere: keep a background gc from killing a rebase\n    \n    Changes since v1:\n    \n     * Once rerere.lockTimeout is up, setup_rerere() warns and returns -1\n       instead of dying, so a merge, commit or pick goes on without rerere\n       (Phillip). Only \"git rerere\", \"git rerere forget\" and \"git rerere\n       clear\" keep dying, through a RERERE_LOCK_OR_DIE flag. rerere_clear()\n       takes flags for that, since \"am\" and \"rebase\" call it too and go on.\n     * RERERE_SKIP_LOCKED is now RERERE_NOWAIT: skipping is what everyone\n       does, not waiting is what sets the gc apart. The wait and its default\n       stay (Patrick).\n     * The warning stays hand-rolled rather than LOCK_REPORT_ON_ERROR\n       (Patrick): the lockfile message tells the user to terminate other git\n       processes and try again, which a command that goes on without rerere\n       should not say. It names the lock file, says that rerere is skipped,\n       and carries the errno, so the reason still shows.\n     * The repro uses 5000 stale entries named after blobs, so the gc has\n       something to prune and the run still triggers a gc that only prunes\n       what is due, as with Patrick's heuristic series. The prune holds the\n       lock for about half a second here, within the default timeout, so the\n       fixed rebase records the second conflict rather than skip it.\n     * Tests: the timeout=0 test now checks that the merge goes on, and new\n       ones cover a commit and a rebase going on under a held lock, \"git\n       rebase --abort\", and the three explicit commands. test_grep\n       throughout.\n     * Commit message: the fix gets the discussion, the repro one paragraph\n       and its script (Phillip). What waits, what goes on and what keeps\n       dying is spelled out, and the script has one continue, not two.\n    \n    Still based on maint, where the bug ships (2.54.0 and 2.55.0). Merged up\n    it conflicts with d43f701d32 (lockfile: add\n    repo_hold_lock_file_for_update{,_timeout}{,_mode}(), 2026-07-14) in\n    setup_rerere(), where the resolution takes the repo-scoped helper. With\n    that, t4200 and t7900 pass on top of Patrick's series and with the\n    sequencer series that keeps auto maintenance out of a rebase, sent\n    separately.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2214%2Fthomasbachem%2Frerere-gc-lock-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2214/thomasbachem/rerere-gc-lock-v2\nPull-Request: https://github.com/gitgitgadget/git/pull/2214\n\nRange-diff vs v1:\n\n 1:  ecd9e5b4ec ! 1:  bbb2338572 rerere: keep a background gc from killing a rebase\n     @@ Commit message\n      \n          A rebase with two conflicts in a row shows it. The filler makes the\n          pick slower than the ~5 ms the background task needs to take the\n     -    lock, and keeps the lock held for about 0.4 s. It hit 6 of 6 runs\n     -    here on 2.55.0, and a test suite driving rebases on toy repositories\n     -    with a single rr-cache entry hit it in both runs that were traced:\n     +    lock, and the stale entries give the gc something to prune, which\n     +    keeps the lock held for about half a second. It hit 6 of 6 runs here\n     +    on 2.55.0, and a test suite driving rebases on toy repositories with\n     +    a single rr-cache entry hit it in both runs that were traced:\n      \n              git init -q -b main r && cd r\n              git config rerere.enabled true\n     @@ Commit message\n              git checkout -q main\n              echo a >f && git commit -qam A\n              git repack -adq\n     -        seq 20000 | awk '{printf \".git/rr-cache/%040x\\n\", $1}' \\\n     -            | xargs mkdir -p\n     +        git ls-files -s pad | head -n 5000 |\n     +            awk '{print \".git/rr-cache/\" $2}' | xargs mkdir -p\n              for d in .git/rr-cache/*/; do echo x >$d/preimage; done\n     +        touch -t 202001010000 .git/rr-cache/*/preimage\n              git config --unset maintenance.auto\n              git checkout -q topic\n              git rebase main\n              echo ab >f && git add f\n              GIT_EDITOR=true git rebase --continue\n      \n     -    The second continue dies with \"Unable to create '.git/MERGE_RR.lock':\n     -    File exists\" while the gc spawned by its own commit holds the lock,\n     -    and after resolving C every further continue refuses. Maintenance\n     -    stays off during the setup so that no repack is pending: a repack due\n     -    at that commit runs ahead of rerere-gc in the task list and would\n     -    spend the window.\n     +    The continue dies with \"Unable to create '.git/MERGE_RR.lock': File\n     +    exists\" while the gc spawned by its own commit holds the lock, and\n     +    after resolving C every further continue refuses. Maintenance stays\n     +    off during the setup so that no repack is pending: a repack due at\n     +    that commit runs ahead of rerere-gc in the task list and would spend\n     +    the window.\n      \n          The gc needs the lock: it removes every rr-cache directory it finds\n          empty, and a rerere that has just created its directory but not yet\n     -    written the preimage looks exactly like that. So keep the lock and fix\n     -    both orders. When the gc finds the lock busy, let it warn and do\n     -    nothing this time, the way \"maintenance run\" treats its own lock, so a\n     -    manual \"git rerere gc\" sees the warning and the maintenance task and\n     -    \"git gc\" see a clean exit. When the gc holds the lock, let every other\n     -    caller wait it out instead of dying at once, for rerere.lockTimeout\n     -    milliseconds with the semantics of core.packedRefsTimeout: 1000 by\n     -    default, 0 for the old behaviour, -1 for an unbounded wait. Walking a\n     -    20000-entry rr-cache takes about 0.4 s here.\n     -\n     -    That rebase now completes. The tests cover the gc under a held lock,\n     -    directly and through the maintenance task, a merge that waits a lock\n     -    out within a five second rerere.lockTimeout, and one that fails at\n     -    once with a timeout of 0.\n     +    written the preimage looks exactly like that. So keep the lock and\n     +    stop dying over it. A caller that finds the lock held now waits for\n     +    rerere.lockTimeout milliseconds, with the semantics of\n     +    core.packedRefsTimeout and the same default of 1000, and then warns\n     +    and goes on without rerere: a merge, a commit or a pick loses one\n     +    recording or replay, which is nothing next to a rebase that cannot\n     +    continue. The gc itself never waits, since it has nothing to lose\n     +    from a skipped run, and \"git rerere\", \"git rerere forget\" and \"git\n     +    rerere clear\" keep dying, since they exist for nothing but the state\n     +    behind the lock. The clearing \"git am\" and \"git rebase\" do on --abort\n     +    and --skip goes on without it: the cleanup that follows removes\n     +    MERGE_RR anyway, and the unresolved entries it would have dropped are\n     +    left for the gc. A stale MERGE_RR.lock, which used to stop every\n     +    merge, now costs each command a second and a warning until it is\n     +    removed.\n     +\n     +    That rebase now stops at C the normal way, with its preimage recorded\n     +    once the prune is over, and continues once C is resolved. The tests\n     +    cover the gc under a held lock, directly and through the maintenance\n     +    task, a merge that waits a lock out within rerere.lockTimeout, a\n     +    merge, a commit and a rebase that go on without rerere once it is\n     +    up, \"git rebase --abort\" doing the same, and the three explicit\n     +    commands failing.\n      \n          Assisted-by: Claude Fable 5.1\n          Signed-off-by: Thomas Bachem <mail@thomasbachem.com>\n     @@ Documentation/config/rerere.adoc: rerere.enabled::\n      +rerere.lockTimeout::\n      +\tThe length of time, in milliseconds, to retry when trying to\n      +\ttake the rerere lock while another process holds it, typically\n     -+\ta background `git rerere gc`.  Value 0 means not to retry at\n     -+\tall; -1 means to try indefinitely.  Default is 1000 (i.e.,\n     -+\tretry for 1 second).  `git rerere gc` itself does not wait and\n     -+\tskips its run instead.\n     ++\ta background `git rerere gc`.  When the time is up, the command\n     ++\twarns and goes on without rerere.  Value 0 means not to retry\n     ++\tat all; -1 means to try indefinitely.  Default is 1000 (i.e.,\n     ++\tretry for 1 second).  `git rerere gc` does not retry, and\n     ++\t`git rerere`, `git rerere forget` and `git rerere clear` fail\n     ++\tinstead of going on.\n      \n       ## Documentation/git-rerere.adoc ##\n      @@ Documentation/git-rerere.adoc: occurred a long time ago.  By default, unresolved conflicts older\n     @@ Documentation/git-rerere.adoc: occurred a long time ago.  By default, unresolved\n       \n       DISCUSSION\n      \n     + ## builtin/am.c ##\n     +@@ builtin/am.c: static int clean_index(const struct object_id *head, const struct object_id *rem\n     + static void am_rerere_clear(void)\n     + {\n     + \tstruct string_list merge_rr = STRING_LIST_INIT_DUP;\n     +-\trerere_clear(the_repository, &merge_rr);\n     ++\trerere_clear(the_repository, &merge_rr, 0);\n     + \tstring_list_clear(&merge_rr, 1);\n     + }\n     + \n     +\n     + ## builtin/rebase.c ##\n     +@@ builtin/rebase.c: static int run_sequencer_rebase(struct rebase_options *opts)\n     + \tcase ACTION_SKIP: {\n     + \t\tstruct string_list merge_rr = STRING_LIST_INIT_DUP;\n     + \n     +-\t\trerere_clear(the_repository, &merge_rr);\n     ++\t\trerere_clear(the_repository, &merge_rr, 0);\n     + \t}\n     + \t\t/* fallthrough */\n     + \tcase ACTION_CONTINUE: {\n     +@@ builtin/rebase.c: int cmd_rebase(int argc,\n     + \tcase ACTION_SKIP: {\n     + \t\tstruct string_list merge_rr = STRING_LIST_INIT_DUP;\n     + \n     +-\t\trerere_clear(the_repository, &merge_rr);\n     ++\t\trerere_clear(the_repository, &merge_rr, 0);\n     + \t\tstring_list_clear(&merge_rr, 1);\n     + \t\tropts.flags = RESET_HEAD_HARD;\n     + \t\tif (reset_head(the_repository, &ropts) < 0)\n     +@@ builtin/rebase.c: int cmd_rebase(int argc,\n     + \t\tstruct string_list merge_rr = STRING_LIST_INIT_DUP;\n     + \t\tstruct strbuf head_msg = STRBUF_INIT;\n     + \n     +-\t\trerere_clear(the_repository, &merge_rr);\n     ++\t\trerere_clear(the_repository, &merge_rr, 0);\n     + \t\tstring_list_clear(&merge_rr, 1);\n     + \n     + \t\tif (read_basic_state(&options))\n     +\n     + ## builtin/rerere.c ##\n     +@@ builtin/rerere.c: int cmd_rerere(int argc,\n     + \t\tflags = RERERE_NOAUTOUPDATE;\n     + \n     + \tif (argc < 1)\n     +-\t\treturn repo_rerere(the_repository, flags);\n     ++\t\treturn repo_rerere(the_repository, flags | RERERE_LOCK_OR_DIE);\n     + \n     + \tif (!strcmp(argv[0], \"forget\")) {\n     + \t\tstruct pathspec pathspec;\n     +@@ builtin/rerere.c: int cmd_rerere(int argc,\n     + \t\tparse_pathspec(&pathspec, 0, PATHSPEC_PREFER_CWD,\n     + \t\t\t       prefix, argv + 1);\n     + \n     +-\t\tret = rerere_forget(the_repository, &pathspec);\n     ++\t\tret = rerere_forget(the_repository, &pathspec,\n     ++\t\t\t\t    RERERE_LOCK_OR_DIE);\n     + \n     + \t\tclear_pathspec(&pathspec);\n     + \t\treturn ret;\n     + \t}\n     + \n     + \tif (!strcmp(argv[0], \"clear\")) {\n     +-\t\trerere_clear(the_repository, &merge_rr);\n     ++\t\trerere_clear(the_repository, &merge_rr, RERERE_LOCK_OR_DIE);\n     + \t} else if (!strcmp(argv[0], \"gc\"))\n     + \t\trerere_gc(the_repository, &merge_rr);\n     + \telse if (!strcmp(argv[0], \"status\")) {\n     +\n       ## rerere.c ##\n      @@ rerere.c: static int rerere_enabled = -1;\n       \n     @@ rerere.c: int setup_rerere(struct repository *r, struct string_list *merge_rr, i\n      +\tif (flags & RERERE_READONLY) {\n       \t\tfd = 0;\n      -\telse\n     -+\t} else if (flags & RERERE_SKIP_LOCKED) {\n     - \t\tfd = hold_lock_file_for_update(&write_lock,\n     +-\t\tfd = hold_lock_file_for_update(&write_lock,\n      -\t\t\t\t\t       git_path_merge_rr(r),\n      -\t\t\t\t\t       LOCK_DIE_ON_ERROR);\n     -+\t\t\t\t\t       git_path_merge_rr(r), 0);\n     -+\t\tif (fd < 0) {\n     -+\t\t\twarning_errno(_(\"unable to lock '%s', skipping\"),\n     -+\t\t\t\t      git_path_merge_rr(r));\n     -+\t\t\treturn -1;\n     -+\t\t}\n      +\t} else {\n     ++\t\tint lock_flags = 0;\n     ++\t\tlong timeout_ms = rerere_lock_timeout_ms;\n     ++\n     ++\t\tif (flags & RERERE_LOCK_OR_DIE)\n     ++\t\t\tlock_flags = LOCK_DIE_ON_ERROR;\n     ++\t\tif (flags & RERERE_NOWAIT)\n     ++\t\t\ttimeout_ms = 0;\n      +\t\t/*\n      +\t\t * A background \"rerere gc\" holds the lock for as long as it\n     -+\t\t * takes to walk rr-cache, so wait it out rather than die.\n     ++\t\t * takes to prune rr-cache, so wait it out rather than fail\n     ++\t\t * at once.  The gc itself has nothing to lose from a skipped\n     ++\t\t * run and never waits.\n      +\t\t */\n      +\t\tfd = hold_lock_file_for_update_timeout(&write_lock,\n      +\t\t\t\t\t\t       git_path_merge_rr(r),\n     -+\t\t\t\t\t\t       LOCK_DIE_ON_ERROR,\n     -+\t\t\t\t\t\t       rerere_lock_timeout_ms);\n     ++\t\t\t\t\t\t       lock_flags, timeout_ms);\n     ++\t\tif (fd < 0) {\n     ++\t\t\twarning_errno(_(\"skipping rerere, unable to create '%s.lock'\"),\n     ++\t\t\t\t      git_path_merge_rr(r));\n     ++\t\t\treturn -1;\n     ++\t\t}\n      +\t}\n       \tread_rr(r, merge_rr);\n       \treturn fd;\n       }\n     +@@ rerere.c: fail_exit:\n     + \treturn -1;\n     + }\n     + \n     +-int rerere_forget(struct repository *r, struct pathspec *pathspec)\n     ++int rerere_forget(struct repository *r, struct pathspec *pathspec, int flags)\n     + {\n     + \tint i, fd, ret;\n     + \tstruct string_list conflict = STRING_LIST_INIT_DUP;\n     +@@ rerere.c: int rerere_forget(struct repository *r, struct pathspec *pathspec)\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 = setup_rerere(r, &merge_rr, RERERE_NOAUTOUPDATE | flags);\n     + \tif (fd < 0)\n     + \t\treturn 0;\n     + \n      @@ rerere.c: void rerere_gc(struct repository *r, struct string_list *rr)\n       \ttimestamp_t cutoff_resolve = now - 60 * 86400;\n       \tstruct strbuf buf = STRBUF_INIT;\n       \n      -\tif (setup_rerere(r, rr, 0) < 0)\n     -+\tif (setup_rerere(r, rr, RERERE_SKIP_LOCKED) < 0)\n     ++\tif (setup_rerere(r, rr, RERERE_NOWAIT) < 0)\n       \t\treturn;\n       \n       \trepo_config_get_expiry_in_days(the_repository, \"gc.rerereresolved\",\n     +@@ rerere.c: void rerere_gc(struct repository *r, struct string_list *rr)\n     +  *\n     +  * NEEDSWORK: shouldn't we be calling this from \"reset --hard\"?\n     +  */\n     +-void rerere_clear(struct repository *r, struct string_list *merge_rr)\n     ++void rerere_clear(struct repository *r, struct string_list *merge_rr, int flags)\n     + {\n     + \tint i;\n     + \n     +-\tif (setup_rerere(r, merge_rr, 0) < 0)\n     ++\tif (setup_rerere(r, merge_rr, flags) < 0)\n     + \t\treturn;\n     + \n     + \tfor (i = 0; i < merge_rr->nr; i++) {\n      \n       ## rerere.h ##\n      @@ rerere.h: struct repository;\n       #define RERERE_AUTOUPDATE   01\n       #define RERERE_NOAUTOUPDATE 02\n       #define RERERE_READONLY     04\n     -+#define RERERE_SKIP_LOCKED  010\n     ++/* Do not wait for the lock when another process holds it */\n     ++#define RERERE_NOWAIT       010\n     ++/* Die on a lock that cannot be taken instead of going on without rerere */\n     ++#define RERERE_LOCK_OR_DIE  020\n       \n       /*\n        * Marks paths that have been hand-resolved and added to the\n     +@@ rerere.h: int repo_rerere(struct repository *, int);\n     +  */\n     + const char *rerere_path(struct strbuf *buf, const struct rerere_id *,\n     + \t\t\tconst char *file);\n     +-int rerere_forget(struct repository *, struct pathspec *);\n     ++int rerere_forget(struct repository *, struct pathspec *, int);\n     + int rerere_remaining(struct repository *, struct string_list *);\n     +-void rerere_clear(struct repository *, struct string_list *);\n     ++void rerere_clear(struct repository *, struct string_list *, int);\n     + void rerere_gc(struct repository *, struct string_list *);\n     + \n     + #define OPT_RERERE_AUTOUPDATE(v) OPT_UYN(0, \"rerere-autoupdate\", (v), \\\n      \n       ## t/t4200-rerere.sh ##\n      @@ t/t4200-rerere.sh: test_expect_success 'old records rest in peace' '\n     @@ t/t4200-rerere.sh: test_expect_success 'old records rest in peace' '\n      +\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n      +\t>.git/MERGE_RR.lock &&\n      +\tgit rerere gc 2>err &&\n     -+\ttest_grep \"MERGE_RR\" err &&\n     ++\ttest_grep \"MERGE_RR.lock\" err &&\n      +\ttest_path_is_file $rr2/preimage &&\n      +\n      +\trm .git/MERGE_RR.lock &&\n     @@ t/t4200-rerere.sh: test_expect_success 'old records rest in peace' '\n      +\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n      +\t>.git/MERGE_RR.lock &&\n      +\t{\n     -+\t\t(sleep 1 && rm -f .git/MERGE_RR.lock) &\n     ++\t\t( sleep 1 && rm -f .git/MERGE_RR.lock ) &\n      +\t} &&\n      +\ttest_must_fail git -c rerere.lockTimeout=5000 merge first 2>err &&\n      +\twait &&\n     -+\ttest_grep ! \"Unable to create\" err &&\n     -+\tgrep \"^=======\\$\" $rr/preimage\n     ++\ttest_grep ! \"MERGE_RR\" err &&\n     ++\ttest_grep \"^=======\\$\" $rr/preimage\n      +'\n      +\n     -+test_expect_success 'rerere.lockTimeout=0 fails at once on a held lock' '\n     ++test_expect_success 'merge goes on without rerere once rerere.lockTimeout is up' '\n      +\tgit reset --hard &&\n      +\trm -rf $rr &&\n      +\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n      +\t>.git/MERGE_RR.lock &&\n      +\ttest_must_fail git -c rerere.lockTimeout=0 merge first 2>err &&\n     ++\ttest_grep \"skipping rerere\" err &&\n     ++\ttest_grep \"^=======\\$\" a1 &&\n     ++\ttest_path_is_missing $rr/preimage\n     ++'\n     ++\n     ++test_expect_success 'commit goes on without rerere once rerere.lockTimeout is up' '\n     ++\tgit reset --hard &&\n     ++\trm -rf $rr &&\n     ++\tgit checkout -b lock-held-commit third &&\n     ++\ttest_when_finished \"git checkout third && git branch -D lock-held-commit\" &&\n     ++\ttest_must_fail git merge first &&\n     ++\ttest_path_is_file $rr/preimage &&\n     ++\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n     ++\t>.git/MERGE_RR.lock &&\n     ++\techo resolved >a1 &&\n     ++\tgit add a1 &&\n     ++\tgit -c rerere.lockTimeout=0 commit -qm resolved 2>err &&\n     ++\ttest_grep \"skipping rerere\" err &&\n     ++\ttest_path_is_missing $rr/postimage\n     ++'\n     ++\n     ++test_expect_success 'rerere, forget and clear fail on a lock they cannot take' '\n     ++\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n     ++\t>.git/MERGE_RR.lock &&\n     ++\ttest_must_fail git -c rerere.lockTimeout=0 rerere 2>err &&\n     ++\ttest_grep \"Unable to create\" err &&\n     ++\ttest_must_fail git -c rerere.lockTimeout=0 rerere forget a1 2>err &&\n      +\ttest_grep \"Unable to create\" err &&\n     ++\ttest_must_fail git -c rerere.lockTimeout=0 rerere clear 2>err &&\n     ++\ttest_grep \"Unable to create\" err\n     ++'\n     ++\n     ++test_expect_success 'rebase goes on without rerere once rerere.lockTimeout is up' '\n     ++\tgit reset --hard &&\n     ++\trm -rf $rr &&\n     ++\tgit checkout -b lock-held third &&\n     ++\ttest_when_finished \"git checkout third && git branch -D lock-held\" &&\n     ++\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n     ++\t>.git/MERGE_RR.lock &&\n     ++\ttest_must_fail git -c rerere.lockTimeout=0 rebase first 2>err &&\n     ++\ttest_grep \"skipping rerere\" err &&\n     ++\ttest_path_is_file .git/rebase-merge/stopped-sha &&\n     ++\techo resolved >a1 &&\n     ++\tgit add a1 &&\n     ++\tgit -c rerere.lockTimeout=0 rebase --continue &&\n     ++\ttest_path_is_missing .git/rebase-merge &&\n      +\ttest_path_is_missing $rr/preimage\n      +'\n     ++\n     ++test_expect_success 'rebase --abort goes on without rerere on a held lock' '\n     ++\tgit checkout -b lock-held-abort third &&\n     ++\ttest_when_finished \"git checkout third && git branch -D lock-held-abort\" &&\n     ++\ttest_must_fail git rebase first &&\n     ++\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n     ++\t>.git/MERGE_RR.lock &&\n     ++\tgit -c rerere.lockTimeout=0 rebase --abort 2>err &&\n     ++\ttest_grep \"skipping rerere\" err &&\n     ++\ttest_path_is_missing .git/rebase-merge\n     ++'\n      +\n       rerere_gc_custom_expiry_test () {\n       \tfive_days=\"$1\" right_now=\"$2\"\n\n\n Documentation/config/rerere.adoc | 10 ++++\n Documentation/git-rerere.adoc    |  4 +-\n builtin/am.c                     |  2 +-\n builtin/rebase.c                 |  6 +-\n builtin/rerere.c                 |  7 ++-\n rerere.c                         | 42 ++++++++++----\n rerere.h                         |  8 ++-\n t/t4200-rerere.sh                | 96 ++++++++++++++++++++++++++++++++\n t/t7900-maintenance.sh           |  8 +++\n 9 files changed, 163 insertions(+), 20 deletions(-)\n\ndiff --git a/Documentation/config/rerere.adoc b/Documentation/config/rerere.adoc\nindex 3a78b5ebb1..b67323fc46 100644\n--- a/Documentation/config/rerere.adoc\n+++ b/Documentation/config/rerere.adoc\n@@ -10,3 +10,13 @@ rerere.enabled::\n \tenabled if there is an `rr-cache` directory under the\n \t`$GIT_DIR`, e.g. if \"rerere\" was previously used in the\n \trepository.\n+\n+rerere.lockTimeout::\n+\tThe length of time, in milliseconds, to retry when trying to\n+\ttake the rerere lock while another process holds it, typically\n+\ta background `git rerere gc`.  When the time is up, the command\n+\twarns and goes on without rerere.  Value 0 means not to retry\n+\tat all; -1 means to try indefinitely.  Default is 1000 (i.e.,\n+\tretry for 1 second).  `git rerere gc` does not retry, and\n+\t`git rerere`, `git rerere forget` and `git rerere clear` fail\n+\tinstead of going on.\ndiff --git a/Documentation/git-rerere.adoc b/Documentation/git-rerere.adoc\nindex 4e6ab9a27c..05935b0603 100644\n--- a/Documentation/git-rerere.adoc\n+++ b/Documentation/git-rerere.adoc\n@@ -70,7 +70,9 @@ occurred a long time ago.  By default, unresolved conflicts older\n than 15 days and resolved conflicts older than 60\n days are pruned.  These defaults are controlled via the\n `gc.rerereUnresolved` and `gc.rerereResolved` configuration\n-variables respectively.\n+variables respectively.  If another process holds the lock on the\n+recorded resolutions, for example a merge or rebase that is recording\n+a conflict, `gc` does nothing and reports so.\n \n \n DISCUSSION\ndiff --git a/builtin/am.c b/builtin/am.c\nindex e9623b8307..32f11161b4 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -2112,7 +2112,7 @@ static int clean_index(const struct object_id *head, const struct object_id *rem\n static void am_rerere_clear(void)\n {\n \tstruct string_list merge_rr = STRING_LIST_INIT_DUP;\n-\trerere_clear(the_repository, &merge_rr);\n+\trerere_clear(the_repository, &merge_rr, 0);\n \tstring_list_clear(&merge_rr, 1);\n }\n \ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex fa4f5d9306..363d177472 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -367,7 +367,7 @@ static int run_sequencer_rebase(struct rebase_options *opts)\n \tcase ACTION_SKIP: {\n \t\tstruct string_list merge_rr = STRING_LIST_INIT_DUP;\n \n-\t\trerere_clear(the_repository, &merge_rr);\n+\t\trerere_clear(the_repository, &merge_rr, 0);\n \t}\n \t\t/* fallthrough */\n \tcase ACTION_CONTINUE: {\n@@ -1382,7 +1382,7 @@ int cmd_rebase(int argc,\n \tcase ACTION_SKIP: {\n \t\tstruct string_list merge_rr = STRING_LIST_INIT_DUP;\n \n-\t\trerere_clear(the_repository, &merge_rr);\n+\t\trerere_clear(the_repository, &merge_rr, 0);\n \t\tstring_list_clear(&merge_rr, 1);\n \t\tropts.flags = RESET_HEAD_HARD;\n \t\tif (reset_head(the_repository, &ropts) < 0)\n@@ -1396,7 +1396,7 @@ int cmd_rebase(int argc,\n \t\tstruct string_list merge_rr = STRING_LIST_INIT_DUP;\n \t\tstruct strbuf head_msg = STRBUF_INIT;\n \n-\t\trerere_clear(the_repository, &merge_rr);\n+\t\trerere_clear(the_repository, &merge_rr, 0);\n \t\tstring_list_clear(&merge_rr, 1);\n \n \t\tif (read_basic_state(&options))\ndiff --git a/builtin/rerere.c b/builtin/rerere.c\nindex a056cb791b..70a4bd1683 100644\n--- a/builtin/rerere.c\n+++ b/builtin/rerere.c\n@@ -74,7 +74,7 @@ int cmd_rerere(int argc,\n \t\tflags = RERERE_NOAUTOUPDATE;\n \n \tif (argc < 1)\n-\t\treturn repo_rerere(the_repository, flags);\n+\t\treturn repo_rerere(the_repository, flags | RERERE_LOCK_OR_DIE);\n \n \tif (!strcmp(argv[0], \"forget\")) {\n \t\tstruct pathspec pathspec;\n@@ -85,14 +85,15 @@ int cmd_rerere(int argc,\n \t\tparse_pathspec(&pathspec, 0, PATHSPEC_PREFER_CWD,\n \t\t\t       prefix, argv + 1);\n \n-\t\tret = rerere_forget(the_repository, &pathspec);\n+\t\tret = rerere_forget(the_repository, &pathspec,\n+\t\t\t\t    RERERE_LOCK_OR_DIE);\n \n \t\tclear_pathspec(&pathspec);\n \t\treturn ret;\n \t}\n \n \tif (!strcmp(argv[0], \"clear\")) {\n-\t\trerere_clear(the_repository, &merge_rr);\n+\t\trerere_clear(the_repository, &merge_rr, RERERE_LOCK_OR_DIE);\n \t} else if (!strcmp(argv[0], \"gc\"))\n \t\trerere_gc(the_repository, &merge_rr);\n \telse if (!strcmp(argv[0], \"status\")) {\ndiff --git a/rerere.c b/rerere.c\nindex 8232542585..4e2ececc09 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -32,6 +32,7 @@ static int rerere_enabled = -1;\n \n /* automatically update cleanly resolved paths to the index */\n static int rerere_autoupdate;\n+static int rerere_lock_timeout_ms = 1000;\n \n #define RR_HAS_POSTIMAGE 1\n #define RR_HAS_PREIMAGE 2\n@@ -876,6 +877,8 @@ static void git_rerere_config(void)\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 }\n \n@@ -908,12 +911,31 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n \n \tif (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))\n \t\trerere_autoupdate = !!(flags & RERERE_AUTOUPDATE);\n-\tif (flags & RERERE_READONLY)\n+\tif (flags & RERERE_READONLY) {\n \t\tfd = 0;\n-\telse\n-\t\tfd = hold_lock_file_for_update(&write_lock,\n-\t\t\t\t\t       git_path_merge_rr(r),\n-\t\t\t\t\t       LOCK_DIE_ON_ERROR);\n+\t} else {\n+\t\tint lock_flags = 0;\n+\t\tlong timeout_ms = rerere_lock_timeout_ms;\n+\n+\t\tif (flags & RERERE_LOCK_OR_DIE)\n+\t\t\tlock_flags = LOCK_DIE_ON_ERROR;\n+\t\tif (flags & RERERE_NOWAIT)\n+\t\t\ttimeout_ms = 0;\n+\t\t/*\n+\t\t * A background \"rerere gc\" holds the lock for as long as it\n+\t\t * takes to prune rr-cache, so wait it out rather than fail\n+\t\t * at once.  The gc itself has nothing to lose from a skipped\n+\t\t * run and never waits.\n+\t\t */\n+\t\tfd = hold_lock_file_for_update_timeout(&write_lock,\n+\t\t\t\t\t\t       git_path_merge_rr(r),\n+\t\t\t\t\t\t       lock_flags, timeout_ms);\n+\t\tif (fd < 0) {\n+\t\t\twarning_errno(_(\"skipping rerere, unable to create '%s.lock'\"),\n+\t\t\t\t      git_path_merge_rr(r));\n+\t\t\treturn -1;\n+\t\t}\n+\t}\n \tread_rr(r, merge_rr);\n \treturn fd;\n }\n@@ -1124,7 +1146,7 @@ fail_exit:\n \treturn -1;\n }\n \n-int rerere_forget(struct repository *r, struct pathspec *pathspec)\n+int rerere_forget(struct repository *r, struct pathspec *pathspec, int flags)\n {\n \tint i, fd, ret;\n \tstruct string_list conflict = STRING_LIST_INIT_DUP;\n@@ -1133,7 +1155,7 @@ int rerere_forget(struct repository *r, struct pathspec *pathspec)\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 = setup_rerere(r, &merge_rr, RERERE_NOAUTOUPDATE | flags);\n \tif (fd < 0)\n \t\treturn 0;\n \n@@ -1237,7 +1259,7 @@ void rerere_gc(struct repository *r, struct string_list *rr)\n \ttimestamp_t cutoff_resolve = now - 60 * 86400;\n \tstruct strbuf buf = STRBUF_INIT;\n \n-\tif (setup_rerere(r, rr, 0) < 0)\n+\tif (setup_rerere(r, rr, RERERE_NOWAIT) < 0)\n \t\treturn;\n \n \trepo_config_get_expiry_in_days(the_repository, \"gc.rerereresolved\",\n@@ -1289,11 +1311,11 @@ void rerere_gc(struct repository *r, struct string_list *rr)\n  *\n  * NEEDSWORK: shouldn't we be calling this from \"reset --hard\"?\n  */\n-void rerere_clear(struct repository *r, struct string_list *merge_rr)\n+void rerere_clear(struct repository *r, struct string_list *merge_rr, int flags)\n {\n \tint i;\n \n-\tif (setup_rerere(r, merge_rr, 0) < 0)\n+\tif (setup_rerere(r, merge_rr, flags) < 0)\n \t\treturn;\n \n \tfor (i = 0; i < merge_rr->nr; i++) {\ndiff --git a/rerere.h b/rerere.h\nindex d4b5f7c932..3a9f58acd9 100644\n--- a/rerere.h\n+++ b/rerere.h\n@@ -10,6 +10,10 @@ struct repository;\n #define RERERE_AUTOUPDATE   01\n #define RERERE_NOAUTOUPDATE 02\n #define RERERE_READONLY     04\n+/* Do not wait for the lock when another process holds it */\n+#define RERERE_NOWAIT       010\n+/* Die on a lock that cannot be taken instead of going on without rerere */\n+#define RERERE_LOCK_OR_DIE  020\n \n /*\n  * Marks paths that have been hand-resolved and added to the\n@@ -34,9 +38,9 @@ int repo_rerere(struct repository *, int);\n  */\n const char *rerere_path(struct strbuf *buf, const struct rerere_id *,\n \t\t\tconst char *file);\n-int rerere_forget(struct repository *, struct pathspec *);\n+int rerere_forget(struct repository *, struct pathspec *, int);\n int rerere_remaining(struct repository *, struct string_list *);\n-void rerere_clear(struct repository *, struct string_list *);\n+void rerere_clear(struct repository *, struct string_list *, int);\n void rerere_gc(struct repository *, struct string_list *);\n \n #define OPT_RERERE_AUTOUPDATE(v) OPT_UYN(0, \"rerere-autoupdate\", (v), \\\ndiff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh\nindex 1717f407c8..243b3ebed3 100755\n--- a/t/t4200-rerere.sh\n+++ b/t/t4200-rerere.sh\n@@ -242,6 +242,102 @@ test_expect_success 'old records rest in peace' '\n \ttest_path_is_missing $rr2/preimage\n '\n \n+test_expect_success 'gc does nothing while MERGE_RR is locked' '\n+\tmkdir -p $rr2 &&\n+\techo Hello >$rr2/preimage &&\n+\ttest-tool chmtime =$just_over_15_days_ago $rr2/preimage &&\n+\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\tgit rerere gc 2>err &&\n+\ttest_grep \"MERGE_RR.lock\" err &&\n+\ttest_path_is_file $rr2/preimage &&\n+\n+\trm .git/MERGE_RR.lock &&\n+\tgit rerere gc &&\n+\ttest_path_is_missing $rr2/preimage\n+'\n+\n+test_expect_success 'a held lock is waited out within rerere.lockTimeout' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\t{\n+\t\t( sleep 1 && rm -f .git/MERGE_RR.lock ) &\n+\t} &&\n+\ttest_must_fail git -c rerere.lockTimeout=5000 merge first 2>err &&\n+\twait &&\n+\ttest_grep ! \"MERGE_RR\" err &&\n+\ttest_grep \"^=======\\$\" $rr/preimage\n+'\n+\n+test_expect_success 'merge goes on without rerere once rerere.lockTimeout is up' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 merge first 2>err &&\n+\ttest_grep \"skipping rerere\" err &&\n+\ttest_grep \"^=======\\$\" a1 &&\n+\ttest_path_is_missing $rr/preimage\n+'\n+\n+test_expect_success 'commit goes on without rerere once rerere.lockTimeout is up' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\tgit checkout -b lock-held-commit third &&\n+\ttest_when_finished \"git checkout third && git branch -D lock-held-commit\" &&\n+\ttest_must_fail git merge first &&\n+\ttest_path_is_file $rr/preimage &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\techo resolved >a1 &&\n+\tgit add a1 &&\n+\tgit -c rerere.lockTimeout=0 commit -qm resolved 2>err &&\n+\ttest_grep \"skipping rerere\" err &&\n+\ttest_path_is_missing $rr/postimage\n+'\n+\n+test_expect_success 'rerere, forget and clear fail on a lock they cannot take' '\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rerere 2>err &&\n+\ttest_grep \"Unable to create\" err &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rerere forget a1 2>err &&\n+\ttest_grep \"Unable to create\" err &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rerere clear 2>err &&\n+\ttest_grep \"Unable to create\" err\n+'\n+\n+test_expect_success 'rebase goes on without rerere once rerere.lockTimeout is up' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\tgit checkout -b lock-held third &&\n+\ttest_when_finished \"git checkout third && git branch -D lock-held\" &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rebase first 2>err &&\n+\ttest_grep \"skipping rerere\" err &&\n+\ttest_path_is_file .git/rebase-merge/stopped-sha &&\n+\techo resolved >a1 &&\n+\tgit add a1 &&\n+\tgit -c rerere.lockTimeout=0 rebase --continue &&\n+\ttest_path_is_missing .git/rebase-merge &&\n+\ttest_path_is_missing $rr/preimage\n+'\n+\n+test_expect_success 'rebase --abort goes on without rerere on a held lock' '\n+\tgit checkout -b lock-held-abort third &&\n+\ttest_when_finished \"git checkout third && git branch -D lock-held-abort\" &&\n+\ttest_must_fail git rebase first &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\tgit -c rerere.lockTimeout=0 rebase --abort 2>err &&\n+\ttest_grep \"skipping rerere\" err &&\n+\ttest_path_is_missing .git/rebase-merge\n+'\n+\n rerere_gc_custom_expiry_test () {\n \tfive_days=\"$1\" right_now=\"$2\"\n \ttest_expect_success \"rerere gc with custom expiry ($five_days, $right_now)\" '\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex d7f82e1bec..a55ca2e829 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -885,6 +885,14 @@ test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.aut\n \ttest_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 maintenance run --auto --task=rerere-gc\n '\n \n+test_expect_success 'rerere-gc task succeeds while MERGE_RR is locked' '\n+\ttest_when_finished \"rm -rf .git/rr-cache .git/MERGE_RR.lock\" &&\n+\tmkdir .git/rr-cache &&\n+\t: >.git/rr-cache/entry &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_expect_rerere_gc git maintenance run --task=rerere-gc\n+'\n+\n test_expect_success '--auto and --schedule incompatible' '\n \ttest_must_fail git maintenance run --auto --schedule=daily 2>err &&\n \ttest_grep \"cannot be used together\" err\n\nbase-commit: e9019fcafe0040228b8631c30f97ae1adb61bcdc\n-- \ngitgitgadget\n"},{"id":"551971","messageId":"5e613735-60e2-429d-a5bb-1a4f03578604@gmail.com","threadId":"66250","inReplyTo":"pull.2214.v2.git.1788507876543.gitgitgadget@gmail.com","subject":"Re: [PATCH v2] rerere: keep a background gc from killing a rebase","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-04T15:21:34Z","receivedAt":"2026-09-04T15:21:38Z","isPatch":true,"body":"Hi Thomas\n\nOn 04/09/2026 08:44, Thomas Bachem via GitGitGadget wrote:\n> From: Thomas Bachem <mail@thomasbachem.com>\n> \n> Since 2.54 unscheduled maintenance uses the \"geometric\" strategy, so\n> the \"git maintenance run --auto --detach\" behind every \"git commit\"\n> runs \"git rerere gc\" in the background whenever rr-cache has an entry.\n\nWith Patricks patches that's no-longer true I think. I think a better \nmotivation, as the cache is per-repository, rather than per-worktree, is \nconcurrent writers running in different worktrees. That makes the \ntimeout much more sensible as we expect writing a conflict resolution to \nbe much faster than gc.\n\nOverall, this commit message is rather long and it would be helpful if \nyou could distill it to remove unnecessary and unrelated details.\n\n> \n>   Documentation/config/rerere.adoc | 10 ++++\n>   Documentation/git-rerere.adoc    |  4 +-\n>   builtin/am.c                     |  2 +-\n>   builtin/rebase.c                 |  6 +-\n>   builtin/rerere.c                 |  7 ++-\n>   rerere.c                         | 42 ++++++++++----\n>   rerere.h                         |  8 ++-\n>   t/t4200-rerere.sh                | 96 ++++++++++++++++++++++++++++++++\n>   t/t7900-maintenance.sh           |  8 +++\n>   9 files changed, 163 insertions(+), 20 deletions(-)\n> \n> diff --git a/Documentation/config/rerere.adoc b/Documentation/config/rerere.adoc\n> index 3a78b5ebb1..b67323fc46 100644\n> --- a/Documentation/config/rerere.adoc\n> +++ b/Documentation/config/rerere.adoc\n> @@ -10,3 +10,13 @@ rerere.enabled::\n>   \tenabled if there is an `rr-cache` directory under the\n>   \t`$GIT_DIR`, e.g. if \"rerere\" was previously used in the\n>   \trepository.\n> +\n> +rerere.lockTimeout::\n> +\tThe length of time, in milliseconds, to retry when trying to\n> +\ttake the rerere lock while another process holds it, typically\n> +\ta background `git rerere gc`.  When the time is up, the command\n> +\twarns and goes on without rerere.  Value 0 means not to retry\n> +\tat all; -1 means to try indefinitely.  Default is 1000 (i.e.,\n> +\tretry for 1 second).  `git rerere gc` does not retry, and\n> +\t`git rerere`, `git rerere forget` and `git rerere clear` fail\n> +\tinstead of going on.\n\nWhy do those commands fail rather than wait?\n\n> @@ -908,12 +911,31 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n>   \n>   \tif (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))\n>   \t\trerere_autoupdate = !!(flags & RERERE_AUTOUPDATE);\n> -\tif (flags & RERERE_READONLY)\n> +\tif (flags & RERERE_READONLY) {\n>   \t\tfd = 0;\n> -\telse\n> -\t\tfd = hold_lock_file_for_update(&write_lock,\n> -\t\t\t\t\t       git_path_merge_rr(r),\n> -\t\t\t\t\t       LOCK_DIE_ON_ERROR);\n> +\t} else {\n> +\t\tint lock_flags = 0;\n> +\t\tlong timeout_ms = rerere_lock_timeout_ms;\n> +\n> +\t\tif (flags & RERERE_LOCK_OR_DIE)\n> +\t\t\tlock_flags = LOCK_DIE_ON_ERROR;\n> +\t\tif (flags & RERERE_NOWAIT)\n> +\t\t\ttimeout_ms = 0;\n\nIt might be worth adding a check above here that BUG()s out if the \ncaller passes an incompatible set of flags.\n\n> +\t\t/*\n> +\t\t * A background \"rerere gc\" holds the lock for as long as it\n> +\t\t * takes to prune rr-cache, so wait it out rather than fail\n> +\t\t * at once.  The gc itself has nothing to lose from a skipped\n> +\t\t * run and never waits.\n> +\t\t */\n> +\t\tfd = hold_lock_file_for_update_timeout(&write_lock,\n> +\t\t\t\t\t\t       git_path_merge_rr(r),\n> +\t\t\t\t\t\t       lock_flags, timeout_ms);\n> +\t\tif (fd < 0) {\n> +\t\t\twarning_errno(_(\"skipping rerere, unable to create '%s.lock'\"),\n> +\t\t\t\t      git_path_merge_rr(r));\n\nA background job that the user did not explicitly start printing to the \nterminal is rather confusing as it is likely to get mixed in with the \noutput of whatever is running in the foreground.\n\nThanks\n\nPhillip\n\n> +\t\t\treturn -1;\n> +\t\t}\n> +\t}\n>   \tread_rr(r, merge_rr);\n>   \treturn fd;\n>   }\n> @@ -1124,7 +1146,7 @@ fail_exit:\n>   \treturn -1;\n>   }\n>   \n> -int rerere_forget(struct repository *r, struct pathspec *pathspec)\n> +int rerere_forget(struct repository *r, struct pathspec *pathspec, int flags)\n>   {\n>   \tint i, fd, ret;\n>   \tstruct string_list conflict = STRING_LIST_INIT_DUP;\n> @@ -1133,7 +1155,7 @@ int rerere_forget(struct repository *r, struct pathspec *pathspec)\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 = setup_rerere(r, &merge_rr, RERERE_NOAUTOUPDATE | flags);\n>   \tif (fd < 0)\n>   \t\treturn 0;\n>   \n> @@ -1237,7 +1259,7 @@ void rerere_gc(struct repository *r, struct string_list *rr)\n>   \ttimestamp_t cutoff_resolve = now - 60 * 86400;\n>   \tstruct strbuf buf = STRBUF_INIT;\n>   \n> -\tif (setup_rerere(r, rr, 0) < 0)\n> +\tif (setup_rerere(r, rr, RERERE_NOWAIT) < 0)\n>   \t\treturn;\n>   \n>   \trepo_config_get_expiry_in_days(the_repository, \"gc.rerereresolved\",\n> @@ -1289,11 +1311,11 @@ void rerere_gc(struct repository *r, struct string_list *rr)\n>    *\n>    * NEEDSWORK: shouldn't we be calling this from \"reset --hard\"?\n>    */\n> -void rerere_clear(struct repository *r, struct string_list *merge_rr)\n> +void rerere_clear(struct repository *r, struct string_list *merge_rr, int flags)\n>   {\n>   \tint i;\n>   \n> -\tif (setup_rerere(r, merge_rr, 0) < 0)\n> +\tif (setup_rerere(r, merge_rr, flags) < 0)\n>   \t\treturn;\n>   \n>   \tfor (i = 0; i < merge_rr->nr; i++) {\n> diff --git a/rerere.h b/rerere.h\n> index d4b5f7c932..3a9f58acd9 100644\n> --- a/rerere.h\n> +++ b/rerere.h\n> @@ -10,6 +10,10 @@ struct repository;\n>   #define RERERE_AUTOUPDATE   01\n>   #define RERERE_NOAUTOUPDATE 02\n>   #define RERERE_READONLY     04\n> +/* Do not wait for the lock when another process holds it */\n> +#define RERERE_NOWAIT       010\n> +/* Die on a lock that cannot be taken instead of going on without rerere */\n> +#define RERERE_LOCK_OR_DIE  020\n>   \n>   /*\n>    * Marks paths that have been hand-resolved and added to the\n> @@ -34,9 +38,9 @@ int repo_rerere(struct repository *, int);\n>    */\n>   const char *rerere_path(struct strbuf *buf, const struct rerere_id *,\n>   \t\t\tconst char *file);\n> -int rerere_forget(struct repository *, struct pathspec *);\n> +int rerere_forget(struct repository *, struct pathspec *, int);\n>   int rerere_remaining(struct repository *, struct string_list *);\n> -void rerere_clear(struct repository *, struct string_list *);\n> +void rerere_clear(struct repository *, struct string_list *, int);\n>   void rerere_gc(struct repository *, struct string_list *);\n>   \n>   #define OPT_RERERE_AUTOUPDATE(v) OPT_UYN(0, \"rerere-autoupdate\", (v), \\\n> diff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh\n> index 1717f407c8..243b3ebed3 100755\n> --- a/t/t4200-rerere.sh\n> +++ b/t/t4200-rerere.sh\n> @@ -242,6 +242,102 @@ test_expect_success 'old records rest in peace' '\n>   \ttest_path_is_missing $rr2/preimage\n>   '\n>   \n> +test_expect_success 'gc does nothing while MERGE_RR is locked' '\n> +\tmkdir -p $rr2 &&\n> +\techo Hello >$rr2/preimage &&\n> +\ttest-tool chmtime =$just_over_15_days_ago $rr2/preimage &&\n> +\n> +\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n> +\t>.git/MERGE_RR.lock &&\n> +\tgit rerere gc 2>err &&\n> +\ttest_grep \"MERGE_RR.lock\" err &&\n> +\ttest_path_is_file $rr2/preimage &&\n> +\n> +\trm .git/MERGE_RR.lock &&\n> +\tgit rerere gc &&\n> +\ttest_path_is_missing $rr2/preimage\n> +'\n> +\n> +test_expect_success 'a held lock is waited out within rerere.lockTimeout' '\n> +\tgit reset --hard &&\n> +\trm -rf $rr &&\n> +\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n> +\t>.git/MERGE_RR.lock &&\n> +\t{\n> +\t\t( sleep 1 && rm -f .git/MERGE_RR.lock ) &\n> +\t} &&\n> +\ttest_must_fail git -c rerere.lockTimeout=5000 merge first 2>err &&\n> +\twait &&\n> +\ttest_grep ! \"MERGE_RR\" err &&\n> +\ttest_grep \"^=======\\$\" $rr/preimage\n> +'\n> +\n> +test_expect_success 'merge goes on without rerere once rerere.lockTimeout is up' '\n> +\tgit reset --hard &&\n> +\trm -rf $rr &&\n> +\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n> +\t>.git/MERGE_RR.lock &&\n> +\ttest_must_fail git -c rerere.lockTimeout=0 merge first 2>err &&\n> +\ttest_grep \"skipping rerere\" err &&\n> +\ttest_grep \"^=======\\$\" a1 &&\n> +\ttest_path_is_missing $rr/preimage\n> +'\n> +\n> +test_expect_success 'commit goes on without rerere once rerere.lockTimeout is up' '\n> +\tgit reset --hard &&\n> +\trm -rf $rr &&\n> +\tgit checkout -b lock-held-commit third &&\n> +\ttest_when_finished \"git checkout third && git branch -D lock-held-commit\" &&\n> +\ttest_must_fail git merge first &&\n> +\ttest_path_is_file $rr/preimage &&\n> +\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n> +\t>.git/MERGE_RR.lock &&\n> +\techo resolved >a1 &&\n> +\tgit add a1 &&\n> +\tgit -c rerere.lockTimeout=0 commit -qm resolved 2>err &&\n> +\ttest_grep \"skipping rerere\" err &&\n> +\ttest_path_is_missing $rr/postimage\n> +'\n> +\n> +test_expect_success 'rerere, forget and clear fail on a lock they cannot take' '\n> +\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n> +\t>.git/MERGE_RR.lock &&\n> +\ttest_must_fail git -c rerere.lockTimeout=0 rerere 2>err &&\n> +\ttest_grep \"Unable to create\" err &&\n> +\ttest_must_fail git -c rerere.lockTimeout=0 rerere forget a1 2>err &&\n> +\ttest_grep \"Unable to create\" err &&\n> +\ttest_must_fail git -c rerere.lockTimeout=0 rerere clear 2>err &&\n> +\ttest_grep \"Unable to create\" err\n> +'\n> +\n> +test_expect_success 'rebase goes on without rerere once rerere.lockTimeout is up' '\n> +\tgit reset --hard &&\n> +\trm -rf $rr &&\n> +\tgit checkout -b lock-held third &&\n> +\ttest_when_finished \"git checkout third && git branch -D lock-held\" &&\n> +\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n> +\t>.git/MERGE_RR.lock &&\n> +\ttest_must_fail git -c rerere.lockTimeout=0 rebase first 2>err &&\n> +\ttest_grep \"skipping rerere\" err &&\n> +\ttest_path_is_file .git/rebase-merge/stopped-sha &&\n> +\techo resolved >a1 &&\n> +\tgit add a1 &&\n> +\tgit -c rerere.lockTimeout=0 rebase --continue &&\n> +\ttest_path_is_missing .git/rebase-merge &&\n> +\ttest_path_is_missing $rr/preimage\n> +'\n> +\n> +test_expect_success 'rebase --abort goes on without rerere on a held lock' '\n> +\tgit checkout -b lock-held-abort third &&\n> +\ttest_when_finished \"git checkout third && git branch -D lock-held-abort\" &&\n> +\ttest_must_fail git rebase first &&\n> +\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n> +\t>.git/MERGE_RR.lock &&\n> +\tgit -c rerere.lockTimeout=0 rebase --abort 2>err &&\n> +\ttest_grep \"skipping rerere\" err &&\n> +\ttest_path_is_missing .git/rebase-merge\n> +'\n> +\n>   rerere_gc_custom_expiry_test () {\n>   \tfive_days=\"$1\" right_now=\"$2\"\n>   \ttest_expect_success \"rerere gc with custom expiry ($five_days, $right_now)\" '\n> diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\n> index d7f82e1bec..a55ca2e829 100755\n> --- a/t/t7900-maintenance.sh\n> +++ b/t/t7900-maintenance.sh\n> @@ -885,6 +885,14 @@ test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.aut\n>   \ttest_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 maintenance run --auto --task=rerere-gc\n>   '\n>   \n> +test_expect_success 'rerere-gc task succeeds while MERGE_RR is locked' '\n> +\ttest_when_finished \"rm -rf .git/rr-cache .git/MERGE_RR.lock\" &&\n> +\tmkdir .git/rr-cache &&\n> +\t: >.git/rr-cache/entry &&\n> +\t>.git/MERGE_RR.lock &&\n> +\ttest_expect_rerere_gc git maintenance run --task=rerere-gc\n> +'\n> +\n>   test_expect_success '--auto and --schedule incompatible' '\n>   \ttest_must_fail git maintenance run --auto --schedule=daily 2>err &&\n>   \ttest_grep \"cannot be used together\" err\n> \n> base-commit: e9019fcafe0040228b8631c30f97ae1adb61bcdc\n\n"},{"id":"551974","messageId":"pull.2214.v3.git.1788537081930.gitgitgadget@gmail.com","threadId":"66250","inReplyTo":"pull.2214.git.1788337897490.gitgitgadget@gmail.com","subject":"[PATCH v3] rerere: keep a background gc from killing a rebase","fromName":"Thomas Bachem via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-04T15:51:21Z","receivedAt":"2026-09-04T15:51:26Z","isPatch":true,"body":"From: Thomas Bachem <mail@thomasbachem.com>\n\nA \"git rerere gc\" holds MERGE_RR.lock for as long as pruning rr-cache\ntakes, and since 2.54 the auto maintenance after every commit runs\none whenever rr-cache has an entry. The commit a rebase spawns for a\nresolved pick starts it too, and the sequencer's repo_rerere() at the\nnext conflict wants the lock a few milliseconds later. Both take it\nwith LOCK_DIE_ON_ERROR, so whichever comes second dies. When it is\nthe rebase, the index is written but the state for \"git rebase\n--continue\" is not, and every later continue refuses with \"you have\nstaged changes\".\n\nThe gc needs the lock, since a rerere that has just created its\ndirectory looks like the empty ones it prunes. So wait for it\ninstead, rerere.lockTimeout milliseconds, 1000 by default with the\nsemantics of core.packedRefsTimeout, then warn and go on without\nrerere: a lost recording or replay is nothing next to a rebase that\ncannot continue. The gc itself never waits, and \"git rerere\", \"git\nrerere forget\" and \"git rerere clear\" wait but then die, since the\nstate behind the lock is all they are for. The clearing \"am\" and\n\"rebase\" do on --abort, --skip and --quit goes on without it, and\nleaves the entries for the gc.\n\nAssisted-by: Claude Fable 5.1\nSigned-off-by: Thomas Bachem <mail@thomasbachem.com>\n---\n    rerere: keep a background gc from killing a rebase\n    \n    Changes since v2:\n    \n     * The description is a quarter of the size and says what the patch does\n       and why (Junio, Phillip).\n     * setup_rerere() BUG()s on RERERE_NOWAIT combined with\n       RERERE_LOCK_OR_DIE, and on RERERE_READONLY combined with either\n       (Phillip).\n     * The rerere.lockTimeout entry now says that \"git rerere\", \"git rerere\n       forget\" and \"git rerere clear\" retry like everything else and only\n       fail once the time is up (Phillip).\n    \n    Patrick's heuristic in ps/tune-rerere-gc narrows the auto trigger from\n    \"rr-cache has an entry\" to \"enough entries are stale\". A gc that does\n    run races the same way, so the fix stands on its own.\n    \n    Still based on maint, where the bug ships (2.54.0 and 2.55.0). Merged up\n    it conflicts with 2e486bfbf7 (use\n    repo_hold_lock_file_for_update{,_mode,_timeout}() with custom repos,\n    2026-07-14) in setup_rerere(), where the resolution takes the\n    repo-scoped helper.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2214%2Fthomasbachem%2Frerere-gc-lock-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2214/thomasbachem/rerere-gc-lock-v3\nPull-Request: https://github.com/gitgitgadget/git/pull/2214\n\nRange-diff vs v2:\n\n 1:  bbb2338572 ! 1:  5bfda65baa rerere: keep a background gc from killing a rebase\n     @@ Metadata\n       ## Commit message ##\n          rerere: keep a background gc from killing a rebase\n      \n     -    Since 2.54 unscheduled maintenance uses the \"geometric\" strategy, so\n     -    the \"git maintenance run --auto --detach\" behind every \"git commit\"\n     -    runs \"git rerere gc\" in the background whenever rr-cache has an entry.\n     -    That includes the \"git commit\" the sequencer runs for a resolved pick\n     -    on \"git rebase --continue\".\n     +    A \"git rerere gc\" holds MERGE_RR.lock for as long as pruning rr-cache\n     +    takes, and since 2.54 the auto maintenance after every commit runs\n     +    one whenever rr-cache has an entry. The commit a rebase spawns for a\n     +    resolved pick starts it too, and the sequencer's repo_rerere() at the\n     +    next conflict wants the lock a few milliseconds later. Both take it\n     +    with LOCK_DIE_ON_ERROR, so whichever comes second dies. When it is\n     +    the rebase, the index is written but the state for \"git rebase\n     +    --continue\" is not, and every later continue refuses with \"you have\n     +    staged changes\".\n      \n     -    rerere_gc() takes MERGE_RR.lock through setup_rerere(), which uses\n     -    LOCK_DIE_ON_ERROR, and so does the sequencer's repo_rerere() at the\n     -    next conflict a few milliseconds later. Whichever comes second dies.\n     -    When it is the rebase, it dies in do_pick_commit() with the index\n     -    written but before make_patch() writes rebase-merge/{message,patch,\n     -    stopped-sha}, and every later \"git rebase --continue\" refuses with\n     -    \"you have staged changes in your working tree\". When it is the \"git\n     -    commit\" of a later continue, that one dies in its post-commit\n     -    repo_rerere() after the commit was made. Before 2.54 the same\n     -    collision needed an auto gc to actually run, since gc runs\n     -    \"rerere gc\" at its end.\n     -\n     -    A rebase with two conflicts in a row shows it. The filler makes the\n     -    pick slower than the ~5 ms the background task needs to take the\n     -    lock, and the stale entries give the gc something to prune, which\n     -    keeps the lock held for about half a second. It hit 6 of 6 runs here\n     -    on 2.55.0, and a test suite driving rebases on toy repositories with\n     -    a single rr-cache entry hit it in both runs that were traced:\n     -\n     -        git init -q -b main r && cd r\n     -        git config rerere.enabled true\n     -        git config maintenance.auto false\n     -        mkdir pad && seq 20000 | (cd pad && split -l 1 -a 5)\n     -        echo base >f && git add -A && git commit -qm base\n     -        git checkout -q -b topic\n     -        echo b >f && git commit -qam B\n     -        echo c >f && git commit -qam C\n     -        git checkout -q main\n     -        echo a >f && git commit -qam A\n     -        git repack -adq\n     -        git ls-files -s pad | head -n 5000 |\n     -            awk '{print \".git/rr-cache/\" $2}' | xargs mkdir -p\n     -        for d in .git/rr-cache/*/; do echo x >$d/preimage; done\n     -        touch -t 202001010000 .git/rr-cache/*/preimage\n     -        git config --unset maintenance.auto\n     -        git checkout -q topic\n     -        git rebase main\n     -        echo ab >f && git add f\n     -        GIT_EDITOR=true git rebase --continue\n     -\n     -    The continue dies with \"Unable to create '.git/MERGE_RR.lock': File\n     -    exists\" while the gc spawned by its own commit holds the lock, and\n     -    after resolving C every further continue refuses. Maintenance stays\n     -    off during the setup so that no repack is pending: a repack due at\n     -    that commit runs ahead of rerere-gc in the task list and would spend\n     -    the window.\n     -\n     -    The gc needs the lock: it removes every rr-cache directory it finds\n     -    empty, and a rerere that has just created its directory but not yet\n     -    written the preimage looks exactly like that. So keep the lock and\n     -    stop dying over it. A caller that finds the lock held now waits for\n     -    rerere.lockTimeout milliseconds, with the semantics of\n     -    core.packedRefsTimeout and the same default of 1000, and then warns\n     -    and goes on without rerere: a merge, a commit or a pick loses one\n     -    recording or replay, which is nothing next to a rebase that cannot\n     -    continue. The gc itself never waits, since it has nothing to lose\n     -    from a skipped run, and \"git rerere\", \"git rerere forget\" and \"git\n     -    rerere clear\" keep dying, since they exist for nothing but the state\n     -    behind the lock. The clearing \"git am\" and \"git rebase\" do on --abort\n     -    and --skip goes on without it: the cleanup that follows removes\n     -    MERGE_RR anyway, and the unresolved entries it would have dropped are\n     -    left for the gc. A stale MERGE_RR.lock, which used to stop every\n     -    merge, now costs each command a second and a warning until it is\n     -    removed.\n     -\n     -    That rebase now stops at C the normal way, with its preimage recorded\n     -    once the prune is over, and continues once C is resolved. The tests\n     -    cover the gc under a held lock, directly and through the maintenance\n     -    task, a merge that waits a lock out within rerere.lockTimeout, a\n     -    merge, a commit and a rebase that go on without rerere once it is\n     -    up, \"git rebase --abort\" doing the same, and the three explicit\n     -    commands failing.\n     +    The gc needs the lock, since a rerere that has just created its\n     +    directory looks like the empty ones it prunes. So wait for it\n     +    instead, rerere.lockTimeout milliseconds, 1000 by default with the\n     +    semantics of core.packedRefsTimeout, then warn and go on without\n     +    rerere: a lost recording or replay is nothing next to a rebase that\n     +    cannot continue. The gc itself never waits, and \"git rerere\", \"git\n     +    rerere forget\" and \"git rerere clear\" wait but then die, since the\n     +    state behind the lock is all they are for. The clearing \"am\" and\n     +    \"rebase\" do on --abort, --skip and --quit goes on without it, and\n     +    leaves the entries for the gc.\n      \n          Assisted-by: Claude Fable 5.1\n          Signed-off-by: Thomas Bachem <mail@thomasbachem.com>\n     @@ Documentation/config/rerere.adoc: rerere.enabled::\n      +\ta background `git rerere gc`.  When the time is up, the command\n      +\twarns and goes on without rerere.  Value 0 means not to retry\n      +\tat all; -1 means to try indefinitely.  Default is 1000 (i.e.,\n     -+\tretry for 1 second).  `git rerere gc` does not retry, and\n     -+\t`git rerere`, `git rerere forget` and `git rerere clear` fail\n     -+\tinstead of going on.\n     ++\tretry for 1 second).  `git rerere gc` does not retry at all.\n     ++\t`git rerere`, `git rerere forget` and `git rerere clear` retry\n     ++\tthe same way, but fail when the time is up instead of going on.\n      \n       ## Documentation/git-rerere.adoc ##\n      @@ Documentation/git-rerere.adoc: occurred a long time ago.  By default, unresolved conflicts older\n     @@ rerere.c: int setup_rerere(struct repository *r, struct string_list *merge_rr, i\n       \tif (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))\n       \t\trerere_autoupdate = !!(flags & RERERE_AUTOUPDATE);\n      -\tif (flags & RERERE_READONLY)\n     ++\tif ((flags & RERERE_NOWAIT) && (flags & RERERE_LOCK_OR_DIE))\n     ++\t\tBUG(\"RERERE_NOWAIT and RERERE_LOCK_OR_DIE are mutually exclusive\");\n     ++\tif ((flags & RERERE_READONLY) &&\n     ++\t    (flags & (RERERE_NOWAIT | RERERE_LOCK_OR_DIE)))\n     ++\t\tBUG(\"RERERE_READONLY takes no lock, so no lock flag applies\");\n      +\tif (flags & RERERE_READONLY) {\n       \t\tfd = 0;\n      -\telse\n\n\n Documentation/config/rerere.adoc | 10 ++++\n Documentation/git-rerere.adoc    |  4 +-\n builtin/am.c                     |  2 +-\n builtin/rebase.c                 |  6 +-\n builtin/rerere.c                 |  7 ++-\n rerere.c                         | 47 ++++++++++++----\n rerere.h                         |  8 ++-\n t/t4200-rerere.sh                | 96 ++++++++++++++++++++++++++++++++\n t/t7900-maintenance.sh           |  8 +++\n 9 files changed, 168 insertions(+), 20 deletions(-)\n\ndiff --git a/Documentation/config/rerere.adoc b/Documentation/config/rerere.adoc\nindex 3a78b5ebb1..14ef193545 100644\n--- a/Documentation/config/rerere.adoc\n+++ b/Documentation/config/rerere.adoc\n@@ -10,3 +10,13 @@ rerere.enabled::\n \tenabled if there is an `rr-cache` directory under the\n \t`$GIT_DIR`, e.g. if \"rerere\" was previously used in the\n \trepository.\n+\n+rerere.lockTimeout::\n+\tThe length of time, in milliseconds, to retry when trying to\n+\ttake the rerere lock while another process holds it, typically\n+\ta background `git rerere gc`.  When the time is up, the command\n+\twarns and goes on without rerere.  Value 0 means not to retry\n+\tat all; -1 means to try indefinitely.  Default is 1000 (i.e.,\n+\tretry for 1 second).  `git rerere gc` does not retry at all.\n+\t`git rerere`, `git rerere forget` and `git rerere clear` retry\n+\tthe same way, but fail when the time is up instead of going on.\ndiff --git a/Documentation/git-rerere.adoc b/Documentation/git-rerere.adoc\nindex 4e6ab9a27c..05935b0603 100644\n--- a/Documentation/git-rerere.adoc\n+++ b/Documentation/git-rerere.adoc\n@@ -70,7 +70,9 @@ occurred a long time ago.  By default, unresolved conflicts older\n than 15 days and resolved conflicts older than 60\n days are pruned.  These defaults are controlled via the\n `gc.rerereUnresolved` and `gc.rerereResolved` configuration\n-variables respectively.\n+variables respectively.  If another process holds the lock on the\n+recorded resolutions, for example a merge or rebase that is recording\n+a conflict, `gc` does nothing and reports so.\n \n \n DISCUSSION\ndiff --git a/builtin/am.c b/builtin/am.c\nindex e9623b8307..32f11161b4 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -2112,7 +2112,7 @@ static int clean_index(const struct object_id *head, const struct object_id *rem\n static void am_rerere_clear(void)\n {\n \tstruct string_list merge_rr = STRING_LIST_INIT_DUP;\n-\trerere_clear(the_repository, &merge_rr);\n+\trerere_clear(the_repository, &merge_rr, 0);\n \tstring_list_clear(&merge_rr, 1);\n }\n \ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex fa4f5d9306..363d177472 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -367,7 +367,7 @@ static int run_sequencer_rebase(struct rebase_options *opts)\n \tcase ACTION_SKIP: {\n \t\tstruct string_list merge_rr = STRING_LIST_INIT_DUP;\n \n-\t\trerere_clear(the_repository, &merge_rr);\n+\t\trerere_clear(the_repository, &merge_rr, 0);\n \t}\n \t\t/* fallthrough */\n \tcase ACTION_CONTINUE: {\n@@ -1382,7 +1382,7 @@ int cmd_rebase(int argc,\n \tcase ACTION_SKIP: {\n \t\tstruct string_list merge_rr = STRING_LIST_INIT_DUP;\n \n-\t\trerere_clear(the_repository, &merge_rr);\n+\t\trerere_clear(the_repository, &merge_rr, 0);\n \t\tstring_list_clear(&merge_rr, 1);\n \t\tropts.flags = RESET_HEAD_HARD;\n \t\tif (reset_head(the_repository, &ropts) < 0)\n@@ -1396,7 +1396,7 @@ int cmd_rebase(int argc,\n \t\tstruct string_list merge_rr = STRING_LIST_INIT_DUP;\n \t\tstruct strbuf head_msg = STRBUF_INIT;\n \n-\t\trerere_clear(the_repository, &merge_rr);\n+\t\trerere_clear(the_repository, &merge_rr, 0);\n \t\tstring_list_clear(&merge_rr, 1);\n \n \t\tif (read_basic_state(&options))\ndiff --git a/builtin/rerere.c b/builtin/rerere.c\nindex a056cb791b..70a4bd1683 100644\n--- a/builtin/rerere.c\n+++ b/builtin/rerere.c\n@@ -74,7 +74,7 @@ int cmd_rerere(int argc,\n \t\tflags = RERERE_NOAUTOUPDATE;\n \n \tif (argc < 1)\n-\t\treturn repo_rerere(the_repository, flags);\n+\t\treturn repo_rerere(the_repository, flags | RERERE_LOCK_OR_DIE);\n \n \tif (!strcmp(argv[0], \"forget\")) {\n \t\tstruct pathspec pathspec;\n@@ -85,14 +85,15 @@ int cmd_rerere(int argc,\n \t\tparse_pathspec(&pathspec, 0, PATHSPEC_PREFER_CWD,\n \t\t\t       prefix, argv + 1);\n \n-\t\tret = rerere_forget(the_repository, &pathspec);\n+\t\tret = rerere_forget(the_repository, &pathspec,\n+\t\t\t\t    RERERE_LOCK_OR_DIE);\n \n \t\tclear_pathspec(&pathspec);\n \t\treturn ret;\n \t}\n \n \tif (!strcmp(argv[0], \"clear\")) {\n-\t\trerere_clear(the_repository, &merge_rr);\n+\t\trerere_clear(the_repository, &merge_rr, RERERE_LOCK_OR_DIE);\n \t} else if (!strcmp(argv[0], \"gc\"))\n \t\trerere_gc(the_repository, &merge_rr);\n \telse if (!strcmp(argv[0], \"status\")) {\ndiff --git a/rerere.c b/rerere.c\nindex 8232542585..bae780f584 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -32,6 +32,7 @@ static int rerere_enabled = -1;\n \n /* automatically update cleanly resolved paths to the index */\n static int rerere_autoupdate;\n+static int rerere_lock_timeout_ms = 1000;\n \n #define RR_HAS_POSTIMAGE 1\n #define RR_HAS_PREIMAGE 2\n@@ -876,6 +877,8 @@ static void git_rerere_config(void)\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 }\n \n@@ -908,12 +911,36 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n \n \tif (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))\n \t\trerere_autoupdate = !!(flags & RERERE_AUTOUPDATE);\n-\tif (flags & RERERE_READONLY)\n+\tif ((flags & RERERE_NOWAIT) && (flags & RERERE_LOCK_OR_DIE))\n+\t\tBUG(\"RERERE_NOWAIT and RERERE_LOCK_OR_DIE are mutually exclusive\");\n+\tif ((flags & RERERE_READONLY) &&\n+\t    (flags & (RERERE_NOWAIT | RERERE_LOCK_OR_DIE)))\n+\t\tBUG(\"RERERE_READONLY takes no lock, so no lock flag applies\");\n+\tif (flags & RERERE_READONLY) {\n \t\tfd = 0;\n-\telse\n-\t\tfd = hold_lock_file_for_update(&write_lock,\n-\t\t\t\t\t       git_path_merge_rr(r),\n-\t\t\t\t\t       LOCK_DIE_ON_ERROR);\n+\t} else {\n+\t\tint lock_flags = 0;\n+\t\tlong timeout_ms = rerere_lock_timeout_ms;\n+\n+\t\tif (flags & RERERE_LOCK_OR_DIE)\n+\t\t\tlock_flags = LOCK_DIE_ON_ERROR;\n+\t\tif (flags & RERERE_NOWAIT)\n+\t\t\ttimeout_ms = 0;\n+\t\t/*\n+\t\t * A background \"rerere gc\" holds the lock for as long as it\n+\t\t * takes to prune rr-cache, so wait it out rather than fail\n+\t\t * at once.  The gc itself has nothing to lose from a skipped\n+\t\t * run and never waits.\n+\t\t */\n+\t\tfd = hold_lock_file_for_update_timeout(&write_lock,\n+\t\t\t\t\t\t       git_path_merge_rr(r),\n+\t\t\t\t\t\t       lock_flags, timeout_ms);\n+\t\tif (fd < 0) {\n+\t\t\twarning_errno(_(\"skipping rerere, unable to create '%s.lock'\"),\n+\t\t\t\t      git_path_merge_rr(r));\n+\t\t\treturn -1;\n+\t\t}\n+\t}\n \tread_rr(r, merge_rr);\n \treturn fd;\n }\n@@ -1124,7 +1151,7 @@ fail_exit:\n \treturn -1;\n }\n \n-int rerere_forget(struct repository *r, struct pathspec *pathspec)\n+int rerere_forget(struct repository *r, struct pathspec *pathspec, int flags)\n {\n \tint i, fd, ret;\n \tstruct string_list conflict = STRING_LIST_INIT_DUP;\n@@ -1133,7 +1160,7 @@ int rerere_forget(struct repository *r, struct pathspec *pathspec)\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 = setup_rerere(r, &merge_rr, RERERE_NOAUTOUPDATE | flags);\n \tif (fd < 0)\n \t\treturn 0;\n \n@@ -1237,7 +1264,7 @@ void rerere_gc(struct repository *r, struct string_list *rr)\n \ttimestamp_t cutoff_resolve = now - 60 * 86400;\n \tstruct strbuf buf = STRBUF_INIT;\n \n-\tif (setup_rerere(r, rr, 0) < 0)\n+\tif (setup_rerere(r, rr, RERERE_NOWAIT) < 0)\n \t\treturn;\n \n \trepo_config_get_expiry_in_days(the_repository, \"gc.rerereresolved\",\n@@ -1289,11 +1316,11 @@ void rerere_gc(struct repository *r, struct string_list *rr)\n  *\n  * NEEDSWORK: shouldn't we be calling this from \"reset --hard\"?\n  */\n-void rerere_clear(struct repository *r, struct string_list *merge_rr)\n+void rerere_clear(struct repository *r, struct string_list *merge_rr, int flags)\n {\n \tint i;\n \n-\tif (setup_rerere(r, merge_rr, 0) < 0)\n+\tif (setup_rerere(r, merge_rr, flags) < 0)\n \t\treturn;\n \n \tfor (i = 0; i < merge_rr->nr; i++) {\ndiff --git a/rerere.h b/rerere.h\nindex d4b5f7c932..3a9f58acd9 100644\n--- a/rerere.h\n+++ b/rerere.h\n@@ -10,6 +10,10 @@ struct repository;\n #define RERERE_AUTOUPDATE   01\n #define RERERE_NOAUTOUPDATE 02\n #define RERERE_READONLY     04\n+/* Do not wait for the lock when another process holds it */\n+#define RERERE_NOWAIT       010\n+/* Die on a lock that cannot be taken instead of going on without rerere */\n+#define RERERE_LOCK_OR_DIE  020\n \n /*\n  * Marks paths that have been hand-resolved and added to the\n@@ -34,9 +38,9 @@ int repo_rerere(struct repository *, int);\n  */\n const char *rerere_path(struct strbuf *buf, const struct rerere_id *,\n \t\t\tconst char *file);\n-int rerere_forget(struct repository *, struct pathspec *);\n+int rerere_forget(struct repository *, struct pathspec *, int);\n int rerere_remaining(struct repository *, struct string_list *);\n-void rerere_clear(struct repository *, struct string_list *);\n+void rerere_clear(struct repository *, struct string_list *, int);\n void rerere_gc(struct repository *, struct string_list *);\n \n #define OPT_RERERE_AUTOUPDATE(v) OPT_UYN(0, \"rerere-autoupdate\", (v), \\\ndiff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh\nindex 1717f407c8..243b3ebed3 100755\n--- a/t/t4200-rerere.sh\n+++ b/t/t4200-rerere.sh\n@@ -242,6 +242,102 @@ test_expect_success 'old records rest in peace' '\n \ttest_path_is_missing $rr2/preimage\n '\n \n+test_expect_success 'gc does nothing while MERGE_RR is locked' '\n+\tmkdir -p $rr2 &&\n+\techo Hello >$rr2/preimage &&\n+\ttest-tool chmtime =$just_over_15_days_ago $rr2/preimage &&\n+\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\tgit rerere gc 2>err &&\n+\ttest_grep \"MERGE_RR.lock\" err &&\n+\ttest_path_is_file $rr2/preimage &&\n+\n+\trm .git/MERGE_RR.lock &&\n+\tgit rerere gc &&\n+\ttest_path_is_missing $rr2/preimage\n+'\n+\n+test_expect_success 'a held lock is waited out within rerere.lockTimeout' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\t{\n+\t\t( sleep 1 && rm -f .git/MERGE_RR.lock ) &\n+\t} &&\n+\ttest_must_fail git -c rerere.lockTimeout=5000 merge first 2>err &&\n+\twait &&\n+\ttest_grep ! \"MERGE_RR\" err &&\n+\ttest_grep \"^=======\\$\" $rr/preimage\n+'\n+\n+test_expect_success 'merge goes on without rerere once rerere.lockTimeout is up' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 merge first 2>err &&\n+\ttest_grep \"skipping rerere\" err &&\n+\ttest_grep \"^=======\\$\" a1 &&\n+\ttest_path_is_missing $rr/preimage\n+'\n+\n+test_expect_success 'commit goes on without rerere once rerere.lockTimeout is up' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\tgit checkout -b lock-held-commit third &&\n+\ttest_when_finished \"git checkout third && git branch -D lock-held-commit\" &&\n+\ttest_must_fail git merge first &&\n+\ttest_path_is_file $rr/preimage &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\techo resolved >a1 &&\n+\tgit add a1 &&\n+\tgit -c rerere.lockTimeout=0 commit -qm resolved 2>err &&\n+\ttest_grep \"skipping rerere\" err &&\n+\ttest_path_is_missing $rr/postimage\n+'\n+\n+test_expect_success 'rerere, forget and clear fail on a lock they cannot take' '\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rerere 2>err &&\n+\ttest_grep \"Unable to create\" err &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rerere forget a1 2>err &&\n+\ttest_grep \"Unable to create\" err &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rerere clear 2>err &&\n+\ttest_grep \"Unable to create\" err\n+'\n+\n+test_expect_success 'rebase goes on without rerere once rerere.lockTimeout is up' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\tgit checkout -b lock-held third &&\n+\ttest_when_finished \"git checkout third && git branch -D lock-held\" &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rebase first 2>err &&\n+\ttest_grep \"skipping rerere\" err &&\n+\ttest_path_is_file .git/rebase-merge/stopped-sha &&\n+\techo resolved >a1 &&\n+\tgit add a1 &&\n+\tgit -c rerere.lockTimeout=0 rebase --continue &&\n+\ttest_path_is_missing .git/rebase-merge &&\n+\ttest_path_is_missing $rr/preimage\n+'\n+\n+test_expect_success 'rebase --abort goes on without rerere on a held lock' '\n+\tgit checkout -b lock-held-abort third &&\n+\ttest_when_finished \"git checkout third && git branch -D lock-held-abort\" &&\n+\ttest_must_fail git rebase first &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\tgit -c rerere.lockTimeout=0 rebase --abort 2>err &&\n+\ttest_grep \"skipping rerere\" err &&\n+\ttest_path_is_missing .git/rebase-merge\n+'\n+\n rerere_gc_custom_expiry_test () {\n \tfive_days=\"$1\" right_now=\"$2\"\n \ttest_expect_success \"rerere gc with custom expiry ($five_days, $right_now)\" '\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex d7f82e1bec..a55ca2e829 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -885,6 +885,14 @@ test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.aut\n \ttest_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 maintenance run --auto --task=rerere-gc\n '\n \n+test_expect_success 'rerere-gc task succeeds while MERGE_RR is locked' '\n+\ttest_when_finished \"rm -rf .git/rr-cache .git/MERGE_RR.lock\" &&\n+\tmkdir .git/rr-cache &&\n+\t: >.git/rr-cache/entry &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_expect_rerere_gc git maintenance run --task=rerere-gc\n+'\n+\n test_expect_success '--auto and --schedule incompatible' '\n \ttest_must_fail git maintenance run --auto --schedule=daily 2>err &&\n \ttest_grep \"cannot be used together\" err\n\nbase-commit: e9019fcafe0040228b8631c30f97ae1adb61bcdc\n-- \ngitgitgadget\n"},{"id":"551979","messageId":"CAA0xjtrkjaOC_+jhN=Vjm9e0T+iqAZeeMKx-ymVaQcLA37bm-w@mail.gmail.com","threadId":"66250","inReplyTo":"5e613735-60e2-429d-a5bb-1a4f03578604@gmail.com","subject":"Re: [PATCH v2] rerere: keep a background gc from killing a rebase","fromName":"Thomas Bachem","fromEmail":"mail@thomasbachem.com","sentAt":"2026-09-04T15:55:15Z","receivedAt":"2026-09-04T15:55:28Z","isPatch":true,"body":"Hi Phillip,\n\nOn 04/09/2026 16:21, Phillip Wood wrote:\n> With Patricks patches that's no-longer true I think. I think a better\n> motivation, as the cache is per-repository, rather than per-worktree, is\n> concurrent writers running in different worktrees.\n\nMERGE_RR is per worktree, though, and so is its lock:\n\n    $ git -C linked rev-parse --git-path MERGE_RR\n    /path/to/main/.git/worktrees/linked/MERGE_RR\n\nso writers in different worktrees never meet on it. What they share is\nrr-cache, which a gc in one worktree prunes under its own worktree's\nlock only. That is a gap of its own, and not one this patch closes.\n\nWhat remains after Patrick's series is any \"git rerere gc\" that runs\nwhile a command records a conflict, from \"git gc\", from a maintenance\nrun, or from auto maintenance once enough entries are stale. The v3\nmessage says it that way.\n\n> Overall, this commit message is rather long and it would be helpful if\n> you could distill it to remove unnecessary and unrelated details.\n\nDone, it is a quarter of the size now.\n\n> Why do those commands fail rather than wait?\n\nThey wait like everything else, and once the time is up they fail\ninstead of going on without rerere, which is all they are for. That\nway a stale lock gets the usual advice to remove it. The config text\nsaid otherwise, fixed.\n\n> It might be worth adding a check above here that BUG()s out if the\n> caller passes an incompatible set of flags.\n\nAdded, for RERERE_NOWAIT with RERERE_LOCK_OR_DIE and for\nRERERE_READONLY with either.\n\n> A background job that the user did not explicitly start printing to the\n> terminal is rather confusing as it is likely to get mixed in with the\n> output of whatever is running in the foreground.\n\nThe detached maintenance run has no terminal: daemonize() closes the\nstandard descriptors and reopens them on /dev/null, so the gc's\nwarning goes nowhere when it loses the lock. Where it cannot detach,\non Windows, it runs in the foreground of the commit that started it\nand there is no race to lose. The warning the user does see is the\nforeground command's own, when it gives up waiting.\n\nThanks,\nThomas\n"},{"id":"551989","messageId":"xmqqfqzp3q10.fsf@gitster.g","threadId":"66250","inReplyTo":"5e613735-60e2-429d-a5bb-1a4f03578604@gmail.com","subject":"Re: [PATCH v2] rerere: keep a background gc from killing a rebase","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-04T17:06:03Z","receivedAt":"2026-09-04T17:06:07Z","isPatch":true,"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> Overall, this commit message is rather long and it would be helpful if \n> you could distill it to remove unnecessary and unrelated details.\n\nHear hear.\n\n>> +rerere.lockTimeout::\n>> +\tThe length of time, in milliseconds, to retry when trying to\n>> +\ttake the rerere lock while another process holds it, typically\n>> +\ta background `git rerere gc`.  When the time is up, the command\n>> +\twarns and goes on without rerere.  Value 0 means not to retry\n>> +\tat all; -1 means to try indefinitely.  Default is 1000 (i.e.,\n>> +\tretry for 1 second).  `git rerere gc` does not retry, and\n>> +\t`git rerere`, `git rerere forget` and `git rerere clear` fail\n>> +\tinstead of going on.\n>\n> Why do those commands fail rather than wait?\n\nIsn't locktimeout about waiting?\n\nAfter waiting enough, why should it not fail but proceed?\n\nWhen there is somebody holding the lock, they acquired the lock\nexactly because they did not want to see others (including\nourselves) to touch the rerere database until they are done.\n\nThe description \"`git rerere gc` does not retry\" is highly\nquestionable.  None of the others retries, either.\n\nWhat makes `git rerere gc` different among all is not that it does\nnot retry.  It just does not insist doing a GC and instead leaves\nwithout doing anything (and without failing).\n\nI think this is justifyable as the actions visible to end-users of\n\"rerere gc\" is a vague \"discard old enough crufts to gain the\ndiskspace back\" (as opposed to \"I know this particular entry is old\nenough and I want to see it gone right now\").\n\nCompared to that, with \"git rerere forget\", the end-user explicitly\nsays \"I know the specific rerere entry i just saw reused is *wrong*\nand I want to get rid of it\".  If another process holding the lock\nprevents it from being carried out, I'd prefer to see it fail loudly\nand let me know that the entry I wanted to remove is still there (so\nif I retried the same merge, I'll see the same mistaken resolution).\n\n>> +\t\tif (fd < 0) {\n>> +\t\t\twarning_errno(_(\"skipping rerere, unable to create '%s.lock'\"),\n>> +\t\t\t\t      git_path_merge_rr(r));\n>\n> A background job that the user did not explicitly start printing to the \n> terminal is rather confusing as it is likely to get mixed in with the \n> output of whatever is running in the foreground.\n\nVery good point.\n\nThanks.\n"},{"id":"551992","messageId":"CAA0xjtr4sDyrkf8VJz3CUBGVvc7LdGhOb1K9kgdskhD+_hbSwQ@mail.gmail.com","threadId":"66250","inReplyTo":"xmqqfqzp3q10.fsf@gitster.g","subject":"Re: [PATCH v2] rerere: keep a background gc from killing a rebase","fromName":"Thomas Bachem","fromEmail":"mail@thomasbachem.com","sentAt":"2026-09-04T18:17:24Z","receivedAt":"2026-09-04T18:17:36Z","isPatch":true,"body":"Hi Junio,\n\nOn 04/09/2026 19:06, Junio C Hamano wrote:\n> Phillip Wood <phillip.wood123@gmail.com> writes:\n>\n>> Overall, this commit message is rather long and it would be helpful if\n>> you could distill it to remove unnecessary and unrelated details.\n>\n> Hear hear.\n\nThe v3 log message is down to 23 lines from 81:\n\n  <pull.2214.v3.git.1788537081930.gitgitgadget@gmail.com>\n\n> When there is somebody holding the lock, they acquired the lock\n> exactly because they did not want to see others (including\n> ourselves) to touch the rerere database until they are done.\n\nThat caught one more case I got wrong in v3. The rerere_clear() that\n--abort and --skip run also waits and then goes on, and that leaves\nMERGE_RR behind. The next rerere run then takes each path in it as\nresolved by the user and records whatever the reset left there. The\nclear is the first thing --abort and --skip do, so I'll let it fail\nlike \"git rerere clear\" does, from every caller. That also drops the\nflag from rerere_clear() and rerere_forget() again and leaves am.c\nand rebase.c untouched.\n\n> What makes `git rerere gc` different among all is not that it does\n> not retry.  It just does not insist doing a GC and instead leaves\n> without doing anything (and without failing).\n\nRight, and I'll say it that way in the config text. All of them wait\nfor the lock except the gc, which loses nothing by giving up at once.\nWhen the time is up, \"git rerere\", \"git rerere forget\" and \"git\nrerere clear\" fail, and a merge or commit that would record or reuse\na resolution on the way warns and goes on without it.\n\n>> A background job that the user did not explicitly start printing to the\n>> terminal is rather confusing as it is likely to get mixed in with the\n>> output of whatever is running in the foreground.\n>\n> Very good point.\n\nI don't think it can happen, though. The detached run has no\nterminal: daemonize() reopens the standard descriptors on /dev/null\nbefore the gc runs. Where it doesn't detach, on Windows or with\nautoDetach off, the command that started it waits for it, so it\nisn't in the background either. The warning a user sees comes from\nthe command in the foreground, once it has given up waiting.\n\nI'll wait for the rest of the v3 comments before rerolling.\n\nThanks,\nThomas\n"},{"id":"551994","messageId":"xmqq4ig44ywy.fsf@gitster.g","threadId":"66250","inReplyTo":"pull.2214.v3.git.1788537081930.gitgitgadget@gmail.com","subject":"Re: [PATCH v3] rerere: keep a background gc from killing a rebase","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-04T19:08:45Z","receivedAt":"2026-09-04T19:08:51Z","isPatch":true,"body":"\"Thomas Bachem via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> semantics of core.packedRefsTimeout, then warn and go on without\n> rerere: a lost recording or replay is nothing next to a rebase that\n> cannot continue.\n\nI do not understand the logic at the latter half of the above\nsentence.  During a rebase, any and all opportunity to reuse a\nconflict resolution you made earlier is preferrable.\n\nI would be fine if \"we cannot grab the lock so let's skip without\ndoing 'rerere gc' at all\".  But if a conflicted step in rebase that\nstops and leaves conflicts in the working tree fails to record the\npreimage of a conflicted path, and makes the user realize that was\nwhat happened only after the user spends significant amount of work\nto resolve the conflicts and the resolution is not added to the\nrerere database, that is a huge loss.\n\nPerhaps it is just the way the above three lines is stated and what\nthe code actually does may not be problematic, but I am not sure if\nthat is what the latter half of the above sentence is trying to say.\n\nThanks.\n"},{"id":"552020","messageId":"CAA0xjtqF_60kKC_B=-=AkBSG0ZiFd_uSjzCZ4Bup8Pvg1_uALQ@mail.gmail.com","threadId":"66250","inReplyTo":"xmqq4ig44ywy.fsf@gitster.g","subject":"Re: [PATCH v3] rerere: keep a background gc from killing a rebase","fromName":"Thomas Bachem","fromEmail":"mail@thomasbachem.com","sentAt":"2026-09-05T05:41:38Z","receivedAt":"2026-09-05T05:41:52Z","isPatch":true,"body":"Hi Junio,\n\nOn 04/09/2026 21:08, Junio C Hamano wrote:\n>> semantics of core.packedRefsTimeout, then warn and go on without\n>> rerere: a lost recording or replay is nothing next to a rebase that\n>> cannot continue.\n>\n> Perhaps it is just the way the above three lines is stated and what\n> the code actually does may not be problematic, but I am not sure if\n> that is what the latter half of the above sentence is trying to say.\n\nNo, it's what the code does. Once the timeout is up, the conflicted\nstep goes on without recording the preimage, and the resolution the\nuser makes after that is lost, as you say.\n\nThe rebase that cannot continue is the one from the message's first\nparagraph. It dies inside rerere, which do_pick_commit() runs before\nerror_with_patch() writes the state \"git rebase --continue\" needs, so\nI traded the recording for a rebase that survives. You've convinced\nme that's the wrong trade.\n\nSo in v4 every caller waits rerere.lockTimeout and then fails as it\ndoes today, and only \"git rerere gc\" gives up at once. That drops the\nRERERE_LOCK_OR_DIE flag and the hunks in the callers, and it takes\nback what I said in my reply to your other mail about merge and\ncommit going on without rerere.\n\nA gc that outlasts the timeout still stops the rebase where it does\ntoday. With the gc giving way whenever it comes second and the\nsequencer series keeping a rebase's own commits from starting one,\nthat should be rare. Whoever would rather wait it out can set\nrerere.lockTimeout to -1, but I'd keep the default finite so a lock\nleft behind by a crash fails like every other lock instead of\nhanging. Writing the stop state before rerere runs would let such a\nrebase continue, which I can look at separately.\n\nThanks,\nThomas\n"},{"id":"552033","messageId":"xmqqwlsz65mu.fsf@gitster.g","threadId":"66250","inReplyTo":"CAA0xjtqF_60kKC_B=-=AkBSG0ZiFd_uSjzCZ4Bup8Pvg1_uALQ@mail.gmail.com","subject":"Re: [PATCH v3] rerere: keep a background gc from killing a rebase","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-05T16:10:33Z","receivedAt":"2026-09-05T16:10:37Z","isPatch":true,"body":"Thomas Bachem <mail@thomasbachem.com> writes:\n\n>> Perhaps it is just the way the above three lines is stated and what\n>> the code actually does may not be problematic, but I am not sure if\n>> that is what the latter half of the above sentence is trying to say.\n>\n> No, it's what the code does. Once the timeout is up, the conflicted\n> step goes on without recording the preimage, and the resolution the\n> user makes after that is lost, as you say.\n\nIt is a hard-to-accept regression without a good justification,\nthough, especially with the other efforts to tame \"rerere gc\" from\nhogging the lock too often going on.\n\nIf you have a 100-commit \"rebase\" that is interrupted in the middle,\nsay at commit #70, at worst you should be able to hard reset and\nabort it, and then restart it starting on top of the result of\napplying up to commit #69 (with \"rebase --onto\") to finish the rest,\nso failing in the middle is not like throwing the effort you made so\nfar away.\n\n> A gc that outlasts the timeout still stops the rebase where it does\n> today. With the gc giving way whenever it comes second and the\n> sequencer series keeping a rebase's own commits from starting one,\n> that should be rare. Whoever would rather wait it out can set\n> rerere.lockTimeout to -1, but I'd keep the default finite so a lock\n> left behind by a crash fails like every other lock instead of\n> hanging. Writing the stop state before rerere runs would let such a\n> rebase continue, which I can look at separately.\n\nStepping back a bit, what does a \"conflicted step goes on without\nrecording the preimage\" exactly look like?  \"git rebase\" goes on\nchugging, and hits a commit that does not cleanly apply.  It leaves\na conflict and in a normal case immediately before returning the\ncontrol back to the user, its \"git rerere\" invocation creates a\npreimage.  Even if we make \"git rerere\" fail to do so, it would not\nbe unrecoverable.  The end user has control at that point, and it is\nnot like the rest of rebase goes on without giving a chance to the\nuser to intervene and recover.\n\nWould it make sense to LOUDLY tell the user when \"git rerere\" fails\nto do what the user expects to do?  The output at the point of time\non the terminal would end with something like\n\n    CONFLICT (content): Merge conflict in t/t0123-frotz.sh\n    Auto-merging nitfol.c\n    error: could not apply 8c7b68a8bf... nitfol: remove frotz\n    Recorded preimage for 't/t0123-frotz.sh'\n    Could not apply 8c7b68a8bf... # nitfol: remove frotz\n\nif \"git rerere\" kicked in correctly, so if we said\n\n    CONFLICT (content): Merge conflict in t/t0123-frotz.sh\n    Auto-merging nitfol.c\n    error: could not apply 8c7b68a8bf... nitfol: remove frotz\n    FAILED TO RECORD PREIMAGE FOR 't/t0123-frotz.sh'\n    Could not apply 8c7b68a8bf... # nitfol: remove frotz\n\n    *** RUN \"git rerere\" MANUALLY BEFORE DOING ANYTHING ELSE ***\n    *** IF YOU DO NOT WANT TO MAKE YOUR EFFORT IN RESOLVING ***\n    *** THIS CONFLICT WASTED ***\n\nor something similar, would that help the user?\n\nI do not expect the \"silent failure\" to run \"rerere\" would not be\nfollowed by automated applications of many subsequent commits that\nmakes it too late when the user notices what happened.  An attempt\nto invoke \"rerere\" will always be followed by a stopped automation\nand the user will have the control at that point.  So in that sense,\nas long as the user is told clearly that some step that usually\nhappens and the user has learned to rely on did *not* happen, and\nalso told how to recover from the failure, it is not too bad.\n\nThanks.\n"},{"id":"552068","messageId":"CAA0xjtoFD3OQqPhf82hxkUZ4-zpSH28arz6aBxSqeMeR1BqBhQ@mail.gmail.com","threadId":"66250","inReplyTo":"xmqqwlsz65mu.fsf@gitster.g","subject":"Re: [PATCH v3] rerere: keep a background gc from killing a rebase","fromName":"Thomas Bachem","fromEmail":"mail@thomasbachem.com","sentAt":"2026-09-06T10:29:32Z","receivedAt":"2026-09-06T10:29:45Z","isPatch":true,"body":"Hi Junio,\n\nOn 05/09/2026 18:10, Junio C Hamano wrote:\n> Would it make sense to LOUDLY tell the user when \"git rerere\" fails\n> to do what the user expects to do?\n\nIt would, and the user gets everything back. At the stop the conflict\nis still in the index, so \"git rerere\" run by hand right there records\nthe preimage that was skipped, \"git rebase --continue\" then records\nthe resolution, and replaying the same conflict resolves it from the\ncache. A resolution it would have replayed comes back the same way, so\none command covers whatever was skipped. I checked all of it.\n\nYour \"BEFORE DOING ANYTHING ELSE\" has to be in there, though. Resolve\nthe file first and the conflict markers go with it, and \"git rerere\"\nrecords nothing at all.\n\nFailing doesn't record anything either, and the conflict is still\nsitting there to record by hand afterwards, so what it buys over a\nwarning is that the user can't miss it. I agreed to that trade without\nchecking what it costs.\n\n> If you have a 100-commit \"rebase\" that is interrupted in the middle,\n> say at commit #70, at worst you should be able to hard reset and\n> abort it, and then restart it starting on top of the result of\n> applying up to commit #69 (with \"rebase --onto\") to finish the rest,\n> so failing in the middle is not like throwing the effort you made so\n> far away.\n\nIt's rougher than that. The \"you have staged changes\" refusal from the\nlog message offers \"git commit --amend\" or \"git commit\", and neither\ngets commit #70 back as it was. The first folds its changes into #69\nand drops its message, and the rebase then runs to the end, one commit\nshort. The second keeps the message but not the author. Aborting does\nwork, but the user first has to ignore what git just told them to do.\n\nThat's 2.55 today, not something this patch adds, and waiting only\nmakes it rarer. Writing the stop state before rerere runs would let\nthe rebase continue, but the recording is still gone, so the message\nis needed there too.\n\nI'd rather not stop a rebase the user can otherwise finish over a\nrecording we can tell them how to get back, so I'd keep v3's behavior\nat a conflict, where the command stops anyway and the warning can say\nto run \"git rerere\" before resolving. The recording after the\nresolution is different: \"git am --continue\" and a rebase's\n\"git commit\" go on with the rest, and a leftover MERGE_RR entry then\nrecords whatever the file holds at the next rerere run, as with the\nclears. So those keep failing after the wait. \"git rerere\", \"forget\"\nand \"clear\" fail after it too, so do the clears that \"am\" and \"rebase\"\nrun, and the gc gives up at once. Say if you'd still rather all of it\nfailed and I'll build that instead.\n\nThanks,\nThomas\n"},{"id":"552097","messageId":"ap5qj9wckDeKlI7i@pks.im","threadId":"66250","inReplyTo":"pull.2214.v3.git.1788537081930.gitgitgadget@gmail.com","subject":"Re: [PATCH v3] rerere: keep a background gc from killing a rebase","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-09-07T07:41:03Z","receivedAt":"2026-09-07T07:41:10Z","isPatch":true,"body":"On Fri, Sep 04, 2026 at 03:51:21PM +0000, Thomas Bachem via GitGitGadget wrote:\n> From: Thomas Bachem <mail@thomasbachem.com>\n> \n> A \"git rerere gc\" holds MERGE_RR.lock for as long as pruning rr-cache\n> takes, and since 2.54 the auto maintenance after every commit runs\n> one whenever rr-cache has an entry. The commit a rebase spawns for a\n> resolved pick starts it too, and the sequencer's repo_rerere() at the\n> next conflict wants the lock a few milliseconds later. Both take it\n> with LOCK_DIE_ON_ERROR, so whichever comes second dies. When it is\n> the rebase, the index is written but the state for \"git rebase\n> --continue\" is not, and every later continue refuses with \"you have\n> staged changes\".\n\nHaven't we said that this race is not exclusive to `git rerere gc` with\na concurrent writer though? It also happens between two normal writers.\nSo it's good to have the context that we discovered this race because of\nthe changed heuristics in maintenance, but we should clarify that it's a\nlonger-standing conceptual issue.\n\n> diff --git a/Documentation/config/rerere.adoc b/Documentation/config/rerere.adoc\n> index 3a78b5ebb1..14ef193545 100644\n> --- a/Documentation/config/rerere.adoc\n> +++ b/Documentation/config/rerere.adoc\n> @@ -10,3 +10,13 @@ rerere.enabled::\n>  \tenabled if there is an `rr-cache` directory under the\n>  \t`$GIT_DIR`, e.g. if \"rerere\" was previously used in the\n>  \trepository.\n> +\n> +rerere.lockTimeout::\n> +\tThe length of time, in milliseconds, to retry when trying to\n> +\ttake the rerere lock while another process holds it, typically\n> +\ta background `git rerere gc`.  When the time is up, the command\n> +\twarns and goes on without rerere.  Value 0 means not to retry\n> +\tat all; -1 means to try indefinitely.  Default is 1000 (i.e.,\n> +\tretry for 1 second).  `git rerere gc` does not retry at all.\n> +\t`git rerere`, `git rerere forget` and `git rerere clear` retry\n> +\tthe same way, but fail when the time is up instead of going on.\n\nI'm not a 100% sold that it's sensible to just skip writing the rerere\nentry. But maybe it's more sensible to regress gracefully compared to\njust aborting the whole command?\n\nIn any case, I feel like this change warrants its own preparatory commit\nso that we can discuss separately why it's a good idea to ignore those\nfailures.\n\nPatrick\n"},{"id":"552120","messageId":"595d0d45-7000-4c52-8430-f18ce8f99c71@gmail.com","threadId":"66250","inReplyTo":"CAA0xjtrkjaOC_+jhN=Vjm9e0T+iqAZeeMKx-ymVaQcLA37bm-w@mail.gmail.com","subject":"Re: [PATCH v2] rerere: keep a background gc from killing a rebase","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-07T10:07:41Z","receivedAt":"2026-09-07T10:07:45Z","isPatch":true,"body":"Hi Thomas\n\nOn 04/09/2026 16:55, Thomas Bachem wrote:\n> On 04/09/2026 16:21, Phillip Wood wrote:\n>> With Patricks patches that's no-longer true I think. I think a better\n>> motivation, as the cache is per-repository, rather than per-worktree, is\n>> concurrent writers running in different worktrees.\n> \n> MERGE_RR is per worktree, though, and so is its lock:\n> \n>      $ git -C linked rev-parse --git-path MERGE_RR\n>      /path/to/main/.git/worktrees/linked/MERGE_RR\n> \n> so writers in different worktrees never meet on it. What they share is\n> rr-cache, which a gc in one worktree prunes under its own worktree's\n> lock only. That is a gap of its own, and not one this patch closes.\n\nOh, I didn't realize the lock was per-worktree. So the lock \"rerere gc\" \ntakes does not actually stop another process running in a different \nworktree from altering the rerere cache.\n\n> What remains after Patrick's series is any \"git rerere gc\" that runs\n> while a command records a conflict, from \"git gc\", from a maintenance\n> run, or from auto maintenance once enough entries are stale. The v3\n> message says it that way.\n> \n>> Overall, this commit message is rather long and it would be helpful if\n>> you could distill it to remove unnecessary and unrelated details.\n> \n> Done, it is a quarter of the size now.\n> \n>> Why do those commands fail rather than wait?\n> \n> They wait like everything else, and once the time is up they fail\n> instead of going on without rerere, which is all they are for. That\n> way a stale lock gets the usual advice to remove it. The config text\n> said otherwise, fixed.\n\nThat's good, I think I'd maybe misunderstood what the original patch was \ntrying to say.\n\n>> It might be worth adding a check above here that BUG()s out if the\n>> caller passes an incompatible set of flags.\n> \n> Added, for RERERE_NOWAIT with RERERE_LOCK_OR_DIE and for\n> RERERE_READONLY with either.\n> \n>> A background job that the user did not explicitly start printing to the\n>> terminal is rather confusing as it is likely to get mixed in with the\n>> output of whatever is running in the foreground.\n> \n> The detached maintenance run has no terminal: daemonize() closes the\n> standard descriptors and reopens them on /dev/null, so the gc's\n> warning goes nowhere when it loses the lock. Where it cannot detach,\n> on Windows, it runs in the foreground of the commit that started it\n> and there is no race to lose. The warning the user does see is the\n> foreground command's own, when it gives up waiting.\n\nThanks for clarifying that\n\nPhillip\n\n> \n> Thanks,\n> Thomas\n\n"},{"id":"552679","messageId":"pull.2214.v4.git.1789373061.gitgitgadget@gmail.com","threadId":"66250","inReplyTo":"pull.2214.git.1788337897490.gitgitgadget@gmail.com","subject":"[PATCH v4 0/2] rerere: wait for MERGE_RR.lock, and go on at a conflict","fromName":"Thomas Bachem via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-14T08:04:19Z","receivedAt":"2026-09-14T08:04:23Z","isPatch":true,"body":"This is now two patches, as Patrick asked: the wait first, the skip on top,\nso that the second can be discussed on its own.\n\nPatch 1 makes every writer wait rerere.lockTimeout for MERGE_RR.lock before\nit fails as it does today, and lets \"git rerere gc\" skip its run instead of\nwaiting. That alone keeps a rebase alive through the gc that a commit's auto\nmaintenance starts: the gc gives way whenever it comes second, and the one\nsecond default is longer than pruning a few thousand entries takes.\n\nPatch 2 is what I proposed in my 6 September reply to Junio: a command that\nstops at a conflict warns and goes on when the lock is still held after the\nwait, and the warning tells the user to run \"git rerere\" before resolving.\nEverything else keeps failing: the recordings after a resolution, the user's\nown rerere commands, and the clears that am and rebase run.\n\nChanges since v3:\n\n * Based on master instead of maint. On master setup_rerere() already uses\n   the repository-scoped lock helpers, so the series merges cleanly into\n   next and seen, including ps/tune-rerere-gc (which makes the gc rarer, not\n   the race) and tb/rerere-lock-grace (which only stops the commands a\n   rebase spawns from starting the gc).\n\n * The first log message says that the race is as old as the lock and that\n   the 2.54 maintenance default only made it easy to hit (Patrick).\n\n * Only the six callers that stop at a conflict go on without rerere: the\n   sequencer's pick and merge, \"git merge\", the 3-way fallback of \"git am\",\n   \"git stash\" and \"git apply --3way\". \"git commit\" and \"git am --continue\"\n   fail after the wait, as in 2.55 (Junio). Merge, rebase, am, stash and\n   apply each have a test for this.\n\n * The config entry describes the outcome per command and no longer says\n   \"retry\" (Junio).\n\n * The \"rerere clear\" that am and rebase run for --skip and --abort waits\n   and then fails like every other caller, so a held lock cannot leave a\n   stale MERGE_RR entry behind. The signatures of rerere_clear() and\n   rerere_forget() are back to what upstream has.\n\n * A new test runs \"git rerere\" at the stop after a skipped recording and\n   checks that the preimage, and after the resolution the postimage, get\n   recorded.\n\nThomas Bachem (2):\n  rerere: wait for MERGE_RR.lock, and let the gc skip it\n  rerere: go on at a conflict when the lock stays busy\n\n Documentation/config/rerere.adoc |  13 +++\n Documentation/git-rerere.adoc    |   4 +-\n apply.c                          |   2 +-\n builtin/am.c                     |   3 +-\n builtin/merge.c                  |   2 +-\n builtin/stash.c                  |   2 +-\n rerere.c                         |  49 +++++++--\n rerere.h                         |   4 +\n sequencer.c                      |   4 +-\n t/t4200-rerere.sh                | 168 +++++++++++++++++++++++++++++++\n t/t7900-maintenance.sh           |   8 ++\n 11 files changed, 246 insertions(+), 13 deletions(-)\n\n\nbase-commit: 47ce80527c56f462cb97db4ca8125342204d3783\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2214%2Fthomasbachem%2Frerere-gc-lock-v4\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2214/thomasbachem/rerere-gc-lock-v4\nPull-Request: https://github.com/gitgitgadget/git/pull/2214\n\nRange-diff vs v3:\n\n 1:  5bfda65baa ! 1:  8a7a74d6aa rerere: keep a background gc from killing a rebase\n     @@ Metadata\n      Author: Thomas Bachem <mail@thomasbachem.com>\n      \n       ## Commit message ##\n     -    rerere: keep a background gc from killing a rebase\n     +    rerere: wait for MERGE_RR.lock, and let the gc skip it\n      \n     -    A \"git rerere gc\" holds MERGE_RR.lock for as long as pruning rr-cache\n     -    takes, and since 2.54 the auto maintenance after every commit runs\n     -    one whenever rr-cache has an entry. The commit a rebase spawns for a\n     -    resolved pick starts it too, and the sequencer's repo_rerere() at the\n     -    next conflict wants the lock a few milliseconds later. Both take it\n     -    with LOCK_DIE_ON_ERROR, so whichever comes second dies. When it is\n     -    the rebase, the index is written but the state for \"git rebase\n     -    --continue\" is not, and every later continue refuses with \"you have\n     -    staged changes\".\n     +    setup_rerere() takes MERGE_RR.lock with LOCK_DIE_ON_ERROR. When two\n     +    processes want the lock at the same time, the second one dies. This\n     +    was always the case, but since 452b12c2e0 (builtin/maintenance: use\n     +    \"geometric\" strategy by default, 2026-02-24) it is easy to hit: auto\n     +    maintenance now runs \"git rerere gc\" after every commit whenever\n     +    rr-cache contains at least one entry, and the gc holds the lock\n     +    while it prunes.\n      \n     -    The gc needs the lock, since a rerere that has just created its\n     -    directory looks like the empty ones it prunes. So wait for it\n     -    instead, rerere.lockTimeout milliseconds, 1000 by default with the\n     -    semantics of core.packedRefsTimeout, then warn and go on without\n     -    rerere: a lost recording or replay is nothing next to a rebase that\n     -    cannot continue. The gc itself never waits, and \"git rerere\", \"git\n     -    rerere forget\" and \"git rerere clear\" wait but then die, since the\n     -    state behind the lock is all they are for. The clearing \"am\" and\n     -    \"rebase\" do on --abort, --skip and --quit goes on without it, and\n     -    leaves the entries for the gc.\n     +    A rebase whose next pick conflicts while the gc holds the lock dies\n     +    inside repo_rerere(). That runs before the sequencer writes the state\n     +    that \"git rebase --continue\" needs, so every later \"git rebase\n     +    --continue\" fails with \"you have staged changes\".\n     +\n     +    Instead of dying right away, wait for the lock for up to\n     +    rerere.lockTimeout milliseconds, 1000 by default, and only then fail\n     +    as before. The gc itself does not wait: when the lock is held, it\n     +    skips this run and leaves the pruning to the next one.\n      \n          Assisted-by: Claude Fable 5.1\n          Signed-off-by: Thomas Bachem <mail@thomasbachem.com>\n     @@ Documentation/config/rerere.adoc: rerere.enabled::\n       \trepository.\n      +\n      +rerere.lockTimeout::\n     -+\tThe length of time, in milliseconds, to retry when trying to\n     -+\ttake the rerere lock while another process holds it, typically\n     -+\ta background `git rerere gc`.  When the time is up, the command\n     -+\twarns and goes on without rerere.  Value 0 means not to retry\n     -+\tat all; -1 means to try indefinitely.  Default is 1000 (i.e.,\n     -+\tretry for 1 second).  `git rerere gc` does not retry at all.\n     -+\t`git rerere`, `git rerere forget` and `git rerere clear` retry\n     -+\tthe same way, but fail when the time is up instead of going on.\n     ++\tThe length of time, in milliseconds, to wait for the rerere\n     ++\tlock when another process holds it, typically a background\n     ++\t`git rerere gc`.  Value 0 means not to wait at all; -1 means\n     ++\tto wait indefinitely.  Default is 1000 (i.e., wait for 1\n     ++\tsecond).  When the time is up, the command fails as it does\n     ++\tfor any other lock it cannot take.  `git rerere gc` never\n     ++\twaits and skips its run while the lock is held.\n      \n       ## Documentation/git-rerere.adoc ##\n      @@ Documentation/git-rerere.adoc: occurred a long time ago.  By default, unresolved conflicts older\n     @@ Documentation/git-rerere.adoc: occurred a long time ago.  By default, unresolved\n       days are pruned.  These defaults are controlled via the\n       `gc.rerereUnresolved` and `gc.rerereResolved` configuration\n      -variables respectively.\n     -+variables respectively.  If another process holds the lock on the\n     -+recorded resolutions, for example a merge or rebase that is recording\n     -+a conflict, `gc` does nothing and reports so.\n     ++variables respectively.  If another process holds the rerere lock,\n     ++for example a merge or rebase that is recording a conflict, `gc`\n     ++does nothing and says so.\n       \n       \n       DISCUSSION\n      \n     - ## builtin/am.c ##\n     -@@ builtin/am.c: static int clean_index(const struct object_id *head, const struct object_id *rem\n     - static void am_rerere_clear(void)\n     - {\n     - \tstruct string_list merge_rr = STRING_LIST_INIT_DUP;\n     --\trerere_clear(the_repository, &merge_rr);\n     -+\trerere_clear(the_repository, &merge_rr, 0);\n     - \tstring_list_clear(&merge_rr, 1);\n     - }\n     - \n     -\n     - ## builtin/rebase.c ##\n     -@@ builtin/rebase.c: static int run_sequencer_rebase(struct rebase_options *opts)\n     - \tcase ACTION_SKIP: {\n     - \t\tstruct string_list merge_rr = STRING_LIST_INIT_DUP;\n     - \n     --\t\trerere_clear(the_repository, &merge_rr);\n     -+\t\trerere_clear(the_repository, &merge_rr, 0);\n     - \t}\n     - \t\t/* fallthrough */\n     - \tcase ACTION_CONTINUE: {\n     -@@ builtin/rebase.c: int cmd_rebase(int argc,\n     - \tcase ACTION_SKIP: {\n     - \t\tstruct string_list merge_rr = STRING_LIST_INIT_DUP;\n     - \n     --\t\trerere_clear(the_repository, &merge_rr);\n     -+\t\trerere_clear(the_repository, &merge_rr, 0);\n     - \t\tstring_list_clear(&merge_rr, 1);\n     - \t\tropts.flags = RESET_HEAD_HARD;\n     - \t\tif (reset_head(the_repository, &ropts) < 0)\n     -@@ builtin/rebase.c: int cmd_rebase(int argc,\n     - \t\tstruct string_list merge_rr = STRING_LIST_INIT_DUP;\n     - \t\tstruct strbuf head_msg = STRBUF_INIT;\n     - \n     --\t\trerere_clear(the_repository, &merge_rr);\n     -+\t\trerere_clear(the_repository, &merge_rr, 0);\n     - \t\tstring_list_clear(&merge_rr, 1);\n     - \n     - \t\tif (read_basic_state(&options))\n     -\n     - ## builtin/rerere.c ##\n     -@@ builtin/rerere.c: int cmd_rerere(int argc,\n     - \t\tflags = RERERE_NOAUTOUPDATE;\n     - \n     - \tif (argc < 1)\n     --\t\treturn repo_rerere(the_repository, flags);\n     -+\t\treturn repo_rerere(the_repository, flags | RERERE_LOCK_OR_DIE);\n     - \n     - \tif (!strcmp(argv[0], \"forget\")) {\n     - \t\tstruct pathspec pathspec;\n     -@@ builtin/rerere.c: int cmd_rerere(int argc,\n     - \t\tparse_pathspec(&pathspec, 0, PATHSPEC_PREFER_CWD,\n     - \t\t\t       prefix, argv + 1);\n     - \n     --\t\tret = rerere_forget(the_repository, &pathspec);\n     -+\t\tret = rerere_forget(the_repository, &pathspec,\n     -+\t\t\t\t    RERERE_LOCK_OR_DIE);\n     - \n     - \t\tclear_pathspec(&pathspec);\n     - \t\treturn ret;\n     - \t}\n     - \n     - \tif (!strcmp(argv[0], \"clear\")) {\n     --\t\trerere_clear(the_repository, &merge_rr);\n     -+\t\trerere_clear(the_repository, &merge_rr, RERERE_LOCK_OR_DIE);\n     - \t} else if (!strcmp(argv[0], \"gc\"))\n     - \t\trerere_gc(the_repository, &merge_rr);\n     - \telse if (!strcmp(argv[0], \"status\")) {\n     -\n       ## rerere.c ##\n      @@ rerere.c: static int rerere_enabled = -1;\n     - \n       /* automatically update cleanly resolved paths to the index */\n       static int rerere_autoupdate;\n     -+static int rerere_lock_timeout_ms = 1000;\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      @@ rerere.c: static void git_rerere_config(void)\n       {\n       \trepo_config_get_bool(the_repository, \"rerere.enabled\", &rerere_enabled);\n     @@ rerere.c: int setup_rerere(struct repository *r, struct string_list *merge_rr, i\n       \tif (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))\n       \t\trerere_autoupdate = !!(flags & RERERE_AUTOUPDATE);\n      -\tif (flags & RERERE_READONLY)\n     -+\tif ((flags & RERERE_NOWAIT) && (flags & RERERE_LOCK_OR_DIE))\n     -+\t\tBUG(\"RERERE_NOWAIT and RERERE_LOCK_OR_DIE are mutually exclusive\");\n     -+\tif ((flags & RERERE_READONLY) &&\n     -+\t    (flags & (RERERE_NOWAIT | RERERE_LOCK_OR_DIE)))\n     -+\t\tBUG(\"RERERE_READONLY takes no lock, so no lock flag applies\");\n     ++\tif ((flags & RERERE_READONLY) && (flags & RERERE_NOWAIT))\n     ++\t\tBUG(\"RERERE_READONLY takes no lock, so RERERE_NOWAIT does not apply\");\n      +\tif (flags & RERERE_READONLY) {\n       \t\tfd = 0;\n      -\telse\n     --\t\tfd = hold_lock_file_for_update(&write_lock,\n     --\t\t\t\t\t       git_path_merge_rr(r),\n     --\t\t\t\t\t       LOCK_DIE_ON_ERROR);\n     +-\t\tfd = repo_hold_lock_file_for_update(r, &write_lock,\n     +-\t\t\t\t\t\t    git_path_merge_rr(r),\n     +-\t\t\t\t\t\t    LOCK_DIE_ON_ERROR);\n      +\t} else {\n     -+\t\tint lock_flags = 0;\n     ++\t\tconst char *path = git_path_merge_rr(r);\n     ++\t\tint lock_flags = LOCK_DIE_ON_ERROR;\n      +\t\tlong timeout_ms = rerere_lock_timeout_ms;\n      +\n     -+\t\tif (flags & RERERE_LOCK_OR_DIE)\n     -+\t\t\tlock_flags = LOCK_DIE_ON_ERROR;\n     -+\t\tif (flags & RERERE_NOWAIT)\n     -+\t\t\ttimeout_ms = 0;\n      +\t\t/*\n     -+\t\t * A background \"rerere gc\" holds the lock for as long as it\n     -+\t\t * takes to prune rr-cache, so wait it out rather than fail\n     -+\t\t * at once.  The gc itself has nothing to lose from a skipped\n     -+\t\t * run and never waits.\n     ++\t\t * Another process may hold the lock for a while, e.g.\n     ++\t\t * \"git rerere gc\" while it prunes rr-cache, so wait for\n     ++\t\t * it instead of dying right away.  The gc itself never\n     ++\t\t * waits: skipping one of its runs costs nothing.\n      +\t\t */\n     -+\t\tfd = hold_lock_file_for_update_timeout(&write_lock,\n     -+\t\t\t\t\t\t       git_path_merge_rr(r),\n     -+\t\t\t\t\t\t       lock_flags, timeout_ms);\n     ++\t\tif (flags & RERERE_NOWAIT) {\n     ++\t\t\tlock_flags = 0;\n     ++\t\t\ttimeout_ms = 0;\n     ++\t\t}\n     ++\t\tfd = repo_hold_lock_file_for_update_timeout(r, &write_lock,\n     ++\t\t\t\t\t\t\t    path, lock_flags,\n     ++\t\t\t\t\t\t\t    timeout_ms);\n      +\t\tif (fd < 0) {\n     -+\t\t\twarning_errno(_(\"skipping rerere, unable to create '%s.lock'\"),\n     -+\t\t\t\t      git_path_merge_rr(r));\n     ++\t\t\twarning_errno(_(\"skipping rerere, \"\n     ++\t\t\t\t\t\"unable to create '%s.lock'\"), path);\n      +\t\t\treturn -1;\n      +\t\t}\n      +\t}\n       \tread_rr(r, merge_rr);\n       \treturn fd;\n       }\n     -@@ rerere.c: fail_exit:\n     - \treturn -1;\n     - }\n     - \n     --int rerere_forget(struct repository *r, struct pathspec *pathspec)\n     -+int rerere_forget(struct repository *r, struct pathspec *pathspec, int flags)\n     - {\n     - \tint i, fd, ret;\n     - \tstruct string_list conflict = STRING_LIST_INIT_DUP;\n     -@@ rerere.c: int rerere_forget(struct repository *r, struct pathspec *pathspec)\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 = setup_rerere(r, &merge_rr, RERERE_NOAUTOUPDATE | flags);\n     - \tif (fd < 0)\n     - \t\treturn 0;\n     - \n      @@ rerere.c: void rerere_gc(struct repository *r, struct string_list *rr)\n       \ttimestamp_t cutoff_resolve = now - 60 * 86400;\n       \tstruct strbuf buf = STRBUF_INIT;\n     @@ rerere.c: void rerere_gc(struct repository *r, struct string_list *rr)\n       \t\treturn;\n       \n       \trepo_config_get_expiry_in_days(the_repository, \"gc.rerereresolved\",\n     -@@ rerere.c: void rerere_gc(struct repository *r, struct string_list *rr)\n     -  *\n     -  * NEEDSWORK: shouldn't we be calling this from \"reset --hard\"?\n     -  */\n     --void rerere_clear(struct repository *r, struct string_list *merge_rr)\n     -+void rerere_clear(struct repository *r, struct string_list *merge_rr, int flags)\n     - {\n     - \tint i;\n     - \n     --\tif (setup_rerere(r, merge_rr, 0) < 0)\n     -+\tif (setup_rerere(r, merge_rr, flags) < 0)\n     - \t\treturn;\n     - \n     - \tfor (i = 0; i < merge_rr->nr; i++) {\n      \n       ## rerere.h ##\n      @@ rerere.h: struct repository;\n       #define RERERE_AUTOUPDATE   01\n       #define RERERE_NOAUTOUPDATE 02\n       #define RERERE_READONLY     04\n     -+/* Do not wait for the lock when another process holds it */\n     ++/* Never wait for MERGE_RR.lock, and skip the run when it is held */\n      +#define RERERE_NOWAIT       010\n     -+/* Die on a lock that cannot be taken instead of going on without rerere */\n     -+#define RERERE_LOCK_OR_DIE  020\n       \n       /*\n        * Marks paths that have been hand-resolved and added to the\n     -@@ rerere.h: int repo_rerere(struct repository *, int);\n     -  */\n     - const char *rerere_path(struct strbuf *buf, const struct rerere_id *,\n     - \t\t\tconst char *file);\n     --int rerere_forget(struct repository *, struct pathspec *);\n     -+int rerere_forget(struct repository *, struct pathspec *, int);\n     - int rerere_remaining(struct repository *, struct string_list *);\n     --void rerere_clear(struct repository *, struct string_list *);\n     -+void rerere_clear(struct repository *, struct string_list *, int);\n     - void rerere_gc(struct repository *, struct string_list *);\n     - \n     - #define OPT_RERERE_AUTOUPDATE(v) OPT_UYN(0, \"rerere-autoupdate\", (v), \\\n      \n       ## t/t4200-rerere.sh ##\n      @@ t/t4200-rerere.sh: test_expect_success 'old records rest in peace' '\n     @@ t/t4200-rerere.sh: test_expect_success 'old records rest in peace' '\n      +\ttest_grep \"^=======\\$\" $rr/preimage\n      +'\n      +\n     -+test_expect_success 'merge goes on without rerere once rerere.lockTimeout is up' '\n     ++test_expect_success 'merge fails once rerere.lockTimeout is up' '\n      +\tgit reset --hard &&\n      +\trm -rf $rr &&\n      +\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n      +\t>.git/MERGE_RR.lock &&\n      +\ttest_must_fail git -c rerere.lockTimeout=0 merge first 2>err &&\n     -+\ttest_grep \"skipping rerere\" err &&\n     ++\ttest_grep \"Unable to create\" err &&\n      +\ttest_grep \"^=======\\$\" a1 &&\n      +\ttest_path_is_missing $rr/preimage\n      +'\n      +\n     -+test_expect_success 'commit goes on without rerere once rerere.lockTimeout is up' '\n     -+\tgit reset --hard &&\n     -+\trm -rf $rr &&\n     -+\tgit checkout -b lock-held-commit third &&\n     -+\ttest_when_finished \"git checkout third && git branch -D lock-held-commit\" &&\n     -+\ttest_must_fail git merge first &&\n     -+\ttest_path_is_file $rr/preimage &&\n     -+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n     -+\t>.git/MERGE_RR.lock &&\n     -+\techo resolved >a1 &&\n     -+\tgit add a1 &&\n     -+\tgit -c rerere.lockTimeout=0 commit -qm resolved 2>err &&\n     -+\ttest_grep \"skipping rerere\" err &&\n     -+\ttest_path_is_missing $rr/postimage\n     -+'\n     -+\n      +test_expect_success 'rerere, forget and clear fail on a lock they cannot take' '\n      +\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n      +\t>.git/MERGE_RR.lock &&\n     @@ t/t4200-rerere.sh: test_expect_success 'old records rest in peace' '\n      +\ttest_grep \"Unable to create\" err\n      +'\n      +\n     -+test_expect_success 'rebase goes on without rerere once rerere.lockTimeout is up' '\n     ++test_expect_success 'rebase --abort fails on a lock it cannot take' '\n      +\tgit reset --hard &&\n     -+\trm -rf $rr &&\n     -+\tgit checkout -b lock-held third &&\n     -+\ttest_when_finished \"git checkout third && git branch -D lock-held\" &&\n     -+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n     -+\t>.git/MERGE_RR.lock &&\n     -+\ttest_must_fail git -c rerere.lockTimeout=0 rebase first 2>err &&\n     -+\ttest_grep \"skipping rerere\" err &&\n     -+\ttest_path_is_file .git/rebase-merge/stopped-sha &&\n     -+\techo resolved >a1 &&\n     -+\tgit add a1 &&\n     -+\tgit -c rerere.lockTimeout=0 rebase --continue &&\n     -+\ttest_path_is_missing .git/rebase-merge &&\n     -+\ttest_path_is_missing $rr/preimage\n     -+'\n     -+\n     -+test_expect_success 'rebase --abort goes on without rerere on a held lock' '\n      +\tgit checkout -b lock-held-abort third &&\n      +\ttest_when_finished \"git checkout third && git branch -D lock-held-abort\" &&\n      +\ttest_must_fail git rebase first &&\n      +\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n      +\t>.git/MERGE_RR.lock &&\n     -+\tgit -c rerere.lockTimeout=0 rebase --abort 2>err &&\n     -+\ttest_grep \"skipping rerere\" err &&\n     ++\ttest_must_fail git -c rerere.lockTimeout=0 rebase --abort 2>err &&\n     ++\ttest_grep \"Unable to create\" err &&\n     ++\ttest_path_is_dir .git/rebase-merge &&\n     ++\trm .git/MERGE_RR.lock &&\n     ++\tgit rebase --abort &&\n      +\ttest_path_is_missing .git/rebase-merge\n      +'\n      +\n -:  ---------- > 2:  1cce403113 rerere: go on at a conflict when the lock stays busy\n\n-- \ngitgitgadget\n"},{"id":"552680","messageId":"8a7a74d6aa359844a49593538ef6178cd1b02031.1789373061.git.gitgitgadget@gmail.com","threadId":"66250","inReplyTo":"pull.2214.v4.git.1789373061.gitgitgadget@gmail.com","subject":"[PATCH v4 1/2] rerere: wait for MERGE_RR.lock, and let the gc skip it","fromName":"Thomas Bachem via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-14T08:04:20Z","receivedAt":"2026-09-14T08:04:24Z","isPatch":true,"body":"From: Thomas Bachem <mail@thomasbachem.com>\n\nsetup_rerere() takes MERGE_RR.lock with LOCK_DIE_ON_ERROR. When two\nprocesses want the lock at the same time, the second one dies. This\nwas always the case, but since 452b12c2e0 (builtin/maintenance: use\n\"geometric\" strategy by default, 2026-02-24) it is easy to hit: auto\nmaintenance now runs \"git rerere gc\" after every commit whenever\nrr-cache contains at least one entry, and the gc holds the lock\nwhile it prunes.\n\nA rebase whose next pick conflicts while the gc holds the lock dies\ninside repo_rerere(). That runs before the sequencer writes the state\nthat \"git rebase --continue\" needs, so every later \"git rebase\n--continue\" fails with \"you have staged changes\".\n\nInstead of dying right away, wait for the lock for up to\nrerere.lockTimeout milliseconds, 1000 by default, and only then fail\nas before. The gc itself does not wait: when the lock is held, it\nskips this run and leaves the pruning to the next one.\n\nAssisted-by: Claude Fable 5.1\nSigned-off-by: Thomas Bachem <mail@thomasbachem.com>\n---\n Documentation/config/rerere.adoc |  9 +++++\n Documentation/git-rerere.adoc    |  4 +-\n rerere.c                         | 39 ++++++++++++++++---\n rerere.h                         |  2 +\n t/t4200-rerere.sh                | 67 ++++++++++++++++++++++++++++++++\n t/t7900-maintenance.sh           |  8 ++++\n 6 files changed, 122 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/config/rerere.adoc b/Documentation/config/rerere.adoc\nindex 3a78b5ebb1..cc9dd0c37b 100644\n--- a/Documentation/config/rerere.adoc\n+++ b/Documentation/config/rerere.adoc\n@@ -10,3 +10,12 @@ rerere.enabled::\n \tenabled if there is an `rr-cache` directory under the\n \t`$GIT_DIR`, e.g. if \"rerere\" was previously used in the\n \trepository.\n+\n+rerere.lockTimeout::\n+\tThe length of time, in milliseconds, to wait for the rerere\n+\tlock when another process holds it, typically a background\n+\t`git rerere gc`.  Value 0 means not to wait at all; -1 means\n+\tto wait indefinitely.  Default is 1000 (i.e., wait for 1\n+\tsecond).  When the time is up, the command fails as it does\n+\tfor any other lock it cannot take.  `git rerere gc` never\n+\twaits and skips its run while the lock is held.\ndiff --git a/Documentation/git-rerere.adoc b/Documentation/git-rerere.adoc\nindex 4e6ab9a27c..4df653367e 100644\n--- a/Documentation/git-rerere.adoc\n+++ b/Documentation/git-rerere.adoc\n@@ -70,7 +70,9 @@ occurred a long time ago.  By default, unresolved conflicts older\n than 15 days and resolved conflicts older than 60\n days are pruned.  These defaults are controlled via the\n `gc.rerereUnresolved` and `gc.rerereResolved` configuration\n-variables respectively.\n+variables respectively.  If another process holds the rerere lock,\n+for example a merge or rebase that is recording a conflict, `gc`\n+does nothing and says so.\n \n \n DISCUSSION\ndiff --git a/rerere.c b/rerere.c\nindex 3d3bd0db16..7d44f3937c 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -33,6 +33,9 @@ static int rerere_enabled = -1;\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@@ -850,6 +853,8 @@ static void git_rerere_config(void)\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 }\n \n@@ -882,12 +887,34 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n \n \tif (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))\n \t\trerere_autoupdate = !!(flags & RERERE_AUTOUPDATE);\n-\tif (flags & RERERE_READONLY)\n+\tif ((flags & RERERE_READONLY) && (flags & RERERE_NOWAIT))\n+\t\tBUG(\"RERERE_READONLY takes no lock, so RERERE_NOWAIT does not apply\");\n+\tif (flags & RERERE_READONLY) {\n \t\tfd = 0;\n-\telse\n-\t\tfd = repo_hold_lock_file_for_update(r, &write_lock,\n-\t\t\t\t\t\t    git_path_merge_rr(r),\n-\t\t\t\t\t\t    LOCK_DIE_ON_ERROR);\n+\t} else {\n+\t\tconst char *path = git_path_merge_rr(r);\n+\t\tint lock_flags = LOCK_DIE_ON_ERROR;\n+\t\tlong timeout_ms = rerere_lock_timeout_ms;\n+\n+\t\t/*\n+\t\t * Another process may hold the lock for a while, e.g.\n+\t\t * \"git rerere gc\" while it prunes rr-cache, so wait for\n+\t\t * it instead of dying right away.  The gc itself never\n+\t\t * waits: skipping one of its runs costs nothing.\n+\t\t */\n+\t\tif (flags & RERERE_NOWAIT) {\n+\t\t\tlock_flags = 0;\n+\t\t\ttimeout_ms = 0;\n+\t\t}\n+\t\tfd = repo_hold_lock_file_for_update_timeout(r, &write_lock,\n+\t\t\t\t\t\t\t    path, lock_flags,\n+\t\t\t\t\t\t\t    timeout_ms);\n+\t\tif (fd < 0) {\n+\t\t\twarning_errno(_(\"skipping rerere, \"\n+\t\t\t\t\t\"unable to create '%s.lock'\"), path);\n+\t\t\treturn -1;\n+\t\t}\n+\t}\n \tread_rr(r, merge_rr);\n \treturn fd;\n }\n@@ -1211,7 +1238,7 @@ void rerere_gc(struct repository *r, struct string_list *rr)\n \ttimestamp_t cutoff_resolve = now - 60 * 86400;\n \tstruct strbuf buf = STRBUF_INIT;\n \n-\tif (setup_rerere(r, rr, 0) < 0)\n+\tif (setup_rerere(r, rr, RERERE_NOWAIT) < 0)\n \t\treturn;\n \n \trepo_config_get_expiry_in_days(the_repository, \"gc.rerereresolved\",\ndiff --git a/rerere.h b/rerere.h\nindex d4b5f7c932..a2712d543e 100644\n--- a/rerere.h\n+++ b/rerere.h\n@@ -10,6 +10,8 @@ struct repository;\n #define RERERE_AUTOUPDATE   01\n #define RERERE_NOAUTOUPDATE 02\n #define RERERE_READONLY     04\n+/* Never wait for MERGE_RR.lock, and skip the run when it is held */\n+#define RERERE_NOWAIT       010\n \n /*\n  * Marks paths that have been hand-resolved and added to the\ndiff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh\nindex 7bb601e117..27082a7676 100755\n--- a/t/t4200-rerere.sh\n+++ b/t/t4200-rerere.sh\n@@ -242,6 +242,73 @@ test_expect_success 'old records rest in peace' '\n \ttest_path_is_missing $rr2/preimage\n '\n \n+test_expect_success 'gc does nothing while MERGE_RR is locked' '\n+\tmkdir -p $rr2 &&\n+\techo Hello >$rr2/preimage &&\n+\ttest-tool chmtime =$just_over_15_days_ago $rr2/preimage &&\n+\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\tgit rerere gc 2>err &&\n+\ttest_grep \"MERGE_RR.lock\" err &&\n+\ttest_path_is_file $rr2/preimage &&\n+\n+\trm .git/MERGE_RR.lock &&\n+\tgit rerere gc &&\n+\ttest_path_is_missing $rr2/preimage\n+'\n+\n+test_expect_success 'a held lock is waited out within rerere.lockTimeout' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\t{\n+\t\t( sleep 1 && rm -f .git/MERGE_RR.lock ) &\n+\t} &&\n+\ttest_must_fail git -c rerere.lockTimeout=5000 merge first 2>err &&\n+\twait &&\n+\ttest_grep ! \"MERGE_RR\" err &&\n+\ttest_grep \"^=======\\$\" $rr/preimage\n+'\n+\n+test_expect_success 'merge fails once rerere.lockTimeout is up' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 merge first 2>err &&\n+\ttest_grep \"Unable to create\" err &&\n+\ttest_grep \"^=======\\$\" a1 &&\n+\ttest_path_is_missing $rr/preimage\n+'\n+\n+test_expect_success 'rerere, forget and clear fail on a lock they cannot take' '\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rerere 2>err &&\n+\ttest_grep \"Unable to create\" err &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rerere forget a1 2>err &&\n+\ttest_grep \"Unable to create\" err &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rerere clear 2>err &&\n+\ttest_grep \"Unable to create\" err\n+'\n+\n+test_expect_success 'rebase --abort fails on a lock it cannot take' '\n+\tgit reset --hard &&\n+\tgit checkout -b lock-held-abort third &&\n+\ttest_when_finished \"git checkout third && git branch -D lock-held-abort\" &&\n+\ttest_must_fail git rebase first &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rebase --abort 2>err &&\n+\ttest_grep \"Unable to create\" err &&\n+\ttest_path_is_dir .git/rebase-merge &&\n+\trm .git/MERGE_RR.lock &&\n+\tgit rebase --abort &&\n+\ttest_path_is_missing .git/rebase-merge\n+'\n+\n rerere_gc_custom_expiry_test () {\n \tfive_days=\"$1\" right_now=\"$2\"\n \ttest_expect_success \"rerere gc with custom expiry ($five_days, $right_now)\" '\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 5fbb16f0f0..4a27767817 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -1051,6 +1051,14 @@ test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.aut\n \ttest_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 maintenance run --auto --task=rerere-gc\n '\n \n+test_expect_success 'rerere-gc task succeeds while MERGE_RR is locked' '\n+\ttest_when_finished \"rm -rf .git/rr-cache .git/MERGE_RR.lock\" &&\n+\tmkdir .git/rr-cache &&\n+\t: >.git/rr-cache/entry &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_expect_rerere_gc git maintenance run --task=rerere-gc\n+'\n+\n test_expect_success '--auto and --schedule incompatible' '\n \ttest_must_fail git maintenance run --auto --schedule=daily 2>err &&\n \ttest_grep \"cannot be used together\" err\n-- \ngitgitgadget\n\n"},{"id":"552681","messageId":"1cce403113833c14a1c4a0da0db0772c5abdeb1c.1789373061.git.gitgitgadget@gmail.com","threadId":"66250","inReplyTo":"pull.2214.v4.git.1789373061.gitgitgadget@gmail.com","subject":"[PATCH v4 2/2] rerere: go on at a conflict when the lock stays busy","fromName":"Thomas Bachem via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-14T08:04:21Z","receivedAt":"2026-09-14T08:04:25Z","isPatch":true,"body":"From: Thomas Bachem <mail@thomasbachem.com>\n\nWhen a merge, rebase, cherry-pick, revert, am, stash or apply stops\nat a conflict, it runs rerere right before it returns to the user.\nIf MERGE_RR.lock is still held when rerere.lockTimeout runs out, the\ncommand dies there. For a rebase that is worse than a lost\nrecording: the sequencer has not yet written the state that\n\"git rebase --continue\" needs, so the rebase cannot continue, and\nfollowing the \"git commit --amend\" advice folds the conflicted pick\ninto the previous commit.\n\nSo print a warning and go on instead. The conflict is still in\nplace, and the warning tells the user to run \"git rerere\" before\nresolving it. That records the preimage, or replays a known\nresolution, just as the command would have done.\n\nAll other callers still fail when the timeout runs out. \"git commit\"\nand \"git am --continue\" record the resolution and then move on to\nthe next commit or patch. A warning would come too late there, and\nthe next rerere run would record whatever the file contains by then.\nThe \"rerere clear\" that am and rebase run for --skip and --abort\nwould leave the same stale entry behind. \"git rerere\", \"git rerere\nforget\" and \"git rerere clear\" fail because the user asked for that\nstate explicitly.\n\nAssisted-by: Claude Fable 5.1\nSigned-off-by: Thomas Bachem <mail@thomasbachem.com>\n---\n Documentation/config/rerere.adoc |  10 ++-\n apply.c                          |   2 +-\n builtin/am.c                     |   3 +-\n builtin/merge.c                  |   2 +-\n builtin/stash.c                  |   2 +-\n rerere.c                         |  16 ++++-\n rerere.h                         |   2 +\n sequencer.c                      |   4 +-\n t/t4200-rerere.sh                | 105 ++++++++++++++++++++++++++++++-\n 9 files changed, 132 insertions(+), 14 deletions(-)\n\ndiff --git a/Documentation/config/rerere.adoc b/Documentation/config/rerere.adoc\nindex cc9dd0c37b..81deefa006 100644\n--- a/Documentation/config/rerere.adoc\n+++ b/Documentation/config/rerere.adoc\n@@ -16,6 +16,10 @@ rerere.lockTimeout::\n \tlock when another process holds it, typically a background\n \t`git rerere gc`.  Value 0 means not to wait at all; -1 means\n \tto wait indefinitely.  Default is 1000 (i.e., wait for 1\n-\tsecond).  When the time is up, the command fails as it does\n-\tfor any other lock it cannot take.  `git rerere gc` never\n-\twaits and skips its run while the lock is held.\n+\tsecond).  When the time is up, a command that stops at a\n+\tconflict, such as `git merge` or `git rebase`, prints a\n+\twarning and goes on without rerere; run `git rerere` before\n+\tresolving the conflict to record it after all.  Any other\n+\tcommand fails, as it does for any other lock it cannot take.\n+\t`git rerere gc` never waits and skips its run while the lock\n+\tis held.\ndiff --git a/apply.c b/apply.c\nindex f00b7ba4d3..3b8502535b 100644\n--- a/apply.c\n+++ b/apply.c\n@@ -4865,7 +4865,7 @@ static int write_out_results(struct apply_state *state, struct patch *list)\n \t\t * tree with conflict markers, but that isn't written with --cached.\n \t\t */\n \t\tif (!state->cached)\n-\t\t\trepo_rerere(state->repo, 0);\n+\t\t\trepo_rerere(state->repo, RERERE_SKIP_LOCKED);\n \t}\n \n \treturn errs;\ndiff --git a/builtin/am.c b/builtin/am.c\nindex e9623b8307..bcb93db515 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -1649,7 +1649,8 @@ static int fall_back_threeway(const struct am_state *state, const char *index_pa\n \t\to.verbosity = 0;\n \n \tif (merge_ort_generic(&o, &our_tree, &their_tree, 1, bases, &result)) {\n-\t\trepo_rerere(the_repository, state->allow_rerere_autoupdate);\n+\t\trepo_rerere(the_repository, state->allow_rerere_autoupdate |\n+\t\t\t    RERERE_SKIP_LOCKED);\n \t\tfree(their_tree_name);\n \t\treturn error(_(\"Failed to merge in the changes.\"));\n \t}\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 5b4eb23a83..1ed5959bfd 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -1061,7 +1061,7 @@ static int suggest_conflicts(void)\n \tfputs(msgbuf.buf, fp);\n \tstrbuf_release(&msgbuf);\n \tfclose(fp);\n-\trepo_rerere(the_repository, allow_rerere_auto);\n+\trepo_rerere(the_repository, allow_rerere_auto | RERERE_SKIP_LOCKED);\n \tprintf(_(\"Automatic merge failed; \"\n \t\t\t\"fix conflicts and then commit the result.\\n\"));\n \treturn 1;\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex 72c52571f8..83b6e1be72 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -729,7 +729,7 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,\n \t\tret = error(_(\"could not write index\"));\n \n \tif (ret) {\n-\t\trepo_rerere(the_repository, 0);\n+\t\trepo_rerere(the_repository, RERERE_SKIP_LOCKED);\n \n \t\tif (index)\n \t\t\tfprintf_ln(stderr, _(\"Index was not unstashed.\"));\ndiff --git a/rerere.c b/rerere.c\nindex 7d44f3937c..a996d39159 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -3,6 +3,7 @@\n \n #include \"git-compat-util.h\"\n #include \"abspath.h\"\n+#include \"advice.h\"\n #include \"config.h\"\n #include \"copy.h\"\n #include \"environment.h\"\n@@ -887,8 +888,9 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n \n \tif (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))\n \t\trerere_autoupdate = !!(flags & RERERE_AUTOUPDATE);\n-\tif ((flags & RERERE_READONLY) && (flags & RERERE_NOWAIT))\n-\t\tBUG(\"RERERE_READONLY takes no lock, so RERERE_NOWAIT does not apply\");\n+\tif ((flags & RERERE_READONLY) &&\n+\t    (flags & (RERERE_NOWAIT | RERERE_SKIP_LOCKED)))\n+\t\tBUG(\"RERERE_READONLY takes no lock, so no lock flag applies\");\n \tif (flags & RERERE_READONLY) {\n \t\tfd = 0;\n \t} else {\n@@ -900,18 +902,26 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n \t\t * Another process may hold the lock for a while, e.g.\n \t\t * \"git rerere gc\" while it prunes rr-cache, so wait for\n \t\t * it instead of dying right away.  The gc itself never\n-\t\t * waits: skipping one of its runs costs nothing.\n+\t\t * waits: skipping one of its runs costs nothing.  A\n+\t\t * command that stops at a conflict must not die here\n+\t\t * either, so it warns and goes on without rerere.\n \t\t */\n \t\tif (flags & RERERE_NOWAIT) {\n \t\t\tlock_flags = 0;\n \t\t\ttimeout_ms = 0;\n \t\t}\n+\t\tif (flags & RERERE_SKIP_LOCKED)\n+\t\t\tlock_flags = 0;\n \t\tfd = repo_hold_lock_file_for_update_timeout(r, &write_lock,\n \t\t\t\t\t\t\t    path, lock_flags,\n \t\t\t\t\t\t\t    timeout_ms);\n \t\tif (fd < 0) {\n \t\t\twarning_errno(_(\"skipping rerere, \"\n \t\t\t\t\t\"unable to create '%s.lock'\"), path);\n+\t\t\tif (flags & RERERE_SKIP_LOCKED)\n+\t\t\t\tadvise(_(\"run \\\"git rerere\\\" before resolving \"\n+\t\t\t\t\t \"the conflict to record or replay \"\n+\t\t\t\t\t \"its resolution\"));\n \t\t\treturn -1;\n \t\t}\n \t}\ndiff --git a/rerere.h b/rerere.h\nindex a2712d543e..e91f3f4afa 100644\n--- a/rerere.h\n+++ b/rerere.h\n@@ -12,6 +12,8 @@ struct repository;\n #define RERERE_READONLY     04\n /* Never wait for MERGE_RR.lock, and skip the run when it is held */\n #define RERERE_NOWAIT       010\n+/* Warn and go on without rerere if MERGE_RR.lock cannot be taken in time */\n+#define RERERE_SKIP_LOCKED  020\n \n /*\n  * Marks paths that have been hand-resolved and added to the\ndiff --git a/sequencer.c b/sequencer.c\nindex 65afd100d9..49776e7c5a 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -2519,7 +2519,7 @@ static enum pick_result do_pick_commit(struct repository *r,\n \t\t      : _(\"could not apply %s... %s\"),\n \t\t      short_commit_name(r, commit), msg.subject);\n \t\tprint_advice(r, res == 1, opts);\n-\t\trepo_rerere(r, opts->allow_rerere_auto);\n+\t\trepo_rerere(r, opts->allow_rerere_auto | RERERE_SKIP_LOCKED);\n \t\tgoto leave;\n \t}\n \n@@ -4448,7 +4448,7 @@ static int do_merge(struct repository *r,\n \n \trollback_lock_file(&lock);\n \tif (ret)\n-\t\trepo_rerere(r, opts->allow_rerere_auto);\n+\t\trepo_rerere(r, opts->allow_rerere_auto | RERERE_SKIP_LOCKED);\n \telse\n \t\t/*\n \t\t * In case of problems, we now want to return a positive\ndiff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh\nindex 27082a7676..d0a9363a1c 100755\n--- a/t/t4200-rerere.sh\n+++ b/t/t4200-rerere.sh\n@@ -272,17 +272,118 @@ test_expect_success 'a held lock is waited out within rerere.lockTimeout' '\n \ttest_grep \"^=======\\$\" $rr/preimage\n '\n \n-test_expect_success 'merge fails once rerere.lockTimeout is up' '\n+test_expect_success 'merge goes on without rerere once rerere.lockTimeout is up' '\n \tgit reset --hard &&\n \trm -rf $rr &&\n \ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n \t>.git/MERGE_RR.lock &&\n \ttest_must_fail git -c rerere.lockTimeout=0 merge first 2>err &&\n-\ttest_grep \"Unable to create\" err &&\n+\ttest_grep \"skipping rerere\" err &&\n+\ttest_grep \"hint: .*git rerere\" err &&\n \ttest_grep \"^=======\\$\" a1 &&\n \ttest_path_is_missing $rr/preimage\n '\n \n+test_expect_success 'rerere run at the stop records what was skipped' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\tgit checkout -b lock-held-catch-up third &&\n+\ttest_when_finished \"git checkout third && git branch -D lock-held-catch-up\" &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 merge first &&\n+\ttest_path_is_missing $rr/preimage &&\n+\trm .git/MERGE_RR.lock &&\n+\tgit rerere &&\n+\ttest_grep \"^=======\\$\" $rr/preimage &&\n+\techo resolved >a1 &&\n+\tgit add a1 &&\n+\tgit commit -qm resolved &&\n+\ttest_path_is_file $rr/postimage\n+'\n+\n+test_expect_success 'rebase goes on without rerere once rerere.lockTimeout is up' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\tgit checkout -b lock-held third &&\n+\ttest_when_finished \"git checkout third && git branch -D lock-held\" &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rebase first 2>err &&\n+\ttest_grep \"skipping rerere\" err &&\n+\ttest_path_is_file .git/rebase-merge/stopped-sha &&\n+\trm .git/MERGE_RR.lock &&\n+\techo resolved >a1 &&\n+\tgit add a1 &&\n+\tgit rebase --continue &&\n+\ttest_path_is_missing .git/rebase-merge &&\n+\ttest_path_is_missing $rr/preimage\n+'\n+\n+test_expect_success 'commit fails on a lock it cannot take' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\tgit checkout -b lock-held-commit third &&\n+\ttest_when_finished \"git checkout third && git branch -D lock-held-commit\" &&\n+\ttest_must_fail git merge first &&\n+\ttest_path_is_file $rr/preimage &&\n+\techo resolved >a1 &&\n+\tgit add a1 &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 commit -qm resolved 2>err &&\n+\ttest_grep \"Unable to create\" err &&\n+\ttest_path_is_missing $rr/postimage\n+'\n+\n+test_expect_success 'am goes on without rerere once rerere.lockTimeout is up' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\tgit checkout -b lock-held-am third &&\n+\ttest_when_finished \"test_might_fail git am --abort &&\n+\t\tgit checkout third && git branch -D lock-held-am\" &&\n+\tgit format-patch -1 --stdout first >first.patch &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 am --3way first.patch 2>err &&\n+\ttest_grep \"skipping rerere\" err &&\n+\ttest_path_is_dir .git/rebase-apply &&\n+\trm .git/MERGE_RR.lock &&\n+\tgit am --abort &&\n+\ttest_path_is_missing .git/rebase-apply\n+'\n+\n+test_expect_success 'stash pop goes on without rerere once rerere.lockTimeout is up' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\tgit checkout -b lock-held-stash third &&\n+\ttest_when_finished \"git reset --hard && git stash drop &&\n+\t\tgit checkout third && git branch -D lock-held-stash\" &&\n+\techo stashed >>a1 &&\n+\tgit stash &&\n+\techo committed >>a1 &&\n+\tgit commit -qam committed &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 stash pop 2>err &&\n+\ttest_grep \"skipping rerere\" err &&\n+\ttest_grep \"^=======\\$\" a1\n+'\n+\n+test_expect_success 'apply --3way goes on without rerere once rerere.lockTimeout is up' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\tgit checkout -b lock-held-apply third &&\n+\ttest_when_finished \"git reset --hard &&\n+\t\tgit checkout third && git branch -D lock-held-apply\" &&\n+\tgit format-patch -1 --stdout first >first.patch &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 apply --3way first.patch 2>err &&\n+\ttest_grep \"skipping rerere\" err &&\n+\ttest_grep \"^=======\\$\" a1\n+'\n+\n test_expect_success 'rerere, forget and clear fail on a lock they cannot take' '\n \ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n \t>.git/MERGE_RR.lock &&\n-- \ngitgitgadget\n"},{"id":"553420","messageId":"aroixgCkqbmKErng@pks.im","threadId":"66250","inReplyTo":"8a7a74d6aa359844a49593538ef6178cd1b02031.1789373061.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 1/2] rerere: wait for MERGE_RR.lock, and let the gc skip it","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-09-28T08:18:14Z","receivedAt":"2026-09-28T08:18:20Z","isPatch":true,"body":"On Mon, Sep 14, 2026 at 08:04:20AM +0000, Thomas Bachem via GitGitGadget wrote:\n> diff --git a/Documentation/git-rerere.adoc b/Documentation/git-rerere.adoc\n> index 4e6ab9a27c..4df653367e 100644\n> --- a/Documentation/git-rerere.adoc\n> +++ b/Documentation/git-rerere.adoc\n> @@ -70,7 +70,9 @@ occurred a long time ago.  By default, unresolved conflicts older\n>  than 15 days and resolved conflicts older than 60\n>  days are pruned.  These defaults are controlled via the\n>  `gc.rerereUnresolved` and `gc.rerereResolved` configuration\n> -variables respectively.\n> +variables respectively.  If another process holds the rerere lock,\n> +for example a merge or rebase that is recording a conflict, `gc`\n> +does nothing and says so.\n\nDo we maybe want to drop these examples? I don't feel like they add any\nvalue.\n\n> diff --git a/rerere.c b/rerere.c\n> index 3d3bd0db16..7d44f3937c 100644\n> --- a/rerere.c\n> +++ b/rerere.c\n> @@ -33,6 +33,9 @@ static int rerere_enabled = -1;\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> @@ -850,6 +853,8 @@ static void git_rerere_config(void)\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>  }\n>  \n> @@ -882,12 +887,34 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n>  \n>  \tif (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))\n>  \t\trerere_autoupdate = !!(flags & RERERE_AUTOUPDATE);\n> -\tif (flags & RERERE_READONLY)\n> +\tif ((flags & RERERE_READONLY) && (flags & RERERE_NOWAIT))\n> +\t\tBUG(\"RERERE_READONLY takes no lock, so RERERE_NOWAIT does not apply\");\n> +\tif (flags & RERERE_READONLY) {\n>  \t\tfd = 0;\n> -\telse\n> -\t\tfd = repo_hold_lock_file_for_update(r, &write_lock,\n> -\t\t\t\t\t\t    git_path_merge_rr(r),\n> -\t\t\t\t\t\t    LOCK_DIE_ON_ERROR);\n> +\t} else {\n> +\t\tconst char *path = git_path_merge_rr(r);\n> +\t\tint lock_flags = LOCK_DIE_ON_ERROR;\n> +\t\tlong timeout_ms = rerere_lock_timeout_ms;\n\nHere you're using a `long` whereas `rerere_lock_timeout_ms` is an `int`.\nOf course we'd ideally use a `long` consistently as that's also what\n`repo_hold_lock_file_for_update_timeout()` accepts. But I guess the\nreason you didn't is that we don't have `repo_config_get_long()`. So I\nguess this is good enough for now.\n\n> @@ -1211,7 +1238,7 @@ void rerere_gc(struct repository *r, struct string_list *rr)\n>  \ttimestamp_t cutoff_resolve = now - 60 * 86400;\n>  \tstruct strbuf buf = STRBUF_INIT;\n>  \n> -\tif (setup_rerere(r, rr, 0) < 0)\n> +\tif (setup_rerere(r, rr, RERERE_NOWAIT) < 0)\n>  \t\treturn;\n>  \n>  \trepo_config_get_expiry_in_days(the_repository, \"gc.rerereresolved\",\n\nI'm not a 100% sold on this change. There's two different scenarios\nunder which we want to perform garbage collection:\n\n  - As part of auto-maintenance, triggered by Git automatically. Here\n    I'm fully aligned that it makes sense to just silently ignore the\n    case where we couldn't acquire the lock, as auto-maintenance is done\n    on a best-effort basis anyway.\n\n  - As part of `git rerere gc`, which is invoked manually by the user.\n    Here I'm less so, as the user has explicitly asked us to garbage\n    collect. Sure, we print a warning now, but the exit code does not\n    signal that we failed garbage collecting.\n\nSo I'd argue that we should discern those two use cases. I think that in\nthe second use case, we'd probably want to use a timeout and if we fail\nto acquire the lock, we should make `git rerere gc` fail with a non-zero\nexit code.\n\nPatrick\n"},{"id":"553421","messageId":"aroiyibdbr7PKqST@pks.im","threadId":"66250","inReplyTo":"1cce403113833c14a1c4a0da0db0772c5abdeb1c.1789373061.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 2/2] rerere: go on at a conflict when the lock stays busy","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-09-28T08:18:18Z","receivedAt":"2026-09-28T08:18:24Z","isPatch":true,"body":"On Mon, Sep 14, 2026 at 08:04:21AM +0000, Thomas Bachem via GitGitGadget wrote:\n> diff --git a/rerere.c b/rerere.c\n> index 7d44f3937c..a996d39159 100644\n> --- a/rerere.c\n> +++ b/rerere.c\n> @@ -900,18 +902,26 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n>  \t\t * Another process may hold the lock for a while, e.g.\n>  \t\t * \"git rerere gc\" while it prunes rr-cache, so wait for\n>  \t\t * it instead of dying right away.  The gc itself never\n> -\t\t * waits: skipping one of its runs costs nothing.\n> +\t\t * waits: skipping one of its runs costs nothing.  A\n> +\t\t * command that stops at a conflict must not die here\n> +\t\t * either, so it warns and goes on without rerere.\n>  \t\t */\n>  \t\tif (flags & RERERE_NOWAIT) {\n>  \t\t\tlock_flags = 0;\n>  \t\t\ttimeout_ms = 0;\n>  \t\t}\n> +\t\tif (flags & RERERE_SKIP_LOCKED)\n> +\t\t\tlock_flags = 0;\n>  \t\tfd = repo_hold_lock_file_for_update_timeout(r, &write_lock,\n>  \t\t\t\t\t\t\t    path, lock_flags,\n>  \t\t\t\t\t\t\t    timeout_ms);\n>  \t\tif (fd < 0) {\n>  \t\t\twarning_errno(_(\"skipping rerere, \"\n>  \t\t\t\t\t\"unable to create '%s.lock'\"), path);\n> +\t\t\tif (flags & RERERE_SKIP_LOCKED)\n> +\t\t\t\tadvise(_(\"run \\\"git rerere\\\" before resolving \"\n> +\t\t\t\t\t \"the conflict to record or replay \"\n> +\t\t\t\t\t \"its resolution\"));\n>  \t\t\treturn -1;\n>  \t\t}\n>  \t}\n\nShould this use `advise_if_enabled()`?\n\nPatrick\n"},{"id":"553445","messageId":"3dc3d02f12a3118ac9e270c19960815f6b8170cb.1790596702.git.gitgitgadget@gmail.com","threadId":"66250","inReplyTo":"pull.2214.v5.git.1790596702.gitgitgadget@gmail.com","subject":"[PATCH v5 1/3] rerere: wait for MERGE_RR.lock before giving up","fromName":"Thomas Bachem via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-28T11:58:20Z","receivedAt":"2026-09-28T11:58:26Z","isPatch":true,"body":"From: Thomas Bachem <mail@thomasbachem.com>\n\nsetup_rerere() takes MERGE_RR.lock with LOCK_DIE_ON_ERROR, so of two\nprocesses that want it at the same time the second one dies. That\nused to be rare. Since 452b12c2e0 (builtin/maintenance: use\n\"geometric\" strategy by default, 2026-02-24) the auto maintenance\nafter a commit runs \"git rerere gc\" whenever rr-cache has enough\nstale entries, and the gc holds the lock while it prunes.\n\nA rebase whose next pick conflicts while that happens dies inside\nrepo_rerere(), before the sequencer has written the state that\n\"git rebase --continue\" needs. Every later \"git rebase --continue\"\nthen fails with \"you have staged changes in your working tree\".\n\nWait for the lock for up to rerere.lockTimeout milliseconds, 1000 by\ndefault, and only then fail as before. Pruning a few thousand entries\ntakes well under a second, so the default covers a rebase that runs\ninto the gc.\n\nAssisted-by: Claude Fable 5.1\nSigned-off-by: Thomas Bachem <mail@thomasbachem.com>\n---\n Documentation/config/rerere.adoc |  8 +++++\n rerere.c                         | 22 ++++++++++---\n t/t4200-rerere.sh                | 53 ++++++++++++++++++++++++++++++++\n 3 files changed, 78 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/config/rerere.adoc b/Documentation/config/rerere.adoc\nindex 3a78b5ebb1..30e827f32b 100644\n--- a/Documentation/config/rerere.adoc\n+++ b/Documentation/config/rerere.adoc\n@@ -10,3 +10,11 @@ rerere.enabled::\n \tenabled if there is an `rr-cache` directory under the\n \t`$GIT_DIR`, e.g. if \"rerere\" was previously used in the\n \trepository.\n+\n+rerere.lockTimeout::\n+\tThe length of time, in milliseconds, to wait for the rerere\n+\tlock when another process holds it, typically a background\n+\t`git rerere gc`.  Value 0 means not to wait at all; -1 means\n+\tto wait indefinitely.  Default is 1000 (i.e., wait for 1\n+\tsecond).  When the time is up, the command fails as it does\n+\tfor any other lock it cannot take.\ndiff --git a/rerere.c b/rerere.c\nindex 1c3745d9e3..64fac07c71 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -33,6 +33,9 @@ static int rerere_enabled = -1;\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@@ -850,6 +853,8 @@ static void git_rerere_config(void)\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 }\n \n@@ -882,12 +887,19 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n \n \tif (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))\n \t\trerere_autoupdate = !!(flags & RERERE_AUTOUPDATE);\n-\tif (flags & RERERE_READONLY)\n+\tif (flags & RERERE_READONLY) {\n \t\tfd = 0;\n-\telse\n-\t\tfd = repo_hold_lock_file_for_update(r, &write_lock,\n-\t\t\t\t\t\t    git_path_merge_rr(r),\n-\t\t\t\t\t\t    LOCK_DIE_ON_ERROR);\n+\t} else {\n+\t\t/*\n+\t\t * Another process may hold the lock for a while, e.g.\n+\t\t * \"git rerere gc\" while it prunes rr-cache, so wait for\n+\t\t * it instead of dying right away.\n+\t\t */\n+\t\tfd = repo_hold_lock_file_for_update_timeout(r, &write_lock,\n+\t\t\t\t\t\t\t    git_path_merge_rr(r),\n+\t\t\t\t\t\t\t    LOCK_DIE_ON_ERROR,\n+\t\t\t\t\t\t\t    rerere_lock_timeout_ms);\n+\t}\n \tread_rr(r, merge_rr);\n \treturn fd;\n }\ndiff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh\nindex 7bb601e117..7bd92235dc 100755\n--- a/t/t4200-rerere.sh\n+++ b/t/t4200-rerere.sh\n@@ -242,6 +242,59 @@ test_expect_success 'old records rest in peace' '\n \ttest_path_is_missing $rr2/preimage\n '\n \n+test_expect_success 'a held lock is waited out within rerere.lockTimeout' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\t{\n+\t\t( sleep 1 && rm -f .git/MERGE_RR.lock ) &\n+\t} &&\n+\ttest_must_fail git -c rerere.lockTimeout=5000 merge first 2>err &&\n+\twait &&\n+\ttest_grep ! \"MERGE_RR\" err &&\n+\ttest_grep \"^=======\\$\" $rr/preimage\n+'\n+\n+test_expect_success 'merge fails once rerere.lockTimeout is up' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 merge first 2>err &&\n+\ttest_grep \"Unable to create\" err &&\n+\ttest_grep \"^=======\\$\" a1 &&\n+\ttest_path_is_missing $rr/preimage\n+'\n+\n+test_expect_success 'rerere, forget, clear and gc fail on a lock they cannot take' '\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rerere 2>err &&\n+\ttest_grep \"Unable to create\" err &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rerere forget a1 2>err &&\n+\ttest_grep \"Unable to create\" err &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rerere clear 2>err &&\n+\ttest_grep \"Unable to create\" err &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rerere gc 2>err &&\n+\ttest_grep \"Unable to create\" err\n+'\n+\n+test_expect_success 'rebase --abort fails on a lock it cannot take' '\n+\tgit reset --hard &&\n+\tgit checkout -b lock-held-abort third &&\n+\ttest_when_finished \"git checkout third && git branch -D lock-held-abort\" &&\n+\ttest_must_fail git rebase first &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rebase --abort 2>err &&\n+\ttest_grep \"Unable to create\" err &&\n+\ttest_path_is_dir .git/rebase-merge &&\n+\trm .git/MERGE_RR.lock &&\n+\tgit rebase --abort &&\n+\ttest_path_is_missing .git/rebase-merge\n+'\n+\n rerere_gc_custom_expiry_test () {\n \tfive_days=\"$1\" right_now=\"$2\"\n \ttest_expect_success \"rerere gc with custom expiry ($five_days, $right_now)\" '\n-- \ngitgitgadget\n\n"},{"id":"553446","messageId":"pull.2214.v5.git.1790596702.gitgitgadget@gmail.com","threadId":"66250","inReplyTo":"pull.2214.git.1788337897490.gitgitgadget@gmail.com","subject":"[PATCH v5 0/3] rerere: wait for MERGE_RR.lock, and go on at a conflict","fromName":"Thomas Bachem via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-28T11:58:19Z","receivedAt":"2026-09-28T11:58:26Z","isPatch":true,"body":"This is now three patches: the wait on its own, the gc's skip as \"git rerere\ngc --auto\", and the conflict-time callers on top, as before.\n\nPatch 1 makes every writer wait rerere.lockTimeout for MERGE_RR.lock before\nit fails as it does today. That alone keeps a rebase alive through the gc\nthat a commit's auto maintenance starts, since pruning a few thousand\nentries takes well under the one second default.\n\nPatch 2 is the split Patrick asked for: a \"git rerere gc\" run by hand waits\nand then fails as it does today, like every other rerere command, and only\n\"git rerere gc --auto\" skips a held lock, quietly, as \"git gc --auto\" does\nwith its own lock. \"git maintenance run --auto\" and \"git gc --auto\" pass the\noption, as they do for \"git pack-refs --auto\". A manual or scheduled\nmaintenance run does not and fails on a held lock like the command itself. I\nleft scheduled runs on that side since maintenance treats them like manual\nruns for pack-refs and gc too.\n\nPatch 3 is v4's second patch: a command that stops at a conflict warns and\ngoes on when the lock is still held after the wait, and the warning says to\nrun \"git rerere\" before resolving.\n\nChanges since v4:\n\n * Based on today's master, which has ps/tune-rerere-gc, and merges cleanly\n   into next. In seen it only conflicts with its own v4.\n\n * \"git rerere gc\" no longer skips a held lock on its own. Only \"git rerere\n   gc --auto\" does, and silently, so a background run prints nothing\n   (Patrick, Phillip).\n\n * The git-rerere(1) sentence with the merge and rebase examples is gone.\n   The gc paragraph describes --auto instead (Patrick).\n\n * I kept the timeout an int, like core.filesRefLockTimeout and\n   core.packedRefsTimeout, and dropped the long local (Patrick).\n\n * The hint to run \"git rerere\" goes through advise_if_enabled() under\n   advice.mergeConflict (Patrick).\n\n * Tests: the old gc test became the \"gc --auto\" test of patch 2, \"git\n   rerere gc\" joined the commands that fail on a held lock in patch 1, and\n   t7900 checks that maintenance passes --auto and that a run without it\n   fails on a held lock.\n\nThomas Bachem (3):\n  rerere: wait for MERGE_RR.lock before giving up\n  rerere: add \"gc --auto\" that skips a held lock\n  rerere: go on at a conflict when the lock stays busy\n\n Documentation/config/rerere.adoc |  13 +++\n Documentation/git-rerere.adoc    |   9 +-\n apply.c                          |   2 +-\n builtin/am.c                     |   3 +-\n builtin/gc.c                     |   4 +-\n builtin/merge.c                  |   2 +-\n builtin/rerere.c                 |  13 ++-\n builtin/stash.c                  |   2 +-\n rerere.c                         |  56 ++++++++--\n rerere.h                         |   6 +-\n sequencer.c                      |   4 +-\n t/t4200-rerere.sh                | 175 +++++++++++++++++++++++++++++++\n t/t7900-maintenance.sh           |  25 ++++-\n 13 files changed, 292 insertions(+), 22 deletions(-)\n\n\nbase-commit: 34f06850c16c7f7ac822b1adc71354f11b0f2ca3\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2214%2Fthomasbachem%2Frerere-gc-lock-v5\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2214/thomasbachem/rerere-gc-lock-v5\nPull-Request: https://github.com/gitgitgadget/git/pull/2214\n\nRange-diff vs v4:\n\n 1:  8a7a74d6aa ! 1:  3dc3d02f12 rerere: wait for MERGE_RR.lock, and let the gc skip it\n     @@ Metadata\n      Author: Thomas Bachem <mail@thomasbachem.com>\n      \n       ## Commit message ##\n     -    rerere: wait for MERGE_RR.lock, and let the gc skip it\n     +    rerere: wait for MERGE_RR.lock before giving up\n      \n     -    setup_rerere() takes MERGE_RR.lock with LOCK_DIE_ON_ERROR. When two\n     -    processes want the lock at the same time, the second one dies. This\n     -    was always the case, but since 452b12c2e0 (builtin/maintenance: use\n     -    \"geometric\" strategy by default, 2026-02-24) it is easy to hit: auto\n     -    maintenance now runs \"git rerere gc\" after every commit whenever\n     -    rr-cache contains at least one entry, and the gc holds the lock\n     -    while it prunes.\n     +    setup_rerere() takes MERGE_RR.lock with LOCK_DIE_ON_ERROR, so of two\n     +    processes that want it at the same time the second one dies. That\n     +    used to be rare. Since 452b12c2e0 (builtin/maintenance: use\n     +    \"geometric\" strategy by default, 2026-02-24) the auto maintenance\n     +    after a commit runs \"git rerere gc\" whenever rr-cache has enough\n     +    stale entries, and the gc holds the lock while it prunes.\n      \n     -    A rebase whose next pick conflicts while the gc holds the lock dies\n     -    inside repo_rerere(). That runs before the sequencer writes the state\n     -    that \"git rebase --continue\" needs, so every later \"git rebase\n     -    --continue\" fails with \"you have staged changes\".\n     +    A rebase whose next pick conflicts while that happens dies inside\n     +    repo_rerere(), before the sequencer has written the state that\n     +    \"git rebase --continue\" needs. Every later \"git rebase --continue\"\n     +    then fails with \"you have staged changes in your working tree\".\n      \n     -    Instead of dying right away, wait for the lock for up to\n     -    rerere.lockTimeout milliseconds, 1000 by default, and only then fail\n     -    as before. The gc itself does not wait: when the lock is held, it\n     -    skips this run and leaves the pruning to the next one.\n     +    Wait for the lock for up to rerere.lockTimeout milliseconds, 1000 by\n     +    default, and only then fail as before. Pruning a few thousand entries\n     +    takes well under a second, so the default covers a rebase that runs\n     +    into the gc.\n      \n          Assisted-by: Claude Fable 5.1\n          Signed-off-by: Thomas Bachem <mail@thomasbachem.com>\n     @@ Documentation/config/rerere.adoc: rerere.enabled::\n      +\t`git rerere gc`.  Value 0 means not to wait at all; -1 means\n      +\tto wait indefinitely.  Default is 1000 (i.e., wait for 1\n      +\tsecond).  When the time is up, the command fails as it does\n     -+\tfor any other lock it cannot take.  `git rerere gc` never\n     -+\twaits and skips its run while the lock is held.\n     -\n     - ## Documentation/git-rerere.adoc ##\n     -@@ Documentation/git-rerere.adoc: occurred a long time ago.  By default, unresolved conflicts older\n     - than 15 days and resolved conflicts older than 60\n     - days are pruned.  These defaults are controlled via the\n     - `gc.rerereUnresolved` and `gc.rerereResolved` configuration\n     --variables respectively.\n     -+variables respectively.  If another process holds the rerere lock,\n     -+for example a merge or rebase that is recording a conflict, `gc`\n     -+does nothing and says so.\n     - \n     - \n     - DISCUSSION\n     ++\tfor any other lock it cannot take.\n      \n       ## rerere.c ##\n      @@ rerere.c: static int rerere_enabled = -1;\n     @@ rerere.c: int setup_rerere(struct repository *r, struct string_list *merge_rr, i\n       \tif (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))\n       \t\trerere_autoupdate = !!(flags & RERERE_AUTOUPDATE);\n      -\tif (flags & RERERE_READONLY)\n     -+\tif ((flags & RERERE_READONLY) && (flags & RERERE_NOWAIT))\n     -+\t\tBUG(\"RERERE_READONLY takes no lock, so RERERE_NOWAIT does not apply\");\n      +\tif (flags & RERERE_READONLY) {\n       \t\tfd = 0;\n      -\telse\n     @@ rerere.c: int setup_rerere(struct repository *r, struct string_list *merge_rr, i\n      -\t\t\t\t\t\t    git_path_merge_rr(r),\n      -\t\t\t\t\t\t    LOCK_DIE_ON_ERROR);\n      +\t} else {\n     -+\t\tconst char *path = git_path_merge_rr(r);\n     -+\t\tint lock_flags = LOCK_DIE_ON_ERROR;\n     -+\t\tlong timeout_ms = rerere_lock_timeout_ms;\n     -+\n      +\t\t/*\n      +\t\t * Another process may hold the lock for a while, e.g.\n      +\t\t * \"git rerere gc\" while it prunes rr-cache, so wait for\n     -+\t\t * it instead of dying right away.  The gc itself never\n     -+\t\t * waits: skipping one of its runs costs nothing.\n     ++\t\t * it instead of dying right away.\n      +\t\t */\n     -+\t\tif (flags & RERERE_NOWAIT) {\n     -+\t\t\tlock_flags = 0;\n     -+\t\t\ttimeout_ms = 0;\n     -+\t\t}\n      +\t\tfd = repo_hold_lock_file_for_update_timeout(r, &write_lock,\n     -+\t\t\t\t\t\t\t    path, lock_flags,\n     -+\t\t\t\t\t\t\t    timeout_ms);\n     -+\t\tif (fd < 0) {\n     -+\t\t\twarning_errno(_(\"skipping rerere, \"\n     -+\t\t\t\t\t\"unable to create '%s.lock'\"), path);\n     -+\t\t\treturn -1;\n     -+\t\t}\n     ++\t\t\t\t\t\t\t    git_path_merge_rr(r),\n     ++\t\t\t\t\t\t\t    LOCK_DIE_ON_ERROR,\n     ++\t\t\t\t\t\t\t    rerere_lock_timeout_ms);\n      +\t}\n       \tread_rr(r, merge_rr);\n       \treturn fd;\n       }\n     -@@ rerere.c: void rerere_gc(struct repository *r, struct string_list *rr)\n     - \ttimestamp_t cutoff_resolve = now - 60 * 86400;\n     - \tstruct strbuf buf = STRBUF_INIT;\n     - \n     --\tif (setup_rerere(r, rr, 0) < 0)\n     -+\tif (setup_rerere(r, rr, RERERE_NOWAIT) < 0)\n     - \t\treturn;\n     - \n     - \trepo_config_get_expiry_in_days(the_repository, \"gc.rerereresolved\",\n     -\n     - ## rerere.h ##\n     -@@ rerere.h: struct repository;\n     - #define RERERE_AUTOUPDATE   01\n     - #define RERERE_NOAUTOUPDATE 02\n     - #define RERERE_READONLY     04\n     -+/* Never wait for MERGE_RR.lock, and skip the run when it is held */\n     -+#define RERERE_NOWAIT       010\n     - \n     - /*\n     -  * Marks paths that have been hand-resolved and added to the\n      \n       ## t/t4200-rerere.sh ##\n      @@ t/t4200-rerere.sh: test_expect_success 'old records rest in peace' '\n       \ttest_path_is_missing $rr2/preimage\n       '\n       \n     -+test_expect_success 'gc does nothing while MERGE_RR is locked' '\n     -+\tmkdir -p $rr2 &&\n     -+\techo Hello >$rr2/preimage &&\n     -+\ttest-tool chmtime =$just_over_15_days_ago $rr2/preimage &&\n     -+\n     -+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n     -+\t>.git/MERGE_RR.lock &&\n     -+\tgit rerere gc 2>err &&\n     -+\ttest_grep \"MERGE_RR.lock\" err &&\n     -+\ttest_path_is_file $rr2/preimage &&\n     -+\n     -+\trm .git/MERGE_RR.lock &&\n     -+\tgit rerere gc &&\n     -+\ttest_path_is_missing $rr2/preimage\n     -+'\n     -+\n      +test_expect_success 'a held lock is waited out within rerere.lockTimeout' '\n      +\tgit reset --hard &&\n      +\trm -rf $rr &&\n     @@ t/t4200-rerere.sh: test_expect_success 'old records rest in peace' '\n      +\ttest_path_is_missing $rr/preimage\n      +'\n      +\n     -+test_expect_success 'rerere, forget and clear fail on a lock they cannot take' '\n     ++test_expect_success 'rerere, forget, clear and gc fail on a lock they cannot take' '\n      +\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n      +\t>.git/MERGE_RR.lock &&\n      +\ttest_must_fail git -c rerere.lockTimeout=0 rerere 2>err &&\n     @@ t/t4200-rerere.sh: test_expect_success 'old records rest in peace' '\n      +\ttest_must_fail git -c rerere.lockTimeout=0 rerere forget a1 2>err &&\n      +\ttest_grep \"Unable to create\" err &&\n      +\ttest_must_fail git -c rerere.lockTimeout=0 rerere clear 2>err &&\n     ++\ttest_grep \"Unable to create\" err &&\n     ++\ttest_must_fail git -c rerere.lockTimeout=0 rerere gc 2>err &&\n      +\ttest_grep \"Unable to create\" err\n      +'\n      +\n     @@ t/t4200-rerere.sh: test_expect_success 'old records rest in peace' '\n       rerere_gc_custom_expiry_test () {\n       \tfive_days=\"$1\" right_now=\"$2\"\n       \ttest_expect_success \"rerere gc with custom expiry ($five_days, $right_now)\" '\n     -\n     - ## t/t7900-maintenance.sh ##\n     -@@ t/t7900-maintenance.sh: test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.aut\n     - \ttest_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 maintenance run --auto --task=rerere-gc\n     - '\n     - \n     -+test_expect_success 'rerere-gc task succeeds while MERGE_RR is locked' '\n     -+\ttest_when_finished \"rm -rf .git/rr-cache .git/MERGE_RR.lock\" &&\n     -+\tmkdir .git/rr-cache &&\n     -+\t: >.git/rr-cache/entry &&\n     -+\t>.git/MERGE_RR.lock &&\n     -+\ttest_expect_rerere_gc git maintenance run --task=rerere-gc\n     -+'\n     -+\n     - test_expect_success '--auto and --schedule incompatible' '\n     - \ttest_must_fail git maintenance run --auto --schedule=daily 2>err &&\n     - \ttest_grep \"cannot be used together\" err\n -:  ---------- > 2:  27673137aa rerere: add \"gc --auto\" that skips a held lock\n 2:  1cce403113 ! 3:  3984c7666b rerere: go on at a conflict when the lock stays busy\n     @@ Commit message\n          When a merge, rebase, cherry-pick, revert, am, stash or apply stops\n          at a conflict, it runs rerere right before it returns to the user.\n          If MERGE_RR.lock is still held when rerere.lockTimeout runs out, the\n     -    command dies there. For a rebase that is worse than a lost\n     -    recording: the sequencer has not yet written the state that\n     -    \"git rebase --continue\" needs, so the rebase cannot continue, and\n     -    following the \"git commit --amend\" advice folds the conflicted pick\n     -    into the previous commit.\n     +    command dies there. A rebase loses more than a recording that way.\n     +    The sequencer has not yet written the state that \"git rebase\n     +    --continue\" needs, so the rebase cannot go on, and the \"git commit\n     +    --amend\" it suggests instead folds the conflicted pick into the\n     +    previous commit.\n      \n     -    So print a warning and go on instead. The conflict is still in\n     -    place, and the warning tells the user to run \"git rerere\" before\n     -    resolving it. That records the preimage, or replays a known\n     -    resolution, just as the command would have done.\n     +    So warn and go on. The conflict is still in place, and the warning\n     +    tells the user to run \"git rerere\" before resolving it, which records\n     +    the preimage or replays a known resolution as the command would have.\n     +    The hint is under advice.mergeConflict like the other hints printed\n     +    at a conflict stop.\n      \n     -    All other callers still fail when the timeout runs out. \"git commit\"\n     -    and \"git am --continue\" record the resolution and then move on to\n     -    the next commit or patch. A warning would come too late there, and\n     -    the next rerere run would record whatever the file contains by then.\n     -    The \"rerere clear\" that am and rebase run for --skip and --abort\n     -    would leave the same stale entry behind. \"git rerere\", \"git rerere\n     -    forget\" and \"git rerere clear\" fail because the user asked for that\n     -    state explicitly.\n     +    Callers that run rerere after a resolution, like \"git commit\" and\n     +    \"git am --continue\", still fail when the wait is up. They move on\n     +    right away, and a leftover MERGE_RR entry would make the next rerere\n     +    run record whatever the file holds by then. The user's own rerere\n     +    commands and the \"rerere clear\" of --skip and --abort fail as well.\n      \n          Assisted-by: Claude Fable 5.1\n          Signed-off-by: Thomas Bachem <mail@thomasbachem.com>\n     @@ Documentation/config/rerere.adoc: rerere.lockTimeout::\n       \t`git rerere gc`.  Value 0 means not to wait at all; -1 means\n       \tto wait indefinitely.  Default is 1000 (i.e., wait for 1\n      -\tsecond).  When the time is up, the command fails as it does\n     --\tfor any other lock it cannot take.  `git rerere gc` never\n     --\twaits and skips its run while the lock is held.\n     +-\tfor any other lock it cannot take.  `git rerere gc --auto`\n     +-\tdoes not wait and does nothing while the lock is held.\n      +\tsecond).  When the time is up, a command that stops at a\n      +\tconflict, such as `git merge` or `git rebase`, prints a\n      +\twarning and goes on without rerere; run `git rerere` before\n      +\tresolving the conflict to record it after all.  Any other\n      +\tcommand fails, as it does for any other lock it cannot take.\n     -+\t`git rerere gc` never waits and skips its run while the lock\n     -+\tis held.\n     ++\t`git rerere gc --auto` does not wait and does nothing while\n     ++\tthe lock is held.\n      \n       ## apply.c ##\n      @@ apply.c: static int write_out_results(struct apply_state *state, struct patch *list)\n     @@ builtin/merge.c: static int suggest_conflicts(void)\n       \treturn 1;\n      \n       ## builtin/stash.c ##\n     -@@ builtin/stash.c: static int do_apply_stash(const char *prefix, struct stash_info *info,\n     +@@ builtin/stash.c: static enum stash_apply_result do_apply_stash(const char *prefix,\n       \t\tret = error(_(\"could not write index\"));\n       \n       \tif (ret) {\n     @@ rerere.c: int setup_rerere(struct repository *r, struct string_list *merge_rr, i\n       \tif (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))\n       \t\trerere_autoupdate = !!(flags & RERERE_AUTOUPDATE);\n      -\tif ((flags & RERERE_READONLY) && (flags & RERERE_NOWAIT))\n     --\t\tBUG(\"RERERE_READONLY takes no lock, so RERERE_NOWAIT does not apply\");\n     +-\t\tBUG(\"RERERE_NOWAIT does not apply with RERERE_READONLY\");\n      +\tif ((flags & RERERE_READONLY) &&\n      +\t    (flags & (RERERE_NOWAIT | RERERE_SKIP_LOCKED)))\n      +\t\tBUG(\"RERERE_READONLY takes no lock, so no lock flag applies\");\n       \tif (flags & RERERE_READONLY) {\n       \t\tfd = 0;\n       \t} else {\n     ++\t\tconst char *path = git_path_merge_rr(r);\n     + \t\tint lock_flags = LOCK_DIE_ON_ERROR;\n     + \t\tint timeout_ms = rerere_lock_timeout_ms;\n     + \n      @@ rerere.c: int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n     - \t\t * Another process may hold the lock for a while, e.g.\n       \t\t * \"git rerere gc\" while it prunes rr-cache, so wait for\n     - \t\t * it instead of dying right away.  The gc itself never\n     --\t\t * waits: skipping one of its runs costs nothing.\n     -+\t\t * waits: skipping one of its runs costs nothing.  A\n     -+\t\t * command that stops at a conflict must not die here\n     -+\t\t * either, so it warns and goes on without rerere.\n     + \t\t * it instead of dying right away.  The gc of an automatic\n     + \t\t * maintenance run does not wait, since skipping one of\n     +-\t\t * its runs costs nothing.\n     ++\t\t * its runs costs nothing.  A command that stops at a\n     ++\t\t * conflict must not die here either, so it warns and\n     ++\t\t * goes on without rerere.\n       \t\t */\n       \t\tif (flags & RERERE_NOWAIT) {\n       \t\t\tlock_flags = 0;\n     @@ rerere.c: int setup_rerere(struct repository *r, struct string_list *merge_rr, i\n      +\t\tif (flags & RERERE_SKIP_LOCKED)\n      +\t\t\tlock_flags = 0;\n       \t\tfd = repo_hold_lock_file_for_update_timeout(r, &write_lock,\n     - \t\t\t\t\t\t\t    path, lock_flags,\n     - \t\t\t\t\t\t\t    timeout_ms);\n     - \t\tif (fd < 0) {\n     - \t\t\twarning_errno(_(\"skipping rerere, \"\n     - \t\t\t\t\t\"unable to create '%s.lock'\"), path);\n     -+\t\t\tif (flags & RERERE_SKIP_LOCKED)\n     -+\t\t\t\tadvise(_(\"run \\\"git rerere\\\" before resolving \"\n     -+\t\t\t\t\t \"the conflict to record or replay \"\n     -+\t\t\t\t\t \"its resolution\"));\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\tif (fd < 0)\n     ++\t\t\t\t\t\t\t    path, lock_flags,\n     ++\t\t\t\t\t\t\t    timeout_ms);\n     ++\t\tif (fd < 0) {\n     ++\t\t\tif (flags & RERERE_SKIP_LOCKED) {\n     ++\t\t\t\twarning_errno(_(\"skipping rerere, \"\n     ++\t\t\t\t\t\t\"unable to create '%s.lock'\"),\n     ++\t\t\t\t\t      path);\n     ++\t\t\t\tadvise_if_enabled(ADVICE_MERGE_CONFLICT,\n     ++\t\t\t\t\t\t  _(\"run \\\"git rerere\\\" before \"\n     ++\t\t\t\t\t\t    \"resolving the conflict to \"\n     ++\t\t\t\t\t\t    \"record or replay its \"\n     ++\t\t\t\t\t\t    \"resolution\"));\n     ++\t\t\t}\n       \t\t\treturn -1;\n     - \t\t}\n     ++\t\t}\n       \t}\n     + \tread_rr(r, merge_rr);\n     + \treturn fd;\n      \n       ## rerere.h ##\n      @@ rerere.h: struct repository;\n       #define RERERE_READONLY     04\n     - /* Never wait for MERGE_RR.lock, and skip the run when it is held */\n     + /* Take MERGE_RR.lock only if it is free, and return quietly otherwise */\n       #define RERERE_NOWAIT       010\n      +/* Warn and go on without rerere if MERGE_RR.lock cannot be taken in time */\n      +#define RERERE_SKIP_LOCKED  020\n     @@ t/t4200-rerere.sh: test_expect_success 'a held lock is waited out within rerere.\n      +\ttest_grep \"^=======\\$\" a1\n      +'\n      +\n     - test_expect_success 'rerere, forget and clear fail on a lock they cannot take' '\n     + test_expect_success 'rerere, forget, clear and gc fail on a lock they cannot take' '\n       \ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n       \t>.git/MERGE_RR.lock &&\n\n-- \ngitgitgadget\n"},{"id":"553447","messageId":"27673137aae961105a3d3b6ea615879e0686cc65.1790596702.git.gitgitgadget@gmail.com","threadId":"66250","inReplyTo":"pull.2214.v5.git.1790596702.gitgitgadget@gmail.com","subject":"[PATCH v5 2/3] rerere: add \"gc --auto\" that skips a held lock","fromName":"Thomas Bachem via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-28T11:58:21Z","receivedAt":"2026-09-28T11:58:27Z","isPatch":true,"body":"From: Thomas Bachem <mail@thomasbachem.com>\n\nThe previous commit made \"git rerere gc\" wait for MERGE_RR.lock like\nevery other rerere command before it fails, as it always has. That\nsuits a user who runs it by hand and wants to know when nothing was\npruned. It does not suit the gc that auto maintenance starts after\na commit: nobody is waiting for that run, and the next commit starts\nanother one.\n\nSo add \"--auto\", with which \"git rerere gc\" quietly does nothing when\nthe lock is taken, and pass it from \"git maintenance run --auto\" and\n\"git gc --auto\", as they already do for \"git pack-refs --auto\". A run\nwithout it, from the command line or a maintenance schedule, keeps\nwaiting and failing.\n\nAssisted-by: Claude Fable 5.1\nSigned-off-by: Thomas Bachem <mail@thomasbachem.com>\n---\n Documentation/config/rerere.adoc |  3 ++-\n Documentation/git-rerere.adoc    |  9 ++++++---\n builtin/gc.c                     |  4 +++-\n builtin/rerere.c                 | 13 ++++++++++---\n rerere.c                         | 22 +++++++++++++++++-----\n rerere.h                         |  4 +++-\n t/t4200-rerere.sh                | 21 +++++++++++++++++++++\n t/t7900-maintenance.sh           | 25 ++++++++++++++++++++++++-\n 8 files changed, 86 insertions(+), 15 deletions(-)\n\ndiff --git a/Documentation/config/rerere.adoc b/Documentation/config/rerere.adoc\nindex 30e827f32b..a58c2ff684 100644\n--- a/Documentation/config/rerere.adoc\n+++ b/Documentation/config/rerere.adoc\n@@ -17,4 +17,5 @@ rerere.lockTimeout::\n \t`git rerere gc`.  Value 0 means not to wait at all; -1 means\n \tto wait indefinitely.  Default is 1000 (i.e., wait for 1\n \tsecond).  When the time is up, the command fails as it does\n-\tfor any other lock it cannot take.\n+\tfor any other lock it cannot take.  `git rerere gc --auto`\n+\tdoes not wait and does nothing while the lock is held.\ndiff --git a/Documentation/git-rerere.adoc b/Documentation/git-rerere.adoc\nindex 4e6ab9a27c..da7a1d093e 100644\n--- a/Documentation/git-rerere.adoc\n+++ b/Documentation/git-rerere.adoc\n@@ -8,7 +8,7 @@ git-rerere - Reuse recorded resolution of conflicted merges\n SYNOPSIS\n --------\n [verse]\n-'git rerere' [clear | forget <pathspec>... | diff | status | remaining | gc]\n+'git rerere' [clear | forget <pathspec>... | diff | status | remaining | gc [--auto]]\n \n DESCRIPTION\n -----------\n@@ -63,14 +63,17 @@ Print paths with conflicts that have not been autoresolved by rerere.\n This includes paths whose resolutions cannot be tracked by rerere,\n such as conflicting submodules.\n \n-'gc'::\n+'gc' [--auto]::\n \n Prune records of conflicted merges that\n occurred a long time ago.  By default, unresolved conflicts older\n than 15 days and resolved conflicts older than 60\n days are pruned.  These defaults are controlled via the\n `gc.rerereUnresolved` and `gc.rerereResolved` configuration\n-variables respectively.\n+variables respectively.  With `--auto`, which `git maintenance run\n+--auto` and `git gc --auto` pass, `gc` does nothing while another\n+process holds the rerere lock.  Without it, `gc` waits for the lock\n+as long as `rerere.lockTimeout` allows and then fails.\n \n \n DISCUSSION\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 57a3520263..5c33082b5b 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -385,12 +385,14 @@ out:\n \treturn should_prune;\n }\n \n-static int maintenance_task_rerere_gc(struct maintenance_run_opts *opts UNUSED,\n+static int maintenance_task_rerere_gc(struct maintenance_run_opts *opts,\n \t\t\t\t      struct gc_config *cfg UNUSED)\n {\n \tstruct child_process rerere_cmd = CHILD_PROCESS_INIT;\n \trerere_cmd.git_cmd = 1;\n \tstrvec_pushl(&rerere_cmd.args, \"rerere\", \"gc\", NULL);\n+\tif (opts->auto_flag)\n+\t\tstrvec_push(&rerere_cmd.args, \"--auto\");\n \treturn run_command(&rerere_cmd);\n }\n \ndiff --git a/builtin/rerere.c b/builtin/rerere.c\nindex a056cb791b..2a8871df41 100644\n--- a/builtin/rerere.c\n+++ b/builtin/rerere.c\n@@ -12,7 +12,8 @@\n #include \"pathspec.h\"\n \n static const char * const rerere_usage[] = {\n-\tN_(\"git rerere [clear | forget <pathspec>... | diff | status | remaining | gc]\"),\n+\tN_(\"git rerere [clear | forget <pathspec>... | diff | status | \"\n+\t   \"remaining | gc [--auto]]\"),\n \tNULL,\n };\n \n@@ -56,16 +57,21 @@ int cmd_rerere(int argc,\n \t       struct repository *repo UNUSED)\n {\n \tstruct string_list merge_rr = STRING_LIST_INIT_DUP;\n-\tint autoupdate = -1, flags = 0;\n+\tint autoupdate = -1, auto_flag = 0, flags = 0;\n \n \tstruct option options[] = {\n \t\tOPT_SET_INT(0, \"rerere-autoupdate\", &autoupdate,\n \t\t\tN_(\"register clean resolutions in index\"), 1),\n+\t\tOPT_BOOL(0, \"auto\", &auto_flag,\n+\t\t\t N_(\"skip gc while another process holds the lock\")),\n \t\tOPT_END(),\n \t};\n \n \targc = parse_options(argc, argv, prefix, options, rerere_usage, 0);\n \n+\tif (auto_flag && (argc < 1 || strcmp(argv[0], \"gc\")))\n+\t\tdie(_(\"the option '%s' requires '%s'\"), \"--auto\", \"gc\");\n+\n \trepo_config(the_repository, git_xmerge_config, NULL);\n \n \tif (autoupdate == 1)\n@@ -94,7 +100,8 @@ int cmd_rerere(int argc,\n \tif (!strcmp(argv[0], \"clear\")) {\n \t\trerere_clear(the_repository, &merge_rr);\n \t} else if (!strcmp(argv[0], \"gc\"))\n-\t\trerere_gc(the_repository, &merge_rr);\n+\t\trerere_gc(the_repository, &merge_rr,\n+\t\t\t  auto_flag ? 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)\ndiff --git a/rerere.c b/rerere.c\nindex 64fac07c71..43c8eb04db 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -887,18 +887,30 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n \n \tif (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))\n \t\trerere_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\t/*\n \t\t * Another process may hold the lock for a while, e.g.\n \t\t * \"git rerere gc\" while it prunes rr-cache, so wait for\n-\t\t * it instead of dying right away.\n+\t\t * it instead of dying right away.  The gc of an automatic\n+\t\t * maintenance run does not wait, since skipping one of\n+\t\t * its runs costs nothing.\n \t\t */\n+\t\tif (flags & RERERE_NOWAIT) {\n+\t\t\tlock_flags = 0;\n+\t\t\ttimeout_ms = 0;\n+\t\t}\n \t\tfd = repo_hold_lock_file_for_update_timeout(r, &write_lock,\n \t\t\t\t\t\t\t    git_path_merge_rr(r),\n-\t\t\t\t\t\t\t    LOCK_DIE_ON_ERROR,\n-\t\t\t\t\t\t\t    rerere_lock_timeout_ms);\n+\t\t\t\t\t\t\t    lock_flags, timeout_ms);\n+\t\tif (fd < 0)\n+\t\t\treturn -1;\n \t}\n \tread_rr(r, merge_rr);\n \treturn fd;\n@@ -1284,7 +1296,7 @@ out:\n \treturn needed;\n }\n \n-void rerere_gc(struct repository *r, struct string_list *rr)\n+void rerere_gc(struct repository *r, struct string_list *rr, int flags)\n {\n \tstruct string_list to_remove = STRING_LIST_INIT_DUP;\n \tDIR *dir;\n@@ -1294,7 +1306,7 @@ void rerere_gc(struct repository *r, struct string_list *rr)\n \ttimestamp_t cutoff_resolve;\n \tstruct strbuf buf = STRBUF_INIT;\n \n-\tif (setup_rerere(r, rr, 0) < 0)\n+\tif (setup_rerere(r, rr, flags) < 0)\n \t\treturn;\n \n \trerere_gc_cutoffs(r, &cutoff_resolve, &cutoff_noresolve);\ndiff --git a/rerere.h b/rerere.h\nindex feeb0e2c9f..d54c53d0d4 100644\n--- a/rerere.h\n+++ b/rerere.h\n@@ -10,6 +10,8 @@ struct repository;\n #define RERERE_AUTOUPDATE   01\n #define RERERE_NOAUTOUPDATE 02\n #define RERERE_READONLY     04\n+/* Take MERGE_RR.lock only if it is free, and return quietly otherwise */\n+#define RERERE_NOWAIT       010\n \n /*\n  * Marks paths that have been hand-resolved and added to the\n@@ -37,7 +39,7 @@ const char *rerere_path(struct strbuf *buf, const struct rerere_id *,\n int rerere_forget(struct repository *, struct pathspec *);\n int rerere_remaining(struct repository *, struct string_list *);\n void rerere_clear(struct repository *, struct string_list *);\n-void rerere_gc(struct repository *, struct string_list *);\n+void rerere_gc(struct repository *, struct string_list *, int);\n \n /*\n  * Check whether garbage collection for rerere entries is needed, which is\ndiff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh\nindex 7bd92235dc..9435b0ed58 100755\n--- a/t/t4200-rerere.sh\n+++ b/t/t4200-rerere.sh\n@@ -242,6 +242,27 @@ test_expect_success 'old records rest in peace' '\n \ttest_path_is_missing $rr2/preimage\n '\n \n+test_expect_success 'gc --auto does nothing while MERGE_RR is locked' '\n+\tmkdir -p $rr2 &&\n+\techo Hello >$rr2/preimage &&\n+\ttest-tool chmtime =$just_over_15_days_ago $rr2/preimage &&\n+\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\tgit rerere gc --auto 2>err &&\n+\ttest_must_be_empty err &&\n+\ttest_path_is_file $rr2/preimage &&\n+\n+\trm .git/MERGE_RR.lock &&\n+\tgit rerere gc --auto &&\n+\ttest_path_is_missing $rr2/preimage\n+'\n+\n+test_expect_success '--auto is only accepted by gc' '\n+\ttest_must_fail git rerere --auto clear 2>err &&\n+\ttest_grep \"option .--auto. requires .gc.\" err\n+'\n+\n test_expect_success 'a held lock is waited out within rerere.lockTimeout' '\n \tgit reset --hard &&\n \trm -rf $rr &&\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 4f65fa9439..8842340236 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -1007,9 +1007,17 @@ test_expect_rerere_gc () {\n \t\tshift\n \tfi\n \n+\t# an automatic run passes --auto on to \"git rerere gc\"\n+\tauto=\n+\tcase \" $* \" in\n+\t*\" --auto \"*)\n+\t\tauto=--auto\n+\t\t;;\n+\tesac\n+\n \trm -f \"rerere-gc.txt\" &&\n \tGIT_TRACE2_EVENT=\"$(pwd)/rerere-gc.txt\" \"$@\" &&\n-\ttest_subcommand $negate git rerere gc <rerere-gc.txt\n+\ttest_subcommand $negate git rerere gc $auto <rerere-gc.txt\n }\n \n test_expect_success 'rerere-gc task without --auto always collects garbage' '\n@@ -1084,6 +1092,21 @@ test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.aut\n \ttest_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 maintenance run --auto --task=rerere-gc\n '\n \n+test_expect_success 'rerere-gc task with --auto succeeds while MERGE_RR is locked' '\n+\ttest_when_finished \"rm -rf .git/rr-cache .git/MERGE_RR.lock\" &&\n+\tmkdir .git/rr-cache &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_expect_rerere_gc git -c maintenance.rerere-gc.auto=-1 maintenance run --auto --task=rerere-gc\n+'\n+\n+test_expect_success 'rerere-gc task without --auto fails while MERGE_RR is locked' '\n+\ttest_when_finished \"rm -rf .git/rr-cache .git/MERGE_RR.lock\" &&\n+\tmkdir .git/rr-cache &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 maintenance run --task=rerere-gc 2>err &&\n+\ttest_grep \"Unable to create\" err\n+'\n+\n test_expect_success '--auto and --schedule incompatible' '\n \ttest_must_fail git maintenance run --auto --schedule=daily 2>err &&\n \ttest_grep \"cannot be used together\" err\n-- \ngitgitgadget\n\n"},{"id":"553448","messageId":"3984c7666bc3ee1e7670f4e092f1d59c0935c2bc.1790596702.git.gitgitgadget@gmail.com","threadId":"66250","inReplyTo":"pull.2214.v5.git.1790596702.gitgitgadget@gmail.com","subject":"[PATCH v5 3/3] rerere: go on at a conflict when the lock stays busy","fromName":"Thomas Bachem via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-28T11:58:22Z","receivedAt":"2026-09-28T11:58:29Z","isPatch":true,"body":"From: Thomas Bachem <mail@thomasbachem.com>\n\nWhen a merge, rebase, cherry-pick, revert, am, stash or apply stops\nat a conflict, it runs rerere right before it returns to the user.\nIf MERGE_RR.lock is still held when rerere.lockTimeout runs out, the\ncommand dies there. A rebase loses more than a recording that way.\nThe sequencer has not yet written the state that \"git rebase\n--continue\" needs, so the rebase cannot go on, and the \"git commit\n--amend\" it suggests instead folds the conflicted pick into the\nprevious commit.\n\nSo warn and go on. The conflict is still in place, and the warning\ntells the user to run \"git rerere\" before resolving it, which records\nthe preimage or replays a known resolution as the command would have.\nThe hint is under advice.mergeConflict like the other hints printed\nat a conflict stop.\n\nCallers that run rerere after a resolution, like \"git commit\" and\n\"git am --continue\", still fail when the wait is up. They move on\nright away, and a leftover MERGE_RR entry would make the next rerere\nrun record whatever the file holds by then. The user's own rerere\ncommands and the \"rerere clear\" of --skip and --abort fail as well.\n\nAssisted-by: Claude Fable 5.1\nSigned-off-by: Thomas Bachem <mail@thomasbachem.com>\n---\n Documentation/config/rerere.adoc |  10 ++-\n apply.c                          |   2 +-\n builtin/am.c                     |   3 +-\n builtin/merge.c                  |   2 +-\n builtin/stash.c                  |   2 +-\n rerere.c                         |  30 +++++++--\n rerere.h                         |   2 +\n sequencer.c                      |   4 +-\n t/t4200-rerere.sh                | 105 ++++++++++++++++++++++++++++++-\n 9 files changed, 143 insertions(+), 17 deletions(-)\n\ndiff --git a/Documentation/config/rerere.adoc b/Documentation/config/rerere.adoc\nindex a58c2ff684..d9ea56f5b2 100644\n--- a/Documentation/config/rerere.adoc\n+++ b/Documentation/config/rerere.adoc\n@@ -16,6 +16,10 @@ rerere.lockTimeout::\n \tlock when another process holds it, typically a background\n \t`git rerere gc`.  Value 0 means not to wait at all; -1 means\n \tto wait indefinitely.  Default is 1000 (i.e., wait for 1\n-\tsecond).  When the time is up, the command fails as it does\n-\tfor any other lock it cannot take.  `git rerere gc --auto`\n-\tdoes not wait and does nothing while the lock is held.\n+\tsecond).  When the time is up, a command that stops at a\n+\tconflict, such as `git merge` or `git rebase`, prints a\n+\twarning and goes on without rerere; run `git rerere` before\n+\tresolving the conflict to record it after all.  Any other\n+\tcommand fails, as it does for any other lock it cannot take.\n+\t`git rerere gc --auto` does not wait and does nothing while\n+\tthe lock is held.\ndiff --git a/apply.c b/apply.c\nindex f00b7ba4d3..3b8502535b 100644\n--- a/apply.c\n+++ b/apply.c\n@@ -4865,7 +4865,7 @@ static int write_out_results(struct apply_state *state, struct patch *list)\n \t\t * tree with conflict markers, but that isn't written with --cached.\n \t\t */\n \t\tif (!state->cached)\n-\t\t\trepo_rerere(state->repo, 0);\n+\t\t\trepo_rerere(state->repo, RERERE_SKIP_LOCKED);\n \t}\n \n \treturn errs;\ndiff --git a/builtin/am.c b/builtin/am.c\nindex e9623b8307..bcb93db515 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -1649,7 +1649,8 @@ static int fall_back_threeway(const struct am_state *state, const char *index_pa\n \t\to.verbosity = 0;\n \n \tif (merge_ort_generic(&o, &our_tree, &their_tree, 1, bases, &result)) {\n-\t\trepo_rerere(the_repository, state->allow_rerere_autoupdate);\n+\t\trepo_rerere(the_repository, state->allow_rerere_autoupdate |\n+\t\t\t    RERERE_SKIP_LOCKED);\n \t\tfree(their_tree_name);\n \t\treturn error(_(\"Failed to merge in the changes.\"));\n \t}\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 5b4eb23a83..1ed5959bfd 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -1061,7 +1061,7 @@ static int suggest_conflicts(void)\n \tfputs(msgbuf.buf, fp);\n \tstrbuf_release(&msgbuf);\n \tfclose(fp);\n-\trepo_rerere(the_repository, allow_rerere_auto);\n+\trepo_rerere(the_repository, allow_rerere_auto | RERERE_SKIP_LOCKED);\n \tprintf(_(\"Automatic merge failed; \"\n \t\t\t\"fix conflicts and then commit the result.\\n\"));\n \treturn 1;\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex 7a9843413b..3f9fd20569 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -732,7 +732,7 @@ static enum stash_apply_result do_apply_stash(const char *prefix,\n \t\tret = error(_(\"could not write index\"));\n \n \tif (ret) {\n-\t\trepo_rerere(the_repository, 0);\n+\t\trepo_rerere(the_repository, RERERE_SKIP_LOCKED);\n \n \t\tif (index)\n \t\t\tfprintf_ln(stderr, _(\"Index was not unstashed.\"));\ndiff --git a/rerere.c b/rerere.c\nindex 43c8eb04db..5a036fe456 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -3,6 +3,7 @@\n \n #include \"git-compat-util.h\"\n #include \"abspath.h\"\n+#include \"advice.h\"\n #include \"config.h\"\n #include \"copy.h\"\n #include \"environment.h\"\n@@ -887,11 +888,13 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n \n \tif (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))\n \t\trerere_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    (flags & (RERERE_NOWAIT | RERERE_SKIP_LOCKED)))\n+\t\tBUG(\"RERERE_READONLY takes no lock, so no lock flag applies\");\n \tif (flags & RERERE_READONLY) {\n \t\tfd = 0;\n \t} else {\n+\t\tconst char *path = git_path_merge_rr(r);\n \t\tint lock_flags = LOCK_DIE_ON_ERROR;\n \t\tint timeout_ms = rerere_lock_timeout_ms;\n \n@@ -900,17 +903,32 @@ 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 wait for\n \t\t * it instead of dying right away.  The gc of an automatic\n \t\t * maintenance run does not wait, since skipping one of\n-\t\t * its runs costs nothing.\n+\t\t * its runs costs nothing.  A command that stops at a\n+\t\t * conflict must not die here either, so it warns and\n+\t\t * goes on without rerere.\n \t\t */\n \t\tif (flags & RERERE_NOWAIT) {\n \t\t\tlock_flags = 0;\n \t\t\ttimeout_ms = 0;\n \t\t}\n+\t\tif (flags & RERERE_SKIP_LOCKED)\n+\t\t\tlock_flags = 0;\n \t\tfd = repo_hold_lock_file_for_update_timeout(r, &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\tif (fd < 0)\n+\t\t\t\t\t\t\t    path, lock_flags,\n+\t\t\t\t\t\t\t    timeout_ms);\n+\t\tif (fd < 0) {\n+\t\t\tif (flags & RERERE_SKIP_LOCKED) {\n+\t\t\t\twarning_errno(_(\"skipping rerere, \"\n+\t\t\t\t\t\t\"unable to create '%s.lock'\"),\n+\t\t\t\t\t      path);\n+\t\t\t\tadvise_if_enabled(ADVICE_MERGE_CONFLICT,\n+\t\t\t\t\t\t  _(\"run \\\"git rerere\\\" before \"\n+\t\t\t\t\t\t    \"resolving the conflict to \"\n+\t\t\t\t\t\t    \"record or replay its \"\n+\t\t\t\t\t\t    \"resolution\"));\n+\t\t\t}\n \t\t\treturn -1;\n+\t\t}\n \t}\n \tread_rr(r, merge_rr);\n \treturn fd;\ndiff --git a/rerere.h b/rerere.h\nindex d54c53d0d4..ed8a878f8f 100644\n--- a/rerere.h\n+++ b/rerere.h\n@@ -12,6 +12,8 @@ struct repository;\n #define RERERE_READONLY     04\n /* Take MERGE_RR.lock only if it is free, and return quietly otherwise */\n #define RERERE_NOWAIT       010\n+/* Warn and go on without rerere if MERGE_RR.lock cannot be taken in time */\n+#define RERERE_SKIP_LOCKED  020\n \n /*\n  * Marks paths that have been hand-resolved and added to the\ndiff --git a/sequencer.c b/sequencer.c\nindex 6dae43e4db..dabba729c7 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -2520,7 +2520,7 @@ static enum pick_result do_pick_commit(struct repository *r,\n \t\t      : _(\"could not apply %s... %s\"),\n \t\t      short_commit_name(r, commit), msg.subject);\n \t\tprint_advice(r, res == 1, opts);\n-\t\trepo_rerere(r, opts->allow_rerere_auto);\n+\t\trepo_rerere(r, opts->allow_rerere_auto | RERERE_SKIP_LOCKED);\n \t\tgoto leave;\n \t}\n \n@@ -4449,7 +4449,7 @@ static int do_merge(struct repository *r,\n \n \trollback_lock_file(&lock);\n \tif (ret)\n-\t\trepo_rerere(r, opts->allow_rerere_auto);\n+\t\trepo_rerere(r, opts->allow_rerere_auto | RERERE_SKIP_LOCKED);\n \telse\n \t\t/*\n \t\t * In case of problems, we now want to return a positive\ndiff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh\nindex 9435b0ed58..b5209d8c04 100755\n--- a/t/t4200-rerere.sh\n+++ b/t/t4200-rerere.sh\n@@ -277,17 +277,118 @@ test_expect_success 'a held lock is waited out within rerere.lockTimeout' '\n \ttest_grep \"^=======\\$\" $rr/preimage\n '\n \n-test_expect_success 'merge fails once rerere.lockTimeout is up' '\n+test_expect_success 'merge goes on without rerere once rerere.lockTimeout is up' '\n \tgit reset --hard &&\n \trm -rf $rr &&\n \ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n \t>.git/MERGE_RR.lock &&\n \ttest_must_fail git -c rerere.lockTimeout=0 merge first 2>err &&\n-\ttest_grep \"Unable to create\" err &&\n+\ttest_grep \"skipping rerere\" err &&\n+\ttest_grep \"hint: .*git rerere\" err &&\n \ttest_grep \"^=======\\$\" a1 &&\n \ttest_path_is_missing $rr/preimage\n '\n \n+test_expect_success 'rerere run at the stop records what was skipped' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\tgit checkout -b lock-held-catch-up third &&\n+\ttest_when_finished \"git checkout third && git branch -D lock-held-catch-up\" &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 merge first &&\n+\ttest_path_is_missing $rr/preimage &&\n+\trm .git/MERGE_RR.lock &&\n+\tgit rerere &&\n+\ttest_grep \"^=======\\$\" $rr/preimage &&\n+\techo resolved >a1 &&\n+\tgit add a1 &&\n+\tgit commit -qm resolved &&\n+\ttest_path_is_file $rr/postimage\n+'\n+\n+test_expect_success 'rebase goes on without rerere once rerere.lockTimeout is up' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\tgit checkout -b lock-held third &&\n+\ttest_when_finished \"git checkout third && git branch -D lock-held\" &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rebase first 2>err &&\n+\ttest_grep \"skipping rerere\" err &&\n+\ttest_path_is_file .git/rebase-merge/stopped-sha &&\n+\trm .git/MERGE_RR.lock &&\n+\techo resolved >a1 &&\n+\tgit add a1 &&\n+\tgit rebase --continue &&\n+\ttest_path_is_missing .git/rebase-merge &&\n+\ttest_path_is_missing $rr/preimage\n+'\n+\n+test_expect_success 'commit fails on a lock it cannot take' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\tgit checkout -b lock-held-commit third &&\n+\ttest_when_finished \"git checkout third && git branch -D lock-held-commit\" &&\n+\ttest_must_fail git merge first &&\n+\ttest_path_is_file $rr/preimage &&\n+\techo resolved >a1 &&\n+\tgit add a1 &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 commit -qm resolved 2>err &&\n+\ttest_grep \"Unable to create\" err &&\n+\ttest_path_is_missing $rr/postimage\n+'\n+\n+test_expect_success 'am goes on without rerere once rerere.lockTimeout is up' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\tgit checkout -b lock-held-am third &&\n+\ttest_when_finished \"test_might_fail git am --abort &&\n+\t\tgit checkout third && git branch -D lock-held-am\" &&\n+\tgit format-patch -1 --stdout first >first.patch &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 am --3way first.patch 2>err &&\n+\ttest_grep \"skipping rerere\" err &&\n+\ttest_path_is_dir .git/rebase-apply &&\n+\trm .git/MERGE_RR.lock &&\n+\tgit am --abort &&\n+\ttest_path_is_missing .git/rebase-apply\n+'\n+\n+test_expect_success 'stash pop goes on without rerere once rerere.lockTimeout is up' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\tgit checkout -b lock-held-stash third &&\n+\ttest_when_finished \"git reset --hard && git stash drop &&\n+\t\tgit checkout third && git branch -D lock-held-stash\" &&\n+\techo stashed >>a1 &&\n+\tgit stash &&\n+\techo committed >>a1 &&\n+\tgit commit -qam committed &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 stash pop 2>err &&\n+\ttest_grep \"skipping rerere\" err &&\n+\ttest_grep \"^=======\\$\" a1\n+'\n+\n+test_expect_success 'apply --3way goes on without rerere once rerere.lockTimeout is up' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\tgit checkout -b lock-held-apply third &&\n+\ttest_when_finished \"git reset --hard &&\n+\t\tgit checkout third && git branch -D lock-held-apply\" &&\n+\tgit format-patch -1 --stdout first >first.patch &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 apply --3way first.patch 2>err &&\n+\ttest_grep \"skipping rerere\" err &&\n+\ttest_grep \"^=======\\$\" a1\n+'\n+\n test_expect_success 'rerere, forget, clear and gc fail on a lock they cannot take' '\n \ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n \t>.git/MERGE_RR.lock &&\n-- \ngitgitgadget\n"},{"id":"553716","messageId":"ar0kJPdY1WSsWvP8@pks.im","threadId":"66250","inReplyTo":"27673137aae961105a3d3b6ea615879e0686cc65.1790596702.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v5 2/3] rerere: add \"gc --auto\" that skips a held lock","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-09-30T15:00:52Z","receivedAt":"2026-09-30T15:01:03Z","isPatch":true,"body":"On Mon, Sep 28, 2026 at 11:58:21AM +0000, Thomas Bachem via GitGitGadget wrote:\n> diff --git a/Documentation/git-rerere.adoc b/Documentation/git-rerere.adoc\n> index 4e6ab9a27c..da7a1d093e 100644\n> --- a/Documentation/git-rerere.adoc\n> +++ b/Documentation/git-rerere.adoc\n> @@ -63,14 +63,17 @@ Print paths with conflicts that have not been autoresolved by rerere.\n>  This includes paths whose resolutions cannot be tracked by rerere,\n>  such as conflicting submodules.\n>  \n> -'gc'::\n> +'gc' [--auto]::\n>  \n>  Prune records of conflicted merges that\n>  occurred a long time ago.  By default, unresolved conflicts older\n>  than 15 days and resolved conflicts older than 60\n>  days are pruned.  These defaults are controlled via the\n>  `gc.rerereUnresolved` and `gc.rerereResolved` configuration\n> -variables respectively.\n> +variables respectively.  With `--auto`, which `git maintenance run\n> +--auto` and `git gc --auto` pass, `gc` does nothing while another\n> +process holds the rerere lock.  Without it, `gc` waits for the lock\n> +as long as `rerere.lockTimeout` allows and then fails.\n\nIt's a bit weird to have git-rerere(1) document who calls it. We may\nwant to document why specifically this is useful though.\n\n> diff --git a/builtin/rerere.c b/builtin/rerere.c\n> index a056cb791b..2a8871df41 100644\n> --- a/builtin/rerere.c\n> +++ b/builtin/rerere.c\n> @@ -56,16 +57,21 @@ int cmd_rerere(int argc,\n>  \t       struct repository *repo UNUSED)\n>  {\n>  \tstruct string_list merge_rr = STRING_LIST_INIT_DUP;\n> -\tint autoupdate = -1, flags = 0;\n> +\tint autoupdate = -1, auto_flag = 0, flags = 0;\n>  \n>  \tstruct option options[] = {\n>  \t\tOPT_SET_INT(0, \"rerere-autoupdate\", &autoupdate,\n>  \t\t\tN_(\"register clean resolutions in index\"), 1),\n> +\t\tOPT_BOOL(0, \"auto\", &auto_flag,\n> +\t\t\t N_(\"skip gc while another process holds the lock\")),\n>  \t\tOPT_END(),\n>  \t};\n\nThinking about this a bit... I know it was my suggestion, but I wonder\nwhether \"auto\" is misnamed. We don't let any heuristics kick in like we\ntypically do for other commands like `git pack-refs --auto`, we only\nknow to skip garbage collection if the lock is taken. So there is a bit\nof a mismatch here.\n\nHow about we instead call this \"--skip-locked\"? We could even mark it as\na hidden option and not even document it, as it feels very specific to\nhow git-maintenance(1) wants to invoke it. If so, we could maybe remove\nit again at a later point.\n\nAn alternative could be to instead call `rerere_gc()` directly, and if\nso we wouldn't have to add this flag at all. But that may result in some\nbigger changes, so I'll leave it up to you to decide.\n\n>  \targc = parse_options(argc, argv, prefix, options, rerere_usage, 0);\n>  \n> +\tif (auto_flag && (argc < 1 || strcmp(argv[0], \"gc\")))\n> +\t\tdie(_(\"the option '%s' requires '%s'\"), \"--auto\", \"gc\");\n> +\n>  \trepo_config(the_repository, git_xmerge_config, NULL);\n>  \n>  \tif (autoupdate == 1)\n\nOh dear, this is a mess. The file could really use a refactoring to use\nproper subcommands.\n\nBut anyway, that's certainly outside the scope of this patch series.\n\nPatrick\n"},{"id":"553816","messageId":"CAA0xjtoj_uf-f+kzjRpmOkq1RsbGnkXdodeSS2ND0R-FsP4qRg@mail.gmail.com","threadId":"66250","inReplyTo":"ar0kJPdY1WSsWvP8@pks.im","subject":"Re: [PATCH v5 2/3] rerere: add \"gc --auto\" that skips a held lock","fromName":"Thomas Bachem","fromEmail":"mail@thomasbachem.com","sentAt":"2026-10-01T08:08:16Z","receivedAt":"2026-10-01T08:08:27Z","isPatch":true,"body":"Hi Patrick,\n\nOn 30/09/2026 17:00, Patrick Steinhardt wrote:\n> It's a bit weird to have git-rerere(1) document who calls it. We may\n> want to document why specifically this is useful though.\n\nI'll take that out of git-rerere(1) again. I'd keep the last sentence\nof the rerere.lockTimeout entry, since that is where I say what each\ncommand does when the time is up, but name the two commands there\ninstead of the option:\n\n\"A `git rerere gc` run by `git maintenance run --auto` or\n`git gc --auto` does not wait and does nothing while the lock is held.\"\n\n> How about we instead call this \"--skip-locked\"? We could even mark it as\n> a hidden option and not even document it, as it feels very specific to\n> how git-maintenance(1) wants to invoke it. If so, we could maybe remove\n> it again at a later point.\n\nI'll take both, the name and hiding it.\n\nPatch 3 has a RERERE_SKIP_LOCKED flag for the conflict-time callers.\nI'll rename that one to RERERE_WARN_LOCKED so it doesn't look like the\noption's flag, which stays RERERE_NOWAIT.\n\n> An alternative could be to instead call `rerere_gc()` directly, and if\n> so we wouldn't have to add this flag at all. But that may result in some\n> bigger changes, so I'll leave it up to you to decide.\n\nI tried it. It is six lines in builtin/gc.c, but rerere_gc() dies when\nit can't take the lock. A manual or scheduled \"git maintenance run\"\nthen dies with the lockfile's message and exit code 128, where it now\nreports \"task 'rerere-gc' failed\" and exits with 1. The rerere-gc\ntests in t7900 fail too, since their helper looks for the\n\"git rerere gc\" child. So I'd keep the option for this series. Say if\nyou'd rather have the direct call.\n\nI'll wait a day or two for other comments before I send v6.\n\nThanks,\nThomas\n"},{"id":"553830","messageId":"ar5Bx5btU1AiONqA@pks.im","threadId":"66250","inReplyTo":"CAA0xjtoj_uf-f+kzjRpmOkq1RsbGnkXdodeSS2ND0R-FsP4qRg@mail.gmail.com","subject":"Re: [PATCH v5 2/3] rerere: add \"gc --auto\" that skips a held lock","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-01T11:19:35Z","receivedAt":"2026-10-01T11:19:46Z","isPatch":true,"body":"On Thu, Oct 01, 2026 at 10:08:16AM +0200, Thomas Bachem wrote:\n> Hi Patrick,\n> \n> On 30/09/2026 17:00, Patrick Steinhardt wrote:\n> > It's a bit weird to have git-rerere(1) document who calls it. We may\n> > want to document why specifically this is useful though.\n> \n> I'll take that out of git-rerere(1) again. I'd keep the last sentence\n> of the rerere.lockTimeout entry, since that is where I say what each\n> command does when the time is up, but name the two commands there\n> instead of the option:\n> \n> \"A `git rerere gc` run by `git maintenance run --auto` or\n> `git gc --auto` does not wait and does nothing while the lock is held.\"\n> \n> > How about we instead call this \"--skip-locked\"? We could even mark it as\n> > a hidden option and not even document it, as it feels very specific to\n> > how git-maintenance(1) wants to invoke it. If so, we could maybe remove\n> > it again at a later point.\n> \n> I'll take both, the name and hiding it.\n> \n> Patch 3 has a RERERE_SKIP_LOCKED flag for the conflict-time callers.\n> I'll rename that one to RERERE_WARN_LOCKED so it doesn't look like the\n> option's flag, which stays RERERE_NOWAIT.\n> \n> > An alternative could be to instead call `rerere_gc()` directly, and if\n> > so we wouldn't have to add this flag at all. But that may result in some\n> > bigger changes, so I'll leave it up to you to decide.\n> \n> I tried it. It is six lines in builtin/gc.c, but rerere_gc() dies when\n> it can't take the lock. A manual or scheduled \"git maintenance run\"\n> then dies with the lockfile's message and exit code 128, where it now\n> reports \"task 'rerere-gc' failed\" and exits with 1. The rerere-gc\n> tests in t7900 fail too, since their helper looks for the\n> \"git rerere gc\" child. So I'd keep the option for this series. Say if\n> you'd rather have the direct call.\n\nAh, right, that makes sense. Let's keep the hidden option in that case.\nThanks!\n\nPatrick\n"},{"id":"553953","messageId":"pull.2214.v6.git.1790939492.gitgitgadget@gmail.com","threadId":"66250","inReplyTo":"pull.2214.git.1788337897490.gitgitgadget@gmail.com","subject":"[PATCH v6 0/3] rerere: wait for MERGE_RR.lock, and go on at a conflict","fromName":"Thomas Bachem via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-02T11:11:29Z","receivedAt":"2026-10-02T11:11:35Z","isPatch":true,"body":"A rebase dies at a conflict when a background \"git rerere gc\" holds\nMERGE_RR.lock at that moment. With the first patch, rerere waits for the\nlock instead of dying at once. With the second, the \"git rerere gc\" started\nby auto maintenance does nothing while the lock is held. With the third, a\ncommand that stops at a conflict goes on without rerere if the lock is still\nheld after the wait.\n\nFor v6 I took Patrick's suggestions for the second patch. Changes since v5:\n\n * Patch 2's option is \"--skip-locked\" instead of \"--auto\", since there is\n   no heuristic behind it (Patrick).\n\n * The option is hidden and undocumented, like the \"--skip-foreground-tasks\"\n   that maintenance passes to \"git gc\", and I no longer touch git-rerere(1)\n   (Patrick). I kept the last sentence of the rerere.lockTimeout entry and\n   named \"git maintenance run --auto\" and \"git gc --auto\" in it instead of\n   the option.\n\n * I renamed patch 3's flag from RERERE_SKIP_LOCKED to RERERE_WARN_LOCKED,\n   so that it does not look like the flag of \"--skip-locked\", which is still\n   RERERE_NOWAIT.\n\n * I added a test to patch 3 for the merge that \"git rebase -r\" re-creates,\n   the only call site without one.\n\n * I corrected and reworded the log messages of patches 2 and 3. Both\n   implied that every rerere command takes the lock, but status, diff and\n   remaining do not. Patch 2's also said that nobody waits for the gc of\n   auto maintenance, but where maintenance does not detach, the command that\n   starts it does. From patch 3's I dropped my explanation of why \"git\n   commit\" and \"git am --continue\" still fail, which does not hold for \"git\n   commit\": it has made its commit by the time it runs rerere. The message\n   now gives the plain reason, that the rebase or am can still be continued\n   when they fail.\n\nI kept the option rather than have maintenance call rerere_gc() directly.\nrerere_gc() dies when it cannot take the lock, so a manual or scheduled \"git\nmaintenance run\" would die where it now reports the failed task (Patrick\nagreed).\n\nThomas Bachem (3):\n  rerere: wait for MERGE_RR.lock before giving up\n  rerere: add \"gc --skip-locked\" for auto maintenance\n  rerere: go on at a conflict when the lock stays busy\n\n Documentation/config/rerere.adoc |  14 +++\n apply.c                          |   2 +-\n builtin/am.c                     |   3 +-\n builtin/gc.c                     |   4 +-\n builtin/merge.c                  |   2 +-\n builtin/rerere.c                 |  10 +-\n builtin/stash.c                  |   2 +-\n rerere.c                         |  56 +++++++--\n rerere.h                         |   6 +-\n sequencer.c                      |   4 +-\n t/t4200-rerere.sh                | 190 +++++++++++++++++++++++++++++++\n t/t7900-maintenance.sh           |  25 +++-\n 12 files changed, 300 insertions(+), 18 deletions(-)\n\n\nbase-commit: 34f06850c16c7f7ac822b1adc71354f11b0f2ca3\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2214%2Fthomasbachem%2Frerere-gc-lock-v6\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2214/thomasbachem/rerere-gc-lock-v6\nPull-Request: https://github.com/gitgitgadget/git/pull/2214\n\nRange-diff vs v5:\n\n 1:  3dc3d02f12 = 1:  3dc3d02f12 rerere: wait for MERGE_RR.lock before giving up\n 2:  27673137aa ! 2:  2ef141410a rerere: add \"gc --auto\" that skips a held lock\n     @@ Metadata\n      Author: Thomas Bachem <mail@thomasbachem.com>\n      \n       ## Commit message ##\n     -    rerere: add \"gc --auto\" that skips a held lock\n     +    rerere: add \"gc --skip-locked\" for auto maintenance\n      \n     -    The previous commit made \"git rerere gc\" wait for MERGE_RR.lock like\n     -    every other rerere command before it fails, as it always has. That\n     -    suits a user who runs it by hand and wants to know when nothing was\n     -    pruned. It does not suit the gc that auto maintenance starts after\n     -    a commit: nobody is waiting for that run, and the next commit starts\n     -    another one.\n     +    Since the previous commit, \"git rerere gc\" waits for MERGE_RR.lock\n     +    like every other command that takes it, and fails only if the wait\n     +    times out. That suits a user who runs it by hand and wants to know\n     +    when nothing was pruned. But the user did not ask for the gc that auto\n     +    maintenance starts after a commit, and the next commit starts another\n     +    one.\n      \n     -    So add \"--auto\", with which \"git rerere gc\" quietly does nothing when\n     -    the lock is taken, and pass it from \"git maintenance run --auto\" and\n     -    \"git gc --auto\", as they already do for \"git pack-refs --auto\". A run\n     -    without it, from the command line or a maintenance schedule, keeps\n     -    waiting and failing.\n     +    So add \"--skip-locked\", with which \"git rerere gc\" quietly does\n     +    nothing while the lock is held, and pass it from\n     +    \"git maintenance run --auto\" and \"git gc --auto\". Only these two need\n     +    the option, so hide it and leave it undocumented, like the\n     +    \"--skip-foreground-tasks\" that \"git maintenance run\" passes to\n     +    \"git gc\". A run without it, from the command line or a maintenance\n     +    schedule, still waits for the lock and fails if the wait times out.\n      \n          Assisted-by: Claude Fable 5.1\n          Signed-off-by: Thomas Bachem <mail@thomasbachem.com>\n     @@ Documentation/config/rerere.adoc: rerere.lockTimeout::\n       \tto wait indefinitely.  Default is 1000 (i.e., wait for 1\n       \tsecond).  When the time is up, the command fails as it does\n      -\tfor any other lock it cannot take.\n     -+\tfor any other lock it cannot take.  `git rerere gc --auto`\n     -+\tdoes not wait and does nothing while the lock is held.\n     -\n     - ## Documentation/git-rerere.adoc ##\n     -@@ Documentation/git-rerere.adoc: git-rerere - Reuse recorded resolution of conflicted merges\n     - SYNOPSIS\n     - --------\n     - [verse]\n     --'git rerere' [clear | forget <pathspec>... | diff | status | remaining | gc]\n     -+'git rerere' [clear | forget <pathspec>... | diff | status | remaining | gc [--auto]]\n     - \n     - DESCRIPTION\n     - -----------\n     -@@ Documentation/git-rerere.adoc: Print paths with conflicts that have not been autoresolved by rerere.\n     - This includes paths whose resolutions cannot be tracked by rerere,\n     - such as conflicting submodules.\n     - \n     --'gc'::\n     -+'gc' [--auto]::\n     - \n     - Prune records of conflicted merges that\n     - occurred a long time ago.  By default, unresolved conflicts older\n     - than 15 days and resolved conflicts older than 60\n     - days are pruned.  These defaults are controlled via the\n     - `gc.rerereUnresolved` and `gc.rerereResolved` configuration\n     --variables respectively.\n     -+variables respectively.  With `--auto`, which `git maintenance run\n     -+--auto` and `git gc --auto` pass, `gc` does nothing while another\n     -+process holds the rerere lock.  Without it, `gc` waits for the lock\n     -+as long as `rerere.lockTimeout` allows and then fails.\n     - \n     - \n     - DISCUSSION\n     ++\tfor any other lock it cannot take.  A `git rerere gc` run by\n     ++\t`git maintenance run --auto` or `git gc --auto` does not wait\n     ++\tand does nothing while the lock is held.\n      \n       ## builtin/gc.c ##\n      @@ builtin/gc.c: out:\n     @@ builtin/gc.c: out:\n       \trerere_cmd.git_cmd = 1;\n       \tstrvec_pushl(&rerere_cmd.args, \"rerere\", \"gc\", NULL);\n      +\tif (opts->auto_flag)\n     -+\t\tstrvec_push(&rerere_cmd.args, \"--auto\");\n     ++\t\tstrvec_push(&rerere_cmd.args, \"--skip-locked\");\n       \treturn run_command(&rerere_cmd);\n       }\n       \n      \n       ## builtin/rerere.c ##\n     -@@\n     - #include \"pathspec.h\"\n     - \n     - static const char * const rerere_usage[] = {\n     --\tN_(\"git rerere [clear | forget <pathspec>... | diff | status | remaining | gc]\"),\n     -+\tN_(\"git rerere [clear | forget <pathspec>... | diff | status | \"\n     -+\t   \"remaining | gc [--auto]]\"),\n     - \tNULL,\n     - };\n     - \n      @@ builtin/rerere.c: int cmd_rerere(int argc,\n       \t       struct repository *repo UNUSED)\n       {\n       \tstruct string_list merge_rr = STRING_LIST_INIT_DUP;\n      -\tint autoupdate = -1, flags = 0;\n     -+\tint autoupdate = -1, auto_flag = 0, flags = 0;\n     ++\tint autoupdate = -1, skip_locked = 0, flags = 0;\n       \n       \tstruct option options[] = {\n       \t\tOPT_SET_INT(0, \"rerere-autoupdate\", &autoupdate,\n       \t\t\tN_(\"register clean resolutions in index\"), 1),\n     -+\t\tOPT_BOOL(0, \"auto\", &auto_flag,\n     -+\t\t\t N_(\"skip gc while another process holds the lock\")),\n     ++\t\tOPT_HIDDEN_BOOL(0, \"skip-locked\", &skip_locked,\n     ++\t\t\tN_(\"skip gc while another process holds the lock\")),\n       \t\tOPT_END(),\n       \t};\n       \n       \targc = parse_options(argc, argv, prefix, options, rerere_usage, 0);\n       \n     -+\tif (auto_flag && (argc < 1 || strcmp(argv[0], \"gc\")))\n     -+\t\tdie(_(\"the option '%s' requires '%s'\"), \"--auto\", \"gc\");\n     ++\tif (skip_locked && (argc < 1 || strcmp(argv[0], \"gc\")))\n     ++\t\tdie(_(\"the option '%s' requires '%s'\"), \"--skip-locked\", \"gc\");\n      +\n       \trepo_config(the_repository, git_xmerge_config, NULL);\n       \n     @@ builtin/rerere.c: int cmd_rerere(int argc,\n       \t} else if (!strcmp(argv[0], \"gc\"))\n      -\t\trerere_gc(the_repository, &merge_rr);\n      +\t\trerere_gc(the_repository, &merge_rr,\n     -+\t\t\t  auto_flag ? RERERE_NOWAIT : 0);\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/t4200-rerere.sh: test_expect_success 'old records rest in peace' '\n       \ttest_path_is_missing $rr2/preimage\n       '\n       \n     -+test_expect_success 'gc --auto does nothing while MERGE_RR is locked' '\n     ++test_expect_success 'gc --skip-locked does nothing while MERGE_RR is locked' '\n      +\tmkdir -p $rr2 &&\n      +\techo Hello >$rr2/preimage &&\n      +\ttest-tool chmtime =$just_over_15_days_ago $rr2/preimage &&\n      +\n      +\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n      +\t>.git/MERGE_RR.lock &&\n     -+\tgit rerere gc --auto 2>err &&\n     ++\tgit rerere gc --skip-locked 2>err &&\n      +\ttest_must_be_empty err &&\n      +\ttest_path_is_file $rr2/preimage &&\n      +\n      +\trm .git/MERGE_RR.lock &&\n     -+\tgit rerere gc --auto &&\n     ++\tgit rerere gc --skip-locked &&\n      +\ttest_path_is_missing $rr2/preimage\n      +'\n      +\n     -+test_expect_success '--auto is only accepted by gc' '\n     -+\ttest_must_fail git rerere --auto clear 2>err &&\n     -+\ttest_grep \"option .--auto. requires .gc.\" err\n     ++test_expect_success '--skip-locked is only accepted by gc' '\n     ++\ttest_must_fail git rerere --skip-locked clear 2>err &&\n     ++\ttest_grep \"option .--skip-locked. requires .gc.\" err\n      +'\n      +\n       test_expect_success 'a held lock is waited out within rerere.lockTimeout' '\n     @@ t/t7900-maintenance.sh: test_expect_rerere_gc () {\n       \t\tshift\n       \tfi\n       \n     -+\t# an automatic run passes --auto on to \"git rerere gc\"\n     -+\tauto=\n     ++\t# An automatic run passes --skip-locked to \"git rerere gc\".\n     ++\tskip_locked=\n      +\tcase \" $* \" in\n      +\t*\" --auto \"*)\n     -+\t\tauto=--auto\n     ++\t\tskip_locked=--skip-locked\n      +\t\t;;\n      +\tesac\n      +\n       \trm -f \"rerere-gc.txt\" &&\n       \tGIT_TRACE2_EVENT=\"$(pwd)/rerere-gc.txt\" \"$@\" &&\n      -\ttest_subcommand $negate git rerere gc <rerere-gc.txt\n     -+\ttest_subcommand $negate git rerere gc $auto <rerere-gc.txt\n     ++\ttest_subcommand $negate git rerere gc $skip_locked <rerere-gc.txt\n       }\n       \n       test_expect_success 'rerere-gc task without --auto always collects garbage' '\n 3:  3984c7666b ! 3:  cd018289bb rerere: go on at a conflict when the lock stays busy\n     @@ Commit message\n          When a merge, rebase, cherry-pick, revert, am, stash or apply stops\n          at a conflict, it runs rerere right before it returns to the user.\n          If MERGE_RR.lock is still held when rerere.lockTimeout runs out, the\n     -    command dies there. A rebase loses more than a recording that way.\n     -    The sequencer has not yet written the state that \"git rebase\n     -    --continue\" needs, so the rebase cannot go on, and the \"git commit\n     -    --amend\" it suggests instead folds the conflicted pick into the\n     -    previous commit.\n     +    command dies there. In a rebase, the sequencer has not yet written the\n     +    state that \"git rebase --continue\" needs. A later\n     +    \"git rebase --continue\" fails, and the \"git commit --amend\" that its\n     +    message offers first folds the conflicted pick into the previous\n     +    commit.\n      \n     -    So warn and go on. The conflict is still in place, and the warning\n     -    tells the user to run \"git rerere\" before resolving it, which records\n     -    the preimage or replays a known resolution as the command would have.\n     -    The hint is under advice.mergeConflict like the other hints printed\n     -    at a conflict stop.\n     +    So warn and go on without rerere. The conflict is still in place, and\n     +    a hint tells the user to run \"git rerere\" before resolving it. That\n     +    records the preimage or replays a known resolution, as the command\n     +    would have. The hint is under advice.mergeConflict like other hints\n     +    printed at a conflict stop.\n      \n     -    Callers that run rerere after a resolution, like \"git commit\" and\n     -    \"git am --continue\", still fail when the wait is up. They move on\n     -    right away, and a leftover MERGE_RR entry would make the next rerere\n     -    run record whatever the file holds by then. The user's own rerere\n     -    commands and the \"rerere clear\" of --skip and --abort fail as well.\n     +    Everything else that waits for the lock is left as it is and still\n     +    fails if the wait times out. That includes \"git commit\" and\n     +    \"git am --continue\", which run rerere after a resolution. When they\n     +    fail, the rebase or am can still be continued.\n      \n          Assisted-by: Claude Fable 5.1\n          Signed-off-by: Thomas Bachem <mail@thomasbachem.com>\n     @@ Documentation/config/rerere.adoc: rerere.lockTimeout::\n       \t`git rerere gc`.  Value 0 means not to wait at all; -1 means\n       \tto wait indefinitely.  Default is 1000 (i.e., wait for 1\n      -\tsecond).  When the time is up, the command fails as it does\n     --\tfor any other lock it cannot take.  `git rerere gc --auto`\n     --\tdoes not wait and does nothing while the lock is held.\n     +-\tfor any other lock it cannot take.  A `git rerere gc` run by\n     +-\t`git maintenance run --auto` or `git gc --auto` does not wait\n     +-\tand does nothing while the lock is held.\n      +\tsecond).  When the time is up, a command that stops at a\n      +\tconflict, such as `git merge` or `git rebase`, prints a\n      +\twarning and goes on without rerere; run `git rerere` before\n      +\tresolving the conflict to record it after all.  Any other\n      +\tcommand fails, as it does for any other lock it cannot take.\n     -+\t`git rerere gc --auto` does not wait and does nothing while\n     -+\tthe lock is held.\n     ++\tA `git rerere gc` run by `git maintenance run --auto` or\n     ++\t`git gc --auto` does not wait and does nothing while the lock\n     ++\tis held.\n      \n       ## apply.c ##\n      @@ apply.c: static int write_out_results(struct apply_state *state, struct patch *list)\n     @@ apply.c: static int write_out_results(struct apply_state *state, struct patch *l\n       \t\t */\n       \t\tif (!state->cached)\n      -\t\t\trepo_rerere(state->repo, 0);\n     -+\t\t\trepo_rerere(state->repo, RERERE_SKIP_LOCKED);\n     ++\t\t\trepo_rerere(state->repo, RERERE_WARN_LOCKED);\n       \t}\n       \n       \treturn errs;\n     @@ builtin/am.c: static int fall_back_threeway(const struct am_state *state, const\n       \tif (merge_ort_generic(&o, &our_tree, &their_tree, 1, bases, &result)) {\n      -\t\trepo_rerere(the_repository, state->allow_rerere_autoupdate);\n      +\t\trepo_rerere(the_repository, state->allow_rerere_autoupdate |\n     -+\t\t\t    RERERE_SKIP_LOCKED);\n     ++\t\t\t    RERERE_WARN_LOCKED);\n       \t\tfree(their_tree_name);\n       \t\treturn error(_(\"Failed to merge in the changes.\"));\n       \t}\n     @@ builtin/merge.c: static int suggest_conflicts(void)\n       \tstrbuf_release(&msgbuf);\n       \tfclose(fp);\n      -\trepo_rerere(the_repository, allow_rerere_auto);\n     -+\trepo_rerere(the_repository, allow_rerere_auto | RERERE_SKIP_LOCKED);\n     ++\trepo_rerere(the_repository, allow_rerere_auto | RERERE_WARN_LOCKED);\n       \tprintf(_(\"Automatic merge failed; \"\n       \t\t\t\"fix conflicts and then commit the result.\\n\"));\n       \treturn 1;\n     @@ builtin/stash.c: static enum stash_apply_result do_apply_stash(const char *prefi\n       \n       \tif (ret) {\n      -\t\trepo_rerere(the_repository, 0);\n     -+\t\trepo_rerere(the_repository, RERERE_SKIP_LOCKED);\n     ++\t\trepo_rerere(the_repository, RERERE_WARN_LOCKED);\n       \n       \t\tif (index)\n       \t\t\tfprintf_ln(stderr, _(\"Index was not unstashed.\"));\n     @@ rerere.c: int setup_rerere(struct repository *r, struct string_list *merge_rr, i\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    (flags & (RERERE_NOWAIT | RERERE_SKIP_LOCKED)))\n     ++\t    (flags & (RERERE_NOWAIT | RERERE_WARN_LOCKED)))\n      +\t\tBUG(\"RERERE_READONLY takes no lock, so no lock flag applies\");\n       \tif (flags & RERERE_READONLY) {\n       \t\tfd = 0;\n     @@ rerere.c: int setup_rerere(struct repository *r, struct string_list *merge_rr, i\n       \t\t\tlock_flags = 0;\n       \t\t\ttimeout_ms = 0;\n       \t\t}\n     -+\t\tif (flags & RERERE_SKIP_LOCKED)\n     ++\t\tif (flags & RERERE_WARN_LOCKED)\n      +\t\t\tlock_flags = 0;\n       \t\tfd = repo_hold_lock_file_for_update_timeout(r, &write_lock,\n      -\t\t\t\t\t\t\t    git_path_merge_rr(r),\n     @@ rerere.c: int setup_rerere(struct repository *r, struct string_list *merge_rr, i\n      +\t\t\t\t\t\t\t    path, lock_flags,\n      +\t\t\t\t\t\t\t    timeout_ms);\n      +\t\tif (fd < 0) {\n     -+\t\t\tif (flags & RERERE_SKIP_LOCKED) {\n     ++\t\t\tif (flags & RERERE_WARN_LOCKED) {\n      +\t\t\t\twarning_errno(_(\"skipping rerere, \"\n      +\t\t\t\t\t\t\"unable to create '%s.lock'\"),\n      +\t\t\t\t\t      path);\n     @@ rerere.h: struct repository;\n       /* Take MERGE_RR.lock only if it is free, and return quietly otherwise */\n       #define RERERE_NOWAIT       010\n      +/* Warn and go on without rerere if MERGE_RR.lock cannot be taken in time */\n     -+#define RERERE_SKIP_LOCKED  020\n     ++#define RERERE_WARN_LOCKED  020\n       \n       /*\n        * Marks paths that have been hand-resolved and added to the\n     @@ sequencer.c: static enum pick_result do_pick_commit(struct repository *r,\n       \t\t      short_commit_name(r, commit), msg.subject);\n       \t\tprint_advice(r, res == 1, opts);\n      -\t\trepo_rerere(r, opts->allow_rerere_auto);\n     -+\t\trepo_rerere(r, opts->allow_rerere_auto | RERERE_SKIP_LOCKED);\n     ++\t\trepo_rerere(r, opts->allow_rerere_auto | RERERE_WARN_LOCKED);\n       \t\tgoto leave;\n       \t}\n       \n     @@ sequencer.c: static int do_merge(struct repository *r,\n       \trollback_lock_file(&lock);\n       \tif (ret)\n      -\t\trepo_rerere(r, opts->allow_rerere_auto);\n     -+\t\trepo_rerere(r, opts->allow_rerere_auto | RERERE_SKIP_LOCKED);\n     ++\t\trepo_rerere(r, opts->allow_rerere_auto | RERERE_WARN_LOCKED);\n       \telse\n       \t\t/*\n       \t\t * In case of problems, we now want to return a positive\n     @@ t/t4200-rerere.sh: test_expect_success 'a held lock is waited out within rerere.\n      +\ttest_path_is_missing $rr/preimage\n      +'\n      +\n     ++test_expect_success 'rebase -r goes on without rerere once rerere.lockTimeout is up' '\n     ++\tgit reset --hard &&\n     ++\tgit checkout -b lock-held-merge second &&\n     ++\ttest_when_finished \"test_might_fail git rebase --abort &&\n     ++\t\tgit checkout third && git branch -D lock-held-merge\" &&\n     ++\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n     ++\t>.git/MERGE_RR.lock &&\n     ++\ttest_must_fail git -c rerere.lockTimeout=0 rebase -r --force-rebase main 2>err &&\n     ++\ttest_grep \"skipping rerere\" err &&\n     ++\ttest_cmp_rev REBASE_HEAD second &&\n     ++\trm .git/MERGE_RR.lock &&\n     ++\tgit rebase --abort &&\n     ++\ttest_path_is_missing .git/rebase-merge\n     ++'\n     ++\n      +test_expect_success 'commit fails on a lock it cannot take' '\n      +\tgit reset --hard &&\n      +\trm -rf $rr &&\n\n-- \ngitgitgadget\n"},{"id":"553954","messageId":"3dc3d02f12a3118ac9e270c19960815f6b8170cb.1790939492.git.gitgitgadget@gmail.com","threadId":"66250","inReplyTo":"pull.2214.v6.git.1790939492.gitgitgadget@gmail.com","subject":"[PATCH v6 1/3] rerere: wait for MERGE_RR.lock before giving up","fromName":"Thomas Bachem via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-02T11:11:30Z","receivedAt":"2026-10-02T11:11:36Z","isPatch":true,"body":"From: Thomas Bachem <mail@thomasbachem.com>\n\nsetup_rerere() takes MERGE_RR.lock with LOCK_DIE_ON_ERROR, so of two\nprocesses that want it at the same time the second one dies. That\nused to be rare. Since 452b12c2e0 (builtin/maintenance: use\n\"geometric\" strategy by default, 2026-02-24) the auto maintenance\nafter a commit runs \"git rerere gc\" whenever rr-cache has enough\nstale entries, and the gc holds the lock while it prunes.\n\nA rebase whose next pick conflicts while that happens dies inside\nrepo_rerere(), before the sequencer has written the state that\n\"git rebase --continue\" needs. Every later \"git rebase --continue\"\nthen fails with \"you have staged changes in your working tree\".\n\nWait for the lock for up to rerere.lockTimeout milliseconds, 1000 by\ndefault, and only then fail as before. Pruning a few thousand entries\ntakes well under a second, so the default covers a rebase that runs\ninto the gc.\n\nAssisted-by: Claude Fable 5.1\nSigned-off-by: Thomas Bachem <mail@thomasbachem.com>\n---\n Documentation/config/rerere.adoc |  8 +++++\n rerere.c                         | 22 ++++++++++---\n t/t4200-rerere.sh                | 53 ++++++++++++++++++++++++++++++++\n 3 files changed, 78 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/config/rerere.adoc b/Documentation/config/rerere.adoc\nindex 3a78b5ebb1..30e827f32b 100644\n--- a/Documentation/config/rerere.adoc\n+++ b/Documentation/config/rerere.adoc\n@@ -10,3 +10,11 @@ rerere.enabled::\n \tenabled if there is an `rr-cache` directory under the\n \t`$GIT_DIR`, e.g. if \"rerere\" was previously used in the\n \trepository.\n+\n+rerere.lockTimeout::\n+\tThe length of time, in milliseconds, to wait for the rerere\n+\tlock when another process holds it, typically a background\n+\t`git rerere gc`.  Value 0 means not to wait at all; -1 means\n+\tto wait indefinitely.  Default is 1000 (i.e., wait for 1\n+\tsecond).  When the time is up, the command fails as it does\n+\tfor any other lock it cannot take.\ndiff --git a/rerere.c b/rerere.c\nindex 1c3745d9e3..64fac07c71 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -33,6 +33,9 @@ static int rerere_enabled = -1;\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@@ -850,6 +853,8 @@ static void git_rerere_config(void)\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 }\n \n@@ -882,12 +887,19 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n \n \tif (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))\n \t\trerere_autoupdate = !!(flags & RERERE_AUTOUPDATE);\n-\tif (flags & RERERE_READONLY)\n+\tif (flags & RERERE_READONLY) {\n \t\tfd = 0;\n-\telse\n-\t\tfd = repo_hold_lock_file_for_update(r, &write_lock,\n-\t\t\t\t\t\t    git_path_merge_rr(r),\n-\t\t\t\t\t\t    LOCK_DIE_ON_ERROR);\n+\t} else {\n+\t\t/*\n+\t\t * Another process may hold the lock for a while, e.g.\n+\t\t * \"git rerere gc\" while it prunes rr-cache, so wait for\n+\t\t * it instead of dying right away.\n+\t\t */\n+\t\tfd = repo_hold_lock_file_for_update_timeout(r, &write_lock,\n+\t\t\t\t\t\t\t    git_path_merge_rr(r),\n+\t\t\t\t\t\t\t    LOCK_DIE_ON_ERROR,\n+\t\t\t\t\t\t\t    rerere_lock_timeout_ms);\n+\t}\n \tread_rr(r, merge_rr);\n \treturn fd;\n }\ndiff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh\nindex 7bb601e117..7bd92235dc 100755\n--- a/t/t4200-rerere.sh\n+++ b/t/t4200-rerere.sh\n@@ -242,6 +242,59 @@ test_expect_success 'old records rest in peace' '\n \ttest_path_is_missing $rr2/preimage\n '\n \n+test_expect_success 'a held lock is waited out within rerere.lockTimeout' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\t{\n+\t\t( sleep 1 && rm -f .git/MERGE_RR.lock ) &\n+\t} &&\n+\ttest_must_fail git -c rerere.lockTimeout=5000 merge first 2>err &&\n+\twait &&\n+\ttest_grep ! \"MERGE_RR\" err &&\n+\ttest_grep \"^=======\\$\" $rr/preimage\n+'\n+\n+test_expect_success 'merge fails once rerere.lockTimeout is up' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 merge first 2>err &&\n+\ttest_grep \"Unable to create\" err &&\n+\ttest_grep \"^=======\\$\" a1 &&\n+\ttest_path_is_missing $rr/preimage\n+'\n+\n+test_expect_success 'rerere, forget, clear and gc fail on a lock they cannot take' '\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rerere 2>err &&\n+\ttest_grep \"Unable to create\" err &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rerere forget a1 2>err &&\n+\ttest_grep \"Unable to create\" err &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rerere clear 2>err &&\n+\ttest_grep \"Unable to create\" err &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rerere gc 2>err &&\n+\ttest_grep \"Unable to create\" err\n+'\n+\n+test_expect_success 'rebase --abort fails on a lock it cannot take' '\n+\tgit reset --hard &&\n+\tgit checkout -b lock-held-abort third &&\n+\ttest_when_finished \"git checkout third && git branch -D lock-held-abort\" &&\n+\ttest_must_fail git rebase first &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rebase --abort 2>err &&\n+\ttest_grep \"Unable to create\" err &&\n+\ttest_path_is_dir .git/rebase-merge &&\n+\trm .git/MERGE_RR.lock &&\n+\tgit rebase --abort &&\n+\ttest_path_is_missing .git/rebase-merge\n+'\n+\n rerere_gc_custom_expiry_test () {\n \tfive_days=\"$1\" right_now=\"$2\"\n \ttest_expect_success \"rerere gc with custom expiry ($five_days, $right_now)\" '\n-- \ngitgitgadget\n\n"},{"id":"553955","messageId":"2ef141410a1508477976f9e57cec05f1a7603264.1790939492.git.gitgitgadget@gmail.com","threadId":"66250","inReplyTo":"pull.2214.v6.git.1790939492.gitgitgadget@gmail.com","subject":"[PATCH v6 2/3] rerere: add \"gc --skip-locked\" for auto maintenance","fromName":"Thomas Bachem via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-02T11:11:31Z","receivedAt":"2026-10-02T11:11:38Z","isPatch":true,"body":"From: Thomas Bachem <mail@thomasbachem.com>\n\nSince the previous commit, \"git rerere gc\" waits for MERGE_RR.lock\nlike every other command that takes it, and fails only if the wait\ntimes out. That suits a user who runs it by hand and wants to know\nwhen nothing was pruned. But the user did not ask for the gc that auto\nmaintenance starts after a commit, and the next commit starts another\none.\n\nSo add \"--skip-locked\", with which \"git rerere gc\" quietly does\nnothing while the lock is held, and pass it from\n\"git maintenance run --auto\" and \"git gc --auto\". Only these two need\nthe option, so hide it and leave it undocumented, like the\n\"--skip-foreground-tasks\" that \"git maintenance run\" passes to\n\"git gc\". A run without it, from the command line or a maintenance\nschedule, still waits for the lock and fails if the wait times out.\n\nAssisted-by: Claude Fable 5.1\nSigned-off-by: Thomas Bachem <mail@thomasbachem.com>\n---\n Documentation/config/rerere.adoc |  4 +++-\n builtin/gc.c                     |  4 +++-\n builtin/rerere.c                 | 10 ++++++++--\n rerere.c                         | 22 +++++++++++++++++-----\n rerere.h                         |  4 +++-\n t/t4200-rerere.sh                | 21 +++++++++++++++++++++\n t/t7900-maintenance.sh           | 25 ++++++++++++++++++++++++-\n 7 files changed, 79 insertions(+), 11 deletions(-)\n\ndiff --git a/Documentation/config/rerere.adoc b/Documentation/config/rerere.adoc\nindex 30e827f32b..80c38ee951 100644\n--- a/Documentation/config/rerere.adoc\n+++ b/Documentation/config/rerere.adoc\n@@ -17,4 +17,6 @@ rerere.lockTimeout::\n \t`git rerere gc`.  Value 0 means not to wait at all; -1 means\n \tto wait indefinitely.  Default is 1000 (i.e., wait for 1\n \tsecond).  When the time is up, the command fails as it does\n-\tfor any other lock it cannot take.\n+\tfor any other lock it cannot take.  A `git rerere gc` run by\n+\t`git maintenance run --auto` or `git gc --auto` does not wait\n+\tand does nothing while the lock is held.\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 57a3520263..7ad3987b71 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -385,12 +385,14 @@ out:\n \treturn should_prune;\n }\n \n-static int maintenance_task_rerere_gc(struct maintenance_run_opts *opts UNUSED,\n+static int maintenance_task_rerere_gc(struct maintenance_run_opts *opts,\n \t\t\t\t      struct gc_config *cfg UNUSED)\n {\n \tstruct child_process rerere_cmd = CHILD_PROCESS_INIT;\n \trerere_cmd.git_cmd = 1;\n \tstrvec_pushl(&rerere_cmd.args, \"rerere\", \"gc\", NULL);\n+\tif (opts->auto_flag)\n+\t\tstrvec_push(&rerere_cmd.args, \"--skip-locked\");\n \treturn run_command(&rerere_cmd);\n }\n \ndiff --git a/builtin/rerere.c b/builtin/rerere.c\nindex a056cb791b..445f1df032 100644\n--- a/builtin/rerere.c\n+++ b/builtin/rerere.c\n@@ -56,16 +56,21 @@ int cmd_rerere(int argc,\n \t       struct repository *repo UNUSED)\n {\n \tstruct string_list merge_rr = STRING_LIST_INIT_DUP;\n-\tint autoupdate = -1, flags = 0;\n+\tint autoupdate = -1, skip_locked = 0, flags = 0;\n \n \tstruct option options[] = {\n \t\tOPT_SET_INT(0, \"rerere-autoupdate\", &autoupdate,\n \t\t\tN_(\"register clean resolutions in index\"), 1),\n+\t\tOPT_HIDDEN_BOOL(0, \"skip-locked\", &skip_locked,\n+\t\t\tN_(\"skip gc while another process holds the lock\")),\n \t\tOPT_END(),\n \t};\n \n \targc = parse_options(argc, argv, prefix, options, rerere_usage, 0);\n \n+\tif (skip_locked && (argc < 1 || strcmp(argv[0], \"gc\")))\n+\t\tdie(_(\"the option '%s' requires '%s'\"), \"--skip-locked\", \"gc\");\n+\n \trepo_config(the_repository, git_xmerge_config, NULL);\n \n \tif (autoupdate == 1)\n@@ -94,7 +99,8 @@ int cmd_rerere(int argc,\n \tif (!strcmp(argv[0], \"clear\")) {\n \t\trerere_clear(the_repository, &merge_rr);\n \t} else if (!strcmp(argv[0], \"gc\"))\n-\t\trerere_gc(the_repository, &merge_rr);\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)\ndiff --git a/rerere.c b/rerere.c\nindex 64fac07c71..43c8eb04db 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -887,18 +887,30 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n \n \tif (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))\n \t\trerere_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\t/*\n \t\t * Another process may hold the lock for a while, e.g.\n \t\t * \"git rerere gc\" while it prunes rr-cache, so wait for\n-\t\t * it instead of dying right away.\n+\t\t * it instead of dying right away.  The gc of an automatic\n+\t\t * maintenance run does not wait, since skipping one of\n+\t\t * its runs costs nothing.\n \t\t */\n+\t\tif (flags & RERERE_NOWAIT) {\n+\t\t\tlock_flags = 0;\n+\t\t\ttimeout_ms = 0;\n+\t\t}\n \t\tfd = repo_hold_lock_file_for_update_timeout(r, &write_lock,\n \t\t\t\t\t\t\t    git_path_merge_rr(r),\n-\t\t\t\t\t\t\t    LOCK_DIE_ON_ERROR,\n-\t\t\t\t\t\t\t    rerere_lock_timeout_ms);\n+\t\t\t\t\t\t\t    lock_flags, timeout_ms);\n+\t\tif (fd < 0)\n+\t\t\treturn -1;\n \t}\n \tread_rr(r, merge_rr);\n \treturn fd;\n@@ -1284,7 +1296,7 @@ out:\n \treturn needed;\n }\n \n-void rerere_gc(struct repository *r, struct string_list *rr)\n+void rerere_gc(struct repository *r, struct string_list *rr, int flags)\n {\n \tstruct string_list to_remove = STRING_LIST_INIT_DUP;\n \tDIR *dir;\n@@ -1294,7 +1306,7 @@ void rerere_gc(struct repository *r, struct string_list *rr)\n \ttimestamp_t cutoff_resolve;\n \tstruct strbuf buf = STRBUF_INIT;\n \n-\tif (setup_rerere(r, rr, 0) < 0)\n+\tif (setup_rerere(r, rr, flags) < 0)\n \t\treturn;\n \n \trerere_gc_cutoffs(r, &cutoff_resolve, &cutoff_noresolve);\ndiff --git a/rerere.h b/rerere.h\nindex feeb0e2c9f..d54c53d0d4 100644\n--- a/rerere.h\n+++ b/rerere.h\n@@ -10,6 +10,8 @@ struct repository;\n #define RERERE_AUTOUPDATE   01\n #define RERERE_NOAUTOUPDATE 02\n #define RERERE_READONLY     04\n+/* Take MERGE_RR.lock only if it is free, and return quietly otherwise */\n+#define RERERE_NOWAIT       010\n \n /*\n  * Marks paths that have been hand-resolved and added to the\n@@ -37,7 +39,7 @@ const char *rerere_path(struct strbuf *buf, const struct rerere_id *,\n int rerere_forget(struct repository *, struct pathspec *);\n int rerere_remaining(struct repository *, struct string_list *);\n void rerere_clear(struct repository *, struct string_list *);\n-void rerere_gc(struct repository *, struct string_list *);\n+void rerere_gc(struct repository *, struct string_list *, int);\n \n /*\n  * Check whether garbage collection for rerere entries is needed, which is\ndiff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh\nindex 7bd92235dc..28152bf456 100755\n--- a/t/t4200-rerere.sh\n+++ b/t/t4200-rerere.sh\n@@ -242,6 +242,27 @@ test_expect_success 'old records rest in peace' '\n \ttest_path_is_missing $rr2/preimage\n '\n \n+test_expect_success 'gc --skip-locked does nothing while MERGE_RR is locked' '\n+\tmkdir -p $rr2 &&\n+\techo Hello >$rr2/preimage &&\n+\ttest-tool chmtime =$just_over_15_days_ago $rr2/preimage &&\n+\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\tgit rerere gc --skip-locked 2>err &&\n+\ttest_must_be_empty err &&\n+\ttest_path_is_file $rr2/preimage &&\n+\n+\trm .git/MERGE_RR.lock &&\n+\tgit rerere gc --skip-locked &&\n+\ttest_path_is_missing $rr2/preimage\n+'\n+\n+test_expect_success '--skip-locked is only accepted by gc' '\n+\ttest_must_fail git rerere --skip-locked clear 2>err &&\n+\ttest_grep \"option .--skip-locked. requires .gc.\" err\n+'\n+\n test_expect_success 'a held lock is waited out within rerere.lockTimeout' '\n \tgit reset --hard &&\n \trm -rf $rr &&\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 4f65fa9439..f0f9b37d4f 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -1007,9 +1007,17 @@ test_expect_rerere_gc () {\n \t\tshift\n \tfi\n \n+\t# An automatic run passes --skip-locked to \"git rerere gc\".\n+\tskip_locked=\n+\tcase \" $* \" in\n+\t*\" --auto \"*)\n+\t\tskip_locked=--skip-locked\n+\t\t;;\n+\tesac\n+\n \trm -f \"rerere-gc.txt\" &&\n \tGIT_TRACE2_EVENT=\"$(pwd)/rerere-gc.txt\" \"$@\" &&\n-\ttest_subcommand $negate git rerere gc <rerere-gc.txt\n+\ttest_subcommand $negate git rerere gc $skip_locked <rerere-gc.txt\n }\n \n test_expect_success 'rerere-gc task without --auto always collects garbage' '\n@@ -1084,6 +1092,21 @@ test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.aut\n \ttest_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 maintenance run --auto --task=rerere-gc\n '\n \n+test_expect_success 'rerere-gc task with --auto succeeds while MERGE_RR is locked' '\n+\ttest_when_finished \"rm -rf .git/rr-cache .git/MERGE_RR.lock\" &&\n+\tmkdir .git/rr-cache &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_expect_rerere_gc git -c maintenance.rerere-gc.auto=-1 maintenance run --auto --task=rerere-gc\n+'\n+\n+test_expect_success 'rerere-gc task without --auto fails while MERGE_RR is locked' '\n+\ttest_when_finished \"rm -rf .git/rr-cache .git/MERGE_RR.lock\" &&\n+\tmkdir .git/rr-cache &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 maintenance run --task=rerere-gc 2>err &&\n+\ttest_grep \"Unable to create\" err\n+'\n+\n test_expect_success '--auto and --schedule incompatible' '\n \ttest_must_fail git maintenance run --auto --schedule=daily 2>err &&\n \ttest_grep \"cannot be used together\" err\n-- \ngitgitgadget\n\n"},{"id":"553956","messageId":"cd018289bbb330753e41a1e5b6156b6e85c12dbe.1790939492.git.gitgitgadget@gmail.com","threadId":"66250","inReplyTo":"pull.2214.v6.git.1790939492.gitgitgadget@gmail.com","subject":"[PATCH v6 3/3] rerere: go on at a conflict when the lock stays busy","fromName":"Thomas Bachem via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-02T11:11:32Z","receivedAt":"2026-10-02T11:11:39Z","isPatch":true,"body":"From: Thomas Bachem <mail@thomasbachem.com>\n\nWhen a merge, rebase, cherry-pick, revert, am, stash or apply stops\nat a conflict, it runs rerere right before it returns to the user.\nIf MERGE_RR.lock is still held when rerere.lockTimeout runs out, the\ncommand dies there. In a rebase, the sequencer has not yet written the\nstate that \"git rebase --continue\" needs. A later\n\"git rebase --continue\" fails, and the \"git commit --amend\" that its\nmessage offers first folds the conflicted pick into the previous\ncommit.\n\nSo warn and go on without rerere. The conflict is still in place, and\na hint tells the user to run \"git rerere\" before resolving it. That\nrecords the preimage or replays a known resolution, as the command\nwould have. The hint is under advice.mergeConflict like other hints\nprinted at a conflict stop.\n\nEverything else that waits for the lock is left as it is and still\nfails if the wait times out. That includes \"git commit\" and\n\"git am --continue\", which run rerere after a resolution. When they\nfail, the rebase or am can still be continued.\n\nAssisted-by: Claude Fable 5.1\nSigned-off-by: Thomas Bachem <mail@thomasbachem.com>\n---\n Documentation/config/rerere.adoc |  12 ++--\n apply.c                          |   2 +-\n builtin/am.c                     |   3 +-\n builtin/merge.c                  |   2 +-\n builtin/stash.c                  |   2 +-\n rerere.c                         |  30 ++++++--\n rerere.h                         |   2 +\n sequencer.c                      |   4 +-\n t/t4200-rerere.sh                | 120 ++++++++++++++++++++++++++++++-\n 9 files changed, 159 insertions(+), 18 deletions(-)\n\ndiff --git a/Documentation/config/rerere.adoc b/Documentation/config/rerere.adoc\nindex 80c38ee951..08fb1cd37e 100644\n--- a/Documentation/config/rerere.adoc\n+++ b/Documentation/config/rerere.adoc\n@@ -16,7 +16,11 @@ rerere.lockTimeout::\n \tlock when another process holds it, typically a background\n \t`git rerere gc`.  Value 0 means not to wait at all; -1 means\n \tto wait indefinitely.  Default is 1000 (i.e., wait for 1\n-\tsecond).  When the time is up, the command fails as it does\n-\tfor any other lock it cannot take.  A `git rerere gc` run by\n-\t`git maintenance run --auto` or `git gc --auto` does not wait\n-\tand does nothing while the lock is held.\n+\tsecond).  When the time is up, a command that stops at a\n+\tconflict, such as `git merge` or `git rebase`, prints a\n+\twarning and goes on without rerere; run `git rerere` before\n+\tresolving the conflict to record it after all.  Any other\n+\tcommand fails, as it does for any other lock it cannot take.\n+\tA `git rerere gc` run by `git maintenance run --auto` or\n+\t`git gc --auto` does not wait and does nothing while the lock\n+\tis held.\ndiff --git a/apply.c b/apply.c\nindex f00b7ba4d3..f2b896af9d 100644\n--- a/apply.c\n+++ b/apply.c\n@@ -4865,7 +4865,7 @@ static int write_out_results(struct apply_state *state, struct patch *list)\n \t\t * tree with conflict markers, but that isn't written with --cached.\n \t\t */\n \t\tif (!state->cached)\n-\t\t\trepo_rerere(state->repo, 0);\n+\t\t\trepo_rerere(state->repo, RERERE_WARN_LOCKED);\n \t}\n \n \treturn errs;\ndiff --git a/builtin/am.c b/builtin/am.c\nindex e9623b8307..aa09211461 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -1649,7 +1649,8 @@ static int fall_back_threeway(const struct am_state *state, const char *index_pa\n \t\to.verbosity = 0;\n \n \tif (merge_ort_generic(&o, &our_tree, &their_tree, 1, bases, &result)) {\n-\t\trepo_rerere(the_repository, state->allow_rerere_autoupdate);\n+\t\trepo_rerere(the_repository, state->allow_rerere_autoupdate |\n+\t\t\t    RERERE_WARN_LOCKED);\n \t\tfree(their_tree_name);\n \t\treturn error(_(\"Failed to merge in the changes.\"));\n \t}\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 5b4eb23a83..68511614c7 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -1061,7 +1061,7 @@ static int suggest_conflicts(void)\n \tfputs(msgbuf.buf, fp);\n \tstrbuf_release(&msgbuf);\n \tfclose(fp);\n-\trepo_rerere(the_repository, allow_rerere_auto);\n+\trepo_rerere(the_repository, allow_rerere_auto | RERERE_WARN_LOCKED);\n \tprintf(_(\"Automatic merge failed; \"\n \t\t\t\"fix conflicts and then commit the result.\\n\"));\n \treturn 1;\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex 7a9843413b..08062512c1 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -732,7 +732,7 @@ static enum stash_apply_result do_apply_stash(const char *prefix,\n \t\tret = error(_(\"could not write index\"));\n \n \tif (ret) {\n-\t\trepo_rerere(the_repository, 0);\n+\t\trepo_rerere(the_repository, RERERE_WARN_LOCKED);\n \n \t\tif (index)\n \t\t\tfprintf_ln(stderr, _(\"Index was not unstashed.\"));\ndiff --git a/rerere.c b/rerere.c\nindex 43c8eb04db..bb7052d3a1 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -3,6 +3,7 @@\n \n #include \"git-compat-util.h\"\n #include \"abspath.h\"\n+#include \"advice.h\"\n #include \"config.h\"\n #include \"copy.h\"\n #include \"environment.h\"\n@@ -887,11 +888,13 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n \n \tif (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))\n \t\trerere_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    (flags & (RERERE_NOWAIT | RERERE_WARN_LOCKED)))\n+\t\tBUG(\"RERERE_READONLY takes no lock, so no lock flag applies\");\n \tif (flags & RERERE_READONLY) {\n \t\tfd = 0;\n \t} else {\n+\t\tconst char *path = git_path_merge_rr(r);\n \t\tint lock_flags = LOCK_DIE_ON_ERROR;\n \t\tint timeout_ms = rerere_lock_timeout_ms;\n \n@@ -900,17 +903,32 @@ 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 wait for\n \t\t * it instead of dying right away.  The gc of an automatic\n \t\t * maintenance run does not wait, since skipping one of\n-\t\t * its runs costs nothing.\n+\t\t * its runs costs nothing.  A command that stops at a\n+\t\t * conflict must not die here either, so it warns and\n+\t\t * goes on without rerere.\n \t\t */\n \t\tif (flags & RERERE_NOWAIT) {\n \t\t\tlock_flags = 0;\n \t\t\ttimeout_ms = 0;\n \t\t}\n+\t\tif (flags & RERERE_WARN_LOCKED)\n+\t\t\tlock_flags = 0;\n \t\tfd = repo_hold_lock_file_for_update_timeout(r, &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\tif (fd < 0)\n+\t\t\t\t\t\t\t    path, lock_flags,\n+\t\t\t\t\t\t\t    timeout_ms);\n+\t\tif (fd < 0) {\n+\t\t\tif (flags & RERERE_WARN_LOCKED) {\n+\t\t\t\twarning_errno(_(\"skipping rerere, \"\n+\t\t\t\t\t\t\"unable to create '%s.lock'\"),\n+\t\t\t\t\t      path);\n+\t\t\t\tadvise_if_enabled(ADVICE_MERGE_CONFLICT,\n+\t\t\t\t\t\t  _(\"run \\\"git rerere\\\" before \"\n+\t\t\t\t\t\t    \"resolving the conflict to \"\n+\t\t\t\t\t\t    \"record or replay its \"\n+\t\t\t\t\t\t    \"resolution\"));\n+\t\t\t}\n \t\t\treturn -1;\n+\t\t}\n \t}\n \tread_rr(r, merge_rr);\n \treturn fd;\ndiff --git a/rerere.h b/rerere.h\nindex d54c53d0d4..12ff4a8adb 100644\n--- a/rerere.h\n+++ b/rerere.h\n@@ -12,6 +12,8 @@ struct repository;\n #define RERERE_READONLY     04\n /* Take MERGE_RR.lock only if it is free, and return quietly otherwise */\n #define RERERE_NOWAIT       010\n+/* Warn and go on without rerere if MERGE_RR.lock cannot be taken in time */\n+#define RERERE_WARN_LOCKED  020\n \n /*\n  * Marks paths that have been hand-resolved and added to the\ndiff --git a/sequencer.c b/sequencer.c\nindex 6dae43e4db..17a775806d 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -2520,7 +2520,7 @@ static enum pick_result do_pick_commit(struct repository *r,\n \t\t      : _(\"could not apply %s... %s\"),\n \t\t      short_commit_name(r, commit), msg.subject);\n \t\tprint_advice(r, res == 1, opts);\n-\t\trepo_rerere(r, opts->allow_rerere_auto);\n+\t\trepo_rerere(r, opts->allow_rerere_auto | RERERE_WARN_LOCKED);\n \t\tgoto leave;\n \t}\n \n@@ -4449,7 +4449,7 @@ static int do_merge(struct repository *r,\n \n \trollback_lock_file(&lock);\n \tif (ret)\n-\t\trepo_rerere(r, opts->allow_rerere_auto);\n+\t\trepo_rerere(r, opts->allow_rerere_auto | RERERE_WARN_LOCKED);\n \telse\n \t\t/*\n \t\t * In case of problems, we now want to return a positive\ndiff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh\nindex 28152bf456..a9dfe74091 100755\n--- a/t/t4200-rerere.sh\n+++ b/t/t4200-rerere.sh\n@@ -277,17 +277,133 @@ test_expect_success 'a held lock is waited out within rerere.lockTimeout' '\n \ttest_grep \"^=======\\$\" $rr/preimage\n '\n \n-test_expect_success 'merge fails once rerere.lockTimeout is up' '\n+test_expect_success 'merge goes on without rerere once rerere.lockTimeout is up' '\n \tgit reset --hard &&\n \trm -rf $rr &&\n \ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n \t>.git/MERGE_RR.lock &&\n \ttest_must_fail git -c rerere.lockTimeout=0 merge first 2>err &&\n-\ttest_grep \"Unable to create\" err &&\n+\ttest_grep \"skipping rerere\" err &&\n+\ttest_grep \"hint: .*git rerere\" err &&\n \ttest_grep \"^=======\\$\" a1 &&\n \ttest_path_is_missing $rr/preimage\n '\n \n+test_expect_success 'rerere run at the stop records what was skipped' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\tgit checkout -b lock-held-catch-up third &&\n+\ttest_when_finished \"git checkout third && git branch -D lock-held-catch-up\" &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 merge first &&\n+\ttest_path_is_missing $rr/preimage &&\n+\trm .git/MERGE_RR.lock &&\n+\tgit rerere &&\n+\ttest_grep \"^=======\\$\" $rr/preimage &&\n+\techo resolved >a1 &&\n+\tgit add a1 &&\n+\tgit commit -qm resolved &&\n+\ttest_path_is_file $rr/postimage\n+'\n+\n+test_expect_success 'rebase goes on without rerere once rerere.lockTimeout is up' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\tgit checkout -b lock-held third &&\n+\ttest_when_finished \"git checkout third && git branch -D lock-held\" &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rebase first 2>err &&\n+\ttest_grep \"skipping rerere\" err &&\n+\ttest_path_is_file .git/rebase-merge/stopped-sha &&\n+\trm .git/MERGE_RR.lock &&\n+\techo resolved >a1 &&\n+\tgit add a1 &&\n+\tgit rebase --continue &&\n+\ttest_path_is_missing .git/rebase-merge &&\n+\ttest_path_is_missing $rr/preimage\n+'\n+\n+test_expect_success 'rebase -r goes on without rerere once rerere.lockTimeout is up' '\n+\tgit reset --hard &&\n+\tgit checkout -b lock-held-merge second &&\n+\ttest_when_finished \"test_might_fail git rebase --abort &&\n+\t\tgit checkout third && git branch -D lock-held-merge\" &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 rebase -r --force-rebase main 2>err &&\n+\ttest_grep \"skipping rerere\" err &&\n+\ttest_cmp_rev REBASE_HEAD second &&\n+\trm .git/MERGE_RR.lock &&\n+\tgit rebase --abort &&\n+\ttest_path_is_missing .git/rebase-merge\n+'\n+\n+test_expect_success 'commit fails on a lock it cannot take' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\tgit checkout -b lock-held-commit third &&\n+\ttest_when_finished \"git checkout third && git branch -D lock-held-commit\" &&\n+\ttest_must_fail git merge first &&\n+\ttest_path_is_file $rr/preimage &&\n+\techo resolved >a1 &&\n+\tgit add a1 &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 commit -qm resolved 2>err &&\n+\ttest_grep \"Unable to create\" err &&\n+\ttest_path_is_missing $rr/postimage\n+'\n+\n+test_expect_success 'am goes on without rerere once rerere.lockTimeout is up' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\tgit checkout -b lock-held-am third &&\n+\ttest_when_finished \"test_might_fail git am --abort &&\n+\t\tgit checkout third && git branch -D lock-held-am\" &&\n+\tgit format-patch -1 --stdout first >first.patch &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 am --3way first.patch 2>err &&\n+\ttest_grep \"skipping rerere\" err &&\n+\ttest_path_is_dir .git/rebase-apply &&\n+\trm .git/MERGE_RR.lock &&\n+\tgit am --abort &&\n+\ttest_path_is_missing .git/rebase-apply\n+'\n+\n+test_expect_success 'stash pop goes on without rerere once rerere.lockTimeout is up' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\tgit checkout -b lock-held-stash third &&\n+\ttest_when_finished \"git reset --hard && git stash drop &&\n+\t\tgit checkout third && git branch -D lock-held-stash\" &&\n+\techo stashed >>a1 &&\n+\tgit stash &&\n+\techo committed >>a1 &&\n+\tgit commit -qam committed &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 stash pop 2>err &&\n+\ttest_grep \"skipping rerere\" err &&\n+\ttest_grep \"^=======\\$\" a1\n+'\n+\n+test_expect_success 'apply --3way goes on without rerere once rerere.lockTimeout is up' '\n+\tgit reset --hard &&\n+\trm -rf $rr &&\n+\tgit checkout -b lock-held-apply third &&\n+\ttest_when_finished \"git reset --hard &&\n+\t\tgit checkout third && git branch -D lock-held-apply\" &&\n+\tgit format-patch -1 --stdout first >first.patch &&\n+\ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n+\t>.git/MERGE_RR.lock &&\n+\ttest_must_fail git -c rerere.lockTimeout=0 apply --3way first.patch 2>err &&\n+\ttest_grep \"skipping rerere\" err &&\n+\ttest_grep \"^=======\\$\" a1\n+'\n+\n test_expect_success 'rerere, forget, clear and gc fail on a lock they cannot take' '\n \ttest_when_finished \"rm -f .git/MERGE_RR.lock\" &&\n \t>.git/MERGE_RR.lock &&\n-- \ngitgitgadget\n"}]}