Skip to content

Commit ecd9e5b

Browse files
committed
rerere: keep a background gc from killing a rebase
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>
1 parent e9019fc commit ecd9e5b

6 files changed

Lines changed: 82 additions & 6 deletions

File tree

‎Documentation/config/rerere.adoc‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,11 @@ rerere.enabled::
1010
enabled if there is an `rr-cache` directory under the
1111
`$GIT_DIR`, e.g. if "rerere" was previously used in the
1212
repository.
13+
14+
rerere.lockTimeout::
15+
The length of time, in milliseconds, to retry when trying to
16+
take the rerere lock while another process holds it, typically
17+
a background `git rerere gc`. Value 0 means not to retry at
18+
all; -1 means to try indefinitely. Default is 1000 (i.e.,
19+
retry for 1 second). `git rerere gc` itself does not wait and
20+
skips its run instead.

‎Documentation/git-rerere.adoc‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ occurred a long time ago. By default, unresolved conflicts older
7070
than 15 days and resolved conflicts older than 60
7171
days are pruned. These defaults are controlled via the
7272
`gc.rerereUnresolved` and `gc.rerereResolved` configuration
73-
variables respectively.
73+
variables respectively. If another process holds the lock on the
74+
recorded resolutions, for example a merge or rebase that is recording
75+
a conflict, `gc` does nothing and reports so.
7476

7577

7678
DISCUSSION

‎rerere.c‎

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ static int rerere_enabled = -1;
3232

3333
/* automatically update cleanly resolved paths to the index */
3434
static int rerere_autoupdate;
35+
static int rerere_lock_timeout_ms = 1000;
3536

3637
#define RR_HAS_POSTIMAGE 1
3738
#define RR_HAS_PREIMAGE 2
@@ -876,6 +877,8 @@ static void git_rerere_config(void)
876877
{
877878
repo_config_get_bool(the_repository, "rerere.enabled", &rerere_enabled);
878879
repo_config_get_bool(the_repository, "rerere.autoupdate", &rerere_autoupdate);
880+
repo_config_get_int(the_repository, "rerere.locktimeout",
881+
&rerere_lock_timeout_ms);
879882
repo_config(the_repository, git_default_config, NULL);
880883
}
881884

@@ -908,12 +911,26 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)
908911

909912
if (flags & (RERERE_AUTOUPDATE|RERERE_NOAUTOUPDATE))
910913
rerere_autoupdate = !!(flags & RERERE_AUTOUPDATE);
911-
if (flags & RERERE_READONLY)
914+
if (flags & RERERE_READONLY) {
912915
fd = 0;
913-
else
916+
} else if (flags & RERERE_SKIP_LOCKED) {
914917
fd = hold_lock_file_for_update(&write_lock,
915-
git_path_merge_rr(r),
916-
LOCK_DIE_ON_ERROR);
918+
git_path_merge_rr(r), 0);
919+
if (fd < 0) {
920+
warning_errno(_("unable to lock '%s', skipping"),
921+
git_path_merge_rr(r));
922+
return -1;
923+
}
924+
} else {
925+
/*
926+
* A background "rerere gc" holds the lock for as long as it
927+
* takes to walk rr-cache, so wait it out rather than die.
928+
*/
929+
fd = hold_lock_file_for_update_timeout(&write_lock,
930+
git_path_merge_rr(r),
931+
LOCK_DIE_ON_ERROR,
932+
rerere_lock_timeout_ms);
933+
}
917934
read_rr(r, merge_rr);
918935
return fd;
919936
}
@@ -1237,7 +1254,7 @@ void rerere_gc(struct repository *r, struct string_list *rr)
12371254
timestamp_t cutoff_resolve = now - 60 * 86400;
12381255
struct strbuf buf = STRBUF_INIT;
12391256

1240-
if (setup_rerere(r, rr, 0) < 0)
1257+
if (setup_rerere(r, rr, RERERE_SKIP_LOCKED) < 0)
12411258
return;
12421259

12431260
repo_config_get_expiry_in_days(the_repository, "gc.rerereresolved",

‎rerere.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ struct repository;
1010
#define RERERE_AUTOUPDATE 01
1111
#define RERERE_NOAUTOUPDATE 02
1212
#define RERERE_READONLY 04
13+
#define RERERE_SKIP_LOCKED 010
1314

1415
/*
1516
* Marks paths that have been hand-resolved and added to the

‎t/t4200-rerere.sh‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,46 @@ test_expect_success 'old records rest in peace' '
242242
test_path_is_missing $rr2/preimage
243243
'
244244

245+
test_expect_success 'gc does nothing while MERGE_RR is locked' '
246+
mkdir -p $rr2 &&
247+
echo Hello >$rr2/preimage &&
248+
test-tool chmtime =$just_over_15_days_ago $rr2/preimage &&
249+
250+
test_when_finished "rm -f .git/MERGE_RR.lock" &&
251+
>.git/MERGE_RR.lock &&
252+
git rerere gc 2>err &&
253+
test_grep "MERGE_RR" err &&
254+
test_path_is_file $rr2/preimage &&
255+
256+
rm .git/MERGE_RR.lock &&
257+
git rerere gc &&
258+
test_path_is_missing $rr2/preimage
259+
'
260+
261+
test_expect_success 'a held lock is waited out within rerere.lockTimeout' '
262+
git reset --hard &&
263+
rm -rf $rr &&
264+
test_when_finished "rm -f .git/MERGE_RR.lock" &&
265+
>.git/MERGE_RR.lock &&
266+
{
267+
(sleep 1 && rm -f .git/MERGE_RR.lock) &
268+
} &&
269+
test_must_fail git -c rerere.lockTimeout=5000 merge first 2>err &&
270+
wait &&
271+
test_grep ! "Unable to create" err &&
272+
grep "^=======\$" $rr/preimage
273+
'
274+
275+
test_expect_success 'rerere.lockTimeout=0 fails at once on a held lock' '
276+
git reset --hard &&
277+
rm -rf $rr &&
278+
test_when_finished "rm -f .git/MERGE_RR.lock" &&
279+
>.git/MERGE_RR.lock &&
280+
test_must_fail git -c rerere.lockTimeout=0 merge first 2>err &&
281+
test_grep "Unable to create" err &&
282+
test_path_is_missing $rr/preimage
283+
'
284+
245285
rerere_gc_custom_expiry_test () {
246286
five_days="$1" right_now="$2"
247287
test_expect_success "rerere gc with custom expiry ($five_days, $right_now)" '

‎t/t7900-maintenance.sh‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,14 @@ test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.aut
885885
test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 maintenance run --auto --task=rerere-gc
886886
'
887887

888+
test_expect_success 'rerere-gc task succeeds while MERGE_RR is locked' '
889+
test_when_finished "rm -rf .git/rr-cache .git/MERGE_RR.lock" &&
890+
mkdir .git/rr-cache &&
891+
: >.git/rr-cache/entry &&
892+
>.git/MERGE_RR.lock &&
893+
test_expect_rerere_gc git maintenance run --task=rerere-gc
894+
'
895+
888896
test_expect_success '--auto and --schedule incompatible' '
889897
test_must_fail git maintenance run --auto --schedule=daily 2>err &&
890898
test_grep "cannot be used together" err

0 commit comments

Comments
 (0)