Skip to content

Commit 5a11363

Browse files
committed
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). Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
1 parent e1ca6cd commit 5a11363

1 file changed

Lines changed: 218 additions & 9 deletions

File tree

sequencer.c

Lines changed: 218 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,30 @@ static GIT_PATH_FUNC(git_path_rebase_done, "rebase-merge/done")
4848
* need to be committed following a user interaction.
4949
*/
5050
static GIT_PATH_FUNC(git_path_rebase_msg, "rebase-merge/message")
51+
/*
52+
* The file into which is accumulated the suggested commit message for
53+
* squash/fixup commands. When the first of a series of squash/fixups
54+
* is seen, the file is created and the commit message from the
55+
* previous commit and from the first squash/fixup commit are written
56+
* to it. The commit message for each subsequent squash/fixup commit
57+
* is appended to the file as it is processed.
58+
*
59+
* The first line of the file is of the form
60+
* # This is a combination of $count commits.
61+
* where $count is the number of commits whose messages have been
62+
* written to the file so far (including the initial "pick" commit).
63+
* Each time that a commit message is processed, this line is read and
64+
* updated. It is deleted just before the combined commit is made.
65+
*/
66+
static GIT_PATH_FUNC(squash_msg, "rebase-merge/message-squash")
67+
/*
68+
* If the current series of squash/fixups has not yet included a squash
69+
* command, then this file exists and holds the commit message of the
70+
* original "pick" commit. (If the series ends without a "squash"
71+
* command, then this can be used as the commit message of the combined
72+
* commit without opening the editor.)
73+
*/
74+
static GIT_PATH_FUNC(fixup_msg, "rebase-merge/message-fixup")
5175
/*
5276
* A script to set the GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and
5377
* GIT_AUTHOR_DATE that will be used for the commit that is currently
@@ -591,6 +615,8 @@ enum todo_command {
591615
TODO_PICK,
592616
TODO_REVERT,
593617
TODO_EDIT,
618+
TODO_FIXUP,
619+
TODO_SQUASH,
594620
TODO_EXEC,
595621
TODO_NOOP
596622
};
@@ -599,6 +625,8 @@ static const char *todo_command_strings[] = {
599625
"pick",
600626
"revert",
601627
"edit",
628+
"fixup",
629+
"squash",
602630
"exec",
603631
"noop"
604632
};
@@ -610,16 +638,126 @@ static const char *command_to_string(const enum todo_command command)
610638
die("Unknown command: %d", command);
611639
}
612640

641+
static int is_fixup(enum todo_command command)
642+
{
643+
return command == TODO_FIXUP || command == TODO_SQUASH;
644+
}
645+
646+
static const char *nth_for_number(int n)
647+
{
648+
int n1 = n % 10, n10 = n % 100;
649+
650+
if (n1 == 1 && n10 != 11)
651+
return "st";
652+
if (n1 == 2 && n10 != 12)
653+
return "nd";
654+
if (n1 == 3 && n10 != 13)
655+
return "rd";
656+
return "th";
657+
}
658+
659+
static int update_squash_messages(enum todo_command command,
660+
struct commit *commit, struct replay_opts *opts)
661+
{
662+
struct strbuf buf = STRBUF_INIT;
663+
int count;
664+
const char *message, *body;
665+
666+
if (file_exists(squash_msg())) {
667+
char *p, *p2;
668+
669+
if (strbuf_read_file(&buf, squash_msg(), 2048) <= 0)
670+
return error("Could not read %s", squash_msg());
671+
672+
if (buf.buf[0] == '\n' || !skip_prefix(buf.buf + 1,
673+
" This is a combination of ",
674+
(const char **)&p))
675+
return error("Unexpected 1st line of squash message:\n"
676+
"\n\t%.*s",
677+
(int)(strchrnul(buf.buf, '\n') - buf.buf),
678+
buf.buf);
679+
count = strtol(p, &p2, 10);
680+
681+
if (count < 1 || *p2 != ' ')
682+
return error("Invalid 1st line of squash message:\n"
683+
"\n\t%.*s",
684+
(int)(strchrnul(buf.buf, '\n') - buf.buf),
685+
buf.buf);
686+
687+
sprintf((char *)p, "%d", ++count);
688+
if (!*p2)
689+
*p2 = ' ';
690+
else {
691+
*(++p2) = 'c';
692+
strbuf_insert(&buf, p2 - buf.buf, " ", 1);
693+
}
694+
}
695+
else {
696+
unsigned char head[20];
697+
struct commit *head_commit;
698+
const char *head_message, *body;
699+
700+
if (get_sha1("HEAD", head))
701+
return error("Need a HEAD to fixup");
702+
if (!(head_commit = lookup_commit_reference(head)))
703+
return error("Could not read HEAD");
704+
if (!(head_message = get_commit_buffer(head_commit, NULL)))
705+
return error("Could not read HEAD's commit message");
706+
707+
body = strstr(head_message, "\n\n");
708+
if (!body)
709+
body = "";
710+
if (write_file(fixup_msg(), body))
711+
return error("Cannot write %s", fixup_msg());
712+
713+
count = 2;
714+
strbuf_addf(&buf, "%c This is a combination of 2 commits.\n"
715+
"%c The first commit's message is:\n\n%s",
716+
comment_line_char, comment_line_char, body);
717+
718+
unuse_commit_buffer(head_commit, head_message);
719+
}
720+
721+
if (!(message = get_commit_buffer(commit, NULL)))
722+
return error("Could not read commit message of %s",
723+
oid_to_hex(&commit->object.oid));
724+
body = strstr(message, "\n\n");
725+
if (!body)
726+
body = "";
727+
728+
if (command == TODO_SQUASH) {
729+
unlink(fixup_msg());
730+
strbuf_addf(&buf, "\n%c This is the %d%s commit message:\n\n%s",
731+
comment_line_char,
732+
count, nth_for_number(count), body);
733+
}
734+
else if (command == TODO_FIXUP) {
735+
strbuf_addf(&buf,
736+
"\n%c The %d%s commit message will be skipped:\n\n",
737+
comment_line_char, count, nth_for_number(count));
738+
strbuf_add_commented_lines(&buf, body, strlen(body));
739+
}
740+
else
741+
return error("Unknown command: %d", command);
742+
unuse_commit_buffer(commit, message);
743+
744+
write_file(squash_msg(), "%s", buf.buf);
745+
strbuf_release(&buf);
746+
return 0;
747+
}
748+
613749

614750
static int do_pick_commit(enum todo_command command, struct commit *commit,
615-
struct replay_opts *opts)
751+
struct replay_opts *opts, int final_fixup)
616752
{
753+
const char *msg_file = git_path_merge_msg();
754+
int edit = opts->edit;
617755
unsigned char head[20];
618756
struct commit *base, *next, *parent;
619757
const char *base_label, *next_label;
620758
struct commit_message msg = { NULL, NULL, NULL, NULL };
621759
struct strbuf msgbuf = STRBUF_INIT;
622-
int res, unborn = 0, allow;
760+
int res, unborn = 0, amend = 0, allow;
623761

624762
if (opts->no_commit) {
625763
/*
@@ -665,7 +803,7 @@ static int do_pick_commit(enum todo_command command, struct commit *commit,
665803
else
666804
parent = commit->parents->item;
667805

668-
if (opts->allow_ff &&
806+
if (opts->allow_ff && !is_fixup(command) &&
669807
((parent && !hashcmp(parent->object.oid.hash, head)) ||
670808
(!parent && unborn)))
671809
return fast_forward_to(commit->object.oid.hash, head, unborn, opts);
@@ -732,6 +870,27 @@ static int do_pick_commit(enum todo_command command, struct commit *commit,
732870
}
733871
}
734872

873+
if (is_fixup(command)) {
874+
/* TODO: reflog action */
875+
if (update_squash_messages(command, commit, opts))
876+
return -1;
877+
amend = 1;
878+
if (!final_fixup)
879+
msg_file = squash_msg();
880+
else if (file_exists(fixup_msg()))
881+
msg_file = fixup_msg();
882+
else {
883+
const char *dest = git_path("SQUASH_MSG");
884+
unlink(dest);
885+
if (copy_file(dest, squash_msg(), 0666))
886+
return error("Could not rename %s to "
887+
"%s", squash_msg(), dest);
888+
unlink(git_path("MERGE_MSG"));
889+
msg_file = dest;
890+
edit = 1;
891+
}
892+
}
893+
735894
if (!opts->strategy || !strcmp(opts->strategy, "recursive") || command == TODO_REVERT) {
736895
res = do_recursive_merge(base, next, base_label, next_label,
737896
head, &msgbuf, opts);
@@ -779,8 +938,12 @@ static int do_pick_commit(enum todo_command command, struct commit *commit,
779938
goto leave;
780939
}
781940
if (!opts->no_commit)
782-
res = sequencer_commit(git_path_merge_msg(), opts, allow,
783-
opts->edit, 0);
941+
res = sequencer_commit(msg_file, opts, allow, edit, amend);
942+
943+
if (!res && final_fixup) {
944+
unlink(fixup_msg());
945+
unlink(squash_msg());
946+
}
784947

785948
leave:
786949
free_message(commit, &msg);
@@ -915,7 +1078,7 @@ static int parse_insn_buffer(char *buf, struct todo_list *todo_list,
9151078
{
9161079
struct todo_item *item;
9171080
char *p = buf;
918-
int i;
1081+
int i, fixup_okay = file_exists(git_path_rebase_done());
9191082

9201083
for (i = 1; *p; i++) {
9211084
char *eol = strchrnul(p, '\n');
@@ -924,6 +1087,13 @@ static int parse_insn_buffer(char *buf, struct todo_list *todo_list,
9241087
item->offset_in_buf = p - todo_list->buf.buf;
9251088
if (parse_insn_line(item, p, eol, opts))
9261089
return error(_("Could not parse line %d."), i);
1090+
if (fixup_okay)
1091+
; /* do nothing */
1092+
else if (is_fixup(item->command))
1093+
return error("Cannot '%s' without a previous commit",
1094+
command_to_string(item->command));
1095+
else if (item->command != TODO_NOOP)
1096+
fixup_okay = 1;
9271097
p = *eol ? eol + 1 : eol;
9281098
}
9291099
if (!todo_list->nr)
@@ -1249,6 +1419,22 @@ static int error_with_patch(struct commit *commit,
12491419
return exit_code;
12501420
}
12511421

1422+
static int error_failed_squash(struct commit *commit,
1423+
struct replay_opts *opts, int subject_len, const char *subject)
1424+
{
1425+
if (rename(squash_msg(), git_path_rebase_msg()))
1426+
return error("Could not rename %s to %s",
1427+
squash_msg(), git_path_rebase_msg());
1428+
unlink(fixup_msg());
1429+
unlink(git_path("MERGE_MSG"));
1430+
if (copy_file(git_path("MERGE_MSG"), git_path_rebase_msg(), 0666))
1431+
return error("Could not copy %s to %s", git_path_rebase_msg(),
1432+
git_path("MERGE_MSG"));
1433+
warning("\nCould not apply %s... %.*s", short_commit_name(commit),
1434+
subject_len, subject);
1435+
return error_with_patch(commit, opts, 1);
1436+
}
1437+
12521438
static int do_exec(const char *command_line)
12531439
{
12541440
const char *child_argv[] = { NULL, NULL };
@@ -1286,6 +1472,21 @@ static int do_exec(const char *command_line)
12861472
return status;
12871473
}
12881474

1475+
static int is_final_fixup(struct todo_list *todo_list)
1476+
{
1477+
int i = todo_list->current;
1478+
1479+
if (!is_fixup(todo_list->items[i].command))
1480+
return 0;
1481+
1482+
while (++i < todo_list->nr)
1483+
if (todo_list->items[i].command == TODO_NOOP)
1484+
continue;
1485+
else if (is_fixup(todo_list->items[i].command))
1486+
return 0;
1487+
return 1;
1488+
}
1489+
12891490
static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
12901491
{
12911492
int res = 0;
@@ -1301,9 +1502,14 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
13011502
struct todo_item *item = todo_list->items + todo_list->current;
13021503
if (save_todo(todo_list, opts))
13031504
return -1;
1304-
if (item->command <= TODO_EDIT) {
1505+
if (IS_REBASE_I()) {
1506+
unlink(git_path_rebase_msg());
1507+
unlink(author_script());
1508+
unlink(stopped_sha());
1509+
}
1510+
if (item->command <= TODO_SQUASH) {
13051511
res = do_pick_commit(item->command, item->commit,
1306-
opts);
1512+
opts, is_final_fixup(todo_list));
13071513
if (item->command == TODO_EDIT) {
13081514
struct commit *commit = item->commit;
13091515
if (!res)
@@ -1312,6 +1518,9 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
13121518
item->arg_len, item->arg);
13131519
return error_with_patch(commit, opts, res);
13141520
}
1521+
if (res && is_fixup(item->command))
1522+
return error_failed_squash(item->commit, opts,
1523+
item->arg_len, item->arg);
13151524
}
13161525
else if (item->command == TODO_EXEC) {
13171526
char *end_of_arg = (char *)(item->arg + item->arg_len);
@@ -1403,7 +1612,7 @@ static int single_pick(struct commit *cmit, struct replay_opts *opts)
14031612
{
14041613
setenv(GIT_REFLOG_ACTION, action_name(opts), 0);
14051614
return do_pick_commit(opts->action == REPLAY_PICK ?
1406-
TODO_PICK : TODO_REVERT, cmit, opts);
1615+
TODO_PICK : TODO_REVERT, cmit, opts, 0);
14071616
}
14081617

14091618
int sequencer_pick_revisions(struct replay_opts *opts)

0 commit comments

Comments
 (0)