Skip to content

Commit 4a3bf32

Browse files
committed
Merge branch 'js/rebase-i-clean-msg-after-fixup-continue'
"git rebase -i" sometimes left intermediate "# This is a combination of N commits" message meant for the human consumption inside an editor in the final result in certain corner cases, which has been fixed. * js/rebase-i-clean-msg-after-fixup-continue: rebase --skip: clean up commit message after a failed fixup/squash sequencer: always commit without editing when asked for rebase -i: Handle "combination of <n> commits" with GETTEXT_POISON rebase -i: demonstrate bugs with fixup!/squash! commit messages
2 parents 10174da + 15ef693 commit 4a3bf32

File tree

3 files changed

+200
-48
lines changed

3 files changed

+200
-48
lines changed

sequencer.c

Lines changed: 146 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,6 @@ static GIT_PATH_FUNC(rebase_path_message, "rebase-merge/message")
7474
* previous commit and from the first squash/fixup commit are written
7575
* to it. The commit message for each subsequent squash/fixup commit
7676
* is appended to the file as it is processed.
77-
*
78-
* The first line of the file is of the form
79-
* # This is a combination of $count commits.
80-
* where $count is the number of commits whose messages have been
81-
* written to the file so far (including the initial "pick" commit).
82-
* Each time that a commit message is processed, this line is read and
83-
* updated. It is deleted just before the combined commit is made.
8477
*/
8578
static GIT_PATH_FUNC(rebase_path_squash_msg, "rebase-merge/message-squash")
8679
/*
@@ -91,6 +84,11 @@ static GIT_PATH_FUNC(rebase_path_squash_msg, "rebase-merge/message-squash")
9184
* commit without opening the editor.)
9285
*/
9386
static GIT_PATH_FUNC(rebase_path_fixup_msg, "rebase-merge/message-fixup")
87+
/*
88+
* This file contains the list fixup/squash commands that have been
89+
* accumulated into message-fixup or message-squash so far.
90+
*/
91+
static GIT_PATH_FUNC(rebase_path_current_fixups, "rebase-merge/current-fixups")
9492
/*
9593
* A script to set the GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and
9694
* GIT_AUTHOR_DATE that will be used for the commit that is currently
@@ -253,6 +251,7 @@ int sequencer_remove_state(struct replay_opts *opts)
253251
for (i = 0; i < opts->xopts_nr; i++)
254252
free(opts->xopts[i]);
255253
free(opts->xopts);
254+
strbuf_release(&opts->current_fixups);
256255

257256
strbuf_addstr(&dir, get_dir(opts));
258257
remove_dir_recursively(&dir, 0);
@@ -718,6 +717,8 @@ static int run_git_commit(const char *defmsg, struct replay_opts *opts,
718717
argv_array_pushf(&cmd.args, "-S%s", opts->gpg_sign);
719718
if (defmsg)
720719
argv_array_pushl(&cmd.args, "-F", defmsg, NULL);
720+
else if (!(flags & EDIT_MSG))
721+
argv_array_pushl(&cmd.args, "-C", "HEAD", NULL);
721722
if ((flags & CLEANUP_MSG))
722723
argv_array_push(&cmd.args, "--cleanup=strip");
723724
if ((flags & EDIT_MSG))
@@ -1331,34 +1332,23 @@ static int update_squash_messages(enum todo_command command,
13311332
struct commit *commit, struct replay_opts *opts)
13321333
{
13331334
struct strbuf buf = STRBUF_INIT;
1334-
int count, res;
1335+
int res;
13351336
const char *message, *body;
13361337

1337-
if (file_exists(rebase_path_squash_msg())) {
1338+
if (opts->current_fixup_count > 0) {
13381339
struct strbuf header = STRBUF_INIT;
1339-
char *eol, *p;
1340+
char *eol;
13401341

1341-
if (strbuf_read_file(&buf, rebase_path_squash_msg(), 2048) <= 0)
1342+
if (strbuf_read_file(&buf, rebase_path_squash_msg(), 9) <= 0)
13421343
return error(_("could not read '%s'"),
13431344
rebase_path_squash_msg());
13441345

1345-
p = buf.buf + 1;
1346-
eol = strchrnul(buf.buf, '\n');
1347-
if (buf.buf[0] != comment_line_char ||
1348-
(p += strcspn(p, "0123456789\n")) == eol)
1349-
return error(_("unexpected 1st line of squash message:"
1350-
"\n\n\t%.*s"),
1351-
(int)(eol - buf.buf), buf.buf);
1352-
count = strtol(p, NULL, 10);
1353-
1354-
if (count < 1)
1355-
return error(_("invalid 1st line of squash message:\n"
1356-
"\n\t%.*s"),
1357-
(int)(eol - buf.buf), buf.buf);
1346+
eol = buf.buf[0] != comment_line_char ?
1347+
buf.buf : strchrnul(buf.buf, '\n');
13581348

13591349
strbuf_addf(&header, "%c ", comment_line_char);
1360-
strbuf_addf(&header,
1361-
_("This is a combination of %d commits."), ++count);
1350+
strbuf_addf(&header, _("This is a combination of %d commits."),
1351+
opts->current_fixup_count + 2);
13621352
strbuf_splice(&buf, 0, eol - buf.buf, header.buf, header.len);
13631353
strbuf_release(&header);
13641354
} else {
@@ -1381,10 +1371,8 @@ static int update_squash_messages(enum todo_command command,
13811371
rebase_path_fixup_msg());
13821372
}
13831373

1384-
count = 2;
13851374
strbuf_addf(&buf, "%c ", comment_line_char);
1386-
strbuf_addf(&buf, _("This is a combination of %d commits."),
1387-
count);
1375+
strbuf_addf(&buf, _("This is a combination of %d commits."), 2);
13881376
strbuf_addf(&buf, "\n%c ", comment_line_char);
13891377
strbuf_addstr(&buf, _("This is the 1st commit message:"));
13901378
strbuf_addstr(&buf, "\n\n");
@@ -1401,13 +1389,14 @@ static int update_squash_messages(enum todo_command command,
14011389
if (command == TODO_SQUASH) {
14021390
unlink(rebase_path_fixup_msg());
14031391
strbuf_addf(&buf, "\n%c ", comment_line_char);
1404-
strbuf_addf(&buf, _("This is the commit message #%d:"), count);
1392+
strbuf_addf(&buf, _("This is the commit message #%d:"),
1393+
++opts->current_fixup_count);
14051394
strbuf_addstr(&buf, "\n\n");
14061395
strbuf_addstr(&buf, body);
14071396
} else if (command == TODO_FIXUP) {
14081397
strbuf_addf(&buf, "\n%c ", comment_line_char);
14091398
strbuf_addf(&buf, _("The commit message #%d will be skipped:"),
1410-
count);
1399+
++opts->current_fixup_count);
14111400
strbuf_addstr(&buf, "\n\n");
14121401
strbuf_add_commented_lines(&buf, body, strlen(body));
14131402
} else
@@ -1416,6 +1405,17 @@ static int update_squash_messages(enum todo_command command,
14161405

14171406
res = write_message(buf.buf, buf.len, rebase_path_squash_msg(), 0);
14181407
strbuf_release(&buf);
1408+
1409+
if (!res) {
1410+
strbuf_addf(&opts->current_fixups, "%s%s %s",
1411+
opts->current_fixups.len ? "\n" : "",
1412+
command_to_string(command),
1413+
oid_to_hex(&commit->object.oid));
1414+
res = write_message(opts->current_fixups.buf,
1415+
opts->current_fixups.len,
1416+
rebase_path_current_fixups(), 0);
1417+
}
1418+
14191419
return res;
14201420
}
14211421

@@ -1678,6 +1678,9 @@ static int do_pick_commit(enum todo_command command, struct commit *commit,
16781678
if (!res && final_fixup) {
16791679
unlink(rebase_path_fixup_msg());
16801680
unlink(rebase_path_squash_msg());
1681+
unlink(rebase_path_current_fixups());
1682+
strbuf_reset(&opts->current_fixups);
1683+
opts->current_fixup_count = 0;
16811684
}
16821685

16831686
leave:
@@ -2054,6 +2057,16 @@ static int read_populate_opts(struct replay_opts *opts)
20542057
read_strategy_opts(opts, &buf);
20552058
strbuf_release(&buf);
20562059

2060+
if (read_oneliner(&opts->current_fixups,
2061+
rebase_path_current_fixups(), 1)) {
2062+
const char *p = opts->current_fixups.buf;
2063+
opts->current_fixup_count = 1;
2064+
while ((p = strchr(p, '\n'))) {
2065+
opts->current_fixup_count++;
2066+
p++;
2067+
}
2068+
}
2069+
20572070
return 0;
20582071
}
20592072

@@ -2400,10 +2413,9 @@ static int error_with_patch(struct commit *commit,
24002413
static int error_failed_squash(struct commit *commit,
24012414
struct replay_opts *opts, int subject_len, const char *subject)
24022415
{
2403-
if (rename(rebase_path_squash_msg(), rebase_path_message()))
2404-
return error(_("could not rename '%s' to '%s'"),
2416+
if (copy_file(rebase_path_message(), rebase_path_squash_msg(), 0666))
2417+
return error(_("could not copy '%s' to '%s'"),
24052418
rebase_path_squash_msg(), rebase_path_message());
2406-
unlink(rebase_path_fixup_msg());
24072419
unlink(git_path_merge_msg());
24082420
if (copy_file(git_path_merge_msg(), rebase_path_message(), 0666))
24092421
return error(_("could not copy '%s' to '%s'"),
@@ -2769,19 +2781,16 @@ static int continue_single_pick(void)
27692781
return run_command_v_opt(argv, RUN_GIT_CMD);
27702782
}
27712783

2772-
static int commit_staged_changes(struct replay_opts *opts)
2784+
static int commit_staged_changes(struct replay_opts *opts,
2785+
struct todo_list *todo_list)
27732786
{
27742787
unsigned int flags = ALLOW_EMPTY | EDIT_MSG;
2788+
unsigned int final_fixup = 0, is_clean;
27752789

27762790
if (has_unstaged_changes(1))
27772791
return error(_("cannot rebase: You have unstaged changes."));
2778-
if (!has_uncommitted_changes(0)) {
2779-
const char *cherry_pick_head = git_path_cherry_pick_head();
27802792

2781-
if (file_exists(cherry_pick_head) && unlink(cherry_pick_head))
2782-
return error(_("could not remove CHERRY_PICK_HEAD"));
2783-
return 0;
2784-
}
2793+
is_clean = !has_uncommitted_changes(0);
27852794

27862795
if (file_exists(rebase_path_amend())) {
27872796
struct strbuf rev = STRBUF_INIT;
@@ -2794,19 +2803,107 @@ static int commit_staged_changes(struct replay_opts *opts)
27942803
if (get_oid_hex(rev.buf, &to_amend))
27952804
return error(_("invalid contents: '%s'"),
27962805
rebase_path_amend());
2797-
if (oidcmp(&head, &to_amend))
2806+
if (!is_clean && oidcmp(&head, &to_amend))
27982807
return error(_("\nYou have uncommitted changes in your "
27992808
"working tree. Please, commit them\n"
28002809
"first and then run 'git rebase "
28012810
"--continue' again."));
2811+
/*
2812+
* When skipping a failed fixup/squash, we need to edit the
2813+
* commit message, the current fixup list and count, and if it
2814+
* was the last fixup/squash in the chain, we need to clean up
2815+
* the commit message and if there was a squash, let the user
2816+
* edit it.
2817+
*/
2818+
if (is_clean && !oidcmp(&head, &to_amend) &&
2819+
opts->current_fixup_count > 0 &&
2820+
file_exists(rebase_path_stopped_sha())) {
2821+
const char *p = opts->current_fixups.buf;
2822+
int len = opts->current_fixups.len;
2823+
2824+
opts->current_fixup_count--;
2825+
if (!len)
2826+
BUG("Incorrect current_fixups:\n%s", p);
2827+
while (len && p[len - 1] != '\n')
2828+
len--;
2829+
strbuf_setlen(&opts->current_fixups, len);
2830+
if (write_message(p, len, rebase_path_current_fixups(),
2831+
0) < 0)
2832+
return error(_("could not write file: '%s'"),
2833+
rebase_path_current_fixups());
2834+
2835+
/*
2836+
* If a fixup/squash in a fixup/squash chain failed, the
2837+
* commit message is already correct, no need to commit
2838+
* it again.
2839+
*
2840+
* Only if it is the final command in the fixup/squash
2841+
* chain, and only if the chain is longer than a single
2842+
* fixup/squash command (which was just skipped), do we
2843+
* actually need to re-commit with a cleaned up commit
2844+
* message.
2845+
*/
2846+
if (opts->current_fixup_count > 0 &&
2847+
!is_fixup(peek_command(todo_list, 0))) {
2848+
final_fixup = 1;
2849+
/*
2850+
* If there was not a single "squash" in the
2851+
* chain, we only need to clean up the commit
2852+
* message, no need to bother the user with
2853+
* opening the commit message in the editor.
2854+
*/
2855+
if (!starts_with(p, "squash ") &&
2856+
!strstr(p, "\nsquash "))
2857+
flags = (flags & ~EDIT_MSG) | CLEANUP_MSG;
2858+
} else if (is_fixup(peek_command(todo_list, 0))) {
2859+
/*
2860+
* We need to update the squash message to skip
2861+
* the latest commit message.
2862+
*/
2863+
struct commit *commit;
2864+
const char *path = rebase_path_squash_msg();
2865+
2866+
if (parse_head(&commit) ||
2867+
!(p = get_commit_buffer(commit, NULL)) ||
2868+
write_message(p, strlen(p), path, 0)) {
2869+
unuse_commit_buffer(commit, p);
2870+
return error(_("could not write file: "
2871+
"'%s'"), path);
2872+
}
2873+
unuse_commit_buffer(commit, p);
2874+
}
2875+
}
28022876

