Merge branch 'hs/rebase-continue-edit' into seen

Support for skipping the editor when continuing a rebase after
conflict resolution has been added with the '--no-edit' option, and
forcing it with '--edit'.  A new configuration variable
'rebase.noEdit' can be used to set the default behavior.

* hs/rebase-continue-edit:
  rebase: add --[no-]edit to --continue
seen
Junio C Hamano 2026-07-24 16:12:33 -07:00
commit b75dbd9e0d
5 changed files with 126 additions and 7 deletions

View File

@ -62,6 +62,12 @@ instead of:
+
Defaults to false.

rebase.noEdit::
When set to true, `git rebase --continue` uses the commit message
without launching $EDITOR, as if `--no-edit` were given. The
`--edit` option to `git rebase --continue` overrides this setting.
Defaults to false.

rebase.rescheduleFailedExec::
Automatically reschedule `exec` commands that failed. This only makes
sense in interactive mode (or when an `--exec` option was provided).

View File

@ -181,6 +181,16 @@ including not with each other:

--continue::
Restart the rebasing process after having resolved a merge conflict.
+
-e::
--edit::
--no-edit::
With `--continue`, edit or do not edit the commit message,
respectively. By default, the configured $EDITOR is opened so you
can update the commit message after resolving conflicts.
`--no-edit` reuses the existing message without launching an
editor. The `rebase.noEdit` configuration variable can be used to
enable `--no-edit` by default; `--edit` overrides that setting.

--skip::
Restart the rebasing process by skipping the current patch.
@ -783,9 +793,10 @@ Commit Rewording
When a conflict occurs while rebasing, rebase stops and asks the user
to resolve. Since the user may need to make notable changes while
resolving conflicts, after conflicts are resolved and the user has run
`git rebase --continue`, the rebase should open an editor and ask the
user to update the commit message. The 'merge' backend does this, while
the 'apply' backend blindly applies the original commit message.
`git rebase --continue`, the rebase opens an editor and asks the
user to update the commit message, unless `rebase.noEdit` is set or
`--no-edit` is passed to `--continue`. The 'merge' backend does this,
while the 'apply' backend blindly applies the original commit message.

Miscellaneous differences
~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -43,7 +43,7 @@ static char const * const builtin_rebase_usage[] = {
"[--onto <newbase> | --keep-base] [<upstream> [<branch>]]"),
N_("git rebase [-i] [options] [--exec <cmd>] [--onto <newbase>] "
"--root [<branch>]"),
"git rebase --continue | --abort | --skip | --edit-todo",
"git rebase --continue [--[no-]edit] | --abort | --skip | --edit-todo",
NULL
};

@ -135,6 +135,8 @@ struct rebase_options {
int config_autosquash;
int config_rebase_merges;
int config_update_refs;
int config_no_edit;
int edit;
};

#define REBASE_OPTIONS_INIT { \
@ -156,6 +158,8 @@ struct rebase_options {
.update_refs = -1, \
.config_update_refs = -1, \
.strategy_opts = STRING_LIST_INIT_NODUP,\
.config_no_edit = -1, \
.edit = -1, \
}

static void rebase_options_release(struct rebase_options *opts)
@ -215,6 +219,13 @@ static struct replay_opts get_replay_opts(const struct rebase_options *opts)
replay.have_squash_onto = 1;
}

if (opts->action == ACTION_CONTINUE) {
if (opts->edit >= 0)
replay.edit = opts->edit;
else if (opts->config_no_edit > 0)
replay.edit = 0;
}

return replay;
}

@ -841,6 +852,11 @@ static int rebase_config(const char *var, const char *value,
return 0;
}

if (!strcmp(var, "rebase.noedit")) {
opts->config_no_edit = git_config_bool(var, value);
return 0;
}

if (!strcmp(var, "rebase.forkpoint")) {
opts->fork_point = git_config_bool(var, value) ? -1 : 0;
return 0;
@ -1171,6 +1187,8 @@ int cmd_rebase(int argc,
ACTION_CONTINUE),
OPT_CMDMODE(0, "skip", &options.action,
N_("skip current patch and continue"), ACTION_SKIP),
OPT_BOOL('e', "edit", &options.edit,
N_("edit the commit message")),
OPT_CMDMODE(0, "abort", &options.action,
N_("abort and check out the original branch"),
ACTION_ABORT),
@ -1311,10 +1329,15 @@ int cmd_rebase(int argc,
"which is no longer supported; use 'merges' instead"));

