From ad3a039c313f8dbba8291c82cee4630cf9b3d722 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Fri, 28 Aug 2026 07:44:41 +0000 Subject: [PATCH 1/5] commit: clarify FROM_REBASE_PICK and is_from_rebase() names Commit 430b75f7209c (commit: give correct advice for empty commit during a rebase, 2019-12-06) introduced a FROM_REBASE_PICK enum value and an is_from_rebase() function. Those names failed to convey that they were specifically about hitting a commit that becomes empty when rebasing. Clarify their names now. While at it, change `whence == FROM_REBASE_EMPTY` to use `is_from_rebase_empty(whence)`. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- builtin/commit.c | 14 +++++++------- sequencer.c | 2 +- wt-status.h | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/builtin/commit.c b/builtin/commit.c index 28f6174503..569e31fb60 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -520,7 +520,7 @@ static const char *prepare_index(const char **argv, const char *prefix, die(_("cannot do a partial commit during a merge.")); else if (is_from_cherry_pick(whence)) die(_("cannot do a partial commit during a cherry-pick.")); - else if (is_from_rebase(whence)) + else if (is_from_rebase_empty(whence)) die(_("cannot do a partial commit during a rebase.")); } @@ -893,7 +893,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix, */ else if (whence == FROM_MERGE) hook_arg1 = "merge"; - else if (is_from_cherry_pick(whence) || whence == FROM_REBASE_PICK) { + else if (is_from_cherry_pick(whence) || is_from_rebase_empty(whence)) { hook_arg1 = "commit"; hook_arg2 = "CHERRY_PICK_HEAD"; } @@ -1086,7 +1086,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix, if (amend) fputs(_(empty_amend_advice), stderr); else if (is_from_cherry_pick(whence) || - whence == FROM_REBASE_PICK) { + is_from_rebase_empty(whence)) { fputs(_(empty_cherry_pick_advice), stderr); if (whence == FROM_CHERRY_PICK_SINGLE) fputs(_(empty_cherry_pick_advice_single), stderr); @@ -1333,7 +1333,7 @@ static int parse_and_validate_options(int argc, const char *argv[], die(_("You are in the middle of a merge -- cannot amend.")); else if (is_from_cherry_pick(whence)) die(_("You are in the middle of a cherry-pick -- cannot amend.")); - else if (whence == FROM_REBASE_PICK) + else if (is_from_rebase_empty(whence)) die(_("You are in the middle of a rebase -- cannot amend.")); } if (fixup_message && squash_message) @@ -1353,7 +1353,7 @@ static int parse_and_validate_options(int argc, const char *argv[], if (amend && !use_message && !fixup_message) use_message = "HEAD"; if (!use_message && !is_from_cherry_pick(whence) && - !is_from_rebase(whence) && renew_authorship) + !is_from_rebase_empty(whence) && renew_authorship) die(_("--reset-author can be used only with -C, -c or --amend.")); if (use_message) { use_message_buffer = read_commit_message(use_message); @@ -1362,7 +1362,7 @@ static int parse_and_validate_options(int argc, const char *argv[], author_message_buffer = use_message_buffer; } } - if ((is_from_cherry_pick(whence) || whence == FROM_REBASE_PICK) && + if ((is_from_cherry_pick(whence) || is_from_rebase_empty(whence)) && !renew_authorship) { author_message = "CHERRY_PICK_HEAD"; author_message_buffer = read_commit_message(author_message); @@ -1887,7 +1887,7 @@ int cmd_commit(int argc, if (!reflog_msg) reflog_msg = is_from_cherry_pick(whence) ? "commit (cherry-pick)" - : is_from_rebase(whence) + : is_from_rebase_empty(whence) ? "commit (rebase)" : "commit"; commit_list_insert(current_head, &parents); diff --git a/sequencer.c b/sequencer.c index 57855b0066..4d1516e8c4 100644 --- a/sequencer.c +++ b/sequencer.c @@ -6855,7 +6855,7 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence) !repo_get_oid(r, "REBASE_HEAD", &rebase_head) && !repo_get_oid(r, "CHERRY_PICK_HEAD", &cherry_pick_head) && oideq(&rebase_head, &cherry_pick_head)) - *whence = FROM_REBASE_PICK; + *whence = FROM_REBASE_EMPTY; else *whence = FROM_CHERRY_PICK_SINGLE; diff --git a/wt-status.h b/wt-status.h index e9fe32e98c..9588097dbe 100644 --- a/wt-status.h +++ b/wt-status.h @@ -41,7 +41,7 @@ enum commit_whence { FROM_MERGE, /* commit came from merge */ FROM_CHERRY_PICK_SINGLE, /* commit came from cherry-pick */ FROM_CHERRY_PICK_MULTI, /* commit came from a sequence of cherry-picks */ - FROM_REBASE_PICK /* commit came from a pick/reword/edit */ + FROM_REBASE_EMPTY /* rebase applied a pick that became empty */ }; static inline int is_from_cherry_pick(enum commit_whence whence) @@ -50,9 +50,9 @@ static inline int is_from_cherry_pick(enum commit_whence whence) whence == FROM_CHERRY_PICK_MULTI; } -static inline int is_from_rebase(enum commit_whence whence) +static inline int is_from_rebase_empty(enum commit_whence whence) { - return whence == FROM_REBASE_PICK; + return whence == FROM_REBASE_EMPTY; } struct wt_status_change_data { From 173c3a3d6ed74e9c3c1bbcf905cfd123f33a5d73 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Fri, 28 Aug 2026 07:44:42 +0000 Subject: [PATCH 2/5] commit: allow a partial commit when a rebase pick becomes empty For years, we disallowed partial commits during merges or cherry-picks. In commit 430b75f7209c (commit: give correct advice for empty commit during a rebase, 2019-12-06) it was noted that the "cannot do a partial commit during a cherry-pick" message was also printed when rebasing a commit that became empty, and rather than drop the check in that case, that commit opted to make the message print the actual operation that was in progress. Since a commit that has become empty comes without conflicts, a new partial commit poses no problems; remove the error in that case. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- builtin/commit.c | 2 -- t/t3404-rebase-interactive.sh | 5 ++--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/builtin/commit.c b/builtin/commit.c index 569e31fb60..610820c99f 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -520,8 +520,6 @@ static const char *prepare_index(const char **argv, const char *prefix, die(_("cannot do a partial commit during a merge.")); else if (is_from_cherry_pick(whence)) die(_("cannot do a partial commit during a cherry-pick.")); - else if (is_from_rebase_empty(whence)) - die(_("cannot do a partial commit during a rebase.")); } if (list_paths(&partial, !current_head ? NULL : "HEAD", &pathspec)) diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index 58b3bb0c27..17b30b7825 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -1829,7 +1829,7 @@ test_expect_success 'post-commit hook is called' ' test_cmp expect actual ' -test_expect_success 'correct error message for partial commit after empty pick' ' +test_expect_success 'partial commit is allowed when a rebase pick becomes empty' ' test_when_finished "git rebase --abort" && ( set_fake_editor && @@ -1838,8 +1838,7 @@ test_expect_success 'correct error message for partial commit after empty pick' test_must_fail git rebase -i A D ) && echo x >file1 && - test_must_fail git commit file1 2>err && - test_grep "cannot do a partial commit during a rebase." err + git commit file1 ' test_expect_success 'correct error message for commit --amend after empty pick' ' From fbae0f09b1088f7d0be8c48374702486670de564 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Fri, 28 Aug 2026 07:44:43 +0000 Subject: [PATCH 3/5] commit: reword the empty-commit rebase amend error When a rebase applies a commit that becomes empty, it stops and asks the user to decide whether to keep it or drop it. HEAD still points at the previously-applied commit at that point, so amending is refused, with: You are in the middle of a rebase -- cannot amend. That message would suggest that amending is not allowed during an 'edit' or 'break' stop, which is misleading, plus it lacks the specificity that might help the user know why their particular case is a problem: the commit they intended to amend became empty and was dropped, so amending would affect the wrong commit. Reword the error accordingly. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- builtin/commit.c | 2 +- t/t3404-rebase-interactive.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/builtin/commit.c b/builtin/commit.c index 610820c99f..774fb8299d 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -1332,7 +1332,7 @@ static int parse_and_validate_options(int argc, const char *argv[], else if (is_from_cherry_pick(whence)) die(_("You are in the middle of a cherry-pick -- cannot amend.")); else if (is_from_rebase_empty(whence)) - die(_("You are in the middle of a rebase -- cannot amend.")); + die(_("The now-empty commit has been dropped -- cannot amend.")); } if (fixup_message && squash_message) die(_("options '%s' and '%s' cannot be used together"), "--squash", "--fixup"); diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index 17b30b7825..5a0aa93b10 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -1851,7 +1851,7 @@ test_expect_success 'correct error message for commit --amend after empty pick' ) && echo x>file1 && test_must_fail git commit -a --amend 2>err && - test_grep "middle of a rebase -- cannot amend." err + test_grep "now-empty commit has been dropped -- cannot amend." err ' test_expect_success 'todo has correct onto hash' ' From 4540e759d0c45364d578f179c94b6122988e0a6c Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Fri, 28 Aug 2026 07:44:44 +0000 Subject: [PATCH 4/5] commit: refuse to amend during conflict resolution Running `git commit --amend` during conflict resolution is an ugly foot-gun. For many years, we have rejected amending during conflict resolution in the middle of - a merge - a cherry-pick However, this was never extended to other operations that can also produce conflicts: - an `am` operation - a revert - a rebase Extend it to handle these other cases now. Extending to `am`, revert, and the apply backend of rebase are fairly straightforward. However, with the merge backend of rebase we have to be more careful, since it powers interactive rebases and - the interactive machinery internally uses `git commit --amend` for `squash` and `reword` directives - users are expected to `git commit --amend` after hitting an `edit` or `break` directive So, we need to be careful with rebase to only reject amending when doing conflict resolution. A few files under the rebase-merge/ directory provide us the necessary information: - stopped-sha is written only when the rebase stops and hands control back to the user, so its presence marks a genuine stop -- as opposed to the sequencer's own internal `git commit --amend` while applying a squash, fixup, or reword, during which no stopped-sha exists. - amend is written only when the rebase stops with HEAD already pointing at the commit the user is meant to amend: a clean `edit`, or a fast-forward `reword`. Its absence at a stop therefore means the commit did not apply, so HEAD is the previously-applied commit rather than the one being rebased -- exactly the case we refuse. So for the merge backend we die when stopped-sha exists and amend does not. This covers a plain conflicted pick as well as a conflicted `edit` (both leave HEAD on the previously-applied commit), while still allowing a clean `edit` or `reword` stop and a `break` stop (no stopped-sha). stopped-sha is unlinked at the start of the resume loop, so a resumed squash's internal amend is unaffected. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- builtin/commit.c | 27 +++++++--- sequencer.c | 57 +++++++++++++++++++++ sequencer.h | 23 +++++++++ t/t3404-rebase-interactive.sh | 87 +++++++++++++++++++++++++++++++++ t/t3507-cherry-pick-conflict.sh | 11 +++++ t/t4151-am-abort.sh | 11 +++++ 6 files changed, 210 insertions(+), 6 deletions(-) diff --git a/builtin/commit.c b/builtin/commit.c index 774fb8299d..83ea8619d6 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -1324,15 +1324,30 @@ static int parse_and_validate_options(int argc, const char *argv[], use_editor = 0; /* Sanity check options */ - if (amend && !current_head) - die(_("You have nothing to amend.")); - if (amend && whence != FROM_COMMIT) { - if (whence == FROM_MERGE) + if (amend) { + if (!current_head) + die(_("You have nothing to amend.")); + /* + * Refuse to amend in the middle of any operation that is + * meant to record its result as a new commit on top of HEAD + * rather than by rewriting HEAD. + */ + switch (sequencer_ongoing_operation(s->repo, whence)) { + case ONGOING_NONE: + break; + case ONGOING_MERGE: die(_("You are in the middle of a merge -- cannot amend.")); - else if (is_from_cherry_pick(whence)) + case ONGOING_CHERRY_PICK: die(_("You are in the middle of a cherry-pick -- cannot amend.")); - else if (is_from_rebase_empty(whence)) + case ONGOING_REBASE_EMPTY: die(_("The now-empty commit has been dropped -- cannot amend.")); + case ONGOING_REVERT: + die(_("You are in the middle of a revert -- cannot amend.")); + case ONGOING_AM: + die(_("You are in the middle of an am session -- cannot amend.")); + case ONGOING_REBASE_CONFLICT: + die(_("You are resolving conflicts during a rebase -- cannot amend.")); + } } if (fixup_message && squash_message) die(_("options '%s' and '%s' cannot be used together"), "--squash", "--fixup"); diff --git a/sequencer.c b/sequencer.c index 4d1516e8c4..157eba7899 100644 --- a/sequencer.c +++ b/sequencer.c @@ -142,6 +142,13 @@ static GIT_PATH_FUNC(rebase_path_author_script, "rebase-merge/author-script") * command is processed, this file is deleted. */ static GIT_PATH_FUNC(rebase_path_amend, "rebase-merge/amend") +/* + * The apply ("am") backend keeps its state in the rebase-apply directory; + * the "applying" file within it marks a plain `git am` (as opposed to an + * apply-based rebase). + */ +static GIT_PATH_FUNC(apply_dir, "rebase-apply") +static GIT_PATH_FUNC(apply_path_applying, "rebase-apply/applying") /* * When we stop at a given patch via the "edit" command, this file contains * the commit object name of the corresponding patch. @@ -6865,6 +6872,56 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence) return 0; } +enum ongoing_operation sequencer_ongoing_operation(struct repository *r, + enum commit_whence whence) +{ + /* + * The merge, cherry-pick, and (empty) rebase-pick stops are already + * distinguished by 'whence'. + */ + switch (whence) { + case FROM_MERGE: + return ONGOING_MERGE; + case FROM_CHERRY_PICK_SINGLE: + case FROM_CHERRY_PICK_MULTI: + return ONGOING_CHERRY_PICK; + case FROM_REBASE_EMPTY: + return ONGOING_REBASE_EMPTY; + case FROM_COMMIT: + break; + } + + /* + * 'whence' is FROM_COMMIT, but we may still be in the middle of an + * operation that records its result on top of HEAD; detect those + * from their on-disk state. + */ + + /* In the middle of a revert? */ + if (refs_ref_exists(get_main_ref_store(r), "REVERT_HEAD")) + return ONGOING_REVERT; + + /* In the middle of an `am`? */ + if (file_exists(apply_path_applying())) + return ONGOING_AM; + + /* + * In the middle of a rebase that stopped for conflict resolution? + * The apply backend only ever stops for conflicts, so the presence + * of its state directory is enough. The merge backend writes + * stopped-sha whenever it hands control back to the user, but omits + * `amend` unless it stopped with HEAD already pointing at the commit + * to be amended (a clean edit/reword stop); its absence therefore + * marks a conflicted stop. + */ + if (file_exists(apply_dir()) || + (file_exists(rebase_path_stopped_sha()) && + !file_exists(rebase_path_amend()))) + return ONGOING_REBASE_CONFLICT; + + return ONGOING_NONE; +} + int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs) { diff --git a/sequencer.h b/sequencer.h index 3164bd437d..070126b76f 100644 --- a/sequencer.h +++ b/sequencer.h @@ -269,6 +269,29 @@ int sequencer_get_last_command(struct repository* r, enum replay_action *action); int sequencer_determine_whence(struct repository *r, enum commit_whence *whence); +/* + * An in-progress operation that records its result (often a conflict + * resolution) as a new commit on top of HEAD, during which amending + * HEAD via "git commit --amend" is almost always a mistake. + */ +enum ongoing_operation { + ONGOING_NONE = 0, + ONGOING_MERGE, + ONGOING_CHERRY_PICK, + ONGOING_REBASE_EMPTY, + ONGOING_REVERT, + ONGOING_AM, + ONGOING_REBASE_CONFLICT +}; + +/* + * Return which in-progress operation, if any, is underway; see enum + * ongoing_operation. 'whence' is the origin already computed for the + * pending commit. + */ +enum ongoing_operation sequencer_ongoing_operation(struct repository *r, + enum commit_whence whence); + /** * Append the set of ref-OID pairs that are currently stored for the 'git * rebase --update-refs' feature if such a rebase is currently happening. diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index 5a0aa93b10..c63f520191 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -1854,6 +1854,93 @@ test_expect_success 'correct error message for commit --amend after empty pick' test_grep "now-empty commit has been dropped -- cannot amend." err ' +test_expect_success 'commit --amend is refused at a rebase conflict stop' ' + test_when_finished "git rebase --abort" && + git checkout --detach conflict-branch && + ( + set_fake_editor && + FAKE_LINES="1 3" && + export FAKE_LINES && + test_must_fail git rebase -i A + ) && + test_path_is_file .git/rebase-merge/patch && + test_path_is_missing .git/rebase-merge/amend && + echo resolved >conflict && + git add conflict && + test_must_fail git commit --amend --no-edit 2>err && + test_grep "You are resolving conflicts during a rebase -- cannot amend" err +' + +test_expect_success 'commit --amend is refused when an "edit" pick conflicts' ' + test_when_finished "git rebase --abort" && + git checkout --detach conflict-branch && + ( + set_fake_editor && + FAKE_LINES="1 edit 3" && + export FAKE_LINES && + test_must_fail git rebase -i A + ) && + test_path_is_file .git/rebase-merge/patch && + test_path_is_missing .git/rebase-merge/amend && + echo resolved >conflict && + git add conflict && + test_must_fail git commit --amend --no-edit 2>err && + test_grep "You are resolving conflicts during a rebase -- cannot amend" err +' + +test_expect_success 'commit --amend is allowed at a rebase edit stop' ' + test_when_finished "git rebase --abort" && + git checkout --detach no-conflict-branch && + ( + set_fake_editor && + FAKE_LINES="edit 1 2 3 4" && + export FAKE_LINES && + git rebase -i A + ) && + test_path_is_file .git/rebase-merge/amend && + echo tweak >fileJ && + git add fileJ && + git commit --amend --no-edit +' + +test_expect_success 'commit --amend is allowed at a rebase break stop' ' + test_when_finished "git rebase --abort" && + git checkout --detach no-conflict-branch && + ( + set_fake_editor && + FAKE_LINES="break 1 2 3 4" && + export FAKE_LINES && + git rebase -i A + ) && + test_must_fail git rev-parse --verify REBASE_HEAD && + echo tweak >fileJ && + git add fileJ && + git commit --amend --no-edit +' + +test_expect_success 'commit --amend is refused at an apply-backend conflict stop' ' + test_when_finished "rm -rf apply-backend" && + test_create_repo apply-backend && + ( + cd apply-backend && + test_commit base file && + git branch -M mainline && + test_commit upstream file upstream && + git checkout -b side mainline~1 && + test_commit conflicting file side && + test_commit unrelated other && + test_must_fail git rebase --apply mainline && + # the apply backend only ever stops for conflicts, and + # leaves HEAD on the previously-applied commit + test_path_is_dir .git/rebase-apply && + test_path_is_missing .git/rebase-apply/applying && + echo resolved >file && + git add file && + test_must_fail git commit --amend --no-edit 2>err && + test_grep "You are resolving conflicts during a rebase -- cannot amend" err + ) +' + test_expect_success 'todo has correct onto hash' ' GIT_SEQUENCE_EDITOR=cat git rebase -i no-conflict-branch~4 no-conflict-branch >actual && onto=$(git rev-parse --short HEAD~4) && diff --git a/t/t3507-cherry-pick-conflict.sh b/t/t3507-cherry-pick-conflict.sh index 44596cb1e8..42de398f76 100755 --- a/t/t3507-cherry-pick-conflict.sh +++ b/t/t3507-cherry-pick-conflict.sh @@ -364,6 +364,17 @@ test_expect_success 'failed revert sets REVERT_HEAD' ' test_cmp_rev picked REVERT_HEAD ' +test_expect_success 'commit --amend of revert fails' ' + pristine_detach initial && + + test_must_fail git revert picked && + echo resolved >foo && + git add foo && + test_must_fail git commit --amend 2>err && + + test_grep "in the middle of a revert -- cannot amend." err +' + test_expect_success 'successful revert does not set REVERT_HEAD' ' pristine_detach base && git revert base && diff --git a/t/t4151-am-abort.sh b/t/t4151-am-abort.sh index 8e1ecf8a68..9313a074b2 100755 --- a/t/t4151-am-abort.sh +++ b/t/t4151-am-abort.sh @@ -63,6 +63,17 @@ do done +test_expect_success 'commit --amend during a failed am fails' ' + git reset --hard initial && + cp file-2-expect file-2 && + test_must_fail git am 000[1245]-*.patch && + echo resolved >file-1 && + git add file-1 && + test_must_fail git commit --amend 2>err && + test_grep "in the middle of an am session -- cannot amend." err && + git am --abort +' + test_expect_success 'am -3 --skip removes otherfile-4' ' git reset --hard initial && test_must_fail git am -3 0003-*.patch && From c1eb66fde058ef9b21e5daf7f9e919fb8619ba6a Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Fri, 28 Aug 2026 07:44:45 +0000 Subject: [PATCH 5/5] commit: refuse partial commits during conflict resolution Similar to the previous commit, just as `git commit --amend` is a foot-gun during conflict resolution, so is a partial commit (`git commit `). Recording a conflict resolution is about capturing the state of the entire tree on top of HEAD, not a subset of paths. For many years we have rejected partial commits in the middle of - a merge - a cherry-pick but, just like amending, this was never extended to the other operations that can also leave conflicts to resolve: - an `am` operation - a revert - a rebase that stopped for conflict resolution Reuse sequencer_ongoing_operation(), introduced for the analogous `--amend` check, to detect these and refuse the partial commit. A rebase that stopped because a pick became empty is not conflict resolution and, as an earlier patch established, is deliberately left permitted. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- builtin/commit.c | 24 ++++++++++++++++++----- sequencer.h | 5 +++-- t/t3404-rebase-interactive.sh | 34 +++++++++++++++++++++++++++++++++ t/t3507-cherry-pick-conflict.sh | 11 +++++++++++ t/t4151-am-abort.sh | 11 +++++++++++ 5 files changed, 78 insertions(+), 7 deletions(-) diff --git a/builtin/commit.c b/builtin/commit.c index 83ea8619d6..e96c663bd5 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -515,11 +515,25 @@ static const char *prepare_index(const char **argv, const char *prefix, */ commit_style = COMMIT_PARTIAL; - if (whence != FROM_COMMIT) { - if (whence == FROM_MERGE) - die(_("cannot do a partial commit during a merge.")); - else if (is_from_cherry_pick(whence)) - die(_("cannot do a partial commit during a cherry-pick.")); + switch (sequencer_ongoing_operation(the_repository, whence)) { + case ONGOING_NONE: + break; + case ONGOING_MERGE: + die(_("cannot do a partial commit during a merge.")); + case ONGOING_CHERRY_PICK: + die(_("cannot do a partial commit during a cherry-pick.")); + case ONGOING_REBASE_EMPTY: + /* + * A pick that became empty is not a conflict, and creating + * a new commit (partial or not) poses no problem. + */ + break; + case ONGOING_REVERT: + die(_("cannot do a partial commit during a revert.")); + case ONGOING_AM: + die(_("cannot do a partial commit during an am session.")); + case ONGOING_REBASE_CONFLICT: + die(_("cannot do a partial commit while resolving conflicts during a rebase.")); } if (list_paths(&partial, !current_head ? NULL : "HEAD", &pathspec)) diff --git a/sequencer.h b/sequencer.h index 070126b76f..65133e2418 100644 --- a/sequencer.h +++ b/sequencer.h @@ -271,8 +271,9 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence) /* * An in-progress operation that records its result (often a conflict - * resolution) as a new commit on top of HEAD, during which amending - * HEAD via "git commit --amend" is almost always a mistake. + * resolution) as a new commit on top of HEAD. Some ways of invoking + * "git commit" -- amending HEAD, or a partial commit -- are almost + * always a mistake during such an operation. */ enum ongoing_operation { ONGOING_NONE = 0, diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index c63f520191..61b5a99650 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -1941,6 +1941,40 @@ test_expect_success 'commit --amend is refused at an apply-backend conflict stop ) ' +test_expect_success 'partial commit is refused at a rebase conflict stop' ' + test_when_finished "git rebase --abort" && + git checkout --detach conflict-branch && + ( + set_fake_editor && + FAKE_LINES="1 3" && + export FAKE_LINES && + test_must_fail git rebase -i A + ) && + echo resolved >conflict && + git add conflict && + test_must_fail git commit conflict 2>err && + test_grep "cannot do a partial commit while resolving conflicts during a rebase." err +' + +test_expect_success 'partial commit is refused at an apply-backend conflict stop' ' + test_when_finished "rm -rf apply-backend" && + test_create_repo apply-backend && + ( + cd apply-backend && + test_commit base file && + git branch -M mainline && + test_commit upstream file upstream && + git checkout -b side mainline~1 && + test_commit conflicting file side && + test_commit unrelated other && + test_must_fail git rebase --apply mainline && + echo resolved >file && + git add file && + test_must_fail git commit file 2>err && + test_grep "cannot do a partial commit while resolving conflicts during a rebase." err + ) +' + test_expect_success 'todo has correct onto hash' ' GIT_SEQUENCE_EDITOR=cat git rebase -i no-conflict-branch~4 no-conflict-branch >actual && onto=$(git rev-parse --short HEAD~4) && diff --git a/t/t3507-cherry-pick-conflict.sh b/t/t3507-cherry-pick-conflict.sh index 42de398f76..c3d024c97f 100755 --- a/t/t3507-cherry-pick-conflict.sh +++ b/t/t3507-cherry-pick-conflict.sh @@ -375,6 +375,17 @@ test_expect_success 'commit --amend of revert fails' ' test_grep "in the middle of a revert -- cannot amend." err ' +test_expect_success 'partial commit during a revert fails' ' + pristine_detach initial && + + test_must_fail git revert picked && + echo resolved >foo && + git add foo && + test_must_fail git commit foo 2>err && + + test_grep "cannot do a partial commit during a revert." err +' + test_expect_success 'successful revert does not set REVERT_HEAD' ' pristine_detach base && git revert base && diff --git a/t/t4151-am-abort.sh b/t/t4151-am-abort.sh index 9313a074b2..c80269e015 100755 --- a/t/t4151-am-abort.sh +++ b/t/t4151-am-abort.sh @@ -74,6 +74,17 @@ test_expect_success 'commit --amend during a failed am fails' ' git am --abort ' +test_expect_success 'partial commit during a failed am fails' ' + git reset --hard initial && + cp file-2-expect file-2 && + test_must_fail git am 000[1245]-*.patch && + echo resolved >file-1 && + git add file-1 && + test_must_fail git commit file-1 2>err && + test_grep "cannot do a partial commit during an am session." err && + git am --abort +' + test_expect_success 'am -3 --skip removes otherfile-4' ' git reset --hard initial && test_must_fail git am -3 0003-*.patch &&