Skip to content

Commit 15ef693

Browse files
dschogitster
authored andcommitted
rebase --skip: clean up commit message after a failed fixup/squash
During a series of fixup/squash commands, the interactive rebase builds up a commit message with comments. This will be presented to the user in the editor if at least one of those commands was a `squash`. In any case, the commit message will be cleaned up eventually, removing all those intermediate comments, in the final step of such a fixup/squash chain. However, if the last fixup/squash command in such a chain fails with merge conflicts, and if the user then decides to skip it (or resolve it to a clean worktree and then continue the rebase), the current code fails to clean up the commit message. This commit fixes that behavior. The fix is quite a bit more involved than meets the eye because it is not only about the question whether we are `git rebase --skip`ing a fixup or squash. It is also about removing the skipped fixup/squash's commit message from the accumulated commit message. And it is also about the question whether we should let the user edit the final commit message or not ("Was there a squash in the chain *that was not skipped*?"). For example, in this case we will want to fix the commit message, but not open it in an editor: pick <- succeeds fixup <- succeeds squash <- fails, will be skipped This is where the newly-introduced `current-fixups` file comes in real handy. A quick look and we can determine whether there was a non-skipped squash. We only need to make sure to keep it up to date with respect to skipped fixup/squash commands. As a bonus, we can even avoid committing unnecessarily, e.g. when there was only one fixup, and it failed, and was skipped. To fix only the bug where the final commit message was not cleaned up properly, but without fixing the rest, would have been more complicated than fixing it all in one go, hence this commit lumps together more than a single concern. For the same reason, this commit also adds a bit more to the existing test case for the regression we just fixed. The diff is best viewed with --color-moved. Signed-off-by: Johannes Schindelin <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent dc4b5bc commit 15ef693

File tree

2 files changed

+131
-17
lines changed

2 files changed

+131
-17
lines changed

sequencer.c

Lines changed: 100 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2773,19 +2773,16 @@ static int continue_single_pick(void)
27732773
return run_command_v_opt(argv, RUN_GIT_CMD);
27742774
}
27752775

2776-
static int commit_staged_changes(struct replay_opts *opts)
2776+
static int commit_staged_changes(struct replay_opts *opts,
2777+
struct todo_list *todo_list)
27772778
{
27782779
unsigned int flags = ALLOW_EMPTY | EDIT_MSG;
2780+
unsigned int final_fixup = 0, is_clean;
27792781

27802782
if (has_unstaged_changes(1))
27812783
return error(_("cannot rebase: You have unstaged changes."));
2782-
if (!has_uncommitted_changes(0)) {
2783-
const char *cherry_pick_head = git_path_cherry_pick_head();
27842784

2785-
if (file_exists(cherry_pick_head) && unlink(cherry_pick_head))
2786-
return error(_("could not remove CHERRY_PICK_HEAD"));
2787-
return 0;
2788-
}
2785+
is_clean = !has_uncommitted_changes(0);
27892786

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

28072869
strbuf_release(&rev);
28082870
flags |= AMEND_MSG;
28092871
}
28102872

2811-
if (run_git_commit(rebase_path_message(), opts, flags))
2873+
if (is_clean) {
2874+
const char *cherry_pick_head = git_path_cherry_pick_head();
2875+
2876+
if (file_exists(cherry_pick_head) && unlink(cherry_pick_head))
2877+
return error(_("could not remove CHERRY_PICK_HEAD"));
2878+
if (!final_fixup)
2879+
return 0;
2880+
}
2881+
2882+
if (run_git_commit(final_fixup ? NULL : rebase_path_message(),
2883+
opts, flags))
28122884
return error(_("could not commit staged changes."));
28132885
unlink(rebase_path_amend());
2886+
if (final_fixup) {
2887+
unlink(rebase_path_fixup_msg());
2888+
unlink(rebase_path_squash_msg());
2889+
}
2890+
if (opts->current_fixup_count > 0) {
2891+
/*
2892+
* Whether final fixup or not, we just cleaned up the commit
2893+
* message...
2894+
*/
2895+
unlink(rebase_path_current_fixups());
2896+
strbuf_reset(&opts->current_fixups);
2897+
opts->current_fixup_count = 0;
2898+
}
28142899
return 0;
28152900
}
28162901

@@ -2822,14 +2907,16 @@ int sequencer_continue(struct replay_opts *opts)
28222907
if (read_and_refresh_cache(opts))
28232908
return -1;
28242909

2910+
if (read_populate_opts(opts))
2911+
return -1;
28252912
if (is_rebase_i(opts)) {
2826-
if (commit_staged_changes(opts))
2913+
if ((res = read_populate_todo(&todo_list, opts)))
2914+
goto release_todo_list;
2915+
if (commit_staged_changes(opts, &todo_list))
28272916
return -1;
28282917
} else if (!file_exists(get_todo_path(opts)))
28292918
return continue_single_pick();
2830-
if (read_populate_opts(opts))
2831-
return -1;
2832-
if ((res = read_populate_todo(&todo_list, opts)))
2919+
else if ((res = read_populate_todo(&todo_list, opts)))
28332920
goto release_todo_list;
28342921

28352922
if (!is_rebase_i(opts)) {

t/t3418-rebase-continue.sh

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,26 +88,53 @@ test_expect_success 'rebase passes merge strategy options correctly' '
8888
git rebase --continue
8989
'
9090

91-
test_expect_failure '--skip after failed fixup cleans commit message' '
91+
test_expect_success '--skip after failed fixup cleans commit message' '
9292
test_when_finished "test_might_fail git rebase --abort" &&
9393
git checkout -b with-conflicting-fixup &&
9494
test_commit wants-fixup &&
9595
test_commit "fixup! wants-fixup" wants-fixup.t 1 wants-fixup-1 &&
9696
test_commit "fixup! wants-fixup" wants-fixup.t 2 wants-fixup-2 &&
9797
test_commit "fixup! wants-fixup" wants-fixup.t 3 wants-fixup-3 &&
98-
test_must_fail env FAKE_LINES="1 fixup 2 fixup 4" \
98+
test_must_fail env FAKE_LINES="1 fixup 2 squash 4" \
9999
git rebase -i HEAD~4 &&
100100
101101
: now there is a conflict, and comments in the commit message &&
102102
git show HEAD >out &&
103103
grep "fixup! wants-fixup" out &&
104104
105105
: skip and continue &&
106-
git rebase --skip &&
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 &&
107111
108112
: now the comments in the commit message should have been cleaned up &&
109113
git show HEAD >out &&
110-
! grep "fixup! wants-fixup" 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
111138
'
112139

113140
test_expect_success 'setup rerere database' '

0 commit comments

Comments
 (0)