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

[PATCH] rerere: keep a background gc from killing a rebase

From
Thomas Bachem via GitGitGadget <gitgitgadget@gmail.com>
Date
Sep 2, 2026, 08:31 UTC
Message-ID
<pull.2214.git.1788337897490.gitgitgadget@gmail.com>
From: Thomas Bachem <mail@thomasbachem.com>

Since 2.54 unscheduled maintenance uses the "geometric" strategy, so the "git maintenance run --auto --detach" behind every "git commit" runs "git rerere gc" in the background whenever rr-cache has an entry. That includes the "git commit" the sequencer runs for a resolved pick on "git rebase --continue".

rerere_gc() takes MERGE_RR.lock through setup_rerere(), which uses LOCK_DIE_ON_ERROR, and so does the sequencer's repo_rerere() at the next conflict a few milliseconds later. Whichever comes second dies. When it is the rebase, it dies in do_pick_commit() with the index written but before make_patch() writes rebase-merge/{message,patch, stopped-sha}, and every later "git rebase --continue" refuses with "you have staged changes in your working tree". When it is the "git commit" of a later continue, that one dies in its post-commit repo_rerere() after the commit was made. Before 2.54 the same collision needed an auto gc to actually run, since gc runs "rerere gc" at its end.

A rebase with two conflicts in a row shows it. The filler makes the pick slower than the ~5 ms the background task needs to take the lock, and keeps the lock held for about 0.4 s. It hit 6 of 6 runs here on 2.55.0, and a test suite driving rebases on toy repositories with a single rr-cache entry hit it in both runs that were traced:

    git init -q -b main r && cd r
    git config rerere.enabled true
    git config maintenance.auto false
    mkdir pad && seq 20000 | (cd pad && split -l 1 -a 5)
    echo base >f && git add -A && git commit -qm base
    git checkout -q -b topic
    echo b >f && git commit -qam B
    echo c >f && git commit -qam C
    git checkout -q main
    echo a >f && git commit -qam A
    git repack -adq
    seq 20000 | awk '{printf ".git/rr-cache/%040x\n", $1}' \
        | xargs mkdir -p
    for d in .git/rr-cache/*/; do echo x >$d/preimage; done
    git config --unset maintenance.auto
    git checkout -q topic
    git rebase main
    echo ab >f && git add f
    GIT_EDITOR=true git rebase --continue

The second continue dies with "Unable to create '.git/MERGE_RR.lock': File exists" while the gc spawned by its own commit holds the lock, and after resolving C every further continue refuses. Maintenance stays off during the setup so that no repack is pending: a repack due at that commit runs ahead of rerere-gc in the task list and would spend the window.

The gc needs the lock: it removes every rr-cache directory it finds empty, and a rerere that has just created its directory but not yet written the preimage looks exactly like that. So keep the lock and fix both orders. When the gc finds the lock busy, let it warn and do nothing this time, the way "maintenance run" treats its own lock, so a manual "git rerere gc" sees the warning and the maintenance task and "git gc" see a clean exit. When the gc holds the lock, let every other caller wait it out instead of dying at once, for rerere.lockTimeout milliseconds with the semantics of core.packedRefsTimeout: 1000 by default, 0 for the old behaviour, -1 for an unbounded wait. Walking a 20000-entry rr-cache takes about 0.4 s here.

That rebase now completes. The tests cover the gc under a held lock, directly and through the maintenance task, a merge that waits a lock out within a five second rerere.lockTimeout, and one that fails at once with a timeout of 0.

Assisted-by: Claude Fable 5.1
Signed-off-by: Thomas Bachem <mail@thomasbachem.com>
---
    rerere: keep a background gc from killing a rebase
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2214%2Fthomasbachem%2Frerere-gc-lock-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2214/thomasbachem/rerere-gc-lock-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2214
 Documentation/config/rerere.adoc |  8 +++++++
 Documentation/git-rerere.adoc    |  4 +++-
 rerere.c                         | 27 +++++++++++++++++----
 rerere.h                         |  1 +
 t/t4200-rerere.sh                | 40 ++++++++++++++++++++++++++++++++
 t/t7900-maintenance.sh           |  8 +++++++
 6 files changed, 82 insertions(+), 6 deletions(-)
diff --git a/Documentation/config/rerere.adoc b/Documentation/config/rerere.adoc
index 3a78b5ebb1..8041a1587b 100644
--- a/Documentation/config/rerere.adoc
+++ b/Documentation/config/rerere.adoc
@@ -10,3 +10,11 @@ rerere.enabled::
 	enabled if there is an `rr-cache` directory under the
 	`$GIT_DIR`, e.g. if "rerere" was previously used in the
 	repository.
+
+rerere.lockTimeout::
+	The length of time, in milliseconds, to retry when trying to
+	take the rerere lock while another process holds it, typically
+	a background `git rerere gc`.  Value 0 means not to retry at
+	all; -1 means to try indefinitely.  Default is 1000 (i.e.,
+	retry for 1 second).  `git rerere gc` itself does not wait and
+	skips its run instead.
diff --git a/Documentation/git-rerere.adoc b/Documentation/git-rerere.adoc
index 4e6ab9a27c..05935b0603 100644
--- a/Documentation/git-rerere.adoc
+++ b/Documentation/git-rerere.adoc
@@ -70,7 +70,9 @@ occurred a long time ago.  By default, unresolved conflicts older
 than 15 days and resolved conflicts older than 60
 days are pruned.  These defaults are controlled via the
 `gc.rerereUnresolved` and `gc.rerereResolved` configuration
-variables respectively.
+variables respectively.  If another process holds the lock on the
+recorded resolutions, for example a merge or rebase that is recording
+a conflict, `gc` does nothing and reports so.
 
 
 DISCUSSION
diff --git a/rerere.c b/rerere.c
index 8232542585..22d114262b 100644
--- a/rerere.c
+++ b/rerere.c
@@ -32,6 +32,7 @@ static int rerere_enabled = -1;
 
 /* automatically update cleanly resolved paths to the index */
 static int rerere_autoupdate;