if (options.action != ACTION_NONE && total_argc != 2) {
usage_with_options(builtin_rebase_usage,
builtin_rebase_options);
if (options.action != ACTION_CONTINUE ||
options.edit < 0 || total_argc != 3)
usage_with_options(builtin_rebase_usage,
builtin_rebase_options);
}

if (options.edit >= 0 && options.action != ACTION_CONTINUE)
die(_("--edit and --no-edit can only be used with --continue"));

if (argc > 2)
usage_with_options(builtin_rebase_usage,
builtin_rebase_options);

View File

@ -2231,6 +2231,25 @@ static int should_edit(struct replay_opts *opts) {
return opts->edit;
}

static int should_edit_rebase_continue(struct replay_opts *opts)
{
if (opts->edit < 0)
return 1;
return opts->edit;
}

static void finalize_continue_edit_flags(struct replay_opts *opts,
unsigned int *flags)
{
if (*flags & CLEANUP_MSG)
return;

if (should_edit_rebase_continue(opts))
*flags |= EDIT_MSG;
else
*flags &= ~EDIT_MSG;
}

static void refer_to_commit(struct repository *r, struct strbuf *msgbuf,
const struct commit *commit,
bool use_commit_reference)
@ -5359,7 +5378,7 @@ static int commit_staged_changes(struct repository *r,
struct todo_list *todo_list)
{
struct replay_ctx *ctx = opts->ctx;
unsigned int flags = ALLOW_EMPTY | EDIT_MSG;
unsigned int flags = ALLOW_EMPTY;
unsigned int final_fixup = 0, is_clean;
struct strbuf rev = STRBUF_INIT;
const char *reflog_action = reflog_message(opts, "continue", NULL);
@ -5527,6 +5546,8 @@ static int commit_staged_changes(struct repository *r,
}
}

finalize_continue_edit_flags(opts, &flags);

if (run_git_commit(final_fixup ? NULL : rebase_path_message(),
reflog_action, opts, flags)) {
ret = error(_("could not commit staged changes."));
@ -5584,6 +5605,12 @@ int sequencer_continue(struct repository *r, struct replay_opts *opts)
res = -1;
goto release_todo_list;
}

/*
* Command-line --[no-]edit applies only to this
* --continue invocation, not to subsequent picks.
*/
opts->edit = -1;
} else if (!file_exists(get_todo_path(opts)))
return continue_single_pick(r, opts);
else if ((res = read_populate_todo(r, &todo_list, opts)))

View File

@ -201,6 +201,58 @@ test_expect_success '--ignore-date is an alias for --reset-author-date' '
test_atime_is_ignored -2
'

test_expect_success '--no-edit on continue uses existing commit message' '
git checkout commit2 &&
test_must_fail git rebase -m --onto commit2^^ commit2^ &&
echo resolved >foo &&
git add foo &&
write_script fail-if-editor-invoked <<-\EOF &&
echo editor invoked >&2
exit 1
EOF
GIT_EDITOR=./fail-if-editor-invoked git rebase --continue --no-edit &&
git log --format=%s -1 >actual &&
echo commit2 >expect &&
test_cmp expect actual
'

test_expect_success '--no-edit cannot be used when starting a rebase' '
test_must_fail git rebase --no-edit -m main side 2>err &&
test_grep "only be used with --continue" err
'

test_expect_success 'rebase.noEdit skips editor on continue' '
git config rebase.noEdit true &&
git checkout commit2 &&
test_must_fail git rebase -m --onto commit2^^ commit2^ &&
echo resolved >foo &&
git add foo &&
write_script fail-if-editor-invoked <<-\EOF &&
echo editor invoked >&2
exit 1
EOF
GIT_EDITOR=./fail-if-editor-invoked git rebase --continue &&
git log --format=%s -1 >actual &&
echo commit2 >expect &&
test_cmp expect actual
'

test_expect_success '--edit on continue overrides rebase.noEdit' '
git config rebase.noEdit true &&
git checkout commit2 &&
test_must_fail git rebase -m --onto commit2^^ commit2^ &&
echo resolved >foo &&
git add foo &&
(
set_fake_editor &&
FAKE_COMMIT_MESSAGE="edited on continue" \
git rebase --continue --edit
) &&
test_write_lines "edited on continue" "" >expect &&
git log --format=%B -1 >actual &&
test_cmp expect actual
'

# This must be the last test in this file
test_expect_success '$EDITOR and friends are unchanged' '
test_editor_unchanged