From 724baf2789f77477101bc909b243c67981d78eac Mon Sep 17 00:00:00 2001 From: Thomas Bachem Date: Thu, 17 Sep 2026 18:59:50 +0200 Subject: [PATCH 1/3] config: add git_config_append_parameter() "git -c" passes its settings to the commands it spawns through GIT_CONFIG_PARAMETERS, a list of quoted 'key'='value' pairs. The only place that formats such an entry is git_config_push_split_parameter(), which writes straight into our own environment. Split the formatting out into git_config_append_parameter(), which appends one entry to a strbuf, so that a caller can build the value for a child's environment. The sequencer will use it in a later commit. Assisted-by: Claude Fable 5.1 Signed-off-by: Thomas Bachem --- config.c | 20 +++++++++++++------- config.h | 12 ++++++++++++ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/config.c b/config.c index d9019e7e6c34b0..e0bb29b53d3ca3 100644 --- a/config.c +++ b/config.c @@ -450,18 +450,24 @@ static int git_config_include(const char *var, const char *value, return ret; } +void git_config_append_parameter(struct strbuf *env, const char *key, + const char *value) +{ + if (env->len) + strbuf_addch(env, ' '); + sq_quote_buf(env, key); + strbuf_addch(env, '='); + if (value) + sq_quote_buf(env, value); +} + static void git_config_push_split_parameter(const char *key, const char *value) { struct strbuf env = STRBUF_INIT; const char *old = getenv(CONFIG_DATA_ENVIRONMENT); - if (old && *old) { + if (old && *old) strbuf_addstr(&env, old); - strbuf_addch(&env, ' '); - } - sq_quote_buf(&env, key); - strbuf_addch(&env, '='); - if (value) - sq_quote_buf(&env, value); + git_config_append_parameter(&env, key, value); setenv(CONFIG_DATA_ENVIRONMENT, env.buf, 1); strbuf_release(&env); } diff --git a/config.h b/config.h index b66dd08007c97a..b048f635711429 100644 --- a/config.h +++ b/config.h @@ -22,6 +22,7 @@ */ struct object_id; +struct strbuf; /* git_config_parse_key() returns these negated: */ #define CONFIG_INVALID_KEY 1 @@ -186,6 +187,17 @@ int git_config_from_blob_oid(config_fn_t fn, const char *name, enum config_scope scope); void git_config_push_parameter(const char *text); void git_config_push_env(const char *spec); + +/* + * Append a config option to the buffer that can be exported via the + * GIT_CONFIG_PARAMETERS environment variable, which allows us to + * propagate configuration across Git processes. The format of the + * variable is a space-separated list of quoted "''=''" + * pairs. With a NULL `value`, only 'key'= is appended, which git reads + * back as a boolean true, like "-c key" on the command line. + */ +void git_config_append_parameter(struct strbuf *env, const char *key, + const char *value); int git_config_from_parameters(config_fn_t fn, void *data); /* From f0ec8f1f41dc2c516193dc99cc2e70c3cba25bea Mon Sep 17 00:00:00 2001 From: Thomas Bachem Date: Thu, 17 Sep 2026 18:59:50 +0200 Subject: [PATCH 2/3] rebase, cherry-pick, revert: run auto maintenance when done Commands that use the sequencer with the "merge" backend, like git-cherry-pick(1) or git-rebase(1) with "--merge", create their commits in-process. Consequently, these commands typically don't execute auto maintenance at all. Only the commands they spawn on the way run it, like git-commit(1) for a resolved conflict or an edited message. In contrast to that, the "apply" backend of git-rebase(1) _does_ run auto maintenance after it has processed the sequence of commits. And this is a sensible thing to do: after all, we may just have written lots of objects, so chances are high that we have something to clean up now. Adapt users of the "merge" backend to do the same. The sequencer has no single exit where a call to run_auto_maintenance() could go. A sequence ends in pick_commits(), but a single pick never gets there: it returns from sequencer_pick_revisions() via single_pick(), its "--continue" from sequencer_continue() via continue_single_pick(), and its "--skip" from sequencer_skip(). So call it from the sequencer's two callers instead: run_specific_rebase() for the merge backend, once its state directory is gone, and run_sequencer() in builtin/revert.c after a successful pick, "--continue" or "--skip". Assisted-by: Claude Fable 5.1 Signed-off-by: Thomas Bachem --- builtin/rebase.c | 13 ++++++++++--- builtin/revert.c | 19 ++++++++++++------- t/t3418-rebase-continue.sh | 14 ++++++++++++++ t/t3510-cherry-pick-sequence.sh | 25 +++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 10 deletions(-) diff --git a/builtin/rebase.c b/builtin/rebase.c index 10a306310cd439..535db601818128 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -762,9 +762,16 @@ static int run_specific_rebase(struct rebase_options *opts) if (opts->dont_finish_rebase) ; /* do nothing */ - else if (opts->type == REBASE_MERGE) - ; /* merge backend cleans up after itself */ - else if (status == 0) { + else if (opts->type == REBASE_MERGE) { + int quiet = !(opts->flags & (REBASE_NO_QUIET|REBASE_VERBOSE)); + + /* + * The sequencer cleans up after itself. Its state directory + * is gone once it is done, and stays while it is stopped. + */ + if (status == 0 && !is_directory(opts->state_dir)) + run_auto_maintenance(the_repository, quiet); + } else if (status == 0) { if (!file_exists(state_dir_path("stopped-sha", opts))) finish_rebase(opts); } else if (status == 2) { diff --git a/builtin/revert.c b/builtin/revert.c index bedc40f368eccc..52100a20cba596 100644 --- a/builtin/revert.c +++ b/builtin/revert.c @@ -8,6 +8,7 @@ #include "gettext.h" #include "revision.h" #include "rerere.h" +#include "run-command.h" #include "sequencer.h" #include "branch.h" @@ -116,7 +117,7 @@ static int run_sequencer(int argc, const char **argv, const char *prefix, const char *strategy = &sentinel_value; const char *gpg_sign = &sentinel_value; enum empty_action empty_opt = EMPTY_COMMIT_UNSPECIFIED; - int cmd = 0; + int cmd = 0, ret; struct option base_options[] = { OPT_CMDMODE(0, "quit", &cmd, N_("end revert or cherry-pick sequence"), 'q'), OPT_CMDMODE(0, "continue", &cmd, N_("resume revert or cherry-pick sequence"), 'c'), @@ -264,18 +265,22 @@ static int run_sequencer(int argc, const char **argv, const char *prefix, free(options); if (cmd == 'q') { - int ret = sequencer_remove_state(opts); + ret = sequencer_remove_state(opts); if (!ret) remove_branch_state(the_repository, 0); return ret; } - if (cmd == 'c') - return sequencer_continue(the_repository, opts); if (cmd == 'a') return sequencer_rollback(the_repository, opts); - if (cmd == 's') - return sequencer_skip(the_repository, opts); - return sequencer_pick_revisions(the_repository, opts); + if (cmd == 'c') + ret = sequencer_continue(the_repository, opts); + else if (cmd == 's') + ret = sequencer_skip(the_repository, opts); + else + ret = sequencer_pick_revisions(the_repository, opts); + if (!ret) + run_auto_maintenance(the_repository, opts->quiet); + return ret; } int cmd_revert(int argc, diff --git a/t/t3418-rebase-continue.sh b/t/t3418-rebase-continue.sh index cb5c3a1cb5bc6f..8056b3995530e9 100755 --- a/t/t3418-rebase-continue.sh +++ b/t/t3418-rebase-continue.sh @@ -395,4 +395,18 @@ test_orig_head () { test_orig_head --apply test_orig_head --merge +test_expect_success 'rebase runs auto maintenance once it is done' ' + # topic and main both add F2, so the pick conflicts and the rebase + # stops before the exec runs, and once more when the exec fails + git checkout -b auto-maintenance topic && + test_must_fail env GIT_TRACE2_EVENT="$(pwd)/stop.txt" \ + git rebase -x false main && + test_subcommand_flex ! git maintenance run --auto F2 && + git add F2 && + test_must_fail git rebase --continue && + GIT_TRACE2_EVENT="$(pwd)/end.txt" git rebase --continue && + test_subcommand_flex git maintenance run --auto maintenance && + test_line_count = 1 maintenance +' + +test_expect_success 'cherry-pick runs auto maintenance once a stopped sequence is done' ' + # both picked and anotherpick conflict on foo, so "--continue" stops + # once more before "--skip" ends the sequence + pristine_detach initial && + test_must_fail env GIT_TRACE2_EVENT="$(pwd)/stop.txt" \ + git cherry-pick base..anotherpick && + test_subcommand_flex ! git maintenance run --auto foo && + git add foo && + test_must_fail git cherry-pick --continue && + GIT_TRACE2_EVENT="$(pwd)/end.txt" git cherry-pick --skip && + test_subcommand_flex git maintenance run --auto Date: Thu, 17 Sep 2026 18:59:50 +0200 Subject: [PATCH 3/3] sequencer: disable auto maintenance in spawned commands When the sequencer spawns "git commit" or "git merge", those commands run "git maintenance run --auto" in the background, which can interfere with the sequencer (e.g. 'rerere gc' holding MERGE_RR.lock or repacks deleting active packs). Pass maintenance.auto=false via GIT_CONFIG_PARAMETERS to the spawned commit, merge and exec commands. Appending it after the user's own settings ensures it wins, and the environment reaches whatever they spawn in turn. Auto maintenance now runs exactly once when the sequence completes. Commands run manually by the user while stopped are unaffected and continue to run auto maintenance normally. Assisted-by: Claude Fable 5.1 Signed-off-by: Thomas Bachem --- sequencer.c | 38 ++++++++++++++++++++++++++++++--- t/t3418-rebase-continue.sh | 14 ++++++++---- t/t3510-cherry-pick-sequence.sh | 14 +++++++++--- 3 files changed, 56 insertions(+), 10 deletions(-) diff --git a/sequencer.c b/sequencer.c index 65afd100d98e61..e99ef09f02697f 100644 --- a/sequencer.c +++ b/sequencer.c @@ -234,6 +234,11 @@ struct replay_ctx { * Whether message contains a commit message. */ unsigned have_message :1; + /* + * GIT_CONFIG_PARAMETERS for the commands we spawn, with auto + * maintenance turned off. Built on first use. + */ + char *config_parameters; }; struct replay_ctx* replay_ctx_new(void) @@ -407,6 +412,7 @@ static void replay_ctx_release(struct replay_ctx *ctx) { strbuf_release(&ctx->current_fixups); strbuf_release(&ctx->message); + free(ctx->config_parameters); } void replay_opts_release(struct replay_opts *opts) @@ -1107,6 +1113,27 @@ static int run_command_silent_on_success(struct child_process *cmd) return rc; } +/* + * Don't let the commands we spawn run auto maintenance. It would race + * us for MERGE_RR.lock or delete packs we still have open. Our caller + * runs it once the sequence is done. + */ +static void disable_auto_maintenance(struct replay_opts *opts, + struct child_process *cmd) +{ + if (!opts->ctx->config_parameters) { + const char *old = getenv(CONFIG_DATA_ENVIRONMENT); + struct strbuf buf = STRBUF_INIT; + + if (old && *old) + strbuf_addstr(&buf, old); + git_config_append_parameter(&buf, "maintenance.auto", "false"); + opts->ctx->config_parameters = strbuf_detach(&buf, NULL); + } + strvec_pushf(&cmd->env, "%s=%s", CONFIG_DATA_ENVIRONMENT, + opts->ctx->config_parameters); +} + /* * If we are cherry-pick, and if the merge did not result in * hand-editing, we will hit this commit and inherit the original @@ -1148,6 +1175,7 @@ static int run_git_commit(const char *defmsg, author_date_from_env(&cmd.env)); if (opts->ignore_date) strvec_push(&cmd.env, "GIT_AUTHOR_DATE="); + disable_auto_maintenance(opts, &cmd); strvec_push(&cmd.args, "commit"); @@ -3924,16 +3952,18 @@ static int error_failed_squash(struct repository *r, return error_with_patch(r, commit, subject, subject_len, opts, 1, 1); } -static int do_exec(struct repository *r, const char *command_line, int quiet) +static int do_exec(struct repository *r, const char *command_line, + struct replay_opts *opts) { struct child_process cmd = CHILD_PROCESS_INIT; int dirty, status; - if (!quiet) + if (!opts->quiet) fprintf(stderr, _("Executing: %s\n"), command_line); cmd.use_shell = 1; strvec_push(&cmd.args, command_line); strvec_push(&cmd.env, "GIT_CHERRY_PICK_HELP"); + disable_auto_maintenance(opts, &cmd); status = run_command(&cmd); /* force re-reading of the cache */ @@ -4342,6 +4372,7 @@ static int do_merge(struct repository *r, author_date_from_env(&cmd.env)); if (opts->ignore_date) strvec_push(&cmd.env, "GIT_AUTHOR_DATE="); + disable_auto_maintenance(opts, &cmd); cmd.git_cmd = 1; strvec_push(&cmd.args, "merge"); @@ -5158,7 +5189,7 @@ static int pick_commits(struct repository *r, if (!opts->verbose) term_clear_line(); *end_of_arg = '\0'; - res = do_exec(r, arg, opts->quiet); + res = do_exec(r, arg, opts); *end_of_arg = saved; if (res) { @@ -5329,6 +5360,7 @@ static int continue_single_pick(struct repository *r, struct replay_opts *opts) return error(_("no cherry-pick or revert in progress")); cmd.git_cmd = 1; + disable_auto_maintenance(opts, &cmd); strvec_push(&cmd.args, "commit"); /* diff --git a/t/t3418-rebase-continue.sh b/t/t3418-rebase-continue.sh index 8056b3995530e9..8c43ef64fce3d4 100755 --- a/t/t3418-rebase-continue.sh +++ b/t/t3418-rebase-continue.sh @@ -397,16 +397,22 @@ test_orig_head --merge test_expect_success 'rebase runs auto maintenance once it is done' ' # topic and main both add F2, so the pick conflicts and the rebase - # stops before the exec runs, and once more when the exec fails + # stops before the exec runs. "--continue" commits the resolution + # first, then runs the exec, which fails and stops it again. git checkout -b auto-maintenance topic && test_must_fail env GIT_TRACE2_EVENT="$(pwd)/stop.txt" \ - git rebase -x false main && + git rebase -x "git commit --allow-empty -m exec && false" main && test_subcommand_flex ! git maintenance run --auto F2 && git add F2 && - test_must_fail git rebase --continue && + test_must_fail env GIT_TRACE2_EVENT="$(pwd)/mid.txt" \ + git rebase --continue && + test_subcommand_flex git commit maintenance && + test_line_count = 1 maintenance ' test_done diff --git a/t/t3510-cherry-pick-sequence.sh b/t/t3510-cherry-pick-sequence.sh index 1dbc42e7688d23..9400ddfc45b475 100755 --- a/t/t3510-cherry-pick-sequence.sh +++ b/t/t3510-cherry-pick-sequence.sh @@ -723,8 +723,11 @@ test_expect_success 'commit descriptions in insn sheet are optional' ' test_expect_success 'cherry-pick runs auto maintenance once it is done' ' pristine_detach base && - GIT_TRACE2_EVENT="$(pwd)/single.txt" git cherry-pick picked && + GIT_TRACE2_EVENT="$(pwd)/single.txt" git cherry-pick --edit picked && + test_subcommand_flex git commit maintenance && + test_line_count = 1 maintenance && GIT_TRACE2_EVENT="$(pwd)/sequence.txt" \ git cherry-pick anotherpick yetanotherpick && test_subcommand_flex git maintenance run --auto foo && git add foo && - test_must_fail git cherry-pick --continue && + test_must_fail env GIT_TRACE2_EVENT="$(pwd)/mid.txt" \ + git cherry-pick --continue && + test_subcommand_flex git commit maintenance && + test_line_count = 1 maintenance ' test_done