From 3dc3d02f12a3118ac9e270c19960815f6b8170cb Mon Sep 17 00:00:00 2001 From: Thomas Bachem Date: Mon, 28 Sep 2026 11:47:43 +0200 Subject: [PATCH 1/3] rerere: wait for MERGE_RR.lock before giving up setup_rerere() takes MERGE_RR.lock with LOCK_DIE_ON_ERROR, so of two processes that want it at the same time the second one dies. That used to be rare. Since 452b12c2e0 (builtin/maintenance: use "geometric" strategy by default, 2026-02-24) the auto maintenance after a commit runs "git rerere gc" whenever rr-cache has enough stale entries, and the gc holds the lock while it prunes. A rebase whose next pick conflicts while that happens dies inside repo_rerere(), before the sequencer has written the state that "git rebase --continue" needs. Every later "git rebase --continue" then fails with "you have staged changes in your working tree". Wait for the lock for up to rerere.lockTimeout milliseconds, 1000 by default, and only then fail as before. Pruning a few thousand entries takes well under a second, so the default covers a rebase that runs into the gc. Assisted-by: Claude Fable 5.1 Signed-off-by: Thomas Bachem --- Documentation/config/rerere.adoc | 8 +++++ rerere.c | 22 ++++++++++--- t/t4200-rerere.sh | 53 ++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/Documentation/config/rerere.adoc b/Documentation/config/rerere.adoc index 3a78b5ebb1dc02..30e827f32b289b 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 wait for the rerere + lock when another process holds it, typically a background + `git rerere gc`. Value 0 means not to wait at all; -1 means + to wait indefinitely. Default is 1000 (i.e., wait for 1 + second). When the time is up, the command fails as it does + for any other lock it cannot take. diff --git a/rerere.c b/rerere.c index 1c3745d9e3279a..64fac07c71d9d8 100644 --- a/rerere.c +++ b/rerere.c @@ -33,6 +33,9 @@ static int rerere_enabled = -1; /* automatically update cleanly resolved paths to the index */ static int rerere_autoupdate; +/* how long to wait for MERGE_RR.lock, in milliseconds */ +static int rerere_lock_timeout_ms = 1000; + #define RR_HAS_POSTIMAGE 1 #define RR_HAS_PREIMAGE 2 struct rerere_dir { @@ -850,6 +853,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); } @@ -882,12 +887,19 @@ 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 - fd = repo_hold_lock_file_for_update(r, &write_lock, - git_path_merge_rr(r), - LOCK_DIE_ON_ERROR); + } else { + /* + * Another process may hold the lock for a while, e.g. + * "git rerere gc" while it prunes rr-cache, so wait for + * it instead of dying right away. + */ + fd = repo_hold_lock_file_for_update_timeout(r, &write_lock, + git_path_merge_rr(r), + LOCK_DIE_ON_ERROR, + rerere_lock_timeout_ms); + } read_rr(r, merge_rr); return fd; } diff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh index 7bb601e117ec35..7bd92235dcfed6 100755 --- a/t/t4200-rerere.sh +++ b/t/t4200-rerere.sh @@ -242,6 +242,59 @@ test_expect_success 'old records rest in peace' ' 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 ! "MERGE_RR" err && + test_grep "^=======\$" $rr/preimage +' + +test_expect_success 'merge fails once rerere.lockTimeout is up' ' + 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_grep "^=======\$" a1 && + test_path_is_missing $rr/preimage +' + +test_expect_success 'rerere, forget, clear and gc fail on a lock they cannot take' ' + test_when_finished "rm -f .git/MERGE_RR.lock" && + >.git/MERGE_RR.lock && + test_must_fail git -c rerere.lockTimeout=0 rerere 2>err && + test_grep "Unable to create" err && + test_must_fail git -c rerere.lockTimeout=0 rerere forget a1 2>err && + test_grep "Unable to create" err && + test_must_fail git -c rerere.lockTimeout=0 rerere clear 2>err && + test_grep "Unable to create" err && + test_must_fail git -c rerere.lockTimeout=0 rerere gc 2>err && + test_grep "Unable to create" err +' + +test_expect_success 'rebase --abort fails on a lock it cannot take' ' + git reset --hard && + git checkout -b lock-held-abort third && + test_when_finished "git checkout third && git branch -D lock-held-abort" && + test_must_fail git rebase first && + test_when_finished "rm -f .git/MERGE_RR.lock" && + >.git/MERGE_RR.lock && + test_must_fail git -c rerere.lockTimeout=0 rebase --abort 2>err && + test_grep "Unable to create" err && + test_path_is_dir .git/rebase-merge && + rm .git/MERGE_RR.lock && + git rebase --abort && + test_path_is_missing .git/rebase-merge +' + rerere_gc_custom_expiry_test () { five_days="$1" right_now="$2" test_expect_success "rerere gc with custom expiry ($five_days, $right_now)" ' From 2ef141410a1508477976f9e57cec05f1a7603264 Mon Sep 17 00:00:00 2001 From: Thomas Bachem Date: Mon, 28 Sep 2026 11:51:17 +0200 Subject: [PATCH 2/3] rerere: add "gc --skip-locked" for auto maintenance Since the previous commit, "git rerere gc" waits for MERGE_RR.lock like every other command that takes it, and fails only if the wait times out. That suits a user who runs it by hand and wants to know when nothing was pruned. But the user did not ask for the gc that auto maintenance starts after a commit, and the next commit starts another one. So add "--skip-locked", with which "git rerere gc" quietly does nothing while the lock is held, and pass it from "git maintenance run --auto" and "git gc --auto". Only these two need the option, so hide it and leave it undocumented, like the "--skip-foreground-tasks" that "git maintenance run" passes to "git gc". A run without it, from the command line or a maintenance schedule, still waits for the lock and fails if the wait times out. Assisted-by: Claude Fable 5.1 Signed-off-by: Thomas Bachem --- Documentation/config/rerere.adoc | 4 +++- builtin/gc.c | 4 +++- builtin/rerere.c | 10 ++++++++-- rerere.c | 22 +++++++++++++++++----- rerere.h | 4 +++- t/t4200-rerere.sh | 21 +++++++++++++++++++++ t/t7900-maintenance.sh | 25 ++++++++++++++++++++++++- 7 files changed, 79 insertions(+), 11 deletions(-) diff --git a/Documentation/config/rerere.adoc b/Documentation/config/rerere.adoc index 30e827f32b289b..80c38ee951dd0f 100644 --- a/Documentation/config/rerere.adoc +++ b/Documentation/config/rerere.adoc @@ -17,4 +17,6 @@ rerere.lockTimeout:: `git rerere gc`. Value 0 means not to wait at all; -1 means to wait indefinitely. Default is 1000 (i.e., wait for 1 second). When the time is up, the command fails as it does - for any other lock it cannot take. + for any other lock it cannot take. A `git rerere gc` run by + `git maintenance run --auto` or `git gc --auto` does not wait + and does nothing while the lock is held. diff --git a/builtin/gc.c b/builtin/gc.c index 57a3520263d7be..7ad3987b717ff5 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -385,12 +385,14 @@ static int worktree_prune_condition(struct gc_config *cfg) return should_prune; } -static int maintenance_task_rerere_gc(struct maintenance_run_opts *opts UNUSED, +static int maintenance_task_rerere_gc(struct maintenance_run_opts *opts, struct gc_config *cfg UNUSED) { struct child_process rerere_cmd = CHILD_PROCESS_INIT; rerere_cmd.git_cmd = 1; strvec_pushl(&rerere_cmd.args, "rerere", "gc", NULL); + if (opts->auto_flag) + strvec_push(&rerere_cmd.args, "--skip-locked"); return run_command(&rerere_cmd); } diff --git a/builtin/rerere.c b/builtin/rerere.c index a056cb791b4c80..445f1df03211ce 100644 --- a/builtin/rerere.c +++ b/builtin/rerere.c @@ -56,16 +56,21 @@ int cmd_rerere(int argc, struct repository *repo UNUSED) { struct string_list merge_rr = STRING_LIST_INIT_DUP; - int autoupdate = -1, flags = 0; + int autoupdate = -1, skip_locked = 0, flags = 0; struct option options[] = { OPT_SET_INT(0, "rerere-autoupdate", &autoupdate, N_("register clean resolutions in index"), 1), + OPT_HIDDEN_BOOL(0, "skip-locked", &skip_locked, + N_("skip gc while another process holds the lock")), OPT_END(), }; argc = parse_options(argc, argv, prefix, options, rerere_usage, 0); + if (skip_locked && (argc < 1 || strcmp(argv[0], "gc"))) + die(_("the option '%s' requires '%s'"), "--skip-locked", "gc"); + repo_config(the_repository, git_xmerge_config, NULL); if (autoupdate == 1) @@ -94,7 +99,8 @@ int cmd_rerere(int argc, if (!strcmp(argv[0], "clear")) { rerere_clear(the_repository, &merge_rr); } else if (!strcmp(argv[0], "gc")) - rerere_gc(the_repository, &merge_rr); + rerere_gc(the_repository, &merge_rr, + skip_locked ? RERERE_NOWAIT : 0); else if (!strcmp(argv[0], "status")) { if (setup_rerere(the_repository, &merge_rr, flags | RERERE_READONLY) < 0) diff --git a/rerere.c b/rerere.c index 64fac07c71d9d8..43c8eb04db22af 100644 --- a/rerere.c +++ b/rerere.c @@ -887,18 +887,30 @@ 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) && (flags & RERERE_NOWAIT)) + BUG("RERERE_NOWAIT does not apply with RERERE_READONLY"); if (flags & RERERE_READONLY) { fd = 0; } else { + int lock_flags = LOCK_DIE_ON_ERROR; + int timeout_ms = rerere_lock_timeout_ms; + /* * Another process may hold the lock for a while, e.g. * "git rerere gc" while it prunes rr-cache, so wait for - * it instead of dying right away. + * it instead of dying right away. The gc of an automatic + * maintenance run does not wait, since skipping one of + * its runs costs nothing. */ + if (flags & RERERE_NOWAIT) { + lock_flags = 0; + timeout_ms = 0; + } fd = repo_hold_lock_file_for_update_timeout(r, &write_lock, git_path_merge_rr(r), - LOCK_DIE_ON_ERROR, - rerere_lock_timeout_ms); + lock_flags, timeout_ms); + if (fd < 0) + return -1; } read_rr(r, merge_rr); return fd; @@ -1284,7 +1296,7 @@ bool rerere_gc_needed(struct repository *r, size_t limit) return needed; } -void rerere_gc(struct repository *r, struct string_list *rr) +void rerere_gc(struct repository *r, struct string_list *rr, int flags) { struct string_list to_remove = STRING_LIST_INIT_DUP; DIR *dir; @@ -1294,7 +1306,7 @@ void rerere_gc(struct repository *r, struct string_list *rr) timestamp_t cutoff_resolve; struct strbuf buf = STRBUF_INIT; - if (setup_rerere(r, rr, 0) < 0) + if (setup_rerere(r, rr, flags) < 0) return; rerere_gc_cutoffs(r, &cutoff_resolve, &cutoff_noresolve); diff --git a/rerere.h b/rerere.h index feeb0e2c9fe61b..d54c53d0d4a17d 100644 --- a/rerere.h +++ b/rerere.h @@ -10,6 +10,8 @@ struct repository; #define RERERE_AUTOUPDATE 01 #define RERERE_NOAUTOUPDATE 02 #define RERERE_READONLY 04 +/* Take MERGE_RR.lock only if it is free, and return quietly otherwise */ +#define RERERE_NOWAIT 010 /* * Marks paths that have been hand-resolved and added to the @@ -37,7 +39,7 @@ const char *rerere_path(struct strbuf *buf, const struct rerere_id *, int rerere_forget(struct repository *, struct pathspec *); int rerere_remaining(struct repository *, struct string_list *); void rerere_clear(struct repository *, struct string_list *); -void rerere_gc(struct repository *, struct string_list *); +void rerere_gc(struct repository *, struct string_list *, int); /* * Check whether garbage collection for rerere entries is needed, which is diff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh index 7bd92235dcfed6..28152bf456915d 100755 --- a/t/t4200-rerere.sh +++ b/t/t4200-rerere.sh @@ -242,6 +242,27 @@ test_expect_success 'old records rest in peace' ' test_path_is_missing $rr2/preimage ' +test_expect_success 'gc --skip-locked 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 --skip-locked 2>err && + test_must_be_empty err && + test_path_is_file $rr2/preimage && + + rm .git/MERGE_RR.lock && + git rerere gc --skip-locked && + test_path_is_missing $rr2/preimage +' + +test_expect_success '--skip-locked is only accepted by gc' ' + test_must_fail git rerere --skip-locked clear 2>err && + test_grep "option .--skip-locked. requires .gc." err +' + test_expect_success 'a held lock is waited out within rerere.lockTimeout' ' git reset --hard && rm -rf $rr && diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh index 4f65fa9439c0b8..f0f9b37d4ff7d5 100755 --- a/t/t7900-maintenance.sh +++ b/t/t7900-maintenance.sh @@ -1007,9 +1007,17 @@ test_expect_rerere_gc () { shift fi + # An automatic run passes --skip-locked to "git rerere gc". + skip_locked= + case " $* " in + *" --auto "*) + skip_locked=--skip-locked + ;; + esac + rm -f "rerere-gc.txt" && GIT_TRACE2_EVENT="$(pwd)/rerere-gc.txt" "$@" && - test_subcommand $negate git rerere gc .git/MERGE_RR.lock && + test_expect_rerere_gc git -c maintenance.rerere-gc.auto=-1 maintenance run --auto --task=rerere-gc +' + +test_expect_success 'rerere-gc task without --auto fails while MERGE_RR is locked' ' + test_when_finished "rm -rf .git/rr-cache .git/MERGE_RR.lock" && + mkdir .git/rr-cache && + >.git/MERGE_RR.lock && + test_must_fail git -c rerere.lockTimeout=0 maintenance run --task=rerere-gc 2>err && + test_grep "Unable to create" err +' + 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 From cd018289bbb330753e41a1e5b6156b6e85c12dbe Mon Sep 17 00:00:00 2001 From: Thomas Bachem Date: Mon, 28 Sep 2026 11:54:59 +0200 Subject: [PATCH 3/3] rerere: go on at a conflict when the lock stays busy When a merge, rebase, cherry-pick, revert, am, stash or apply stops at a conflict, it runs rerere right before it returns to the user. If MERGE_RR.lock is still held when rerere.lockTimeout runs out, the command dies there. In a rebase, the sequencer has not yet written the state that "git rebase --continue" needs. A later "git rebase --continue" fails, and the "git commit --amend" that its message offers first folds the conflicted pick into the previous commit. So warn and go on without rerere. The conflict is still in place, and a hint tells the user to run "git rerere" before resolving it. That records the preimage or replays a known resolution, as the command would have. The hint is under advice.mergeConflict like other hints printed at a conflict stop. Everything else that waits for the lock is left as it is and still fails if the wait times out. That includes "git commit" and "git am --continue", which run rerere after a resolution. When they fail, the rebase or am can still be continued. Assisted-by: Claude Fable 5.1 Signed-off-by: Thomas Bachem --- Documentation/config/rerere.adoc | 12 ++-- apply.c | 2 +- builtin/am.c | 3 +- builtin/merge.c | 2 +- builtin/stash.c | 2 +- rerere.c | 30 ++++++-- rerere.h | 2 + sequencer.c | 4 +- t/t4200-rerere.sh | 120 ++++++++++++++++++++++++++++++- 9 files changed, 159 insertions(+), 18 deletions(-) diff --git a/Documentation/config/rerere.adoc b/Documentation/config/rerere.adoc index 80c38ee951dd0f..08fb1cd37ec887 100644 --- a/Documentation/config/rerere.adoc +++ b/Documentation/config/rerere.adoc @@ -16,7 +16,11 @@ rerere.lockTimeout:: lock when another process holds it, typically a background `git rerere gc`. Value 0 means not to wait at all; -1 means to wait indefinitely. Default is 1000 (i.e., wait for 1 - second). When the time is up, the command fails as it does - for any other lock it cannot take. A `git rerere gc` run by - `git maintenance run --auto` or `git gc --auto` does not wait - and does nothing while the lock is held. + second). When the time is up, a command that stops at a + conflict, such as `git merge` or `git rebase`, prints a + warning and goes on without rerere; run `git rerere` before + resolving the conflict to record it after all. Any other + command fails, as it does for any other lock it cannot take. + A `git rerere gc` run by `git maintenance run --auto` or + `git gc --auto` does not wait and does nothing while the lock + is held. diff --git a/apply.c b/apply.c index f00b7ba4d3a7e6..f2b896af9d3802 100644 --- a/apply.c +++ b/apply.c @@ -4865,7 +4865,7 @@ static int write_out_results(struct apply_state *state, struct patch *list) * tree with conflict markers, but that isn't written with --cached. */ if (!state->cached) - repo_rerere(state->repo, 0); + repo_rerere(state->repo, RERERE_WARN_LOCKED); } return errs; diff --git a/builtin/am.c b/builtin/am.c index e9623b8307793f..aa092114615c2b 100644 --- a/builtin/am.c +++ b/builtin/am.c @@ -1649,7 +1649,8 @@ static int fall_back_threeway(const struct am_state *state, const char *index_pa o.verbosity = 0; if (merge_ort_generic(&o, &our_tree, &their_tree, 1, bases, &result)) { - repo_rerere(the_repository, state->allow_rerere_autoupdate); + repo_rerere(the_repository, state->allow_rerere_autoupdate | + RERERE_WARN_LOCKED); free(their_tree_name); return error(_("Failed to merge in the changes.")); } diff --git a/builtin/merge.c b/builtin/merge.c index 5b4eb23a833295..68511614c7d36f 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -1061,7 +1061,7 @@ static int suggest_conflicts(void) fputs(msgbuf.buf, fp); strbuf_release(&msgbuf); fclose(fp); - repo_rerere(the_repository, allow_rerere_auto); + repo_rerere(the_repository, allow_rerere_auto | RERERE_WARN_LOCKED); printf(_("Automatic merge failed; " "fix conflicts and then commit the result.\n")); return 1; diff --git a/builtin/stash.c b/builtin/stash.c index 7a9843413b11e2..08062512c11ec5 100644 --- a/builtin/stash.c +++ b/builtin/stash.c @@ -732,7 +732,7 @@ static enum stash_apply_result do_apply_stash(const char *prefix, ret = error(_("could not write index")); if (ret) { - repo_rerere(the_repository, 0); + repo_rerere(the_repository, RERERE_WARN_LOCKED); if (index) fprintf_ln(stderr, _("Index was not unstashed.")); diff --git a/rerere.c b/rerere.c index 43c8eb04db22af..bb7052d3a167d8 100644 --- a/rerere.c +++ b/rerere.c @@ -3,6 +3,7 @@ #include "git-compat-util.h" #include "abspath.h" +#include "advice.h" #include "config.h" #include "copy.h" #include "environment.h" @@ -887,11 +888,13 @@ 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) && (flags & RERERE_NOWAIT)) - BUG("RERERE_NOWAIT does not apply with RERERE_READONLY"); + if ((flags & RERERE_READONLY) && + (flags & (RERERE_NOWAIT | RERERE_WARN_LOCKED))) + BUG("RERERE_READONLY takes no lock, so no lock flag applies"); if (flags & RERERE_READONLY) { fd = 0; } else { + const char *path = git_path_merge_rr(r); int lock_flags = LOCK_DIE_ON_ERROR; int timeout_ms = rerere_lock_timeout_ms; @@ -900,17 +903,32 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags) * "git rerere gc" while it prunes rr-cache, so wait for * it instead of dying right away. The gc of an automatic * maintenance run does not wait, since skipping one of - * its runs costs nothing. + * its runs costs nothing. A command that stops at a + * conflict must not die here either, so it warns and + * goes on without rerere. */ if (flags & RERERE_NOWAIT) { lock_flags = 0; timeout_ms = 0; } + if (flags & RERERE_WARN_LOCKED) + lock_flags = 0; fd = repo_hold_lock_file_for_update_timeout(r, &write_lock, - git_path_merge_rr(r), - lock_flags, timeout_ms); - if (fd < 0) + path, lock_flags, + timeout_ms); + if (fd < 0) { + if (flags & RERERE_WARN_LOCKED) { + warning_errno(_("skipping rerere, " + "unable to create '%s.lock'"), + path); + advise_if_enabled(ADVICE_MERGE_CONFLICT, + _("run \"git rerere\" before " + "resolving the conflict to " + "record or replay its " + "resolution")); + } return -1; + } } read_rr(r, merge_rr); return fd; diff --git a/rerere.h b/rerere.h index d54c53d0d4a17d..12ff4a8adb718b 100644 --- a/rerere.h +++ b/rerere.h @@ -12,6 +12,8 @@ struct repository; #define RERERE_READONLY 04 /* Take MERGE_RR.lock only if it is free, and return quietly otherwise */ #define RERERE_NOWAIT 010 +/* Warn and go on without rerere if MERGE_RR.lock cannot be taken in time */ +#define RERERE_WARN_LOCKED 020 /* * Marks paths that have been hand-resolved and added to the diff --git a/sequencer.c b/sequencer.c index 6dae43e4db92ae..17a775806d0f66 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2520,7 +2520,7 @@ static enum pick_result do_pick_commit(struct repository *r, : _("could not apply %s... %s"), short_commit_name(r, commit), msg.subject); print_advice(r, res == 1, opts); - repo_rerere(r, opts->allow_rerere_auto); + repo_rerere(r, opts->allow_rerere_auto | RERERE_WARN_LOCKED); goto leave; } @@ -4449,7 +4449,7 @@ static int do_merge(struct repository *r, rollback_lock_file(&lock); if (ret) - repo_rerere(r, opts->allow_rerere_auto); + repo_rerere(r, opts->allow_rerere_auto | RERERE_WARN_LOCKED); else /* * In case of problems, we now want to return a positive diff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh index 28152bf456915d..a9dfe7409191e8 100755 --- a/t/t4200-rerere.sh +++ b/t/t4200-rerere.sh @@ -277,17 +277,133 @@ test_expect_success 'a held lock is waited out within rerere.lockTimeout' ' test_grep "^=======\$" $rr/preimage ' -test_expect_success 'merge fails once rerere.lockTimeout is up' ' +test_expect_success 'merge goes on without rerere once rerere.lockTimeout is up' ' 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_grep "skipping rerere" err && + test_grep "hint: .*git rerere" err && test_grep "^=======\$" a1 && test_path_is_missing $rr/preimage ' +test_expect_success 'rerere run at the stop records what was skipped' ' + git reset --hard && + rm -rf $rr && + git checkout -b lock-held-catch-up third && + test_when_finished "git checkout third && git branch -D lock-held-catch-up" && + test_when_finished "rm -f .git/MERGE_RR.lock" && + >.git/MERGE_RR.lock && + test_must_fail git -c rerere.lockTimeout=0 merge first && + test_path_is_missing $rr/preimage && + rm .git/MERGE_RR.lock && + git rerere && + test_grep "^=======\$" $rr/preimage && + echo resolved >a1 && + git add a1 && + git commit -qm resolved && + test_path_is_file $rr/postimage +' + +test_expect_success 'rebase goes on without rerere once rerere.lockTimeout is up' ' + git reset --hard && + rm -rf $rr && + git checkout -b lock-held third && + test_when_finished "git checkout third && git branch -D lock-held" && + test_when_finished "rm -f .git/MERGE_RR.lock" && + >.git/MERGE_RR.lock && + test_must_fail git -c rerere.lockTimeout=0 rebase first 2>err && + test_grep "skipping rerere" err && + test_path_is_file .git/rebase-merge/stopped-sha && + rm .git/MERGE_RR.lock && + echo resolved >a1 && + git add a1 && + git rebase --continue && + test_path_is_missing .git/rebase-merge && + test_path_is_missing $rr/preimage +' + +test_expect_success 'rebase -r goes on without rerere once rerere.lockTimeout is up' ' + git reset --hard && + git checkout -b lock-held-merge second && + test_when_finished "test_might_fail git rebase --abort && + git checkout third && git branch -D lock-held-merge" && + test_when_finished "rm -f .git/MERGE_RR.lock" && + >.git/MERGE_RR.lock && + test_must_fail git -c rerere.lockTimeout=0 rebase -r --force-rebase main 2>err && + test_grep "skipping rerere" err && + test_cmp_rev REBASE_HEAD second && + rm .git/MERGE_RR.lock && + git rebase --abort && + test_path_is_missing .git/rebase-merge +' + +test_expect_success 'commit fails on a lock it cannot take' ' + git reset --hard && + rm -rf $rr && + git checkout -b lock-held-commit third && + test_when_finished "git checkout third && git branch -D lock-held-commit" && + test_must_fail git merge first && + test_path_is_file $rr/preimage && + echo resolved >a1 && + git add a1 && + test_when_finished "rm -f .git/MERGE_RR.lock" && + >.git/MERGE_RR.lock && + test_must_fail git -c rerere.lockTimeout=0 commit -qm resolved 2>err && + test_grep "Unable to create" err && + test_path_is_missing $rr/postimage +' + +test_expect_success 'am goes on without rerere once rerere.lockTimeout is up' ' + git reset --hard && + rm -rf $rr && + git checkout -b lock-held-am third && + test_when_finished "test_might_fail git am --abort && + git checkout third && git branch -D lock-held-am" && + git format-patch -1 --stdout first >first.patch && + test_when_finished "rm -f .git/MERGE_RR.lock" && + >.git/MERGE_RR.lock && + test_must_fail git -c rerere.lockTimeout=0 am --3way first.patch 2>err && + test_grep "skipping rerere" err && + test_path_is_dir .git/rebase-apply && + rm .git/MERGE_RR.lock && + git am --abort && + test_path_is_missing .git/rebase-apply +' + +test_expect_success 'stash pop goes on without rerere once rerere.lockTimeout is up' ' + git reset --hard && + rm -rf $rr && + git checkout -b lock-held-stash third && + test_when_finished "git reset --hard && git stash drop && + git checkout third && git branch -D lock-held-stash" && + echo stashed >>a1 && + git stash && + echo committed >>a1 && + git commit -qam committed && + test_when_finished "rm -f .git/MERGE_RR.lock" && + >.git/MERGE_RR.lock && + test_must_fail git -c rerere.lockTimeout=0 stash pop 2>err && + test_grep "skipping rerere" err && + test_grep "^=======\$" a1 +' + +test_expect_success 'apply --3way goes on without rerere once rerere.lockTimeout is up' ' + git reset --hard && + rm -rf $rr && + git checkout -b lock-held-apply third && + test_when_finished "git reset --hard && + git checkout third && git branch -D lock-held-apply" && + git format-patch -1 --stdout first >first.patch && + test_when_finished "rm -f .git/MERGE_RR.lock" && + >.git/MERGE_RR.lock && + test_must_fail git -c rerere.lockTimeout=0 apply --3way first.patch 2>err && + test_grep "skipping rerere" err && + test_grep "^=======\$" a1 +' + test_expect_success 'rerere, forget, clear and gc fail on a lock they cannot take' ' test_when_finished "rm -f .git/MERGE_RR.lock" && >.git/MERGE_RR.lock &&