Merge branch 'tb/rerere-lock-grace' into seen

The sequencer machinery (used by 'git rebase', 'git cherry-pick', and
'git revert') has been updated to defer automatic maintenance tasks
until the end of the operation, preventing nested 'git commit', 'git
merge', and 'exec' commands from triggering GC operations that could
contend for locks or delete open packs while the sequence is in
progress.

* tb/rerere-lock-grace:
  sequencer: disable auto maintenance in spawned commands
  rebase, cherry-pick, revert: run auto maintenance when done
  config: add git_config_append_parameter()
seen
Junio C Hamano 2026-09-17 12:54:44 -07:00
commit c0ac87184c
7 changed files with 135 additions and 20 deletions

View File

@ -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) {

View File

@ -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,

View File

@ -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);
}

View File

@ -22,6 +22,7 @@
*/

struct object_id;
struct strbuf;

/* git_config_parse_key() returns these negated: */
#define CONFIG_INVALID_KEY 1
@ -188,6 +189,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 "'<key>'='<value>'"
* 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);

/*

View File

@ -272,6 +272,11 @@ struct replay_ctx {
* commands is being applied to was picked as.
*/
enum fixup_target fixup_target;
/*
* 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)
@ -445,6 +450,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)
@ -1177,6 +1183,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
@ -1218,6 +1245,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");

@ -4136,16 +4164,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 */
@ -4554,6 +4584,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");
@ -5427,7 +5458,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) {
@ -5598,6 +5629,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");

/*

View File

@ -395,4 +395,24 @@ 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. "--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 "git commit --allow-empty -m exec && false" main &&
test_subcommand_flex ! git maintenance run --auto <stop.txt &&
echo resolved >F2 &&
git add F2 &&
test_must_fail env GIT_TRACE2_EVENT="$(pwd)/mid.txt" \
git rebase --continue &&
test_subcommand_flex git commit <mid.txt &&
test_subcommand_flex ! git maintenance run --auto <mid.txt &&
GIT_TRACE2_EVENT="$(pwd)/end.txt" git rebase --continue &&
test_subcommand_flex git maintenance run --auto <end.txt &&
grep "\"child_start\".*\"maintenance\"" end.txt >maintenance &&
test_line_count = 1 maintenance
'

test_done

View File

@ -721,4 +721,37 @@ test_expect_success 'commit descriptions in insn sheet are optional' '
test_line_count = 4 commits
'

test_expect_success 'cherry-pick runs auto maintenance once it is done' '
pristine_detach base &&
GIT_TRACE2_EVENT="$(pwd)/single.txt" git cherry-pick --edit picked &&
test_subcommand_flex git commit <single.txt &&
test_subcommand_flex git maintenance run --auto <single.txt &&
grep "\"child_start\".*\"maintenance\"" single.txt >maintenance &&
test_line_count = 1 maintenance &&
GIT_TRACE2_EVENT="$(pwd)/sequence.txt" \
git cherry-pick anotherpick yetanotherpick &&
test_subcommand_flex git maintenance run --auto <sequence.txt &&
grep "\"child_start\".*\"maintenance\"" sequence.txt >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 <stop.txt &&
echo resolved >foo &&
git add foo &&
test_must_fail env GIT_TRACE2_EVENT="$(pwd)/mid.txt" \
git cherry-pick --continue &&
test_subcommand_flex git commit <mid.txt &&
test_subcommand_flex ! git maintenance run --auto <mid.txt &&
GIT_TRACE2_EVENT="$(pwd)/end.txt" git cherry-pick --skip &&
test_subcommand_flex git maintenance run --auto <end.txt &&
grep "\"child_start\".*\"maintenance\"" end.txt >maintenance &&
test_line_count = 1 maintenance
'

test_done