28032877
strbuf_release(&rev);
28042878
flags |= AMEND_MSG;
28052879
}
28062880

2807-
if (run_git_commit(rebase_path_message(), opts, flags))
2881+
if (is_clean) {
2882+
const char *cherry_pick_head = git_path_cherry_pick_head();
2883+
2884+
if (file_exists(cherry_pick_head) && unlink(cherry_pick_head))
2885+
return error(_("could not remove CHERRY_PICK_HEAD"));
2886+
if (!final_fixup)
2887+
return 0;
2888+
}
2889+
2890+
if (run_git_commit(final_fixup ? NULL : rebase_path_message(),
2891+
opts, flags))
28082892
return error(_("could not commit staged changes."));
28092893
unlink(rebase_path_amend());
2894+
if (final_fixup) {
2895+
unlink(rebase_path_fixup_msg());
2896+
unlink(rebase_path_squash_msg());
2897+
}
2898+
if (opts->current_fixup_count > 0) {
2899+
/*
2900+
* Whether final fixup or not, we just cleaned up the commit
2901+
* message...
2902+
*/
2903+
unlink(rebase_path_current_fixups());
2904+
strbuf_reset(&opts->current_fixups);
2905+
opts->current_fixup_count = 0;
2906+
}
28102907
return 0;
28112908
}
28122909