+static int rerere_lock_timeout_ms = 1000;
 
 #define RR_HAS_POSTIMAGE 1
 #define RR_HAS_PREIMAGE 2
@@ -876,6 +877,8 @@ static void git_rerere_config(void)
 {
 	repo_config_get_bool(the_repository, "rerere.enabled", &rerere_enabled);
 	repo_config_get_bool(the_repository, "rerere.autoupdate", &rerere_autoupdate);
+	repo_config_get_int(the_repository, "rerere.locktimeout",
+			    &rerere_lock_timeout_ms);
 	repo_config(the_repository, git_default_config, NULL);
 }
 
@@ -908,12 +911,26 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)
 
 	if (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))
 		rerere_autoupdate = !!(flags & RERERE_AUTOUPDATE);
-	if (flags & RERERE_READONLY)
+	if (flags & RERERE_READONLY) {
 		fd = 0;
-	else
+	} else if (flags & RERERE_SKIP_LOCKED) {
 		fd = hold_lock_file_for_update(&write_lock,
-					       git_path_merge_rr(r),
-					       LOCK_DIE_ON_ERROR);
+					       git_path_merge_rr(r), 0);
+		if (fd < 0) {
+			warning_errno(_("unable to lock '%s', skipping"),
+				      git_path_merge_rr(r));
+			return -1;
+		}
+	} else {
+		/*
+		 * A background "rerere gc" holds the lock for as long as it
+		 * takes to walk rr-cache, so wait it out rather than die.
+		 */
+		fd = hold_lock_file_for_update_timeout(&write_lock,
+						       git_path_merge_rr(r),
+						       LOCK_DIE_ON_ERROR,
+						       rerere_lock_timeout_ms);
+	}
 	read_rr(r, merge_rr);
 	return fd;
 }
@@ -1237,7 +1254,7 @@ void rerere_gc(struct repository *r, struct string_list *rr)
 	timestamp_t cutoff_resolve = now - 60 * 86400;
 	struct strbuf buf = STRBUF_INIT;
 
