зеркало из https://github.com/microsoft/git.git
sequencer (rebase -i): add support for the 'fixup' and 'squash' commands
This is a huge patch, and at the same time a huge step forward to execute the performance-critical parts of the interactive rebase in a builtin command. Since 'fixup' and 'squash' are not only similar, but also need to know about each other (we want to reduce a series of fixups/squashes into a single, final commit message edit, from the user's point of view), we really have to implement them both at the same time. Most of the actual work is done by the existing code path that already handles the "pick" and the "edit" commands; We added support for other features (e.g. to amend the commit message) in the patches leading up to this one, yet there are still quite a few bits in this patch that simply would not make sense as individual patches (such as: determining whether there was anything to "fix up" in the "todo" script, etc). In theory, it would be possible to reuse the fast-forward code path also for the fixup and the squash code paths, but in practice this would make the code less readable. The end result cannot be fast-forwarded anyway, therefore let's just extend the cherry-picking code path for now. Since the sequencer parses the entire `git-rebase-todo` script in one go, fixup or squash commands without a preceding pick can be reported early (in git-rebase--interactive, we could only report such errors just before executing the fixup/squash). Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Родитель
1df6df0c18
Коммит
6e98de72c0
229
sequencer.c
229
sequencer.c
|
@ -46,6 +46,35 @@ static GIT_PATH_FUNC(rebase_path_todo, "rebase-merge/git-rebase-todo")
|
||||||
* actions.
|
* actions.
|
||||||
*/
|
*/
|
||||||
static GIT_PATH_FUNC(rebase_path_done, "rebase-merge/done")
|
static GIT_PATH_FUNC(rebase_path_done, "rebase-merge/done")
|
||||||
|
/*
|
||||||
|
* The commit message that is planned to be used for any changes that
|
||||||
|
* need to be committed following a user interaction.
|
||||||
|
*/
|
||||||
|
static GIT_PATH_FUNC(rebase_path_message, "rebase-merge/message")
|
||||||
|
/*
|
||||||
|
* The file into which is accumulated the suggested commit message for
|
||||||
|
* squash/fixup commands. When the first of a series of squash/fixups
|
||||||
|
* is seen, the file is created and the commit message from the
|
||||||
|
* previous commit and from the first squash/fixup commit are written
|
||||||
|
* to it. The commit message for each subsequent squash/fixup commit
|
||||||
|
* is appended to the file as it is processed.
|
||||||
|
*
|
||||||
|
* The first line of the file is of the form
|
||||||
|
* # This is a combination of $count commits.
|
||||||
|
* where $count is the number of commits whose messages have been
|
||||||
|
* written to the file so far (including the initial "pick" commit).
|
||||||
|
* Each time that a commit message is processed, this line is read and
|
||||||
|
* updated. It is deleted just before the combined commit is made.
|
||||||
|
*/
|
||||||
|
static GIT_PATH_FUNC(rebase_path_squash_msg, "rebase-merge/message-squash")
|
||||||
|
/*
|
||||||
|
* If the current series of squash/fixups has not yet included a squash
|
||||||
|
* command, then this file exists and holds the commit message of the
|
||||||
|
* original "pick" commit. (If the series ends without a "squash"
|
||||||
|
* command, then this can be used as the commit message of the combined
|
||||||
|
* commit without opening the editor.)
|
||||||
|
*/
|
||||||
|
static GIT_PATH_FUNC(rebase_path_fixup_msg, "rebase-merge/message-fixup")
|
||||||
/*
|
/*
|
||||||
* A script to set the GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and
|
* A script to set the GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and
|
||||||
* GIT_AUTHOR_DATE that will be used for the commit that is currently
|
* GIT_AUTHOR_DATE that will be used for the commit that is currently
|
||||||
|
@ -641,6 +670,8 @@ enum todo_command {
|
||||||
TODO_PICK = 0,
|
TODO_PICK = 0,
|
||||||
TODO_REVERT,
|
TODO_REVERT,
|
||||||
TODO_EDIT,
|
TODO_EDIT,
|
||||||
|
TODO_FIXUP,
|
||||||
|
TODO_SQUASH,
|
||||||
/* commands that do something else than handling a single commit */
|
/* commands that do something else than handling a single commit */
|
||||||
TODO_EXEC,
|
TODO_EXEC,
|
||||||
/* commands that do nothing but are counted for reporting progress */
|
/* commands that do nothing but are counted for reporting progress */
|
||||||
|
@ -651,6 +682,8 @@ static const char *todo_command_strings[] = {
|
||||||
"pick",
|
"pick",
|
||||||
"revert",
|
"revert",
|
||||||
"edit",
|
"edit",
|
||||||
|
"fixup",
|
||||||
|
"squash",
|
||||||
"exec",
|
"exec",
|
||||||
"noop"
|
"noop"
|
||||||
};
|
};
|
||||||
|
@ -667,15 +700,114 @@ static int is_noop(const enum todo_command command)
|
||||||
return TODO_NOOP <= (size_t)command;
|
return TODO_NOOP <= (size_t)command;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int do_pick_commit(enum todo_command command, struct commit *commit,
|
static int is_fixup(enum todo_command command)
|
||||||
struct replay_opts *opts)
|
|
||||||
{
|
{
|
||||||
|
return command == TODO_FIXUP || command == TODO_SQUASH;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int update_squash_messages(enum todo_command command,
|
||||||
|
struct commit *commit, struct replay_opts *opts)
|
||||||
|
{
|
||||||
|
struct strbuf buf = STRBUF_INIT;
|
||||||
|
int count, res;
|
||||||
|
const char *message, *body;
|
||||||
|
|
||||||
|
if (file_exists(rebase_path_squash_msg())) {
|
||||||
|
struct strbuf header = STRBUF_INIT;
|
||||||
|
char *eol, *p;
|
||||||
|
|
||||||
|
if (strbuf_read_file(&buf, rebase_path_squash_msg(), 2048) <= 0)
|
||||||
|
return error(_("could not read '%s'"),
|
||||||
|
rebase_path_squash_msg());
|
||||||
|
|
||||||
|
p = buf.buf + 1;
|
||||||
|
eol = strchrnul(buf.buf, '\n');
|
||||||
|
if (buf.buf[0] != comment_line_char ||
|
||||||
|
(p += strcspn(p, "0123456789\n")) == eol)
|
||||||
|
return error(_("unexpected 1st line of squash message:"
|
||||||
|
"\n\n\t%.*s"),
|
||||||
|
(int)(eol - buf.buf), buf.buf);
|
||||||
|
count = strtol(p, NULL, 10);
|
||||||
|
|
||||||
|
if (count < 1)
|
||||||
|
return error(_("invalid 1st line of squash message:\n"
|
||||||
|
"\n\t%.*s"),
|
||||||
|
(int)(eol - buf.buf), buf.buf);
|
||||||
|
|
||||||
|
strbuf_addf(&header, "%c ", comment_line_char);
|
||||||
|
strbuf_addf(&header,
|
||||||
|
_("This is a combination of %d commits."), ++count);
|
||||||
|
strbuf_splice(&buf, 0, eol - buf.buf, header.buf, header.len);
|
||||||
|
strbuf_release(&header);
|
||||||
|
} else {
|
||||||
|
unsigned char head[20];
|
||||||
|
struct commit *head_commit;
|
||||||
|
const char *head_message, *body;
|
||||||
|
|
||||||
|
if (get_sha1("HEAD", head))
|
||||||
|
return error(_("need a HEAD to fixup"));
|
||||||
|
if (!(head_commit = lookup_commit_reference(head)))
|
||||||
|
return error(_("could not read HEAD"));
|
||||||
|
if (!(head_message = get_commit_buffer(head_commit, NULL)))
|
||||||
|
return error(_("could not read HEAD's commit message"));
|
||||||
|
|
||||||
|
find_commit_subject(head_message, &body);
|
||||||
|
if (write_message(body, strlen(body),
|
||||||
|
rebase_path_fixup_msg(), 0)) {
|
||||||
|
unuse_commit_buffer(head_commit, head_message);
|
||||||
|
return error(_("cannot write '%s'"),
|
||||||
|
rebase_path_fixup_msg());
|
||||||
|
}
|
||||||
|
|
||||||
|
count = 2;
|
||||||
|
strbuf_addf(&buf, "%c ", comment_line_char);
|
||||||
|
strbuf_addf(&buf, _("This is a combination of %d commits."),
|
||||||
|
count);
|
||||||
|
strbuf_addf(&buf, "\n%c ", comment_line_char);
|
||||||
|
strbuf_addstr(&buf, _("This is the 1st commit message:"));
|
||||||
|
strbuf_addstr(&buf, "\n\n");
|
||||||
|
strbuf_addstr(&buf, body);
|
||||||
|
|
||||||
|
unuse_commit_buffer(head_commit, head_message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(message = get_commit_buffer(commit, NULL)))
|
||||||
|
return error(_("could not read commit message of %s"),
|
||||||
|
oid_to_hex(&commit->object.oid));
|
||||||
|
find_commit_subject(message, &body);
|
||||||
|
|
||||||
|
if (command == TODO_SQUASH) {
|
||||||
|
unlink(rebase_path_fixup_msg());
|
||||||
|
strbuf_addf(&buf, "\n%c ", comment_line_char);
|
||||||
|
strbuf_addf(&buf, _("This is the commit message #%d:"), count);
|
||||||
|
strbuf_addstr(&buf, "\n\n");
|
||||||
|
strbuf_addstr(&buf, body);
|
||||||
|
} else if (command == TODO_FIXUP) {
|
||||||
|
strbuf_addf(&buf, "\n%c ", comment_line_char);
|
||||||
|
strbuf_addf(&buf, _("The commit message #%d will be skipped:"),
|
||||||
|
count);
|
||||||
|
strbuf_addstr(&buf, "\n\n");
|
||||||
|
strbuf_add_commented_lines(&buf, body, strlen(body));
|
||||||
|
} else
|
||||||
|
return error(_("unknown command: %d"), command);
|
||||||
|
unuse_commit_buffer(commit, message);
|
||||||
|
|
||||||
|
res = write_message(buf.buf, buf.len, rebase_path_squash_msg(), 0);
|
||||||
|
strbuf_release(&buf);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int do_pick_commit(enum todo_command command, struct commit *commit,
|
||||||
|
struct replay_opts *opts, int final_fixup)
|
||||||
|
{
|
||||||
|
int edit = opts->edit, cleanup_commit_message = 0;
|
||||||
|
const char *msg_file = edit ? NULL : git_path_merge_msg();
|
||||||
unsigned char head[20];
|
unsigned char head[20];
|
||||||
struct commit *base, *next, *parent;
|
struct commit *base, *next, *parent;
|
||||||
const char *base_label, *next_label;
|
const char *base_label, *next_label;
|
||||||
struct commit_message msg = { NULL, NULL, NULL, NULL };
|
struct commit_message msg = { NULL, NULL, NULL, NULL };
|
||||||
struct strbuf msgbuf = STRBUF_INIT;
|
struct strbuf msgbuf = STRBUF_INIT;
|
||||||
int res, unborn = 0, allow;
|
int res, unborn = 0, amend = 0, allow;
|
||||||
|
|
||||||
if (opts->no_commit) {
|
if (opts->no_commit) {
|
||||||
/*
|
/*
|
||||||
|
@ -720,7 +852,7 @@ static int do_pick_commit(enum todo_command command, struct commit *commit,
|
||||||
else
|
else
|
||||||
parent = commit->parents->item;
|
parent = commit->parents->item;
|
||||||
|
|
||||||
if (opts->allow_ff &&
|
if (opts->allow_ff && !is_fixup(command) &&
|
||||||
((parent && !hashcmp(parent->object.oid.hash, head)) ||
|
((parent && !hashcmp(parent->object.oid.hash, head)) ||
|
||||||
(!parent && unborn)))
|
(!parent && unborn)))
|
||||||
return fast_forward_to(commit->object.oid.hash, head, unborn, opts);
|
return fast_forward_to(commit->object.oid.hash, head, unborn, opts);
|
||||||
|
@ -779,6 +911,27 @@ static int do_pick_commit(enum todo_command command, struct commit *commit,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (is_fixup(command)) {
|
||||||
|
if (update_squash_messages(command, commit, opts))
|
||||||
|
return -1;
|
||||||
|
amend = 1;
|
||||||
|
if (!final_fixup)
|
||||||
|
msg_file = rebase_path_squash_msg();
|
||||||
|
else if (file_exists(rebase_path_fixup_msg())) {
|
||||||
|
cleanup_commit_message = 1;
|
||||||
|
msg_file = rebase_path_fixup_msg();
|
||||||
|
} else {
|
||||||
|
const char *dest = git_path("SQUASH_MSG");
|
||||||
|
unlink(dest);
|
||||||
|
if (copy_file(dest, rebase_path_squash_msg(), 0666))
|
||||||
|
return error(_("could not rename '%s' to '%s'"),
|
||||||
|
rebase_path_squash_msg(), dest);
|
||||||
|
unlink(git_path("MERGE_MSG"));
|
||||||
|
msg_file = dest;
|
||||||
|
edit = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!opts->strategy || !strcmp(opts->strategy, "recursive") || command == TODO_REVERT) {
|
if (!opts->strategy || !strcmp(opts->strategy, "recursive") || command == TODO_REVERT) {
|
||||||
res = do_recursive_merge(base, next, base_label, next_label,
|
res = do_recursive_merge(base, next, base_label, next_label,
|
||||||
head, &msgbuf, opts);
|
head, &msgbuf, opts);
|
||||||
|
@ -834,8 +987,13 @@ static int do_pick_commit(enum todo_command command, struct commit *commit,
|
||||||
goto leave;
|
goto leave;
|
||||||
}
|
}
|
||||||
if (!opts->no_commit)
|
if (!opts->no_commit)
|
||||||
res = run_git_commit(opts->edit ? NULL : git_path_merge_msg(),
|
res = run_git_commit(msg_file, opts, allow, edit, amend,
|
||||||
opts, allow, opts->edit, 0, 0);
|
cleanup_commit_message);
|
||||||
|
|
||||||
|
if (!res && final_fixup) {
|
||||||
|
unlink(rebase_path_fixup_msg());
|
||||||
|
unlink(rebase_path_squash_msg());
|
||||||
|
}
|
||||||
|
|
||||||
leave:
|
leave:
|
||||||
free_message(commit, &msg);
|
free_message(commit, &msg);
|
||||||
|
@ -976,7 +1134,7 @@ static int parse_insn_buffer(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;
|
||||||
int i, res = 0;
|
int i, res = 0, fixup_okay = file_exists(rebase_path_done());
|
||||||
|
|
||||||
for (i = 1; *p; i++, p = next_p) {
|
for (i = 1; *p; i++, p = next_p) {
|
||||||
char *eol = strchrnul(p, '\n');
|
char *eol = strchrnul(p, '\n');
|
||||||
|
@ -991,8 +1149,16 @@ static int parse_insn_buffer(char *buf, struct todo_list *todo_list)
|
||||||
if (parse_insn_line(item, p, eol)) {
|
if (parse_insn_line(item, 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 = -1;
|
item->command = TODO_NOOP;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fixup_okay)
|
||||||
|
; /* do nothing */
|
||||||
|
else if (is_fixup(item->command))
|
||||||
|
return error(_("cannot '%s' without a previous commit"),
|
||||||
|
command_to_string(item->command));
|
||||||
|
else if (!is_noop(item->command))
|
||||||
|
fixup_okay = 1;
|
||||||
}
|
}
|
||||||
if (!todo_list->nr)
|
if (!todo_list->nr)
|
||||||
return error(_("no commits parsed."));
|
return error(_("no commits parsed."));
|
||||||
|
@ -1435,6 +1601,20 @@ static int error_with_patch(struct commit *commit,
|
||||||
return exit_code;
|
return exit_code;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int error_failed_squash(struct commit *commit,
|
||||||
|
struct replay_opts *opts, int subject_len, const char *subject)
|
||||||
|
{
|
||||||
|
if (rename(rebase_path_squash_msg(), rebase_path_message()))
|
||||||
|
return error(_("could not rename '%s' to '%s'"),
|
||||||
|
rebase_path_squash_msg(), rebase_path_message());
|
||||||
|
unlink(rebase_path_fixup_msg());
|
||||||
|
unlink(git_path("MERGE_MSG"));
|
||||||
|
if (copy_file(git_path("MERGE_MSG"), rebase_path_message(), 0666))
|
||||||
|
return error(_("could not copy '%s' to '%s'"),
|
||||||
|
rebase_path_message(), git_path("MERGE_MSG"));
|
||||||
|
return error_with_patch(commit, subject, subject_len, opts, 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
static int do_exec(const char *command_line)
|
static int do_exec(const char *command_line)
|
||||||
{
|
{
|
||||||
const char *child_argv[] = { NULL, NULL };
|
const char *child_argv[] = { NULL, NULL };
|
||||||
|
@ -1475,6 +1655,21 @@ static int do_exec(const char *command_line)
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int is_final_fixup(struct todo_list *todo_list)
|
||||||
|
{
|
||||||
|
int i = todo_list->current;
|
||||||
|
|
||||||
|
if (!is_fixup(todo_list->items[i].command))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
while (++i < todo_list->nr)
|
||||||
|
if (is_fixup(todo_list->items[i].command))
|
||||||
|
return 0;
|
||||||
|
else if (!is_noop(todo_list->items[i].command))
|
||||||
|
break;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
|
static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
|
||||||
{
|
{
|
||||||
int res = 0;
|
int res = 0;
|
||||||
|
@ -1490,9 +1685,15 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
|
||||||
struct todo_item *item = todo_list->items + todo_list->current;
|
struct todo_item *item = todo_list->items + todo_list->current;
|
||||||
if (save_todo(todo_list, opts))
|
if (save_todo(todo_list, opts))
|
||||||
return -1;
|
return -1;
|
||||||
if (item->command <= TODO_EDIT) {
|
if (is_rebase_i(opts)) {
|
||||||
|
unlink(rebase_path_message());
|
||||||
|
unlink(rebase_path_author_script());
|
||||||
|
unlink(rebase_path_stopped_sha());
|
||||||
|
unlink(rebase_path_amend());
|
||||||
|
}
|
||||||
|
if (item->command <= TODO_SQUASH) {
|
||||||
res = do_pick_commit(item->command, item->commit,
|
res = do_pick_commit(item->command, item->commit,
|
||||||
opts);
|
opts, is_final_fixup(todo_list));
|
||||||
if (item->command == TODO_EDIT) {
|
if (item->command == TODO_EDIT) {
|
||||||
struct commit *commit = item->commit;
|
struct commit *commit = item->commit;
|
||||||
if (!res)
|
if (!res)
|
||||||
|
@ -1503,6 +1704,12 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
|
||||||
item->arg, item->arg_len, opts, res,
|
item->arg, item->arg_len, opts, res,
|
||||||
!res);
|
!res);
|
||||||
}
|
}
|
||||||
|
if (res && is_fixup(item->command)) {
|
||||||
|
if (res == 1)
|
||||||
|
intend_to_amend();
|
||||||
|
return error_failed_squash(item->commit, opts,
|
||||||
|
item->arg_len, item->arg);
|
||||||
|
}
|
||||||
} else if (item->command == TODO_EXEC) {
|
} else if (item->command == TODO_EXEC) {
|
||||||
char *end_of_arg = (char *)(item->arg + item->arg_len);
|
char *end_of_arg = (char *)(item->arg + item->arg_len);
|
||||||
int saved = *end_of_arg;
|
int saved = *end_of_arg;
|
||||||
|
@ -1601,7 +1808,7 @@ static int single_pick(struct commit *cmit, struct replay_opts *opts)
|
||||||
{
|
{
|
||||||
setenv(GIT_REFLOG_ACTION, action_name(opts), 0);
|
setenv(GIT_REFLOG_ACTION, action_name(opts), 0);
|
||||||
return do_pick_commit(opts->action == REPLAY_PICK ?
|
return do_pick_commit(opts->action == REPLAY_PICK ?
|
||||||
TODO_PICK : TODO_REVERT, cmit, opts);
|
TODO_PICK : TODO_REVERT, cmit, opts, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
int sequencer_pick_revisions(struct replay_opts *opts)
|
int sequencer_pick_revisions(struct replay_opts *opts)
|
||||||
|
|
Загрузка…
Ссылка в новой задаче