summaryrefslogtreecommitdiff
path: root/sequencer.c
diff options
context:
space:
mode:
authorJohannes Schindelin <johannes.schindelin@gmx.de>2018-04-27 20:48:30 (GMT)
committerJunio C Hamano <gitster@pobox.com>2018-05-01 22:47:47 (GMT)
commit15ef69314d54945471603223bef00091673b5725 (patch)
treecf446121019db401fb7c75731978079d27c410e9 /sequencer.c
parentdc4b5bc3534bf4d8f362173d905d294048c48f79 (diff)
downloadgit-15ef69314d54945471603223bef00091673b5725.zip
git-15ef69314d54945471603223bef00091673b5725.tar.gz
git-15ef69314d54945471603223bef00091673b5725.tar.bz2
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 <johannes.schindelin@gmx.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>
Diffstat (limited to 'sequencer.c')
-rw-r--r--sequencer.c113
1 files changed, 100 insertions, 13 deletions
diff --git a/sequencer.c b/sequencer.c
index 6b2f01f..2eb5ec7 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2773,19 +2773,16 @@ static int continue_single_pick(void)
return run_command_v_opt(argv, RUN_GIT_CMD);
}
-static int commit_staged_changes(struct replay_opts *opts)
+static int commit_staged_changes(struct replay_opts *opts,
+ struct todo_list *todo_list)
{
unsigned int flags = ALLOW_EMPTY | EDIT_MSG;
+ unsigned int final_fixup = 0, is_clean;
if (has_unstaged_changes(1))
return error(_("cannot rebase: You have unstaged changes."));
- if (!has_uncommitted_changes(0)) {
- const char *cherry_pick_head = git_path_cherry_pick_head();
- if (file_exists(cherry_pick_head) && unlink(cherry_pick_head))
- return error(_("could not remove CHERRY_PICK_HEAD"));
- return 0;
- }
+ is_clean = !has_uncommitted_changes(0);
if (file_exists(rebase_path_amend())) {
struct strbuf rev = STRBUF_INIT;
@@ -2798,19 +2795,107 @@ static int commit_staged_changes(struct replay_opts *opts)
if (get_oid_hex(rev.buf, &to_amend))
return error(_("invalid contents: '%s'"),
rebase_path_amend());
- if (oidcmp(&head, &to_amend))
+ if (!is_clean && oidcmp(&head, &to_amend))
return error(_("\nYou have uncommitted changes in your "
"working tree. Please, commit them\n"
"first and then run 'git rebase "
"--continue' again."));
+ /*
+ * When skipping a failed fixup/squash, we need to edit the
+ * commit message, the current fixup list and count, and if it
+ * was the last fixup/squash in the chain, we need to clean up
+ * the commit message and if there was a squash, let the user
+ * edit it.
+ */
+ if (is_clean && !oidcmp(&head, &to_amend) &&
+ opts->current_fixup_count > 0 &&
+ file_exists(rebase_path_stopped_sha())) {
+ const char *p = opts->current_fixups.buf;
+ int len = opts->current_fixups.len;
+
+ opts->current_fixup_count--;
+ if (!len)
+ BUG("Incorrect current_fixups:\n%s", p);
+ while (len && p[len - 1] != '\n')
+ len--;
+ strbuf_setlen(&opts->current_fixups, len);
+ if (write_message(p, len, rebase_path_current_fixups(),
+ 0) < 0)
+ return error(_("could not write file: '%s'"),
+ rebase_path_current_fixups());
+
+ /*
+ * If a fixup/squash in a fixup/squash chain failed, the
+ * commit message is already correct, no need to commit
+ * it again.
+ *
+ * Only if it is the final command in the fixup/squash
+ * chain, and only if the chain is longer than a single
+ * fixup/squash command (which was just skipped), do we
+ * actually need to re-commit with a cleaned up commit
+ * message.
+ */
+ if (opts->current_fixup_count > 0 &&
+ !is_fixup(peek_command(todo_list, 0))) {
+ final_fixup = 1;
+ /*
+ * If there was not a single "squash" in the
+ * chain, we only need to clean up the commit
+ * message, no need to bother the user with
+ * opening the commit message in the editor.
+ */
+ if (!starts_with(p, "squash ") &&
+ !strstr(p, "\nsquash "))
+ flags = (flags & ~EDIT_MSG) | CLEANUP_MSG;
+ } else if (is_fixup(peek_command(todo_list, 0))) {
+ /*
+ * We need to update the squash message to skip
+ * the latest commit message.
+ */
+ struct commit *commit;
+ const char *path = rebase_path_squash_msg();
+
+ if (parse_head(&commit) ||
+ !(p = get_commit_buffer(commit, NULL)) ||
+ write_message(p, strlen(p), path, 0)) {
+ unuse_commit_buffer(commit, p);
+ return error(_("could not write file: "
+ "'%s'"), path);
+ }
+ unuse_commit_buffer(commit, p);
+ }
+ }
strbuf_release(&rev);
flags |= AMEND_MSG;
}
- if (run_git_commit(rebase_path_message(), opts, flags))
+ if (is_clean) {
+ const char *cherry_pick_head = git_path_cherry_pick_head();
+
+ if (file_exists(cherry_pick_head) && unlink(cherry_pick_head))
+ return error(_("could not remove CHERRY_PICK_HEAD"));
+ if (!final_fixup)
+ return 0;
+ }
+
+ if (run_git_commit(final_fixup ? NULL : rebase_path_message(),
+ opts, flags))
return error(_("could not commit staged changes."));
unlink(rebase_path_amend());
+ if (final_fixup) {
+ unlink(rebase_path_fixup_msg());
+ unlink(rebase_path_squash_msg());
+ }
+ if (opts->current_fixup_count > 0) {
+ /*
+ * Whether final fixup or not, we just cleaned up the commit
+ * message...
+ */
+ unlink(rebase_path_current_fixups());
+ strbuf_reset(&opts->current_fixups);
+ opts->current_fixup_count = 0;
+ }
return 0;
}
@@ -2822,14 +2907,16 @@ int sequencer_continue(struct replay_opts *opts)
if (read_and_refresh_cache(opts))
return -1;
+ if (read_populate_opts(opts))
+ return -1;
if (is_rebase_i(opts)) {
- if (commit_staged_changes(opts))
+ if ((res = read_populate_todo(&todo_list, opts)))
+ goto release_todo_list;
+ if (commit_staged_changes(opts, &todo_list))
return -1;
} else if (!file_exists(get_todo_path(opts)))
return continue_single_pick();
- if (read_populate_opts(opts))
- return -1;
- if ((res = read_populate_todo(&todo_list, opts)))
+ else if ((res = read_populate_todo(&todo_list, opts)))
goto release_todo_list;
if (!is_rebase_i(opts)) {