From: "Thomas Bachem via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Patrick Steinhardt <ps@pks.im>,
Phillip Wood <phillip.wood@dunelm.org.uk>,
Junio C Hamano <gitster@pobox.com>,
Phillip Wood <phillip.wood123@gmail.com>,
Thomas Bachem <mail@thomasbachem.com>,
Thomas Bachem <mail@thomasbachem.com>
Subject: [PATCH v7 2/2] rerere: add "gc --skip-locked" for auto maintenance
Date: Sat, 10 Oct 2026 10:13:24 +0000 [thread overview]
Message-ID: <000288119671bad19e5e9e37419766b364daaacc.1791627204.git.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2214.v7.git.1791627204.gitgitgadget@gmail.com>
From: Thomas Bachem <mail@thomasbachem.com>
Starting with the preceding commit, processes that want to acquire
the rerere cache's MERGE_RR.lock by default know to wait up to one
second until that lock has been released. This is a sensible default
for many commands that happen to write rerere entries, as we would
otherwise die immediately when the lock is taken by another process.
But for repository maintenance it's a bit more complicated, as there
are two cases that we have to care about. When the user explicitly
asks us to garbage collect rerere entries via `git rerere gc` they
probably want us to try our best to perform this operation. It's thus
sensible to wait for the lock and then die if we weren't able to
acquire it.
But we also prune rerere entries as part of auto-maintenance, which is
only executed on a best-effort basis anyway. Delaying the whole
operation to acquire the lock is somewhat heavy-handed, and neither
does it make sense to die in case we haven't been able to garbage
collect rerere entries as that would impede other housekeeping tasks.
Furthermore, it's totally fine to skip the operation when the rerere
cache is locked already, as we will retry during the next run anyway.
But we do not have an easy way to tell `git rerere gc` to skip the
operation in case the cache is locked already. Add a new
"--skip-locked" flag to plug that gap and have auto-maintenance pass
that flag.
Only auto-maintenance needs that flag, so hide it, like the
"--skip-foreground-tasks" flag that `git maintenance run` passes to
`git gc`.
Helped-by: Patrick Steinhardt <ps@pks.im>
Assisted-by: Claude Fable 5.1
Signed-off-by: Thomas Bachem <mail@thomasbachem.com>
---
Documentation/config/rerere.adoc | 4 +++-
builtin/gc.c | 4 +++-
builtin/rerere.c | 10 ++++++++--
rerere.c | 20 ++++++++++++++++----
rerere.h | 11 ++++++++++-
t/t4200-rerere.sh | 26 ++++++++++++++++++++++++++
t/t7900-maintenance.sh | 25 ++++++++++++++++++++++++-
7 files changed, 90 insertions(+), 10 deletions(-)
diff --git a/Documentation/config/rerere.adoc b/Documentation/config/rerere.adoc
index 30e827f32b..80c38ee951 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 57a3520263..7ad3987b71 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -385,12 +385,14 @@ out:
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 a056cb791b..91cb3c8668 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_GC_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 64fac07c71..889ec700dd 100644
--- a/rerere.c
+++ b/rerere.c
@@ -887,18 +887,28 @@ 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.
*/
+ 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 +1294,8 @@ out:
return needed;
}
-void rerere_gc(struct repository *r, struct string_list *rr)
+void rerere_gc(struct repository *r, struct string_list *rr,
+ enum rerere_gc_flags flags)
{
struct string_list to_remove = STRING_LIST_INIT_DUP;
DIR *dir;
@@ -1294,7 +1305,8 @@ 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 & RERERE_GC_NOWAIT) ? RERERE_NOWAIT : 0) < 0)
return;
rerere_gc_cutoffs(r, &cutoff_resolve, &cutoff_noresolve);
diff --git a/rerere.h b/rerere.h
index feeb0e2c9f..61e986475b 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
+/* If MERGE_RR.lock is taken, return -1 as if rerere were disabled */
+#define RERERE_NOWAIT 010
/*
* Marks paths that have been hand-resolved and added to the
@@ -37,7 +39,14 @@ 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 *);
+
+enum rerere_gc_flags {
+ /* Skip the operation in case the MERGE_RR.lock is already taken. */
+ RERERE_GC_NOWAIT = (1 << 0),
+};
+
+void rerere_gc(struct repository *, struct string_list *,
+ enum rerere_gc_flags flags);
/*
* Check whether garbage collection for rerere entries is needed, which is
diff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh
index 7bd92235dc..527a872c00 100755
--- a/t/t4200-rerere.sh
+++ b/t/t4200-rerere.sh
@@ -242,6 +242,32 @@ 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
+'
+
+test_expect_success 'gc --skip-locked prunes while MERGE_RR is not locked' '
+ mkdir -p $rr2 &&
+ echo Hello >$rr2/preimage &&
+ test-tool chmtime =$just_over_15_days_ago $rr2/preimage &&
+
+ 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 4f65fa9439..f0f9b37d4f 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 <rerere-gc.txt
+ test_subcommand $negate git rerere gc $skip_locked <rerere-gc.txt
}
test_expect_success 'rerere-gc task without --auto always collects garbage' '
@@ -1084,6 +1092,21 @@ 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 with --auto succeeds 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_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
--
gitgitgadget
prev parent reply other threads:[~2026-10-10 10:13 UTC|newest]
Thread overview: 44+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-02 8:31 [PATCH] rerere: keep a background gc from killing a rebase Thomas Bachem via GitGitGadget
2026-09-02 13:27 ` Phillip Wood
2026-09-02 15:07 ` Thomas Bachem
2026-09-03 13:50 ` Phillip Wood
2026-09-03 7:40 ` Patrick Steinhardt
2026-09-03 8:11 ` Thomas Bachem
2026-09-03 8:32 ` Patrick Steinhardt
2026-09-03 12:12 ` Thomas Bachem
2026-09-03 13:50 ` Phillip Wood
2026-09-04 7:44 ` [PATCH v2] " Thomas Bachem via GitGitGadget
2026-09-04 15:21 ` Phillip Wood
2026-09-04 15:55 ` Thomas Bachem
2026-09-07 10:07 ` Phillip Wood
2026-09-04 17:06 ` Junio C Hamano
2026-09-04 18:17 ` Thomas Bachem
2026-09-04 15:51 ` [PATCH v3] " Thomas Bachem via GitGitGadget
2026-09-04 19:08 ` Junio C Hamano
2026-09-05 5:41 ` Thomas Bachem
2026-09-05 16:10 ` Junio C Hamano
2026-09-06 10:29 ` Thomas Bachem
2026-09-07 7:41 ` Patrick Steinhardt
2026-09-14 8:04 ` [PATCH v4 0/2] rerere: wait for MERGE_RR.lock, and go on at a conflict Thomas Bachem via GitGitGadget
2026-09-14 8:04 ` [PATCH v4 1/2] rerere: wait for MERGE_RR.lock, and let the gc skip it Thomas Bachem via GitGitGadget
2026-09-28 8:18 ` Patrick Steinhardt
2026-09-14 8:04 ` [PATCH v4 2/2] rerere: go on at a conflict when the lock stays busy Thomas Bachem via GitGitGadget
2026-09-28 8:18 ` Patrick Steinhardt
2026-09-28 11:58 ` [PATCH v5 0/3] rerere: wait for MERGE_RR.lock, and go on at a conflict Thomas Bachem via GitGitGadget
2026-09-28 11:58 ` [PATCH v5 1/3] rerere: wait for MERGE_RR.lock before giving up Thomas Bachem via GitGitGadget
2026-09-28 11:58 ` [PATCH v5 2/3] rerere: add "gc --auto" that skips a held lock Thomas Bachem via GitGitGadget
2026-09-30 15:00 ` Patrick Steinhardt
2026-10-01 8:08 ` Thomas Bachem
2026-10-01 11:19 ` Patrick Steinhardt
2026-09-28 11:58 ` [PATCH v5 3/3] rerere: go on at a conflict when the lock stays busy Thomas Bachem via GitGitGadget
2026-10-02 11:11 ` [PATCH v6 0/3] rerere: wait for MERGE_RR.lock, and go on at a conflict Thomas Bachem via GitGitGadget
2026-10-02 11:11 ` [PATCH v6 1/3] rerere: wait for MERGE_RR.lock before giving up Thomas Bachem via GitGitGadget
2026-10-02 11:11 ` [PATCH v6 2/3] rerere: add "gc --skip-locked" for auto maintenance Thomas Bachem via GitGitGadget
2026-10-09 9:06 ` Patrick Steinhardt
2026-10-10 9:09 ` Thomas Bachem
2026-10-02 11:11 ` [PATCH v6 3/3] rerere: go on at a conflict when the lock stays busy Thomas Bachem via GitGitGadget
2026-10-09 9:06 ` Patrick Steinhardt
2026-10-10 9:09 ` Thomas Bachem
2026-10-10 10:13 ` [PATCH v7 0/2] rerere: wait for MERGE_RR.lock, but not in auto maintenance Thomas Bachem via GitGitGadget
2026-10-10 10:13 ` [PATCH v7 1/2] rerere: wait for MERGE_RR.lock before giving up Thomas Bachem via GitGitGadget
2026-10-10 10:13 ` Thomas Bachem via GitGitGadget [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=000288119671bad19e5e9e37419766b364daaacc.1791627204.git.gitgitgadget@gmail.com \
--to=gitgitgadget@gmail.com \
--cc=git@vger.kernel.org \
--cc=gitster@pobox.com \
--cc=mail@thomasbachem.com \
--cc=phillip.wood123@gmail.com \
--cc=phillip.wood@dunelm.org.uk \
--cc=ps@pks.im \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox