Merge branch 'pw/rebase-i-error-message'

When the user adds to "git rebase -i" instruction to "pick" a merge
commit, the error experience is not pleasant.  Such an error is now
caught earlier in the process that parses the todo list.

* pw/rebase-i-error-message:
  rebase -i: improve error message when picking merge
  rebase -i: pass struct replay_opts to parse_insn_line()
This commit is contained in:
Junio C Hamano 2024-06-20 15:45:15 -07:00
commit 83ac567781
9 changed files with 153 additions and 30 deletions

View file

@ -96,6 +96,8 @@ advice.*::
`pushNonFFCurrent`, `pushNonFFMatching`, `pushAlreadyExists`, `pushNonFFCurrent`, `pushNonFFMatching`, `pushAlreadyExists`,
`pushFetchFirst`, `pushNeedsForce`, and `pushRefNeedsUpdate` `pushFetchFirst`, `pushNeedsForce`, and `pushRefNeedsUpdate`
simultaneously. simultaneously.
rebaseTodoError::
Shown when there is an error after editing the rebase todo list.
refSyntax:: refSyntax::
Shown when the user provides an illegal ref name, to Shown when the user provides an illegal ref name, to
tell the user about the ref syntax documentation. tell the user about the ref syntax documentation.

View file

@ -70,6 +70,7 @@ static struct {
[ADVICE_PUSH_UNQUALIFIED_REF_NAME] = { "pushUnqualifiedRefName" }, [ADVICE_PUSH_UNQUALIFIED_REF_NAME] = { "pushUnqualifiedRefName" },
[ADVICE_PUSH_UPDATE_REJECTED] = { "pushUpdateRejected" }, [ADVICE_PUSH_UPDATE_REJECTED] = { "pushUpdateRejected" },
[ADVICE_PUSH_UPDATE_REJECTED_ALIAS] = { "pushNonFastForward" }, /* backwards compatibility */ [ADVICE_PUSH_UPDATE_REJECTED_ALIAS] = { "pushNonFastForward" }, /* backwards compatibility */
[ADVICE_REBASE_TODO_ERROR] = { "rebaseTodoError" },
[ADVICE_REF_SYNTAX] = { "refSyntax" }, [ADVICE_REF_SYNTAX] = { "refSyntax" },
[ADVICE_RESET_NO_REFRESH_WARNING] = { "resetNoRefresh" }, [ADVICE_RESET_NO_REFRESH_WARNING] = { "resetNoRefresh" },
[ADVICE_RESOLVE_CONFLICT] = { "resolveConflict" }, [ADVICE_RESOLVE_CONFLICT] = { "resolveConflict" },

View file

@ -37,6 +37,7 @@ enum advice_type {
ADVICE_PUSH_UNQUALIFIED_REF_NAME, ADVICE_PUSH_UNQUALIFIED_REF_NAME,
ADVICE_PUSH_UPDATE_REJECTED, ADVICE_PUSH_UPDATE_REJECTED,
ADVICE_PUSH_UPDATE_REJECTED_ALIAS, ADVICE_PUSH_UPDATE_REJECTED_ALIAS,
ADVICE_REBASE_TODO_ERROR,
ADVICE_REF_SYNTAX, ADVICE_REF_SYNTAX,
ADVICE_RESET_NO_REFRESH_WARNING, ADVICE_RESET_NO_REFRESH_WARNING,
ADVICE_RESOLVE_CONFLICT, ADVICE_RESOLVE_CONFLICT,

View file

@ -206,7 +206,7 @@ static struct replay_opts get_replay_opts(const struct rebase_options *opts)
return replay; return replay;
} }
static int edit_todo_file(unsigned flags) static int edit_todo_file(unsigned flags, struct replay_opts *opts)
{ {
const char *todo_file = rebase_path_todo(); const char *todo_file = rebase_path_todo();
struct todo_list todo_list = TODO_LIST_INIT, struct todo_list todo_list = TODO_LIST_INIT,
@ -217,7 +217,8 @@ static int edit_todo_file(unsigned flags)
return error_errno(_("could not read '%s'."), todo_file); return error_errno(_("could not read '%s'."), todo_file);
strbuf_stripspace(&todo_list.buf, comment_line_str); strbuf_stripspace(&todo_list.buf, comment_line_str);
res = edit_todo_list(the_repository, &todo_list, &new_todo, NULL, NULL, flags); res = edit_todo_list(the_repository, opts, &todo_list, &new_todo,
NULL, NULL, flags);
if (!res && todo_list_write_to_file(the_repository, &new_todo, todo_file, if (!res && todo_list_write_to_file(the_repository, &new_todo, todo_file,
NULL, NULL, -1, flags & ~(TODO_LIST_SHORTEN_IDS))) NULL, NULL, -1, flags & ~(TODO_LIST_SHORTEN_IDS)))
res = error_errno(_("could not write '%s'"), todo_file); res = error_errno(_("could not write '%s'"), todo_file);
@ -308,8 +309,8 @@ static int do_interactive_rebase(struct rebase_options *opts, unsigned flags)
error(_("could not generate todo list")); error(_("could not generate todo list"));
else { else {
discard_index(the_repository->index); discard_index(the_repository->index);
if (todo_list_parse_insn_buffer(the_repository, todo_list.buf.buf, if (todo_list_parse_insn_buffer(the_repository, &replay,
&todo_list)) todo_list.buf.buf, &todo_list))
BUG("unusable todo list"); BUG("unusable todo list");
ret = complete_action(the_repository, &replay, flags, ret = complete_action(the_repository, &replay, flags,
@ -364,9 +365,13 @@ static int run_sequencer_rebase(struct rebase_options *opts)
replay_opts_release(&replay_opts); replay_opts_release(&replay_opts);
break; break;
} }
case ACTION_EDIT_TODO: case ACTION_EDIT_TODO: {
ret = edit_todo_file(flags); struct replay_opts replay_opts = get_replay_opts(opts);
ret = edit_todo_file(flags, &replay_opts);
replay_opts_release(&replay_opts);
break; break;
}
case ACTION_SHOW_CURRENT_PATCH: { case ACTION_SHOW_CURRENT_PATCH: {
struct child_process cmd = CHILD_PROCESS_INIT; struct child_process cmd = CHILD_PROCESS_INIT;

View file

@ -101,9 +101,10 @@ void append_todo_help(int command_count,
strbuf_add_commented_lines(buf, msg, strlen(msg), comment_line_str); strbuf_add_commented_lines(buf, msg, strlen(msg), comment_line_str);
} }
int edit_todo_list(struct repository *r, struct todo_list *todo_list, int edit_todo_list(struct repository *r, struct replay_opts *opts,
struct todo_list *new_todo, const char *shortrevisions, struct todo_list *todo_list, struct todo_list *new_todo,
const char *shortonto, unsigned flags) const char *shortrevisions, const char *shortonto,
unsigned flags)
{ {
const char *todo_file = rebase_path_todo(), const char *todo_file = rebase_path_todo(),
*todo_backup = rebase_path_todo_backup(); *todo_backup = rebase_path_todo_backup();
@ -114,7 +115,9 @@ int edit_todo_list(struct repository *r, struct todo_list *todo_list,
* it. If there is an error, we do not return, because the user * it. If there is an error, we do not return, because the user
* might want to fix it in the first place. */ * might want to fix it in the first place. */
if (!initial) if (!initial)
incorrect = todo_list_parse_insn_buffer(r, todo_list->buf.buf, todo_list) | incorrect = todo_list_parse_insn_buffer(r, opts,
todo_list->buf.buf,
todo_list) |
file_exists(rebase_path_dropped()); file_exists(rebase_path_dropped());
if (todo_list_write_to_file(r, todo_list, todo_file, shortrevisions, shortonto, if (todo_list_write_to_file(r, todo_list, todo_file, shortrevisions, shortonto,
@ -134,13 +137,13 @@ int edit_todo_list(struct repository *r, struct todo_list *todo_list,
if (initial && new_todo->buf.len == 0) if (initial && new_todo->buf.len == 0)
return -3; return -3;
if (todo_list_parse_insn_buffer(r, new_todo->buf.buf, new_todo)) { if (todo_list_parse_insn_buffer(r, opts, new_todo->buf.buf, new_todo)) {
fprintf(stderr, _(edit_todo_list_advice)); fprintf(stderr, _(edit_todo_list_advice));
return -4; return -4;
} }
if (incorrect) { if (incorrect) {
if (todo_list_check_against_backup(r, new_todo)) { if (todo_list_check_against_backup(r, opts, new_todo)) {
write_file(rebase_path_dropped(), "%s", ""); write_file(rebase_path_dropped(), "%s", "");
return -4; return -4;
} }
@ -228,13 +231,15 @@ int todo_list_check(struct todo_list *old_todo, struct todo_list *new_todo)
return res; return res;
} }
int todo_list_check_against_backup(struct repository *r, struct todo_list *todo_list) int todo_list_check_against_backup(struct repository *r,
struct replay_opts *opts,
struct todo_list *todo_list)
{ {
struct todo_list backup = TODO_LIST_INIT; struct todo_list backup = TODO_LIST_INIT;
int res = 0; int res = 0;
if (strbuf_read_file(&backup.buf, rebase_path_todo_backup(), 0) > 0) { if (strbuf_read_file(&backup.buf, rebase_path_todo_backup(), 0) > 0) {
todo_list_parse_insn_buffer(r, backup.buf.buf, &backup); todo_list_parse_insn_buffer(r, opts, backup.buf.buf, &backup);
res = todo_list_check(&backup, todo_list); res = todo_list_check(&backup, todo_list);
} }

View file

@ -3,17 +3,20 @@
struct strbuf; struct strbuf;
struct repository; struct repository;
struct replay_opts;
struct todo_list; struct todo_list;
void append_todo_help(int command_count, void append_todo_help(int command_count,
const char *shortrevisions, const char *shortonto, const char *shortrevisions, const char *shortonto,
struct strbuf *buf); struct strbuf *buf);
int edit_todo_list(struct repository *r, struct todo_list *todo_list, int edit_todo_list(struct repository *r, struct replay_opts *opts,
struct todo_list *new_todo, const char *shortrevisions, struct todo_list *todo_list, struct todo_list *new_todo,
const char *shortonto, unsigned flags); const char *shortrevisions, const char *shortonto,
unsigned flags);
int todo_list_check(struct todo_list *old_todo, struct todo_list *new_todo); int todo_list_check(struct todo_list *old_todo, struct todo_list *new_todo);
int todo_list_check_against_backup(struct repository *r, int todo_list_check_against_backup(struct repository *r,
struct replay_opts *opts,
struct todo_list *todo_list); struct todo_list *todo_list);
#endif #endif

View file

@ -2626,8 +2626,63 @@ static int check_label_or_ref_arg(enum todo_command command, const char *arg)
return 0; return 0;
} }
static int parse_insn_line(struct repository *r, struct todo_item *item, static int check_merge_commit_insn(enum todo_command command)
const char *buf, const char *bol, char *eol) {
switch(command) {
case TODO_PICK:
error(_("'%s' does not accept merge commits"),
todo_command_info[command].str);
advise_if_enabled(ADVICE_REBASE_TODO_ERROR, _(
/*
* TRANSLATORS: 'pick' and 'merge -C' should not be
* translated.
*/
"'pick' does not take a merge commit. If you wanted to\n"
"replay the merge, use 'merge -C' on the commit."));
return -1;
case TODO_REWORD:
error(_("'%s' does not accept merge commits"),
todo_command_info[command].str);
advise_if_enabled(ADVICE_REBASE_TODO_ERROR, _(
/*
* TRANSLATORS: 'reword' and 'merge -c' should not be
* translated.
*/
"'reword' does not take a merge commit. If you wanted to\n"
"replay the merge and reword the commit message, use\n"
"'merge -c' on the commit"));
return -1;
case TODO_EDIT:
error(_("'%s' does not accept merge commits"),
todo_command_info[command].str);
advise_if_enabled(ADVICE_REBASE_TODO_ERROR, _(
/*
* TRANSLATORS: 'edit', 'merge -C' and 'break' should
* not be translated.
*/
"'edit' does not take a merge commit. If you wanted to\n"
"replay the merge, use 'merge -C' on the commit, and then\n"
"'break' to give the control back to you so that you can\n"
"do 'git commit --amend && git rebase --continue'."));
return -1;
case TODO_FIXUP:
case TODO_SQUASH:
return error(_("cannot squash merge commit into another commit"));
case TODO_MERGE:
return 0;
default:
BUG("unexpected todo_command");
}
}
static int parse_insn_line(struct repository *r, struct replay_opts *opts,
struct todo_item *item, const char *buf,
const char *bol, char *eol)
{ {
struct object_id commit_oid; struct object_id commit_oid;
char *end_of_object_name; char *end_of_object_name;
@ -2731,7 +2786,12 @@ static int parse_insn_line(struct repository *r, struct todo_item *item,
return status; return status;
item->commit = lookup_commit_reference(r, &commit_oid); item->commit = lookup_commit_reference(r, &commit_oid);
return item->commit ? 0 : -1; if (!item->commit)
return -1;
if (is_rebase_i(opts) &&
item->commit->parents && item->commit->parents->next)
return check_merge_commit_insn(item->command);
return 0;
} }
int sequencer_get_last_command(struct repository *r UNUSED, enum replay_action *action) int sequencer_get_last_command(struct repository *r UNUSED, enum replay_action *action)
@ -2761,8 +2821,8 @@ int sequencer_get_last_command(struct repository *r UNUSED, enum replay_action *
return ret; return ret;
} }
int todo_list_parse_insn_buffer(struct repository *r, char *buf, int todo_list_parse_insn_buffer(struct repository *r, struct replay_opts *opts,
struct todo_list *todo_list) char *buf, struct todo_list *todo_list)
{ {
struct todo_item *item; struct todo_item *item;
char *p = buf, *next_p; char *p = buf, *next_p;
@ -2780,7 +2840,7 @@ int todo_list_parse_insn_buffer(struct repository *r, char *buf,
item = append_new_todo(todo_list); item = append_new_todo(todo_list);
item->offset_in_buf = p - todo_list->buf.buf; item->offset_in_buf = p - todo_list->buf.buf;
if (parse_insn_line(r, item, buf, p, eol)) { if (parse_insn_line(r, opts, item, buf, p, eol)) {
res = error(_("invalid line %d: %.*s"), res = error(_("invalid line %d: %.*s"),
i, (int)(eol - p), p); i, (int)(eol - p), p);
item->command = TODO_COMMENT + 1; item->command = TODO_COMMENT + 1;
@ -2930,7 +2990,7 @@ static int read_populate_todo(struct repository *r,
if (strbuf_read_file_or_whine(&todo_list->buf, todo_file) < 0) if (strbuf_read_file_or_whine(&todo_list->buf, todo_file) < 0)
return -1; return -1;
res = todo_list_parse_insn_buffer(r, todo_list->buf.buf, todo_list); res = todo_list_parse_insn_buffer(r, opts, todo_list->buf.buf, todo_list);
if (res) { if (res) {
if (is_rebase_i(opts)) if (is_rebase_i(opts))
return error(_("please fix this using " return error(_("please fix this using "
@ -2960,7 +3020,7 @@ static int read_populate_todo(struct repository *r,
struct todo_list done = TODO_LIST_INIT; struct todo_list done = TODO_LIST_INIT;
if (strbuf_read_file(&done.buf, rebase_path_done(), 0) > 0 && if (strbuf_read_file(&done.buf, rebase_path_done(), 0) > 0 &&
!todo_list_parse_insn_buffer(r, done.buf.buf, &done)) !todo_list_parse_insn_buffer(r, opts, done.buf.buf, &done))
todo_list->done_nr = count_commands(&done); todo_list->done_nr = count_commands(&done);
else else
todo_list->done_nr = 0; todo_list->done_nr = 0;
@ -5319,7 +5379,8 @@ int sequencer_continue(struct repository *r, struct replay_opts *opts)
goto release_todo_list; goto release_todo_list;
if (file_exists(rebase_path_dropped())) { if (file_exists(rebase_path_dropped())) {
if ((res = todo_list_check_against_backup(r, &todo_list))) if ((res = todo_list_check_against_backup(r, opts,
&todo_list)))
goto release_todo_list; goto release_todo_list;
unlink(rebase_path_dropped()); unlink(rebase_path_dropped());
@ -6363,7 +6424,7 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla
return error(_("nothing to do")); return error(_("nothing to do"));
} }
res = edit_todo_list(r, todo_list, &new_todo, shortrevisions, res = edit_todo_list(r, opts, todo_list, &new_todo, shortrevisions,
shortonto, flags); shortonto, flags);
if (res == -1) if (res == -1)
return -1; return -1;
@ -6391,7 +6452,7 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla
strbuf_release(&buf2); strbuf_release(&buf2);
/* Nothing is done yet, and we're reparsing, so let's reset the count */ /* Nothing is done yet, and we're reparsing, so let's reset the count */
new_todo.total_nr = 0; new_todo.total_nr = 0;
if (todo_list_parse_insn_buffer(r, new_todo.buf.buf, &new_todo) < 0) if (todo_list_parse_insn_buffer(r, opts, new_todo.buf.buf, &new_todo) < 0)
BUG("invalid todo list after expanding IDs:\n%s", BUG("invalid todo list after expanding IDs:\n%s",
new_todo.buf.buf); new_todo.buf.buf);

View file

@ -136,8 +136,8 @@ struct todo_list {
.buf = STRBUF_INIT, \ .buf = STRBUF_INIT, \
} }
int todo_list_parse_insn_buffer(struct repository *r, char *buf, int todo_list_parse_insn_buffer(struct repository *r, struct replay_opts *opts,
struct todo_list *todo_list); char *buf, struct todo_list *todo_list);
int todo_list_write_to_file(struct repository *r, struct todo_list *todo_list, int todo_list_write_to_file(struct repository *r, struct todo_list *todo_list,
const char *file, const char *shortrevisions, const char *file, const char *shortrevisions,
const char *shortonto, int num, unsigned flags); const char *shortonto, int num, unsigned flags);

View file

@ -2215,6 +2215,51 @@ test_expect_success 'bad labels and refs rejected when parsing todo list' '
test_path_is_missing execed test_path_is_missing execed
' '
test_expect_success 'non-merge commands reject merge commits' '
test_when_finished "test_might_fail git rebase --abort" &&
git checkout E &&
git merge I &&
oid=$(git rev-parse HEAD) &&
cat >todo <<-EOF &&
pick $oid
reword $oid
edit $oid
fixup $oid
squash $oid
EOF
(
set_replace_editor todo &&
test_must_fail git rebase -i HEAD 2>actual
) &&
cat >expect <<-EOF &&
error: ${SQ}pick${SQ} does not accept merge commits
hint: ${SQ}pick${SQ} does not take a merge commit. If you wanted to
hint: replay the merge, use ${SQ}merge -C${SQ} on the commit.
hint: Disable this message with "git config advice.rebaseTodoError false"
error: invalid line 1: pick $oid
error: ${SQ}reword${SQ} does not accept merge commits
hint: ${SQ}reword${SQ} does not take a merge commit. If you wanted to
hint: replay the merge and reword the commit message, use
hint: ${SQ}merge -c${SQ} on the commit
hint: Disable this message with "git config advice.rebaseTodoError false"
error: invalid line 2: reword $oid
error: ${SQ}edit${SQ} does not accept merge commits
hint: ${SQ}edit${SQ} does not take a merge commit. If you wanted to
hint: replay the merge, use ${SQ}merge -C${SQ} on the commit, and then
hint: ${SQ}break${SQ} to give the control back to you so that you can
hint: do ${SQ}git commit --amend && git rebase --continue${SQ}.
hint: Disable this message with "git config advice.rebaseTodoError false"
error: invalid line 3: edit $oid
error: cannot squash merge commit into another commit
error: invalid line 4: fixup $oid
error: cannot squash merge commit into another commit
error: invalid line 5: squash $oid
You can fix this with ${SQ}git rebase --edit-todo${SQ} and then run ${SQ}git rebase --continue${SQ}.
Or you can abort the rebase with ${SQ}git rebase --abort${SQ}.
EOF
test_cmp expect actual
'
# This must be the last test in this file # This must be the last test in this file
test_expect_success '$EDITOR and friends are unchanged' ' test_expect_success '$EDITOR and friends are unchanged' '
test_editor_unchanged test_editor_unchanged