@@ -2818,14 +2915,16 @@ int sequencer_continue(struct replay_opts *opts)
28182915
if (read_and_refresh_cache(opts))
28192916
return -1;
28202917

2918+
if (read_populate_opts(opts))
2919+
return -1;
28212920
if (is_rebase_i(opts)) {
2822-
if (commit_staged_changes(opts))
2921+
if ((res = read_populate_todo(&todo_list, opts)))
2922+
goto release_todo_list;
2923+
if (commit_staged_changes(opts, &todo_list))
28232924
return -1;
28242925
} else if (!file_exists(get_todo_path(opts)))
28252926
return continue_single_pick();
2826-
if (read_populate_opts(opts))
2827-
return -1;
2828-
if ((res = read_populate_todo(&todo_list, opts)))
2927+
else if ((res = read_populate_todo(&todo_list, opts)))
28292928
goto release_todo_list;
28302929

28312930
if (!is_rebase_i(opts)) {

sequencer.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,14 @@ struct replay_opts {
4444
char **xopts;
4545
size_t xopts_nr, xopts_alloc;
4646

47+
/* Used by fixup/squash */
48+
struct strbuf current_fixups;
49+
int current_fixup_count;
50+
4751
/* Only used by REPLAY_NONE */
4852
struct rev_info *revs;
4953
};
50-
#define REPLAY_OPTS_INIT { -1 }
54+
#define REPLAY_OPTS_INIT { .action = -1, .current_fixups = STRBUF_INIT }
5155

5256
/* Call this to setup defaults before parsing command line options */
5357
void sequencer_init_config(struct replay_opts *opts);

t/t3418-rebase-continue.sh

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,55 @@ test_expect_success 'rebase passes merge strategy options correctly' '
8888
git rebase --continue
8989
'
9090

91+
test_expect_success '--skip after failed fixup cleans commit message' '
92+
test_when_finished "test_might_fail git rebase --abort" &&
93+
git checkout -b with-conflicting-fixup &&
94+
test_commit wants-fixup &&
95+
test_commit "fixup! wants-fixup" wants-fixup.t 1 wants-fixup-1 &&
96+
test_commit "fixup! wants-fixup" wants-fixup.t 2 wants-fixup-2 &&
97+
test_commit "fixup! wants-fixup" wants-fixup.t 3 wants-fixup-3 &&
98+
test_must_fail env FAKE_LINES="1 fixup 2 squash 4" \
99+
git rebase -i HEAD~4 &&
100+
101+
: now there is a conflict, and comments in the commit message &&
102+
git show HEAD >out &&
103+
grep "fixup! wants-fixup" out &&
104+
105+
: skip and continue &&
106+
echo "cp \"\$1\" .git/copy.txt" | write_script copy-editor.sh &&
107+
(test_set_editor "$PWD/copy-editor.sh" && git rebase --skip) &&
108+
109+
: the user should not have had to edit the commit message &&
110+
test_path_is_missing .git/copy.txt &&
111+
112+
: now the comments in the commit message should have been cleaned up &&
113+
git show HEAD >out &&
114+
! grep "fixup! wants-fixup" out &&
115+
116+
: now, let us ensure that "squash" is handled correctly &&
117+
git reset --hard wants-fixup-3 &&
118+
test_must_fail env FAKE_LINES="1 squash 4 squash 2 squash 4" \
119+
git rebase -i HEAD~4 &&
120+
121+
: the first squash failed, but there are two more in the chain &&
122+
(test_set_editor "$PWD/copy-editor.sh" &&
123+
test_must_fail git rebase --skip) &&
124+
125+
: not the final squash, no need to edit the commit message &&
126+
test_path_is_missing .git/copy.txt &&
127+
128+
: The first squash was skipped, therefore: &&
129+
git show HEAD >out &&
130+
test_i18ngrep "# This is a combination of 2 commits" out &&
131+
132+
(test_set_editor "$PWD/copy-editor.sh" && git rebase --skip) &&
133+
git show HEAD >out &&
134+
test_i18ngrep ! "# This is a combination" out &&
135+
136+
: Final squash failed, but there was still a squash &&
137+
test_i18ngrep "# This is a combination of 2 commits" .git/copy.txt
138+
'
139+
91140
test_expect_success 'setup rerere database' '
92141
rm -fr .git/rebase-* &&
93142
git reset --hard commit-new-file-F3-on-topic-branch &&

0 commit comments

Comments
 (0)