-	if (setup_rerere(r, rr, 0) < 0)
+	if (setup_rerere(r, rr, RERERE_SKIP_LOCKED) < 0)
 		return;
 
 	repo_config_get_expiry_in_days(the_repository, "gc.rerereresolved",
diff --git a/rerere.h b/rerere.h
index d4b5f7c932..87964bb3c5 100644
--- a/rerere.h
+++ b/rerere.h
@@ -10,6 +10,7 @@ struct repository;
 #define RERERE_AUTOUPDATE   01
 #define RERERE_NOAUTOUPDATE 02
 #define RERERE_READONLY     04
+#define RERERE_SKIP_LOCKED  010
 
 /*
  * Marks paths that have been hand-resolved and added to the
diff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh
index 1717f407c8..6b90294435 100755
--- a/t/t4200-rerere.sh
+++ b/t/t4200-rerere.sh
@@ -242,6 +242,46 @@ test_expect_success 'old records rest in peace' '
 	test_path_is_missing $rr2/preimage
 '
 
+test_expect_success 'gc does nothing while MERGE_RR is locked' '
+	mkdir -p $rr2 &&
+	echo Hello >$rr2/preimage &&
+	test-tool chmtime =$just_over_15_days_ago $rr2/preimage &&
+
+	test_when_finished "rm -f .git/MERGE_RR.lock" &&
+	>.git/MERGE_RR.lock &&
+	git rerere gc 2>err &&
+	test_grep "MERGE_RR" err &&
+	test_path_is_file $rr2/preimage &&
+
+	rm .git/MERGE_RR.lock &&
+	git rerere gc &&
+	test_path_is_missing $rr2/preimage
+'
+
+test_expect_success 'a held lock is waited out within rerere.lockTimeout' '
+	git reset --hard &&
+	rm -rf $rr &&
+	test_when_finished "rm -f .git/MERGE_RR.lock" &&
+	>.git/MERGE_RR.lock &&
+	{
+		(sleep 1 && rm -f .git/MERGE_RR.lock) &
+	} &&
+	test_must_fail git -c rerere.lockTimeout=5000 merge first 2>err &&
+	wait &&
+	test_grep ! "Unable to create" err &&
+	grep "^=======\$" $rr/preimage
+'
+
+test_expect_success 'rerere.lockTimeout=0 fails at once on a held lock' '
+	git reset --hard &&
+	rm -rf $rr &&
+	test_when_finished "rm -f .git/MERGE_RR.lock" &&
+	>.git/MERGE_RR.lock &&
+	test_must_fail git -c rerere.lockTimeout=0 merge first 2>err &&
+	test_grep "Unable to create" err &&
+	test_path_is_missing $rr/preimage
+'
+
 rerere_gc_custom_expiry_test () {
 	five_days="$1" right_now="$2"
 	test_expect_success "rerere gc with custom expiry ($five_days, $right_now)" '
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index d7f82e1bec..a55ca2e829 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -885,6 +885,14 @@ test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.aut
 	test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 maintenance run --auto --task=rerere-gc
 '
 
+test_expect_success 'rerere-gc task succeeds while MERGE_RR is locked' '
+	test_when_finished "rm -rf .git/rr-cache .git/MERGE_RR.lock" &&
+	mkdir .git/rr-cache &&
+	: >.git/rr-cache/entry &&
+	>.git/MERGE_RR.lock &&
+	test_expect_rerere_gc git maintenance run --task=rerere-gc
+'
+
 test_expect_success '--auto and --schedule incompatible' '
 	test_must_fail git maintenance run --auto --schedule=daily 2>err &&
 	test_grep "cannot be used together" err

base-commit: e9019fcafe0040228b8631c30f97ae1adb61bcdc
-- 
gitgitgadget
Next: Phillip Wood
Message 1 of 37 in “rerere: keep a background gc from killing a rebase”
  1. rerere: keep a background gc from killing a rebaseThomas Bachem via GitGitGadget, Sep 2, 2026
  2. Phillip WoodSep 2, 2026
  3. Thomas BachemSep 2, 2026
  4. Phillip WoodSep 3, 2026
  5. Patrick SteinhardtSep 3, 2026
  6. Thomas BachemSep 3, 2026
  7. Patrick SteinhardtSep 3, 2026
  8. Thomas BachemSep 3, 2026
  9. Phillip WoodSep 3, 2026
  10. rerere: keep a background gc from killing a rebaseThomas Bachem via GitGitGadget, Sep 4, 2026
  11. Phillip WoodSep 4, 2026
  12. Thomas BachemSep 4, 2026
  13. Phillip WoodSep 7, 2026
  14. Junio C HamanoSep 4, 2026
  15. Thomas BachemSep 4, 2026
  16. rerere: keep a background gc from killing a rebaseThomas Bachem via GitGitGadget, Sep 4, 2026
  17. Junio C HamanoSep 4, 2026
  18. Thomas BachemSep 5, 2026
  19. Junio C HamanoSep 5, 2026
  20. Thomas BachemSep 6, 2026
  21. Patrick SteinhardtSep 7, 2026
  22. 0/2 rerere: wait for MERGE_RR.lock, and go on at a conflictThomas Bachem via GitGitGadget, Sep 14, 2026
  23. 1/2 rerere: wait for MERGE_RR.lock, and let the gc skip itThomas Bachem via GitGitGadget, Sep 14, 2026
  24. Patrick SteinhardtSep 28, 2026
  25. 2/2 rerere: go on at a conflict when the lock stays busyThomas Bachem via GitGitGadget, Sep 14, 2026
  26. Patrick SteinhardtSep 28, 2026
  27. 0/3 rerere: wait for MERGE_RR.lock, and go on at a conflictThomas Bachem via GitGitGadget, Sep 28, 2026
  28. 1/3 rerere: wait for MERGE_RR.lock before giving upThomas Bachem via GitGitGadget, Sep 28, 2026
  29. 2/3 rerere: add "gc --auto" that skips a held lockThomas Bachem via GitGitGadget, Sep 28, 2026
  30. Patrick SteinhardtSep 30, 2026
  31. Thomas BachemOct 1, 2026
  32. Patrick SteinhardtOct 1, 2026
  33. 3/3 rerere: go on at a conflict when the lock stays busyThomas Bachem via GitGitGadget, Sep 28, 2026
  34. 0/3 rerere: wait for MERGE_RR.lock, and go on at a conflictThomas Bachem via GitGitGadget, Oct 2, 2026
  35. 1/3 rerere: wait for MERGE_RR.lock before giving upThomas Bachem via GitGitGadget, Oct 2, 2026
  36. 2/3 rerere: add "gc --skip-locked" for auto maintenanceThomas Bachem via GitGitGadget, Oct 2, 2026
  37. 3/3 rerere: go on at a conflict when the lock stays busyThomas Bachem via GitGitGadget, Oct 2, 2026

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

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