{"thread":{"id":"66221","subject":"[PATCH] commit: refuse to amend during conflict resolution","startedAt":"2026-08-26T05:21:24Z","lastAt":"2026-09-02T15:41:39Z","messageCount":43,"participants":["Elijah Newren via GitGitGadget","Phillip Wood","Junio C Hamano","Elijah Newren"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"551260","messageId":"pull.2389.git.git.1787721681893.gitgitgadget@gmail.com","threadId":"66221","inReplyTo":null,"subject":"[PATCH] commit: refuse to amend during conflict resolution","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-08-26T05:21:21Z","receivedAt":"2026-08-26T05:21:24Z","isPatch":true,"body":"From: Elijah Newren <newren@gmail.com>\n\nRunning `git commit --amend` during conflict resolution is an ugly\nfoot-gun.  For many years, we have rejected amending during conflict\nresolution in the middle of\n  - a merge\n  - a cherry-pick\nHowever, this was never extended to other operations that can also\nproduce conflicts:\n  - an `am` operation\n  - a revert\n  - a rebase\n\nExtend it to handle these other cases now.\n\nExtending to `am`, revert, and the apply backend of rebase are fairly\nstraightforward.  However, with the merge backend of rebase we have to\nbe more careful, since it powers interactive rebases and\n  - the interactive machinery internally uses `git commit --amend` for\n    `squash` and `reword` directives\n  - users are expected to `git commit --amend` after hitting an `edit`\n    or `break` directive\nSo, we need to be careful with rebase to only reject amending when doing\nconflict resolution.\n\nA few files under the rebase-merge/ directory provide us the necessary\ninformation:\n\n  - stopped-sha is written only when the rebase stops and hands control\n    back to the user, so its presence marks a genuine stop -- as opposed\n    to the sequencer's own internal `git commit --amend` while applying\n    a squash, fixup, or reword, during which no stopped-sha exists.\n\n  - amend is written only when the rebase stops with HEAD already\n    pointing at the commit the user is meant to amend: a clean `edit`,\n    or a fast-forward `reword`.  Its absence at a stop therefore means\n    the commit did not apply, so HEAD is the previously-applied commit\n    rather than the one being rebased -- exactly the case we refuse.\n\nSo for the merge backend we die when stopped-sha exists and amend does\nnot.  This covers a plain conflicted pick as well as a conflicted `edit`\n(both leave HEAD on the previously-applied commit), while still allowing\na clean `edit` or `reword` stop and a `break` stop (no stopped-sha).\nstopped-sha is unlinked at the start of the resume loop, so a resumed\nsquash's internal amend is unaffected.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n    commit: refuse to amend during conflict resolution\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2389%2Fnewren%2Frefuse-amend-during-conflicts-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2389/newren/refuse-amend-during-conflicts-v1\nPull-Request: https://github.com/git/git/pull/2389\n\n builtin/commit.c                | 41 ++++++++++++++++\n t/t3404-rebase-interactive.sh   | 87 +++++++++++++++++++++++++++++++++\n t/t3507-cherry-pick-conflict.sh | 11 +++++\n t/t4151-am-abort.sh             | 11 +++++\n 4 files changed, 150 insertions(+)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 28f6174503..a9fd04366e 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -30,6 +30,7 @@\n #include \"path.h\"\n #include \"preload-index.h\"\n #include \"read-cache.h\"\n+#include \"refs.h\"\n #include \"repository.h\"\n #include \"string-list.h\"\n #include \"rerere.h\"\n@@ -1336,6 +1337,46 @@ static int parse_and_validate_options(int argc, const char *argv[],\n \t\telse if (whence == FROM_REBASE_PICK)\n \t\t\tdie(_(\"You are in the middle of a rebase -- cannot amend.\"));\n \t}\n+\tif (amend && whence == FROM_COMMIT) {\n+\t\tchar *applying, *apply_dir, *stopped_sha, *amend_marker;\n+\t\tint in_am, conflicted_stop;\n+\n+\t\t/* Check middle of revert */\n+\t\tif (refs_ref_exists(get_main_ref_store(the_repository),\n+\t\t\t\t    \"REVERT_HEAD\"))\n+\t\t\tdie(_(\"You are in the middle of a revert -- cannot amend.\"));\n+\n+\t\t/* Check middle of `am` */\n+\t\tapplying = repo_git_path(the_repository,\n+\t\t\t\t\t \"rebase-apply/applying\");\n+\t\tin_am = file_exists(applying);\n+\n+\t\tfree(applying);\n+\t\tif (in_am)\n+\t\t\tdie(_(\"You are in the middle of an am session -- cannot amend.\"));\n+\n+\t\t/* Check middle of rebase specifically stopped for conflicts */\n+\t\tapply_dir = repo_git_path(the_repository,\n+\t\t\t\t\t  \"rebase-apply\");\n+\t\tstopped_sha = repo_git_path(the_repository,\n+\t\t\t\t\t    \"rebase-merge/stopped-sha\");\n+\t\tamend_marker = repo_git_path(the_repository,\n+\t\t\t\t\t     \"rebase-merge/amend\");\n+\t\t/*\n+\t\t * The apply backend only ever stops for conflicts; the\n+\t\t * merge backend writes stopped-sha but omits `amend`,\n+\t\t * which it writes only at a clean edit/reword stop.\n+\t\t */\n+\t\tconflicted_stop =\n+\t\t\tfile_exists(apply_dir) ||\n+\t\t\t(file_exists(stopped_sha) && !file_exists(amend_marker));\n+\n+\t\tfree(apply_dir);\n+\t\tfree(stopped_sha);\n+\t\tfree(amend_marker);\n+\t\tif (conflicted_stop)\n+\t\t\tdie(_(\"You are resolving conflicts during a rebase -- cannot amend.\"));\n+\t}\n \tif (fixup_message && squash_message)\n \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--squash\", \"--fixup\");\n \tdie_for_incompatible_opt4(!!use_message, \"-C\",\ndiff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh\nindex ff11abb2f2..01d4735b3b 100755\n--- a/t/t3404-rebase-interactive.sh\n+++ b/t/t3404-rebase-interactive.sh\n@@ -1884,6 +1884,93 @@ test_expect_success 'correct error message for commit --amend after empty pick'\n \ttest_grep \"middle of a rebase -- cannot amend.\" err\n '\n \n+test_expect_success 'commit --amend is refused at a rebase conflict stop' '\n+\ttest_when_finished \"git rebase --abort\" &&\n+\tgit checkout --detach conflict-branch &&\n+\t(\n+\t\tset_fake_editor &&\n+\t\tFAKE_LINES=\"1 3\" &&\n+\t\texport FAKE_LINES &&\n+\t\ttest_must_fail git rebase -i A\n+\t) &&\n+\ttest_path_is_file .git/rebase-merge/patch &&\n+\ttest_path_is_missing .git/rebase-merge/amend &&\n+\techo resolved >conflict &&\n+\tgit add conflict &&\n+\ttest_must_fail git commit --amend --no-edit 2>err &&\n+\ttest_grep \"You are resolving conflicts during a rebase -- cannot amend\" err\n+'\n+\n+test_expect_success 'commit --amend is refused when an \"edit\" pick conflicts' '\n+\ttest_when_finished \"git rebase --abort\" &&\n+\tgit checkout --detach conflict-branch &&\n+\t(\n+\t\tset_fake_editor &&\n+\t\tFAKE_LINES=\"1 edit 3\" &&\n+\t\texport FAKE_LINES &&\n+\t\ttest_must_fail git rebase -i A\n+\t) &&\n+\ttest_path_is_file .git/rebase-merge/patch &&\n+\ttest_path_is_missing .git/rebase-merge/amend &&\n+\techo resolved >conflict &&\n+\tgit add conflict &&\n+\ttest_must_fail git commit --amend --no-edit 2>err &&\n+\ttest_grep \"You are resolving conflicts during a rebase -- cannot amend\" err\n+'\n+\n+test_expect_success 'commit --amend is allowed at a rebase edit stop' '\n+\ttest_when_finished \"git rebase --abort\" &&\n+\tgit checkout --detach no-conflict-branch &&\n+\t(\n+\t\tset_fake_editor &&\n+\t\tFAKE_LINES=\"edit 1 2 3 4\" &&\n+\t\texport FAKE_LINES &&\n+\t\tgit rebase -i A\n+\t) &&\n+\ttest_path_is_file .git/rebase-merge/amend &&\n+\techo tweak >fileJ &&\n+\tgit add fileJ &&\n+\tgit commit --amend --no-edit\n+'\n+\n+test_expect_success 'commit --amend is allowed at a rebase break stop' '\n+\ttest_when_finished \"git rebase --abort\" &&\n+\tgit checkout --detach no-conflict-branch &&\n+\t(\n+\t\tset_fake_editor &&\n+\t\tFAKE_LINES=\"break 1 2 3 4\" &&\n+\t\texport FAKE_LINES &&\n+\t\tgit rebase -i A\n+\t) &&\n+\ttest_must_fail git rev-parse --verify REBASE_HEAD &&\n+\techo tweak >fileJ &&\n+\tgit add fileJ &&\n+\tgit commit --amend --no-edit\n+'\n+\n+test_expect_success 'commit --amend is refused at an apply-backend conflict stop' '\n+\ttest_when_finished \"rm -rf apply-backend\" &&\n+\ttest_create_repo apply-backend &&\n+\t(\n+\t\tcd apply-backend &&\n+\t\ttest_commit base file &&\n+\t\tgit branch -M mainline &&\n+\t\ttest_commit upstream file upstream &&\n+\t\tgit checkout -b side mainline~1 &&\n+\t\ttest_commit conflicting file side &&\n+\t\ttest_commit unrelated other &&\n+\t\ttest_must_fail git rebase --apply mainline &&\n+\t\t# the apply backend only ever stops for conflicts, and\n+\t\t# leaves HEAD on the previously-applied commit\n+\t\ttest_path_is_dir .git/rebase-apply &&\n+\t\ttest_path_is_missing .git/rebase-apply/applying &&\n+\t\techo resolved >file &&\n+\t\tgit add file &&\n+\t\ttest_must_fail git commit --amend --no-edit 2>err &&\n+\t\ttest_grep \"You are resolving conflicts during a rebase -- cannot amend\" err\n+\t)\n+'\n+\n test_expect_success 'todo has correct onto hash' '\n \tGIT_SEQUENCE_EDITOR=cat git rebase -i no-conflict-branch~4 no-conflict-branch >actual &&\n \tonto=$(git rev-parse --short HEAD~4) &&\ndiff --git a/t/t3507-cherry-pick-conflict.sh b/t/t3507-cherry-pick-conflict.sh\nindex 44596cb1e8..42de398f76 100755\n--- a/t/t3507-cherry-pick-conflict.sh\n+++ b/t/t3507-cherry-pick-conflict.sh\n@@ -364,6 +364,17 @@ test_expect_success 'failed revert sets REVERT_HEAD' '\n \ttest_cmp_rev picked REVERT_HEAD\n '\n \n+test_expect_success 'commit --amend of revert fails' '\n+\tpristine_detach initial &&\n+\n+\ttest_must_fail git revert picked &&\n+\techo resolved >foo &&\n+\tgit add foo &&\n+\ttest_must_fail git commit --amend 2>err &&\n+\n+\ttest_grep \"in the middle of a revert -- cannot amend.\" err\n+'\n+\n test_expect_success 'successful revert does not set REVERT_HEAD' '\n \tpristine_detach base &&\n \tgit revert base &&\ndiff --git a/t/t4151-am-abort.sh b/t/t4151-am-abort.sh\nindex 8e1ecf8a68..9313a074b2 100755\n--- a/t/t4151-am-abort.sh\n+++ b/t/t4151-am-abort.sh\n@@ -63,6 +63,17 @@ do\n \n done\n \n+test_expect_success 'commit --amend during a failed am fails' '\n+\tgit reset --hard initial &&\n+\tcp file-2-expect file-2 &&\n+\ttest_must_fail git am 000[1245]-*.patch &&\n+\techo resolved >file-1 &&\n+\tgit add file-1 &&\n+\ttest_must_fail git commit --amend 2>err &&\n+\ttest_grep \"in the middle of an am session -- cannot amend.\" err &&\n+\tgit am --abort\n+'\n+\n test_expect_success 'am -3 --skip removes otherfile-4' '\n \tgit reset --hard initial &&\n \ttest_must_fail git am -3 0003-*.patch &&\n\nbase-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e\n-- \ngitgitgadget\n"},{"id":"551283","messageId":"4688ee19-b782-456a-bed2-8cd2a4415736@gmail.com","threadId":"66221","inReplyTo":"pull.2389.git.git.1787721681893.gitgitgadget@gmail.com","subject":"Re: [PATCH] commit: refuse to amend during conflict resolution","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-08-26T13:56:25Z","receivedAt":"2026-08-26T13:56:32Z","isPatch":true,"body":"Hi Elijah\n\nOn 26/08/2026 06:21, Elijah Newren via GitGitGadget wrote:\n> From: Elijah Newren <newren@gmail.com>\n> \n> Running `git commit --amend` during conflict resolution is an ugly\n> foot-gun.  For many years, we have rejected amending during conflict\n> resolution in the middle of\n>    - a merge\n>    - a cherry-pick\n> However, this was never extended to other operations that can also\n> produce conflicts:\n>    - an `am` operation\n>    - a revert\n>    - a rebase\n> \n> Extend it to handle these other cases now.\n\nExcellent!\n\n> Extending to `am`, revert, and the apply backend of rebase are fairly\n> straightforward.  However, with the merge backend of rebase we have to\n> be more careful, since it powers interactive rebases and\n>    - the interactive machinery internally uses `git commit --amend` for\n>      `squash` and `reword` directives\n>    - users are expected to `git commit --amend` after hitting an `edit`\n>      or `break` directive\n> So, we need to be careful with rebase to only reject amending when doing\n> conflict resolution.\n> \n> A few files under the rebase-merge/ directory provide us the necessary\n> information:\n> \n>    - stopped-sha is written only when the rebase stops and hands control\n>      back to the user, so its presence marks a genuine stop -- as opposed\n>      to the sequencer's own internal `git commit --amend` while applying\n>      a squash, fixup, or reword, during which no stopped-sha exists.\n> \n>    - amend is written only when the rebase stops with HEAD already\n>      pointing at the commit the user is meant to amend: a clean `edit`,\n>      or a fast-forward `reword`. \n\nAlso a fixup with conflicts, but in that case we do want to allow the \nuser to amend even though there are conflicts so it's ok.\n\n>      Its absence at a stop therefore means\n>      the commit did not apply, so HEAD is the previously-applied commit\n>      rather than the one being rebased -- exactly the case we refuse.\n> \n> So for the merge backend we die when stopped-sha exists and amend does\n> not.  This covers a plain conflicted pick as well as a conflicted `edit`\n> (both leave HEAD on the previously-applied commit), while still allowing\n> a clean `edit` or `reword` stop and a `break` stop (no stopped-sha).\n> stopped-sha is unlinked at the start of the resume loop, so a resumed\n> squash's internal amend is unaffected.\n> \n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>      commit: refuse to amend during conflict resolution\n> \n> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2389%2Fnewren%2Frefuse-amend-during-conflicts-v1\n> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2389/newren/refuse-amend-during-conflicts-v1\n> Pull-Request: https://github.com/git/git/pull/2389\n> \n>   builtin/commit.c                | 41 ++++++++++++++++\n>   t/t3404-rebase-interactive.sh   | 87 +++++++++++++++++++++++++++++++++\n>   t/t3507-cherry-pick-conflict.sh | 11 +++++\n>   t/t4151-am-abort.sh             | 11 +++++\n>   4 files changed, 150 insertions(+)\n> \n> diff --git a/builtin/commit.c b/builtin/commit.c\n> index 28f6174503..a9fd04366e 100644\n> --- a/builtin/commit.c\n> +++ b/builtin/commit.c\n> @@ -30,6 +30,7 @@\n>   #include \"path.h\"\n>   #include \"preload-index.h\"\n>   #include \"read-cache.h\"\n> +#include \"refs.h\"\n>   #include \"repository.h\"\n>   #include \"string-list.h\"\n>   #include \"rerere.h\"\n> @@ -1336,6 +1337,46 @@ static int parse_and_validate_options(int argc, const char *argv[],\n>   \t\telse if (whence == FROM_REBASE_PICK)\n>   \t\t\tdie(_(\"You are in the middle of a rebase -- cannot amend.\"));\n>   \t}\n> +\tif (amend && whence == FROM_COMMIT) {\n> +\t\tchar *applying, *apply_dir, *stopped_sha, *amend_marker;\n> +\t\tint in_am, conflicted_stop;\n> +\n> +\t\t/* Check middle of revert */\n> +\t\tif (refs_ref_exists(get_main_ref_store(the_repository),\n> +\t\t\t\t    \"REVERT_HEAD\"))\n> +\t\t\tdie(_(\"You are in the middle of a revert -- cannot amend.\"));\n\nI think it would be much cleaner to move this check and sequencer \nrelated ones below into sequencer_determine_whence() so that we don't \nhave to hard code the paths here. It might be worth checking for \"am\" \nand the \"apply\" based rebase in that function as well.\n\nThe logic looks sound to me\n\nThanks\n\nPhillip\n\n> +\t\t/* Check middle of `am` */\n> +\t\tapplying = repo_git_path(the_repository,\n> +\t\t\t\t\t \"rebase-apply/applying\");\n> +\t\tin_am = file_exists(applying);\n> +\n> +\t\tfree(applying);\n> +\t\tif (in_am)\n> +\t\t\tdie(_(\"You are in the middle of an am session -- cannot amend.\"));\n> +\n> +\t\t/* Check middle of rebase specifically stopped for conflicts */\n> +\t\tapply_dir = repo_git_path(the_repository,\n> +\t\t\t\t\t  \"rebase-apply\");\n> +\t\tstopped_sha = repo_git_path(the_repository,\n> +\t\t\t\t\t    \"rebase-merge/stopped-sha\");\n> +\t\tamend_marker = repo_git_path(the_repository,\n> +\t\t\t\t\t     \"rebase-merge/amend\");\n> +\t\t/*\n> +\t\t * The apply backend only ever stops for conflicts; the\n> +\t\t * merge backend writes stopped-sha but omits `amend`,\n> +\t\t * which it writes only at a clean edit/reword stop.\n> +\t\t */\n> +\t\tconflicted_stop =\n> +\t\t\tfile_exists(apply_dir) ||\n> +\t\t\t(file_exists(stopped_sha) && !file_exists(amend_marker));\n> +\n> +\t\tfree(apply_dir);\n> +\t\tfree(stopped_sha);\n> +\t\tfree(amend_marker);\n> +\t\tif (conflicted_stop)\n> +\t\t\tdie(_(\"You are resolving conflicts during a rebase -- cannot amend.\"));\n> +\t}\n>   \tif (fixup_message && squash_message)\n>   \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--squash\", \"--fixup\");\n>   \tdie_for_incompatible_opt4(!!use_message, \"-C\",\n> diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh\n> index ff11abb2f2..01d4735b3b 100755\n> --- a/t/t3404-rebase-interactive.sh\n> +++ b/t/t3404-rebase-interactive.sh\n> @@ -1884,6 +1884,93 @@ test_expect_success 'correct error message for commit --amend after empty pick'\n>   \ttest_grep \"middle of a rebase -- cannot amend.\" err\n>   '\n>   \n> +test_expect_success 'commit --amend is refused at a rebase conflict stop' '\n> +\ttest_when_finished \"git rebase --abort\" &&\n> +\tgit checkout --detach conflict-branch &&\n> +\t(\n> +\t\tset_fake_editor &&\n> +\t\tFAKE_LINES=\"1 3\" &&\n> +\t\texport FAKE_LINES &&\n> +\t\ttest_must_fail git rebase -i A\n> +\t) &&\n> +\ttest_path_is_file .git/rebase-merge/patch &&\n> +\ttest_path_is_missing .git/rebase-merge/amend &&\n> +\techo resolved >conflict &&\n> +\tgit add conflict &&\n> +\ttest_must_fail git commit --amend --no-edit 2>err &&\n> +\ttest_grep \"You are resolving conflicts during a rebase -- cannot amend\" err\n> +'\n> +\n> +test_expect_success 'commit --amend is refused when an \"edit\" pick conflicts' '\n> +\ttest_when_finished \"git rebase --abort\" &&\n> +\tgit checkout --detach conflict-branch &&\n> +\t(\n> +\t\tset_fake_editor &&\n> +\t\tFAKE_LINES=\"1 edit 3\" &&\n> +\t\texport FAKE_LINES &&\n> +\t\ttest_must_fail git rebase -i A\n> +\t) &&\n> +\ttest_path_is_file .git/rebase-merge/patch &&\n> +\ttest_path_is_missing .git/rebase-merge/amend &&\n> +\techo resolved >conflict &&\n> +\tgit add conflict &&\n> +\ttest_must_fail git commit --amend --no-edit 2>err &&\n> +\ttest_grep \"You are resolving conflicts during a rebase -- cannot amend\" err\n> +'\n> +\n> +test_expect_success 'commit --amend is allowed at a rebase edit stop' '\n> +\ttest_when_finished \"git rebase --abort\" &&\n> +\tgit checkout --detach no-conflict-branch &&\n> +\t(\n> +\t\tset_fake_editor &&\n> +\t\tFAKE_LINES=\"edit 1 2 3 4\" &&\n> +\t\texport FAKE_LINES &&\n> +\t\tgit rebase -i A\n> +\t) &&\n> +\ttest_path_is_file .git/rebase-merge/amend &&\n> +\techo tweak >fileJ &&\n> +\tgit add fileJ &&\n> +\tgit commit --amend --no-edit\n> +'\n> +\n> +test_expect_success 'commit --amend is allowed at a rebase break stop' '\n> +\ttest_when_finished \"git rebase --abort\" &&\n> +\tgit checkout --detach no-conflict-branch &&\n> +\t(\n> +\t\tset_fake_editor &&\n> +\t\tFAKE_LINES=\"break 1 2 3 4\" &&\n> +\t\texport FAKE_LINES &&\n> +\t\tgit rebase -i A\n> +\t) &&\n> +\ttest_must_fail git rev-parse --verify REBASE_HEAD &&\n> +\techo tweak >fileJ &&\n> +\tgit add fileJ &&\n> +\tgit commit --amend --no-edit\n> +'\n> +\n> +test_expect_success 'commit --amend is refused at an apply-backend conflict stop' '\n> +\ttest_when_finished \"rm -rf apply-backend\" &&\n> +\ttest_create_repo apply-backend &&\n> +\t(\n> +\t\tcd apply-backend &&\n> +\t\ttest_commit base file &&\n> +\t\tgit branch -M mainline &&\n> +\t\ttest_commit upstream file upstream &&\n> +\t\tgit checkout -b side mainline~1 &&\n> +\t\ttest_commit conflicting file side &&\n> +\t\ttest_commit unrelated other &&\n> +\t\ttest_must_fail git rebase --apply mainline &&\n> +\t\t# the apply backend only ever stops for conflicts, and\n> +\t\t# leaves HEAD on the previously-applied commit\n> +\t\ttest_path_is_dir .git/rebase-apply &&\n> +\t\ttest_path_is_missing .git/rebase-apply/applying &&\n> +\t\techo resolved >file &&\n> +\t\tgit add file &&\n> +\t\ttest_must_fail git commit --amend --no-edit 2>err &&\n> +\t\ttest_grep \"You are resolving conflicts during a rebase -- cannot amend\" err\n> +\t)\n> +'\n> +\n>   test_expect_success 'todo has correct onto hash' '\n>   \tGIT_SEQUENCE_EDITOR=cat git rebase -i no-conflict-branch~4 no-conflict-branch >actual &&\n>   \tonto=$(git rev-parse --short HEAD~4) &&\n> diff --git a/t/t3507-cherry-pick-conflict.sh b/t/t3507-cherry-pick-conflict.sh\n> index 44596cb1e8..42de398f76 100755\n> --- a/t/t3507-cherry-pick-conflict.sh\n> +++ b/t/t3507-cherry-pick-conflict.sh\n> @@ -364,6 +364,17 @@ test_expect_success 'failed revert sets REVERT_HEAD' '\n>   \ttest_cmp_rev picked REVERT_HEAD\n>   '\n>   \n> +test_expect_success 'commit --amend of revert fails' '\n> +\tpristine_detach initial &&\n> +\n> +\ttest_must_fail git revert picked &&\n> +\techo resolved >foo &&\n> +\tgit add foo &&\n> +\ttest_must_fail git commit --amend 2>err &&\n> +\n> +\ttest_grep \"in the middle of a revert -- cannot amend.\" err\n> +'\n> +\n>   test_expect_success 'successful revert does not set REVERT_HEAD' '\n>   \tpristine_detach base &&\n>   \tgit revert base &&\n> diff --git a/t/t4151-am-abort.sh b/t/t4151-am-abort.sh\n> index 8e1ecf8a68..9313a074b2 100755\n> --- a/t/t4151-am-abort.sh\n> +++ b/t/t4151-am-abort.sh\n> @@ -63,6 +63,17 @@ do\n>   \n>   done\n>   \n> +test_expect_success 'commit --amend during a failed am fails' '\n> +\tgit reset --hard initial &&\n> +\tcp file-2-expect file-2 &&\n> +\ttest_must_fail git am 000[1245]-*.patch &&\n> +\techo resolved >file-1 &&\n> +\tgit add file-1 &&\n> +\ttest_must_fail git commit --amend 2>err &&\n> +\ttest_grep \"in the middle of an am session -- cannot amend.\" err &&\n> +\tgit am --abort\n> +'\n> +\n>   test_expect_success 'am -3 --skip removes otherfile-4' '\n>   \tgit reset --hard initial &&\n>   \ttest_must_fail git am -3 0003-*.patch &&\n> \n> base-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e\n\n"},{"id":"551292","messageId":"xmqqzey8j1gt.fsf@gitster.g","threadId":"66221","inReplyTo":"pull.2389.git.git.1787721681893.gitgitgadget@gmail.com","subject":"Re: [PATCH] commit: refuse to amend during conflict resolution","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-26T16:22:42Z","receivedAt":"2026-08-26T16:22:48Z","isPatch":true,"body":"\"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> ...  However, with the merge backend of rebase we have to\n> be more careful, since it powers interactive rebases and\n>   - the interactive machinery internally uses `git commit --amend` for\n>     `squash` and `reword` directives\n>   - users are expected to `git commit --amend` after hitting an `edit`\n>     or `break` directive\n> So, we need to be careful with rebase to only reject amending when doing\n> conflict resolution.\n\nTrue.\n\nIn addition, in any and all of these scenarios that lets the user\ndeal with conflicts in his or her working tree files and record the\nresult of conflict resolution in a commit, we should reject not only\n\"git commit --amend\" but also \"git commit <paths>\", shouldn't we?\n\nIt may probably be better done in a separate topic, as the guiding\nprinciple is slightly different (i.e., \"recording the conflict\nresolution is about recording the state on top of the current HEAD\nand never about updating the state recorded in the current HEAD\" is\nthe theme of the current topic.  \"recording the conflict resolution\nis always about the entire tree\" is the other topic), so we may want\nto leave a #leftoverbits marker here.\n\n> A few files under the rebase-merge/ directory provide us the necessary\n> information:\n>\n>   - stopped-sha is written only when the rebase stops and hands control\n>     back to the user, so its presence marks a genuine stop -- as opposed\n>     to the sequencer's own internal `git commit --amend` while applying\n>     a squash, fixup, or reword, during which no stopped-sha exists.\n>\n>   - amend is written only when the rebase stops with HEAD already\n>     pointing at the commit the user is meant to amend: a clean `edit`,\n>     or a fast-forward `reword`.  Its absence at a stop therefore means\n>     the commit did not apply, so HEAD is the previously-applied commit\n>     rather than the one being rebased -- exactly the case we refuse.\n>\n> So for the merge backend we die when stopped-sha exists and amend does\n> not.  This covers a plain conflicted pick as well as a conflicted `edit`\n> (both leave HEAD on the previously-applied commit), while still allowing\n> a clean `edit` or `reword` stop and a `break` stop (no stopped-sha).\n> stopped-sha is unlinked at the start of the resume loop, so a resumed\n> squash's internal amend is unaffected.\n\nThat is a sound reasoning.  Nice.\n\n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n"},{"id":"551294","messageId":"xmqqqzjkj0p2.fsf@gitster.g","threadId":"66221","inReplyTo":"pull.2389.git.git.1787721681893.gitgitgadget@gmail.com","subject":"Re: [PATCH] commit: refuse to amend during conflict resolution","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-26T16:39:21Z","receivedAt":"2026-08-26T16:39:28Z","isPatch":true,"body":"\"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> @@ -1336,6 +1337,46 @@ static int parse_and_validate_options(int argc, const char *argv[],\n>  \t\telse if (whence == FROM_REBASE_PICK)\n>  \t\t\tdie(_(\"You are in the middle of a rebase -- cannot amend.\"));\n>  \t}\n\nLet's make a mental note that the function receives these parameters:\n\nstatic int parse_and_validate_options(int argc, const char *argv[],\n\t\t\t\t      const struct option *options,\n\t\t\t\t      const char * const usage[],\n\t\t\t\t      const char *prefix,\n\t\t\t\t      struct commit *current_head,\n\t\t\t\t      struct wt_status *s)\n\n> +\tif (amend && whence == FROM_COMMIT) {\n> +\t\tchar *applying, *apply_dir, *stopped_sha, *amend_marker;\n> +\t\tint in_am, conflicted_stop;\n> +\n> +\t\t/* Check middle of revert */\n> +\t\tif (refs_ref_exists(get_main_ref_store(the_repository),\n> +\t\t\t\t    \"REVERT_HEAD\"))\n> +\t\t\tdie(_(\"You are in the middle of a revert -- cannot amend.\"));\n\n\"the_repository\" can become \"s->repo\".  The same comment for other\nchecks in this block.\n"},{"id":"551327","messageId":"CABPp-BHx7H5mF8tqs3JkK+Rqjn8hMSjXhohT9NW=NmPsU1by1Q@mail.gmail.com","threadId":"66221","inReplyTo":"4688ee19-b782-456a-bed2-8cd2a4415736@gmail.com","subject":"Re: [PATCH] commit: refuse to amend during conflict resolution","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-08-27T00:21:50Z","receivedAt":"2026-08-27T00:22:02Z","isPatch":true,"body":"On Wed, Aug 26, 2026 at 6:56 AM Phillip Wood <phillip.wood123@gmail.com> wrote:\n>\n[...]\n> > @@ -1336,6 +1337,46 @@ static int parse_and_validate_options(int argc, const char *argv[],\n> >               else if (whence == FROM_REBASE_PICK)\n> >                       die(_(\"You are in the middle of a rebase -- cannot amend.\"));\n> >       }\n> > +     if (amend && whence == FROM_COMMIT) {\n> > +             char *applying, *apply_dir, *stopped_sha, *amend_marker;\n> > +             int in_am, conflicted_stop;\n> > +\n> > +             /* Check middle of revert */\n> > +             if (refs_ref_exists(get_main_ref_store(the_repository),\n> > +                                 \"REVERT_HEAD\"))\n> > +                     die(_(\"You are in the middle of a revert -- cannot amend.\"));\n>\n> I think it would be much cleaner to move this check and sequencer\n> related ones below into sequencer_determine_whence() so that we don't\n> have to hard code the paths here. It might be worth checking for \"am\"\n> and the \"apply\" based rebase in that function as well.\n\nMoving the logic makes sense.  I was a little unsure about putting it\nin sequencer_determine_whence() since commit has its own\ndetermine_whence() based on keeping the merge handling separate.  I\nkind of wanted some function for ongoing_operation, so I just made a\nnew helper, and added merge, am, and the apply-based rebase to it.\n\n> The logic looks sound to me\n\nThanks for taking a look!\n"},{"id":"551328","messageId":"CABPp-BETkvaRUvn36TxgnK4DW6agVbsiHe++FnJFhnRDfkge0A@mail.gmail.com","threadId":"66221","inReplyTo":"xmqqzey8j1gt.fsf@gitster.g","subject":"Re: [PATCH] commit: refuse to amend during conflict resolution","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-08-27T00:23:34Z","receivedAt":"2026-08-27T00:23:48Z","isPatch":true,"body":"On Wed, Aug 26, 2026 at 9:22 AM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> \"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n> > ...  However, with the merge backend of rebase we have to\n> > be more careful, since it powers interactive rebases and\n> >   - the interactive machinery internally uses `git commit --amend` for\n> >     `squash` and `reword` directives\n> >   - users are expected to `git commit --amend` after hitting an `edit`\n> >     or `break` directive\n> > So, we need to be careful with rebase to only reject amending when doing\n> > conflict resolution.\n>\n> True.\n>\n> In addition, in any and all of these scenarios that lets the user\n> deal with conflicts in his or her working tree files and record the\n> result of conflict resolution in a commit, we should reject not only\n> \"git commit --amend\" but also \"git commit <paths>\", shouldn't we?\n>\n> It may probably be better done in a separate topic, as the guiding\n> principle is slightly different (i.e., \"recording the conflict\n> resolution is about recording the state on top of the current HEAD\n> and never about updating the state recorded in the current HEAD\" is\n> the theme of the current topic.  \"recording the conflict resolution\n> is always about the entire tree\" is the other topic), so we may want\n> to leave a #leftoverbits marker here.\n\nOh, good callout.  And later in commit.c we do disallow those, but\nonly for the same operations we previously disallowed and amend\nduring:\n\n        commit_style = COMMIT_PARTIAL;\n\n        if (whence != FROM_COMMIT) {\n                if (whence == FROM_MERGE)\n                        die(_(\"cannot do a partial commit during a merge.\"));\n                else if (is_from_cherry_pick(whence))\n                        die(_(\"cannot do a partial commit during a\ncherry-pick.\"));\n                else if (is_from_rebase(whence))\n                        die(_(\"cannot do a partial commit during a rebase.\"));\n        }\n\nThe exact same additional structure could apply there, and that kind\nof reinforces Phillip's suggestion to factor out a helper that we can\ncall.  I did that in v2.\n"},{"id":"551329","messageId":"CABPp-BFwN_ek_t67V5nPruV5vL0hSzZyoy3ut3rvzNsWv2DKpw@mail.gmail.com","threadId":"66221","inReplyTo":"xmqqqzjkj0p2.fsf@gitster.g","subject":"Re: [PATCH] commit: refuse to amend during conflict resolution","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-08-27T00:24:40Z","receivedAt":"2026-08-27T00:24:54Z","isPatch":true,"body":"On Wed, Aug 26, 2026 at 9:39 AM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> \"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n> > @@ -1336,6 +1337,46 @@ static int parse_and_validate_options(int argc, const char *argv[],\n> >               else if (whence == FROM_REBASE_PICK)\n> >                       die(_(\"You are in the middle of a rebase -- cannot amend.\"));\n> >       }\n>\n> Let's make a mental note that the function receives these parameters:\n>\n> static int parse_and_validate_options(int argc, const char *argv[],\n>                                       const struct option *options,\n>                                       const char * const usage[],\n>                                       const char *prefix,\n>                                       struct commit *current_head,\n>                                       struct wt_status *s)\n>\n> > +     if (amend && whence == FROM_COMMIT) {\n> > +             char *applying, *apply_dir, *stopped_sha, *amend_marker;\n> > +             int in_am, conflicted_stop;\n> > +\n> > +             /* Check middle of revert */\n> > +             if (refs_ref_exists(get_main_ref_store(the_repository),\n> > +                                 \"REVERT_HEAD\"))\n> > +                     die(_(\"You are in the middle of a revert -- cannot amend.\"));\n>\n> \"the_repository\" can become \"s->repo\".  The same comment for other\n> checks in this block.\n\nGood catch; fixed in v2...although the partial commit callsite didn't\nhave a handy repo that I could spot, so I still used the_repository on\nthat one, but I used s->repo as you suggested for the amend site.\n"},{"id":"551330","messageId":"pull.2389.v2.git.git.1787792534.gitgitgadget@gmail.com","threadId":"66221","inReplyTo":"pull.2389.git.git.1787721681893.gitgitgadget@gmail.com","subject":"[PATCH v2 0/3] commit: refuse to amend during conflict resolution","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-08-27T01:02:11Z","receivedAt":"2026-08-27T01:02:17Z","isPatch":true,"body":"Both git commit --amend and a partial commit (git commit <paths>) are\nfoot-guns while the user is in the middle of an operation that resolves\nconflicts on top of HEAD: recording a conflict resolution is about capturing\nthe state of the whole tree as a new commit, not about rewriting HEAD or\ncommitting a subset of paths.\n\nHistorically we only rejected these during a merge or a cherry-pick or when\nresolving an empty pick during a rebase. The same hazard exists for am,\nrevert, and rebase conflict stops, none of which were covered. This series\nextends the refusal to all of them.\n\nThe three patches:\n\n 1. reword the two pre-existing \"empty commit\" rebase messages, which were\n    misleadingly generic\n 2. refuse git commit --amend during these additional operations\n 3. refuse partial commits during the same operations.\n\nElijah Newren (3):\n  commit: reword the empty-commit rebase errors\n  commit: refuse to amend during conflict resolution\n  commit: refuse partial commits during conflict resolution\n\n builtin/commit.c                |  51 +++++++++----\n sequencer.c                     |  65 +++++++++++++++++\n sequencer.h                     |  24 ++++++\n t/t3404-rebase-interactive.sh   | 125 +++++++++++++++++++++++++++++++-\n t/t3507-cherry-pick-conflict.sh |  22 ++++++\n t/t4151-am-abort.sh             |  22 ++++++\n 6 files changed, 293 insertions(+), 16 deletions(-)\n\n\nbase-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2389%2Fnewren%2Frefuse-amend-during-conflicts-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2389/newren/refuse-amend-during-conflicts-v2\nPull-Request: https://github.com/git/git/pull/2389\n\nRange-diff vs v1:\n\n -:  ---------- > 1:  65c48ed3cb commit: reword the empty-commit rebase errors\n 1:  a3d6b059c6 ! 2:  4a1461e527 commit: refuse to amend during conflict resolution\n     @@ Commit message\n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n       ## builtin/commit.c ##\n     -@@\n     - #include \"path.h\"\n     - #include \"preload-index.h\"\n     - #include \"read-cache.h\"\n     -+#include \"refs.h\"\n     - #include \"repository.h\"\n     - #include \"string-list.h\"\n     - #include \"rerere.h\"\n      @@ builtin/commit.c: static int parse_and_validate_options(int argc, const char *argv[],\n     - \t\telse if (whence == FROM_REBASE_PICK)\n     - \t\t\tdie(_(\"You are in the middle of a rebase -- cannot amend.\"));\n     + \t\tuse_editor = 0;\n     + \n     + \t/* Sanity check options */\n     +-\tif (amend && !current_head)\n     +-\t\tdie(_(\"You have nothing to amend.\"));\n     +-\tif (amend && whence != FROM_COMMIT) {\n     +-\t\tif (whence == FROM_MERGE)\n     ++\tif (amend) {\n     ++\t\tif (!current_head)\n     ++\t\t\tdie(_(\"You have nothing to amend.\"));\n     ++\t\t/*\n     ++\t\t * Refuse to amend in the middle of any operation that is\n     ++\t\t * meant to record its result as a new commit on top of HEAD\n     ++\t\t * rather than by rewriting HEAD.\n     ++\t\t */\n     ++\t\tswitch (sequencer_ongoing_operation(s->repo, whence)) {\n     ++\t\tcase ONGOING_NONE:\n     ++\t\t\tbreak;\n     ++\t\tcase ONGOING_MERGE:\n     + \t\t\tdie(_(\"You are in the middle of a merge -- cannot amend.\"));\n     +-\t\telse if (is_from_cherry_pick(whence))\n     ++\t\tcase ONGOING_CHERRY_PICK:\n     + \t\t\tdie(_(\"You are in the middle of a cherry-pick -- cannot amend.\"));\n     +-\t\telse if (whence == FROM_REBASE_PICK)\n     ++\t\tcase ONGOING_REBASE_EMPTY:\n     + \t\t\tdie(_(\"You are resolving a commit that became empty -- cannot amend.\"));\n     ++\t\tcase ONGOING_REVERT:\n     ++\t\t\tdie(_(\"You are in the middle of a revert -- cannot amend.\"));\n     ++\t\tcase ONGOING_AM:\n     ++\t\t\tdie(_(\"You are in the middle of an am session -- cannot amend.\"));\n     ++\t\tcase ONGOING_REBASE_CONFLICT:\n     ++\t\t\tdie(_(\"You are resolving conflicts during a rebase -- cannot amend.\"));\n     ++\t\t}\n       \t}\n     -+\tif (amend && whence == FROM_COMMIT) {\n     -+\t\tchar *applying, *apply_dir, *stopped_sha, *amend_marker;\n     -+\t\tint in_am, conflicted_stop;\n     + \tif (fixup_message && squash_message)\n     + \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--squash\", \"--fixup\");\n     +\n     + ## sequencer.c ##\n     +@@ sequencer.c: int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)\n     + \treturn 0;\n     + }\n     + \n     ++enum ongoing_operation sequencer_ongoing_operation(struct repository *r,\n     ++\t\t\t\t\t\t   enum commit_whence whence)\n     ++{\n     ++\tchar *path;\n     ++\tint found;\n      +\n     -+\t\t/* Check middle of revert */\n     -+\t\tif (refs_ref_exists(get_main_ref_store(the_repository),\n     -+\t\t\t\t    \"REVERT_HEAD\"))\n     -+\t\t\tdie(_(\"You are in the middle of a revert -- cannot amend.\"));\n     ++\t/*\n     ++\t * The merge, cherry-pick, and (empty) rebase-pick stops are already\n     ++\t * distinguished by 'whence'.\n     ++\t */\n     ++\tswitch (whence) {\n     ++\tcase FROM_MERGE:\n     ++\t\treturn ONGOING_MERGE;\n     ++\tcase FROM_CHERRY_PICK_SINGLE:\n     ++\tcase FROM_CHERRY_PICK_MULTI:\n     ++\t\treturn ONGOING_CHERRY_PICK;\n     ++\tcase FROM_REBASE_PICK:\n     ++\t\treturn ONGOING_REBASE_EMPTY;\n     ++\tcase FROM_COMMIT:\n     ++\t\tbreak;\n     ++\t}\n      +\n     -+\t\t/* Check middle of `am` */\n     -+\t\tapplying = repo_git_path(the_repository,\n     -+\t\t\t\t\t \"rebase-apply/applying\");\n     -+\t\tin_am = file_exists(applying);\n     ++\t/*\n     ++\t * 'whence' is FROM_COMMIT, but we may still be in the middle of an\n     ++\t * operation that records its result on top of HEAD; detect those\n     ++\t * from their on-disk state.\n     ++\t */\n      +\n     -+\t\tfree(applying);\n     -+\t\tif (in_am)\n     -+\t\t\tdie(_(\"You are in the middle of an am session -- cannot amend.\"));\n     ++\t/* In the middle of a revert? */\n     ++\tif (refs_ref_exists(get_main_ref_store(r), \"REVERT_HEAD\"))\n     ++\t\treturn ONGOING_REVERT;\n      +\n     -+\t\t/* Check middle of rebase specifically stopped for conflicts */\n     -+\t\tapply_dir = repo_git_path(the_repository,\n     -+\t\t\t\t\t  \"rebase-apply\");\n     -+\t\tstopped_sha = repo_git_path(the_repository,\n     -+\t\t\t\t\t    \"rebase-merge/stopped-sha\");\n     -+\t\tamend_marker = repo_git_path(the_repository,\n     -+\t\t\t\t\t     \"rebase-merge/amend\");\n     -+\t\t/*\n     -+\t\t * The apply backend only ever stops for conflicts; the\n     -+\t\t * merge backend writes stopped-sha but omits `amend`,\n     -+\t\t * which it writes only at a clean edit/reword stop.\n     -+\t\t */\n     -+\t\tconflicted_stop =\n     -+\t\t\tfile_exists(apply_dir) ||\n     -+\t\t\t(file_exists(stopped_sha) && !file_exists(amend_marker));\n     ++\t/* In the middle of an `am`? */\n     ++\tpath = repo_git_path(r, \"rebase-apply/applying\");\n     ++\tfound = file_exists(path);\n     ++\tfree(path);\n     ++\tif (found)\n     ++\t\treturn ONGOING_AM;\n     ++\n     ++\t/*\n     ++\t * In the middle of a rebase that stopped for conflict resolution?\n     ++\t * The apply backend only ever stops for conflicts, so the presence\n     ++\t * of its state directory is enough.  The merge backend writes\n     ++\t * stopped-sha whenever it hands control back to the user, but omits\n     ++\t * `amend` unless it stopped with HEAD already pointing at the commit\n     ++\t * to be amended (a clean edit/reword stop); its absence therefore\n     ++\t * marks a conflicted stop.\n     ++\t */\n     ++\tpath = repo_git_path(r, \"rebase-apply\");\n     ++\tfound = file_exists(path);\n     ++\tfree(path);\n     ++\tif (!found) {\n     ++\t\tchar *stopped_sha = repo_git_path(r, \"rebase-merge/stopped-sha\");\n     ++\t\tchar *amend_marker = repo_git_path(r, \"rebase-merge/amend\");\n      +\n     -+\t\tfree(apply_dir);\n     ++\t\tfound = file_exists(stopped_sha) && !file_exists(amend_marker);\n      +\t\tfree(stopped_sha);\n      +\t\tfree(amend_marker);\n     -+\t\tif (conflicted_stop)\n     -+\t\t\tdie(_(\"You are resolving conflicts during a rebase -- cannot amend.\"));\n      +\t}\n     - \tif (fixup_message && squash_message)\n     - \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--squash\", \"--fixup\");\n     - \tdie_for_incompatible_opt4(!!use_message, \"-C\",\n     ++\tif (found)\n     ++\t\treturn ONGOING_REBASE_CONFLICT;\n     ++\n     ++\treturn ONGOING_NONE;\n     ++}\n     ++\n     + int sequencer_get_update_refs_state(const char *wt_dir,\n     + \t\t\t\t    struct string_list *refs)\n     + {\n     +\n     + ## sequencer.h ##\n     +@@ sequencer.h: int sequencer_get_last_command(struct repository* r,\n     + \t\t\t       enum replay_action *action);\n     + int sequencer_determine_whence(struct repository *r, enum commit_whence *whence);\n     + \n     ++/*\n     ++ * An in-progress operation that records its result (often a conflict\n     ++ * resolution) as a new commit on top of HEAD, during which amending\n     ++ * HEAD via \"git commit --amend\" is almost always a mistake.\n     ++ */\n     ++enum ongoing_operation {\n     ++\tONGOING_NONE = 0,\n     ++\tONGOING_MERGE,\n     ++\tONGOING_CHERRY_PICK,\n     ++\tONGOING_REBASE_EMPTY,\n     ++\tONGOING_REVERT,\n     ++\tONGOING_AM,\n     ++\tONGOING_REBASE_CONFLICT\n     ++};\n     ++\n     ++/*\n     ++ * Return which in-progress operation, if any, is underway; see enum\n     ++ * ongoing_operation.  'whence' is the origin already computed for the\n     ++ * pending commit.\n     ++ */\n     ++enum ongoing_operation sequencer_ongoing_operation(struct repository *r,\n     ++\t\t\t\t\t\t   enum commit_whence whence);\n     ++\n     + /**\n     +  * Append the set of ref-OID pairs that are currently stored for the 'git\n     +  * rebase --update-refs' feature if such a rebase is currently happening.\n      \n       ## t/t3404-rebase-interactive.sh ##\n      @@ t/t3404-rebase-interactive.sh: test_expect_success 'correct error message for commit --amend after empty pick'\n     - \ttest_grep \"middle of a rebase -- cannot amend.\" err\n     + \ttest_grep \"resolving a commit that became empty -- cannot amend.\" err\n       '\n       \n      +test_expect_success 'commit --amend is refused at a rebase conflict stop' '\n -:  ---------- > 3:  e0be8cdf63 commit: refuse partial commits during conflict resolution\n\n-- \ngitgitgadget\n"},{"id":"551331","messageId":"65c48ed3cb638cf0be18a3aa6d86d4c4f2cf01a2.1787792534.git.gitgitgadget@gmail.com","threadId":"66221","inReplyTo":"pull.2389.v2.git.git.1787792534.gitgitgadget@gmail.com","subject":"[PATCH v2 1/3] commit: reword the empty-commit rebase errors","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-08-27T01:02:12Z","receivedAt":"2026-08-27T01:02:18Z","isPatch":true,"body":"From: Elijah Newren <newren@gmail.com>\n\nWhen a rebase applies a commit that becomes empty, it stops and asks the\nuser to decide whether to keep it or drop it.  HEAD still points at the\npreviously-applied commit at that point, so either amending or creating\na partial commit is refused, with one of the following messages:\n\n    You are in the middle of a rebase -- cannot amend.\n    cannot do a partial commit during a rebase.\n\nNeither message hints that the real problem is a commit that became\nempty, and \"during a rebase\" is overly broad besides -- amending and\npartial commits are fine at an `edit` or `break` stop.  Reword both to\ndescribe the actual situation.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/commit.c              | 4 ++--\n t/t3404-rebase-interactive.sh | 4 ++--\n 2 files changed, 4 insertions(+), 4 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 28f6174503..0d908d72bb 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -521,7 +521,7 @@ static const char *prepare_index(const char **argv, const char *prefix,\n \t\telse if (is_from_cherry_pick(whence))\n \t\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n \t\telse if (is_from_rebase(whence))\n-\t\t\tdie(_(\"cannot do a partial commit during a rebase.\"));\n+\t\t\tdie(_(\"cannot do a partial commit while resolving a commit that became empty.\"));\n \t}\n \n \tif (list_paths(&partial, !current_head ? NULL : \"HEAD\", &pathspec))\n@@ -1334,7 +1334,7 @@ static int parse_and_validate_options(int argc, const char *argv[],\n \t\telse if (is_from_cherry_pick(whence))\n \t\t\tdie(_(\"You are in the middle of a cherry-pick -- cannot amend.\"));\n \t\telse if (whence == FROM_REBASE_PICK)\n-\t\t\tdie(_(\"You are in the middle of a rebase -- cannot amend.\"));\n+\t\t\tdie(_(\"You are resolving a commit that became empty -- cannot amend.\"));\n \t}\n \tif (fixup_message && squash_message)\n \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--squash\", \"--fixup\");\ndiff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh\nindex ff11abb2f2..1e78dbfd90 100755\n--- a/t/t3404-rebase-interactive.sh\n+++ b/t/t3404-rebase-interactive.sh\n@@ -1868,7 +1868,7 @@ test_expect_success 'correct error message for partial commit after empty pick'\n \t) &&\n \techo x >file1 &&\n \ttest_must_fail git commit file1 2>err &&\n-\ttest_grep \"cannot do a partial commit during a rebase.\" err\n+\ttest_grep \"cannot do a partial commit while resolving a commit that became empty.\" err\n '\n \n test_expect_success 'correct error message for commit --amend after empty pick' '\n@@ -1881,7 +1881,7 @@ test_expect_success 'correct error message for commit --amend after empty pick'\n \t) &&\n \techo x>file1 &&\n \ttest_must_fail git commit -a --amend 2>err &&\n-\ttest_grep \"middle of a rebase -- cannot amend.\" err\n+\ttest_grep \"resolving a commit that became empty -- cannot amend.\" err\n '\n \n test_expect_success 'todo has correct onto hash' '\n-- \ngitgitgadget\n\n"},{"id":"551332","messageId":"4a1461e52767227d9475de13cf250484f8ecd271.1787792534.git.gitgitgadget@gmail.com","threadId":"66221","inReplyTo":"pull.2389.v2.git.git.1787792534.gitgitgadget@gmail.com","subject":"[PATCH v2 2/3] commit: refuse to amend during conflict resolution","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-08-27T01:02:13Z","receivedAt":"2026-08-27T01:02:19Z","isPatch":true,"body":"From: Elijah Newren <newren@gmail.com>\n\nRunning `git commit --amend` during conflict resolution is an ugly\nfoot-gun.  For many years, we have rejected amending during conflict\nresolution in the middle of\n  - a merge\n  - a cherry-pick\nHowever, this was never extended to other operations that can also\nproduce conflicts:\n  - an `am` operation\n  - a revert\n  - a rebase\n\nExtend it to handle these other cases now.\n\nExtending to `am`, revert, and the apply backend of rebase are fairly\nstraightforward.  However, with the merge backend of rebase we have to\nbe more careful, since it powers interactive rebases and\n  - the interactive machinery internally uses `git commit --amend` for\n    `squash` and `reword` directives\n  - users are expected to `git commit --amend` after hitting an `edit`\n    or `break` directive\nSo, we need to be careful with rebase to only reject amending when doing\nconflict resolution.\n\nA few files under the rebase-merge/ directory provide us the necessary\ninformation:\n\n  - stopped-sha is written only when the rebase stops and hands control\n    back to the user, so its presence marks a genuine stop -- as opposed\n    to the sequencer's own internal `git commit --amend` while applying\n    a squash, fixup, or reword, during which no stopped-sha exists.\n\n  - amend is written only when the rebase stops with HEAD already\n    pointing at the commit the user is meant to amend: a clean `edit`,\n    or a fast-forward `reword`.  Its absence at a stop therefore means\n    the commit did not apply, so HEAD is the previously-applied commit\n    rather than the one being rebased -- exactly the case we refuse.\n\nSo for the merge backend we die when stopped-sha exists and amend does\nnot.  This covers a plain conflicted pick as well as a conflicted `edit`\n(both leave HEAD on the previously-applied commit), while still allowing\na clean `edit` or `reword` stop and a `break` stop (no stopped-sha).\nstopped-sha is unlinked at the start of the resume loop, so a resumed\nsquash's internal amend is unaffected.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/commit.c                | 27 +++++++---\n sequencer.c                     | 65 ++++++++++++++++++++++++\n sequencer.h                     | 23 +++++++++\n t/t3404-rebase-interactive.sh   | 87 +++++++++++++++++++++++++++++++++\n t/t3507-cherry-pick-conflict.sh | 11 +++++\n t/t4151-am-abort.sh             | 11 +++++\n 6 files changed, 218 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 0d908d72bb..4a6054aae0 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -1326,15 +1326,30 @@ static int parse_and_validate_options(int argc, const char *argv[],\n \t\tuse_editor = 0;\n \n \t/* Sanity check options */\n-\tif (amend && !current_head)\n-\t\tdie(_(\"You have nothing to amend.\"));\n-\tif (amend && whence != FROM_COMMIT) {\n-\t\tif (whence == FROM_MERGE)\n+\tif (amend) {\n+\t\tif (!current_head)\n+\t\t\tdie(_(\"You have nothing to amend.\"));\n+\t\t/*\n+\t\t * Refuse to amend in the middle of any operation that is\n+\t\t * meant to record its result as a new commit on top of HEAD\n+\t\t * rather than by rewriting HEAD.\n+\t\t */\n+\t\tswitch (sequencer_ongoing_operation(s->repo, whence)) {\n+\t\tcase ONGOING_NONE:\n+\t\t\tbreak;\n+\t\tcase ONGOING_MERGE:\n \t\t\tdie(_(\"You are in the middle of a merge -- cannot amend.\"));\n-\t\telse if (is_from_cherry_pick(whence))\n+\t\tcase ONGOING_CHERRY_PICK:\n \t\t\tdie(_(\"You are in the middle of a cherry-pick -- cannot amend.\"));\n-\t\telse if (whence == FROM_REBASE_PICK)\n+\t\tcase ONGOING_REBASE_EMPTY:\n \t\t\tdie(_(\"You are resolving a commit that became empty -- cannot amend.\"));\n+\t\tcase ONGOING_REVERT:\n+\t\t\tdie(_(\"You are in the middle of a revert -- cannot amend.\"));\n+\t\tcase ONGOING_AM:\n+\t\t\tdie(_(\"You are in the middle of an am session -- cannot amend.\"));\n+\t\tcase ONGOING_REBASE_CONFLICT:\n+\t\t\tdie(_(\"You are resolving conflicts during a rebase -- cannot amend.\"));\n+\t\t}\n \t}\n \tif (fixup_message && squash_message)\n \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--squash\", \"--fixup\");\ndiff --git a/sequencer.c b/sequencer.c\nindex 65afd100d9..bd4a724410 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -6966,6 +6966,71 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)\n \treturn 0;\n }\n \n+enum ongoing_operation sequencer_ongoing_operation(struct repository *r,\n+\t\t\t\t\t\t   enum commit_whence whence)\n+{\n+\tchar *path;\n+\tint found;\n+\n+\t/*\n+\t * The merge, cherry-pick, and (empty) rebase-pick stops are already\n+\t * distinguished by 'whence'.\n+\t */\n+\tswitch (whence) {\n+\tcase FROM_MERGE:\n+\t\treturn ONGOING_MERGE;\n+\tcase FROM_CHERRY_PICK_SINGLE:\n+\tcase FROM_CHERRY_PICK_MULTI:\n+\t\treturn ONGOING_CHERRY_PICK;\n+\tcase FROM_REBASE_PICK:\n+\t\treturn ONGOING_REBASE_EMPTY;\n+\tcase FROM_COMMIT:\n+\t\tbreak;\n+\t}\n+\n+\t/*\n+\t * 'whence' is FROM_COMMIT, but we may still be in the middle of an\n+\t * operation that records its result on top of HEAD; detect those\n+\t * from their on-disk state.\n+\t */\n+\n+\t/* In the middle of a revert? */\n+\tif (refs_ref_exists(get_main_ref_store(r), \"REVERT_HEAD\"))\n+\t\treturn ONGOING_REVERT;\n+\n+\t/* In the middle of an `am`? */\n+\tpath = repo_git_path(r, \"rebase-apply/applying\");\n+\tfound = file_exists(path);\n+\tfree(path);\n+\tif (found)\n+\t\treturn ONGOING_AM;\n+\n+\t/*\n+\t * In the middle of a rebase that stopped for conflict resolution?\n+\t * The apply backend only ever stops for conflicts, so the presence\n+\t * of its state directory is enough.  The merge backend writes\n+\t * stopped-sha whenever it hands control back to the user, but omits\n+\t * `amend` unless it stopped with HEAD already pointing at the commit\n+\t * to be amended (a clean edit/reword stop); its absence therefore\n+\t * marks a conflicted stop.\n+\t */\n+\tpath = repo_git_path(r, \"rebase-apply\");\n+\tfound = file_exists(path);\n+\tfree(path);\n+\tif (!found) {\n+\t\tchar *stopped_sha = repo_git_path(r, \"rebase-merge/stopped-sha\");\n+\t\tchar *amend_marker = repo_git_path(r, \"rebase-merge/amend\");\n+\n+\t\tfound = file_exists(stopped_sha) && !file_exists(amend_marker);\n+\t\tfree(stopped_sha);\n+\t\tfree(amend_marker);\n+\t}\n+\tif (found)\n+\t\treturn ONGOING_REBASE_CONFLICT;\n+\n+\treturn ONGOING_NONE;\n+}\n+\n int sequencer_get_update_refs_state(const char *wt_dir,\n \t\t\t\t    struct string_list *refs)\n {\ndiff --git a/sequencer.h b/sequencer.h\nindex 64a9c7fb1b..3a4bd97db1 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -277,6 +277,29 @@ int sequencer_get_last_command(struct repository* r,\n \t\t\t       enum replay_action *action);\n int sequencer_determine_whence(struct repository *r, enum commit_whence *whence);\n \n+/*\n+ * An in-progress operation that records its result (often a conflict\n+ * resolution) as a new commit on top of HEAD, during which amending\n+ * HEAD via \"git commit --amend\" is almost always a mistake.\n+ */\n+enum ongoing_operation {\n+\tONGOING_NONE = 0,\n+\tONGOING_MERGE,\n+\tONGOING_CHERRY_PICK,\n+\tONGOING_REBASE_EMPTY,\n+\tONGOING_REVERT,\n+\tONGOING_AM,\n+\tONGOING_REBASE_CONFLICT\n+};\n+\n+/*\n+ * Return which in-progress operation, if any, is underway; see enum\n+ * ongoing_operation.  'whence' is the origin already computed for the\n+ * pending commit.\n+ */\n+enum ongoing_operation sequencer_ongoing_operation(struct repository *r,\n+\t\t\t\t\t\t   enum commit_whence whence);\n+\n /**\n  * Append the set of ref-OID pairs that are currently stored for the 'git\n  * rebase --update-refs' feature if such a rebase is currently happening.\ndiff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh\nindex 1e78dbfd90..7cf06e5f9a 100755\n--- a/t/t3404-rebase-interactive.sh\n+++ b/t/t3404-rebase-interactive.sh\n@@ -1884,6 +1884,93 @@ test_expect_success 'correct error message for commit --amend after empty pick'\n \ttest_grep \"resolving a commit that became empty -- cannot amend.\" err\n '\n \n+test_expect_success 'commit --amend is refused at a rebase conflict stop' '\n+\ttest_when_finished \"git rebase --abort\" &&\n+\tgit checkout --detach conflict-branch &&\n+\t(\n+\t\tset_fake_editor &&\n+\t\tFAKE_LINES=\"1 3\" &&\n+\t\texport FAKE_LINES &&\n+\t\ttest_must_fail git rebase -i A\n+\t) &&\n+\ttest_path_is_file .git/rebase-merge/patch &&\n+\ttest_path_is_missing .git/rebase-merge/amend &&\n+\techo resolved >conflict &&\n+\tgit add conflict &&\n+\ttest_must_fail git commit --amend --no-edit 2>err &&\n+\ttest_grep \"You are resolving conflicts during a rebase -- cannot amend\" err\n+'\n+\n+test_expect_success 'commit --amend is refused when an \"edit\" pick conflicts' '\n+\ttest_when_finished \"git rebase --abort\" &&\n+\tgit checkout --detach conflict-branch &&\n+\t(\n+\t\tset_fake_editor &&\n+\t\tFAKE_LINES=\"1 edit 3\" &&\n+\t\texport FAKE_LINES &&\n+\t\ttest_must_fail git rebase -i A\n+\t) &&\n+\ttest_path_is_file .git/rebase-merge/patch &&\n+\ttest_path_is_missing .git/rebase-merge/amend &&\n+\techo resolved >conflict &&\n+\tgit add conflict &&\n+\ttest_must_fail git commit --amend --no-edit 2>err &&\n+\ttest_grep \"You are resolving conflicts during a rebase -- cannot amend\" err\n+'\n+\n+test_expect_success 'commit --amend is allowed at a rebase edit stop' '\n+\ttest_when_finished \"git rebase --abort\" &&\n+\tgit checkout --detach no-conflict-branch &&\n+\t(\n+\t\tset_fake_editor &&\n+\t\tFAKE_LINES=\"edit 1 2 3 4\" &&\n+\t\texport FAKE_LINES &&\n+\t\tgit rebase -i A\n+\t) &&\n+\ttest_path_is_file .git/rebase-merge/amend &&\n+\techo tweak >fileJ &&\n+\tgit add fileJ &&\n+\tgit commit --amend --no-edit\n+'\n+\n+test_expect_success 'commit --amend is allowed at a rebase break stop' '\n+\ttest_when_finished \"git rebase --abort\" &&\n+\tgit checkout --detach no-conflict-branch &&\n+\t(\n+\t\tset_fake_editor &&\n+\t\tFAKE_LINES=\"break 1 2 3 4\" &&\n+\t\texport FAKE_LINES &&\n+\t\tgit rebase -i A\n+\t) &&\n+\ttest_must_fail git rev-parse --verify REBASE_HEAD &&\n+\techo tweak >fileJ &&\n+\tgit add fileJ &&\n+\tgit commit --amend --no-edit\n+'\n+\n+test_expect_success 'commit --amend is refused at an apply-backend conflict stop' '\n+\ttest_when_finished \"rm -rf apply-backend\" &&\n+\ttest_create_repo apply-backend &&\n+\t(\n+\t\tcd apply-backend &&\n+\t\ttest_commit base file &&\n+\t\tgit branch -M mainline &&\n+\t\ttest_commit upstream file upstream &&\n+\t\tgit checkout -b side mainline~1 &&\n+\t\ttest_commit conflicting file side &&\n+\t\ttest_commit unrelated other &&\n+\t\ttest_must_fail git rebase --apply mainline &&\n+\t\t# the apply backend only ever stops for conflicts, and\n+\t\t# leaves HEAD on the previously-applied commit\n+\t\ttest_path_is_dir .git/rebase-apply &&\n+\t\ttest_path_is_missing .git/rebase-apply/applying &&\n+\t\techo resolved >file &&\n+\t\tgit add file &&\n+\t\ttest_must_fail git commit --amend --no-edit 2>err &&\n+\t\ttest_grep \"You are resolving conflicts during a rebase -- cannot amend\" err\n+\t)\n+'\n+\n test_expect_success 'todo has correct onto hash' '\n \tGIT_SEQUENCE_EDITOR=cat git rebase -i no-conflict-branch~4 no-conflict-branch >actual &&\n \tonto=$(git rev-parse --short HEAD~4) &&\ndiff --git a/t/t3507-cherry-pick-conflict.sh b/t/t3507-cherry-pick-conflict.sh\nindex 44596cb1e8..42de398f76 100755\n--- a/t/t3507-cherry-pick-conflict.sh\n+++ b/t/t3507-cherry-pick-conflict.sh\n@@ -364,6 +364,17 @@ test_expect_success 'failed revert sets REVERT_HEAD' '\n \ttest_cmp_rev picked REVERT_HEAD\n '\n \n+test_expect_success 'commit --amend of revert fails' '\n+\tpristine_detach initial &&\n+\n+\ttest_must_fail git revert picked &&\n+\techo resolved >foo &&\n+\tgit add foo &&\n+\ttest_must_fail git commit --amend 2>err &&\n+\n+\ttest_grep \"in the middle of a revert -- cannot amend.\" err\n+'\n+\n test_expect_success 'successful revert does not set REVERT_HEAD' '\n \tpristine_detach base &&\n \tgit revert base &&\ndiff --git a/t/t4151-am-abort.sh b/t/t4151-am-abort.sh\nindex 8e1ecf8a68..9313a074b2 100755\n--- a/t/t4151-am-abort.sh\n+++ b/t/t4151-am-abort.sh\n@@ -63,6 +63,17 @@ do\n \n done\n \n+test_expect_success 'commit --amend during a failed am fails' '\n+\tgit reset --hard initial &&\n+\tcp file-2-expect file-2 &&\n+\ttest_must_fail git am 000[1245]-*.patch &&\n+\techo resolved >file-1 &&\n+\tgit add file-1 &&\n+\ttest_must_fail git commit --amend 2>err &&\n+\ttest_grep \"in the middle of an am session -- cannot amend.\" err &&\n+\tgit am --abort\n+'\n+\n test_expect_success 'am -3 --skip removes otherfile-4' '\n \tgit reset --hard initial &&\n \ttest_must_fail git am -3 0003-*.patch &&\n-- \ngitgitgadget\n\n"},{"id":"551333","messageId":"e0be8cdf63446e0535e020abbff2ec209cb518d4.1787792534.git.gitgitgadget@gmail.com","threadId":"66221","inReplyTo":"pull.2389.v2.git.git.1787792534.gitgitgadget@gmail.com","subject":"[PATCH v2 3/3] commit: refuse partial commits during conflict resolution","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-08-27T01:02:14Z","receivedAt":"2026-08-27T01:02:21Z","isPatch":true,"body":"From: Elijah Newren <newren@gmail.com>\n\nSimilar to the previous commit, just as `git commit --amend` is a\nfoot-gun during conflict resolution, so is a partial commit (`git commit\n<paths>`).  Recording a conflict resolution is about capturing the state\nof the entire tree on top of HEAD, not a subset of paths.  For many\nyears we have rejected partial commits in the middle of\n  - a merge\n  - a cherry-pick\n  - a rebase that stopped at a pick\n\nbut, just like amending, this was never extended to the other\noperations that can also leave conflicts to resolve:\n  - an `am` operation\n  - a revert\n  - a rebase that stopped for conflict resolution\n\nReuse sequencer_ongoing_operation(), introduced for the analogous\n`--amend` check, to detect all of these and refuse the partial commit.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/commit.c                | 22 ++++++++++++++-------\n sequencer.h                     |  5 +++--\n t/t3404-rebase-interactive.sh   | 34 +++++++++++++++++++++++++++++++++\n t/t3507-cherry-pick-conflict.sh | 11 +++++++++++\n t/t4151-am-abort.sh             | 11 +++++++++++\n 5 files changed, 74 insertions(+), 9 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 4a6054aae0..9da3f1191b 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -515,13 +515,21 @@ static const char *prepare_index(const char **argv, const char *prefix,\n \t */\n \tcommit_style = COMMIT_PARTIAL;\n \n-\tif (whence != FROM_COMMIT) {\n-\t\tif (whence == FROM_MERGE)\n-\t\t\tdie(_(\"cannot do a partial commit during a merge.\"));\n-\t\telse if (is_from_cherry_pick(whence))\n-\t\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n-\t\telse if (is_from_rebase(whence))\n-\t\t\tdie(_(\"cannot do a partial commit while resolving a commit that became empty.\"));\n+\tswitch (sequencer_ongoing_operation(the_repository, whence)) {\n+\tcase ONGOING_NONE:\n+\t\tbreak;\n+\tcase ONGOING_MERGE:\n+\t\tdie(_(\"cannot do a partial commit during a merge.\"));\n+\tcase ONGOING_CHERRY_PICK:\n+\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n+\tcase ONGOING_REBASE_EMPTY:\n+\t\tdie(_(\"cannot do a partial commit while resolving a commit that became empty.\"));\n+\tcase ONGOING_REVERT:\n+\t\tdie(_(\"cannot do a partial commit during a revert.\"));\n+\tcase ONGOING_AM:\n+\t\tdie(_(\"cannot do a partial commit during an am session.\"));\n+\tcase ONGOING_REBASE_CONFLICT:\n+\t\tdie(_(\"cannot do a partial commit while resolving conflicts during a rebase.\"));\n \t}\n \n \tif (list_paths(&partial, !current_head ? NULL : \"HEAD\", &pathspec))\ndiff --git a/sequencer.h b/sequencer.h\nindex 3a4bd97db1..634d1ddcb3 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -279,8 +279,9 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)\n \n /*\n  * An in-progress operation that records its result (often a conflict\n- * resolution) as a new commit on top of HEAD, during which amending\n- * HEAD via \"git commit --amend\" is almost always a mistake.\n+ * resolution) as a new commit on top of HEAD.  Some ways of invoking\n+ * \"git commit\" -- amending HEAD, or a partial commit -- are almost\n+ * always a mistake during such an operation.\n  */\n enum ongoing_operation {\n \tONGOING_NONE = 0,\ndiff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh\nindex 7cf06e5f9a..1314b0fd05 100755\n--- a/t/t3404-rebase-interactive.sh\n+++ b/t/t3404-rebase-interactive.sh\n@@ -1971,6 +1971,40 @@ test_expect_success 'commit --amend is refused at an apply-backend conflict stop\n \t)\n '\n \n+test_expect_success 'partial commit is refused at a rebase conflict stop' '\n+\ttest_when_finished \"git rebase --abort\" &&\n+\tgit checkout --detach conflict-branch &&\n+\t(\n+\t\tset_fake_editor &&\n+\t\tFAKE_LINES=\"1 3\" &&\n+\t\texport FAKE_LINES &&\n+\t\ttest_must_fail git rebase -i A\n+\t) &&\n+\techo resolved >conflict &&\n+\tgit add conflict &&\n+\ttest_must_fail git commit conflict 2>err &&\n+\ttest_grep \"cannot do a partial commit while resolving conflicts during a rebase.\" err\n+'\n+\n+test_expect_success 'partial commit is refused at an apply-backend conflict stop' '\n+\ttest_when_finished \"rm -rf apply-backend\" &&\n+\ttest_create_repo apply-backend &&\n+\t(\n+\t\tcd apply-backend &&\n+\t\ttest_commit base file &&\n+\t\tgit branch -M mainline &&\n+\t\ttest_commit upstream file upstream &&\n+\t\tgit checkout -b side mainline~1 &&\n+\t\ttest_commit conflicting file side &&\n+\t\ttest_commit unrelated other &&\n+\t\ttest_must_fail git rebase --apply mainline &&\n+\t\techo resolved >file &&\n+\t\tgit add file &&\n+\t\ttest_must_fail git commit file 2>err &&\n+\t\ttest_grep \"cannot do a partial commit while resolving conflicts during a rebase.\" err\n+\t)\n+'\n+\n test_expect_success 'todo has correct onto hash' '\n \tGIT_SEQUENCE_EDITOR=cat git rebase -i no-conflict-branch~4 no-conflict-branch >actual &&\n \tonto=$(git rev-parse --short HEAD~4) &&\ndiff --git a/t/t3507-cherry-pick-conflict.sh b/t/t3507-cherry-pick-conflict.sh\nindex 42de398f76..c3d024c97f 100755\n--- a/t/t3507-cherry-pick-conflict.sh\n+++ b/t/t3507-cherry-pick-conflict.sh\n@@ -375,6 +375,17 @@ test_expect_success 'commit --amend of revert fails' '\n \ttest_grep \"in the middle of a revert -- cannot amend.\" err\n '\n \n+test_expect_success 'partial commit during a revert fails' '\n+\tpristine_detach initial &&\n+\n+\ttest_must_fail git revert picked &&\n+\techo resolved >foo &&\n+\tgit add foo &&\n+\ttest_must_fail git commit foo 2>err &&\n+\n+\ttest_grep \"cannot do a partial commit during a revert.\" err\n+'\n+\n test_expect_success 'successful revert does not set REVERT_HEAD' '\n \tpristine_detach base &&\n \tgit revert base &&\ndiff --git a/t/t4151-am-abort.sh b/t/t4151-am-abort.sh\nindex 9313a074b2..c80269e015 100755\n--- a/t/t4151-am-abort.sh\n+++ b/t/t4151-am-abort.sh\n@@ -74,6 +74,17 @@ test_expect_success 'commit --amend during a failed am fails' '\n \tgit am --abort\n '\n \n+test_expect_success 'partial commit during a failed am fails' '\n+\tgit reset --hard initial &&\n+\tcp file-2-expect file-2 &&\n+\ttest_must_fail git am 000[1245]-*.patch &&\n+\techo resolved >file-1 &&\n+\tgit add file-1 &&\n+\ttest_must_fail git commit file-1 2>err &&\n+\ttest_grep \"cannot do a partial commit during an am session.\" err &&\n+\tgit am --abort\n+'\n+\n test_expect_success 'am -3 --skip removes otherfile-4' '\n \tgit reset --hard initial &&\n \ttest_must_fail git am -3 0003-*.patch &&\n-- \ngitgitgadget\n"},{"id":"551366","messageId":"dec05fd6-b99c-4a71-b80e-24e7d6de4b62@gmail.com","threadId":"66221","inReplyTo":"65c48ed3cb638cf0be18a3aa6d86d4c4f2cf01a2.1787792534.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 1/3] commit: reword the empty-commit rebase errors","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-08-27T15:19:09Z","receivedAt":"2026-08-27T15:19:14Z","isPatch":true,"body":"Hi Elijah\n\nOn 27/08/2026 02:02, Elijah Newren via GitGitGadget wrote:\n> From: Elijah Newren <newren@gmail.com>\n> \n> When a rebase applies a commit that becomes empty, it stops and asks the\n> user to decide whether to keep it or drop it.  HEAD still points at the\n> previously-applied commit at that point, so either amending or creating\n> a partial commit is refused, with one of the following messages:\n> \n>      You are in the middle of a rebase -- cannot amend.\n>      cannot do a partial commit during a rebase.\n> \n> Neither message hints that the real problem is a commit that became\n> empty, and \"during a rebase\" is overly broad besides -- amending and\n> partial commits are fine at an `edit` or `break` stop.  Reword both to\n> describe the actual situation.\n> \n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>   builtin/commit.c              | 4 ++--\n>   t/t3404-rebase-interactive.sh | 4 ++--\n>   2 files changed, 4 insertions(+), 4 deletions(-)\n> \n> diff --git a/builtin/commit.c b/builtin/commit.c\n> index 28f6174503..0d908d72bb 100644\n> --- a/builtin/commit.c\n> +++ b/builtin/commit.c\n> @@ -521,7 +521,7 @@ static const char *prepare_index(const char **argv, const char *prefix,\n>   \t\telse if (is_from_cherry_pick(whence))\n>   \t\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n>   \t\telse if (is_from_rebase(whence))\n> -\t\t\tdie(_(\"cannot do a partial commit during a rebase.\"));\n> +\t\t\tdie(_(\"cannot do a partial commit while resolving a commit that became empty.\"));\n\n\"while committing a commit that became empty\" would be clearer to me, \nbut I what you have is definitely an improvement on the existing message.\n\nThanks\n\nPhillip\n\n>   \t}\n>   \n>   \tif (list_paths(&partial, !current_head ? NULL : \"HEAD\", &pathspec))\n> @@ -1334,7 +1334,7 @@ static int parse_and_validate_options(int argc, const char *argv[],\n>   \t\telse if (is_from_cherry_pick(whence))\n>   \t\t\tdie(_(\"You are in the middle of a cherry-pick -- cannot amend.\"));\n>   \t\telse if (whence == FROM_REBASE_PICK)\n> -\t\t\tdie(_(\"You are in the middle of a rebase -- cannot amend.\"));\n> +\t\t\tdie(_(\"You are resolving a commit that became empty -- cannot amend.\"));\n>   \t}\n>   \tif (fixup_message && squash_message)\n>   \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--squash\", \"--fixup\");\n> diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh\n> index ff11abb2f2..1e78dbfd90 100755\n> --- a/t/t3404-rebase-interactive.sh\n> +++ b/t/t3404-rebase-interactive.sh\n> @@ -1868,7 +1868,7 @@ test_expect_success 'correct error message for partial commit after empty pick'\n>   \t) &&\n>   \techo x >file1 &&\n>   \ttest_must_fail git commit file1 2>err &&\n> -\ttest_grep \"cannot do a partial commit during a rebase.\" err\n> +\ttest_grep \"cannot do a partial commit while resolving a commit that became empty.\" err\n>   '\n>   \n>   test_expect_success 'correct error message for commit --amend after empty pick' '\n> @@ -1881,7 +1881,7 @@ test_expect_success 'correct error message for commit --amend after empty pick'\n>   \t) &&\n>   \techo x>file1 &&\n>   \ttest_must_fail git commit -a --amend 2>err &&\n> -\ttest_grep \"middle of a rebase -- cannot amend.\" err\n> +\ttest_grep \"resolving a commit that became empty -- cannot amend.\" err\n>   '\n>   \n>   test_expect_success 'todo has correct onto hash' '\n\n"},{"id":"551367","messageId":"aa248030-5275-465b-a4b5-683ca374672c@gmail.com","threadId":"66221","inReplyTo":"4a1461e52767227d9475de13cf250484f8ecd271.1787792534.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 2/3] commit: refuse to amend during conflict resolution","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-08-27T15:19:23Z","receivedAt":"2026-08-27T15:19:27Z","isPatch":true,"body":"Hi Elijah\n\nOn 27/08/2026 02:02, Elijah Newren via GitGitGadget wrote:\n> From: Elijah Newren <newren@gmail.com>\n> \n> diff --git a/builtin/commit.c b/builtin/commit.c\n> index 0d908d72bb..4a6054aae0 100644\n> --- a/builtin/commit.c\n> +++ b/builtin/commit.c\n> @@ -1326,15 +1326,30 @@ static int parse_and_validate_options(int argc, const char *argv[],\n>   \t\tuse_editor = 0;\n>   \n>   \t/* Sanity check options */\n> -\tif (amend && !current_head)\n> -\t\tdie(_(\"You have nothing to amend.\"));\n> -\tif (amend && whence != FROM_COMMIT) {\n> -\t\tif (whence == FROM_MERGE)\n> +\tif (amend) {\n> +\t\tif (!current_head)\n> +\t\t\tdie(_(\"You have nothing to amend.\"));\n> +\t\t/*\n> +\t\t * Refuse to amend in the middle of any operation that is\n> +\t\t * meant to record its result as a new commit on top of HEAD\n> +\t\t * rather than by rewriting HEAD.\n> +\t\t */\n> +\t\tswitch (sequencer_ongoing_operation(s->repo, whence)) {\n> +\t\tcase ONGOING_NONE:\n> +\t\t\tbreak;\n> +\t\tcase ONGOING_MERGE:\n>   \t\t\tdie(_(\"You are in the middle of a merge -- cannot amend.\"));\n> -\t\telse if (is_from_cherry_pick(whence))\n> +\t\tcase ONGOING_CHERRY_PICK:\n>   \t\t\tdie(_(\"You are in the middle of a cherry-pick -- cannot amend.\"));\n\nFor rebase we distinguish between a conflict and a commit that becomes \nempty, but we don't do that for a cherry-pick. That's an existing \nproblem though, not something we necessarily need to address in this series.\n\nMoving the detection to a separate function and using an enum here is \nmuch nicer than the previous version.\n\n> diff --git a/sequencer.c b/sequencer.c\n> index 65afd100d9..bd4a724410 100644\n> --- a/sequencer.c\n> +++ b/sequencer.c\n> @@ -6966,6 +6966,71 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)\n>   \treturn 0;\n>   }\n>   \n> +enum ongoing_operation sequencer_ongoing_operation(struct repository *r,\n> +\t\t\t\t\t\t   enum commit_whence whence)\n> +{\n> [...]\n> +\t/*\n> +\t * In the middle of a rebase that stopped for conflict resolution?\n> +\t * The apply backend only ever stops for conflicts, so the presence\n> +\t * of its state directory is enough.  The merge backend writes\n> +\t * stopped-sha whenever it hands control back to the user, but omits\n> +\t * `amend` unless it stopped with HEAD already pointing at the commit\n> +\t * to be amended (a clean edit/reword stop); its absence therefore\n> +\t * marks a conflicted stop.\n> +\t */\n> +\tpath = repo_git_path(r, \"rebase-apply\");\n> +\tfound = file_exists(path);\n> +\tfree(path);\n> +\tif (!found) {\n> +\t\tchar *stopped_sha = repo_git_path(r, \"rebase-merge/stopped-sha\");\n> +\t\tchar *amend_marker = repo_git_path(r, \"rebase-merge/amend\");\n> +\n> +\t\tfound = file_exists(stopped_sha) && !file_exists(amend_marker);\n\nThe sequencer defines rebase_path_stoppend_sha() and rebase_path_amend() \nso we can avoid having to hard code these paths throughout the code. \nApart from that this all looks good to me.\n\nThanks\n\nPhillip\n\n> +\t\tfree(stopped_sha);\n> +\t\tfree(amend_marker);\n> +\t}\n> +\tif (found)\n> +\t\treturn ONGOING_REBASE_CONFLICT;\n> +\n> +\treturn ONGOING_NONE;\n> +}\n> +\n>   int sequencer_get_update_refs_state(const char *wt_dir,\n>   \t\t\t\t    struct string_list *refs)\n>   {\n> diff --git a/sequencer.h b/sequencer.h\n> index 64a9c7fb1b..3a4bd97db1 100644\n> --- a/sequencer.h\n> +++ b/sequencer.h\n> @@ -277,6 +277,29 @@ int sequencer_get_last_command(struct repository* r,\n>   \t\t\t       enum replay_action *action);\n>   int sequencer_determine_whence(struct repository *r, enum commit_whence *whence);\n>   \n> +/*\n> + * An in-progress operation that records its result (often a conflict\n> + * resolution) as a new commit on top of HEAD, during which amending\n> + * HEAD via \"git commit --amend\" is almost always a mistake.\n> + */\n> +enum ongoing_operation {\n> +\tONGOING_NONE = 0,\n> +\tONGOING_MERGE,\n> +\tONGOING_CHERRY_PICK,\n> +\tONGOING_REBASE_EMPTY,\n> +\tONGOING_REVERT,\n> +\tONGOING_AM,\n> +\tONGOING_REBASE_CONFLICT\n> +};\n> +\n> +/*\n> + * Return which in-progress operation, if any, is underway; see enum\n> + * ongoing_operation.  'whence' is the origin already computed for the\n> + * pending commit.\n> + */\n> +enum ongoing_operation sequencer_ongoing_operation(struct repository *r,\n> +\t\t\t\t\t\t   enum commit_whence whence);\n> +\n>   /**\n>    * Append the set of ref-OID pairs that are currently stored for the 'git\n>    * rebase --update-refs' feature if such a rebase is currently happening.\n> diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh\n> index 1e78dbfd90..7cf06e5f9a 100755\n> --- a/t/t3404-rebase-interactive.sh\n> +++ b/t/t3404-rebase-interactive.sh\n> @@ -1884,6 +1884,93 @@ test_expect_success 'correct error message for commit --amend after empty pick'\n>   \ttest_grep \"resolving a commit that became empty -- cannot amend.\" err\n>   '\n>   \n> +test_expect_success 'commit --amend is refused at a rebase conflict stop' '\n> +\ttest_when_finished \"git rebase --abort\" &&\n> +\tgit checkout --detach conflict-branch &&\n> +\t(\n> +\t\tset_fake_editor &&\n> +\t\tFAKE_LINES=\"1 3\" &&\n> +\t\texport FAKE_LINES &&\n> +\t\ttest_must_fail git rebase -i A\n> +\t) &&\n> +\ttest_path_is_file .git/rebase-merge/patch &&\n> +\ttest_path_is_missing .git/rebase-merge/amend &&\n> +\techo resolved >conflict &&\n> +\tgit add conflict &&\n> +\ttest_must_fail git commit --amend --no-edit 2>err &&\n> +\ttest_grep \"You are resolving conflicts during a rebase -- cannot amend\" err\n> +'\n> +\n> +test_expect_success 'commit --amend is refused when an \"edit\" pick conflicts' '\n> +\ttest_when_finished \"git rebase --abort\" &&\n> +\tgit checkout --detach conflict-branch &&\n> +\t(\n> +\t\tset_fake_editor &&\n> +\t\tFAKE_LINES=\"1 edit 3\" &&\n> +\t\texport FAKE_LINES &&\n> +\t\ttest_must_fail git rebase -i A\n> +\t) &&\n> +\ttest_path_is_file .git/rebase-merge/patch &&\n> +\ttest_path_is_missing .git/rebase-merge/amend &&\n> +\techo resolved >conflict &&\n> +\tgit add conflict &&\n> +\ttest_must_fail git commit --amend --no-edit 2>err &&\n> +\ttest_grep \"You are resolving conflicts during a rebase -- cannot amend\" err\n> +'\n> +\n> +test_expect_success 'commit --amend is allowed at a rebase edit stop' '\n> +\ttest_when_finished \"git rebase --abort\" &&\n> +\tgit checkout --detach no-conflict-branch &&\n> +\t(\n> +\t\tset_fake_editor &&\n> +\t\tFAKE_LINES=\"edit 1 2 3 4\" &&\n> +\t\texport FAKE_LINES &&\n> +\t\tgit rebase -i A\n> +\t) &&\n> +\ttest_path_is_file .git/rebase-merge/amend &&\n> +\techo tweak >fileJ &&\n> +\tgit add fileJ &&\n> +\tgit commit --amend --no-edit\n> +'\n> +\n> +test_expect_success 'commit --amend is allowed at a rebase break stop' '\n> +\ttest_when_finished \"git rebase --abort\" &&\n> +\tgit checkout --detach no-conflict-branch &&\n> +\t(\n> +\t\tset_fake_editor &&\n> +\t\tFAKE_LINES=\"break 1 2 3 4\" &&\n> +\t\texport FAKE_LINES &&\n> +\t\tgit rebase -i A\n> +\t) &&\n> +\ttest_must_fail git rev-parse --verify REBASE_HEAD &&\n> +\techo tweak >fileJ &&\n> +\tgit add fileJ &&\n> +\tgit commit --amend --no-edit\n> +'\n> +\n> +test_expect_success 'commit --amend is refused at an apply-backend conflict stop' '\n> +\ttest_when_finished \"rm -rf apply-backend\" &&\n> +\ttest_create_repo apply-backend &&\n> +\t(\n> +\t\tcd apply-backend &&\n> +\t\ttest_commit base file &&\n> +\t\tgit branch -M mainline &&\n> +\t\ttest_commit upstream file upstream &&\n> +\t\tgit checkout -b side mainline~1 &&\n> +\t\ttest_commit conflicting file side &&\n> +\t\ttest_commit unrelated other &&\n> +\t\ttest_must_fail git rebase --apply mainline &&\n> +\t\t# the apply backend only ever stops for conflicts, and\n> +\t\t# leaves HEAD on the previously-applied commit\n> +\t\ttest_path_is_dir .git/rebase-apply &&\n> +\t\ttest_path_is_missing .git/rebase-apply/applying &&\n> +\t\techo resolved >file &&\n> +\t\tgit add file &&\n> +\t\ttest_must_fail git commit --amend --no-edit 2>err &&\n> +\t\ttest_grep \"You are resolving conflicts during a rebase -- cannot amend\" err\n> +\t)\n> +'\n> +\n>   test_expect_success 'todo has correct onto hash' '\n>   \tGIT_SEQUENCE_EDITOR=cat git rebase -i no-conflict-branch~4 no-conflict-branch >actual &&\n>   \tonto=$(git rev-parse --short HEAD~4) &&\n> diff --git a/t/t3507-cherry-pick-conflict.sh b/t/t3507-cherry-pick-conflict.sh\n> index 44596cb1e8..42de398f76 100755\n> --- a/t/t3507-cherry-pick-conflict.sh\n> +++ b/t/t3507-cherry-pick-conflict.sh\n> @@ -364,6 +364,17 @@ test_expect_success 'failed revert sets REVERT_HEAD' '\n>   \ttest_cmp_rev picked REVERT_HEAD\n>   '\n>   \n> +test_expect_success 'commit --amend of revert fails' '\n> +\tpristine_detach initial &&\n> +\n> +\ttest_must_fail git revert picked &&\n> +\techo resolved >foo &&\n> +\tgit add foo &&\n> +\ttest_must_fail git commit --amend 2>err &&\n> +\n> +\ttest_grep \"in the middle of a revert -- cannot amend.\" err\n> +'\n> +\n>   test_expect_success 'successful revert does not set REVERT_HEAD' '\n>   \tpristine_detach base &&\n>   \tgit revert base &&\n> diff --git a/t/t4151-am-abort.sh b/t/t4151-am-abort.sh\n> index 8e1ecf8a68..9313a074b2 100755\n> --- a/t/t4151-am-abort.sh\n> +++ b/t/t4151-am-abort.sh\n> @@ -63,6 +63,17 @@ do\n>   \n>   done\n>   \n> +test_expect_success 'commit --amend during a failed am fails' '\n> +\tgit reset --hard initial &&\n> +\tcp file-2-expect file-2 &&\n> +\ttest_must_fail git am 000[1245]-*.patch &&\n> +\techo resolved >file-1 &&\n> +\tgit add file-1 &&\n> +\ttest_must_fail git commit --amend 2>err &&\n> +\ttest_grep \"in the middle of an am session -- cannot amend.\" err &&\n> +\tgit am --abort\n> +'\n> +\n>   test_expect_success 'am -3 --skip removes otherfile-4' '\n>   \tgit reset --hard initial &&\n>   \ttest_must_fail git am -3 0003-*.patch &&\n\n"},{"id":"551368","messageId":"6a04525c-d9e7-415a-845f-f11ed1c0f0d5@gmail.com","threadId":"66221","inReplyTo":"pull.2389.v2.git.git.1787792534.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 0/3] commit: refuse to amend during conflict resolution","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-08-27T15:19:29Z","receivedAt":"2026-08-27T15:19:33Z","isPatch":true,"body":"Hi Elijah\n\nOn 27/08/2026 02:02, Elijah Newren via GitGitGadget wrote:\n> Both git commit --amend and a partial commit (git commit <paths>) are\n> foot-guns while the user is in the middle of an operation that resolves\n> conflicts on top of HEAD: recording a conflict resolution is about capturing\n> the state of the whole tree as a new commit, not about rewriting HEAD or\n> committing a subset of paths.\n> \n> Historically we only rejected these during a merge or a cherry-pick or when\n> resolving an empty pick during a rebase. The same hazard exists for am,\n> revert, and rebase conflict stops, none of which were covered. This series\n> extends the refusal to all of them.\n> \n> The three patches:\n> \n>   1. reword the two pre-existing \"empty commit\" rebase messages, which were\n>      misleadingly generic\n>   2. refuse git commit --amend during these additional operations\n>   3. refuse partial commits during the same operations.\n\nThanks for working on this, it is a useful improvement to our UI. I've \nleft a couple of comments but this all looks pretty sound to me.\n\nThanks\n\nPhillip\n\n> Elijah Newren (3):\n>    commit: reword the empty-commit rebase errors\n>    commit: refuse to amend during conflict resolution\n>    commit: refuse partial commits during conflict resolution\n> \n>   builtin/commit.c                |  51 +++++++++----\n>   sequencer.c                     |  65 +++++++++++++++++\n>   sequencer.h                     |  24 ++++++\n>   t/t3404-rebase-interactive.sh   | 125 +++++++++++++++++++++++++++++++-\n>   t/t3507-cherry-pick-conflict.sh |  22 ++++++\n>   t/t4151-am-abort.sh             |  22 ++++++\n>   6 files changed, 293 insertions(+), 16 deletions(-)\n> \n> \n> base-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e\n> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2389%2Fnewren%2Frefuse-amend-during-conflicts-v2\n> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2389/newren/refuse-amend-during-conflicts-v2\n> Pull-Request: https://github.com/git/git/pull/2389\n> \n> Range-diff vs v1:\n> \n>   -:  ---------- > 1:  65c48ed3cb commit: reword the empty-commit rebase errors\n>   1:  a3d6b059c6 ! 2:  4a1461e527 commit: refuse to amend during conflict resolution\n>       @@ Commit message\n>            Signed-off-by: Elijah Newren <newren@gmail.com>\n>        \n>         ## builtin/commit.c ##\n>       -@@\n>       - #include \"path.h\"\n>       - #include \"preload-index.h\"\n>       - #include \"read-cache.h\"\n>       -+#include \"refs.h\"\n>       - #include \"repository.h\"\n>       - #include \"string-list.h\"\n>       - #include \"rerere.h\"\n>        @@ builtin/commit.c: static int parse_and_validate_options(int argc, const char *argv[],\n>       - \t\telse if (whence == FROM_REBASE_PICK)\n>       - \t\t\tdie(_(\"You are in the middle of a rebase -- cannot amend.\"));\n>       + \t\tuse_editor = 0;\n>       +\n>       + \t/* Sanity check options */\n>       +-\tif (amend && !current_head)\n>       +-\t\tdie(_(\"You have nothing to amend.\"));\n>       +-\tif (amend && whence != FROM_COMMIT) {\n>       +-\t\tif (whence == FROM_MERGE)\n>       ++\tif (amend) {\n>       ++\t\tif (!current_head)\n>       ++\t\t\tdie(_(\"You have nothing to amend.\"));\n>       ++\t\t/*\n>       ++\t\t * Refuse to amend in the middle of any operation that is\n>       ++\t\t * meant to record its result as a new commit on top of HEAD\n>       ++\t\t * rather than by rewriting HEAD.\n>       ++\t\t */\n>       ++\t\tswitch (sequencer_ongoing_operation(s->repo, whence)) {\n>       ++\t\tcase ONGOING_NONE:\n>       ++\t\t\tbreak;\n>       ++\t\tcase ONGOING_MERGE:\n>       + \t\t\tdie(_(\"You are in the middle of a merge -- cannot amend.\"));\n>       +-\t\telse if (is_from_cherry_pick(whence))\n>       ++\t\tcase ONGOING_CHERRY_PICK:\n>       + \t\t\tdie(_(\"You are in the middle of a cherry-pick -- cannot amend.\"));\n>       +-\t\telse if (whence == FROM_REBASE_PICK)\n>       ++\t\tcase ONGOING_REBASE_EMPTY:\n>       + \t\t\tdie(_(\"You are resolving a commit that became empty -- cannot amend.\"));\n>       ++\t\tcase ONGOING_REVERT:\n>       ++\t\t\tdie(_(\"You are in the middle of a revert -- cannot amend.\"));\n>       ++\t\tcase ONGOING_AM:\n>       ++\t\t\tdie(_(\"You are in the middle of an am session -- cannot amend.\"));\n>       ++\t\tcase ONGOING_REBASE_CONFLICT:\n>       ++\t\t\tdie(_(\"You are resolving conflicts during a rebase -- cannot amend.\"));\n>       ++\t\t}\n>         \t}\n>       -+\tif (amend && whence == FROM_COMMIT) {\n>       -+\t\tchar *applying, *apply_dir, *stopped_sha, *amend_marker;\n>       -+\t\tint in_am, conflicted_stop;\n>       + \tif (fixup_message && squash_message)\n>       + \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--squash\", \"--fixup\");\n>       +\n>       + ## sequencer.c ##\n>       +@@ sequencer.c: int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)\n>       + \treturn 0;\n>       + }\n>       +\n>       ++enum ongoing_operation sequencer_ongoing_operation(struct repository *r,\n>       ++\t\t\t\t\t\t   enum commit_whence whence)\n>       ++{\n>       ++\tchar *path;\n>       ++\tint found;\n>        +\n>       -+\t\t/* Check middle of revert */\n>       -+\t\tif (refs_ref_exists(get_main_ref_store(the_repository),\n>       -+\t\t\t\t    \"REVERT_HEAD\"))\n>       -+\t\t\tdie(_(\"You are in the middle of a revert -- cannot amend.\"));\n>       ++\t/*\n>       ++\t * The merge, cherry-pick, and (empty) rebase-pick stops are already\n>       ++\t * distinguished by 'whence'.\n>       ++\t */\n>       ++\tswitch (whence) {\n>       ++\tcase FROM_MERGE:\n>       ++\t\treturn ONGOING_MERGE;\n>       ++\tcase FROM_CHERRY_PICK_SINGLE:\n>       ++\tcase FROM_CHERRY_PICK_MULTI:\n>       ++\t\treturn ONGOING_CHERRY_PICK;\n>       ++\tcase FROM_REBASE_PICK:\n>       ++\t\treturn ONGOING_REBASE_EMPTY;\n>       ++\tcase FROM_COMMIT:\n>       ++\t\tbreak;\n>       ++\t}\n>        +\n>       -+\t\t/* Check middle of `am` */\n>       -+\t\tapplying = repo_git_path(the_repository,\n>       -+\t\t\t\t\t \"rebase-apply/applying\");\n>       -+\t\tin_am = file_exists(applying);\n>       ++\t/*\n>       ++\t * 'whence' is FROM_COMMIT, but we may still be in the middle of an\n>       ++\t * operation that records its result on top of HEAD; detect those\n>       ++\t * from their on-disk state.\n>       ++\t */\n>        +\n>       -+\t\tfree(applying);\n>       -+\t\tif (in_am)\n>       -+\t\t\tdie(_(\"You are in the middle of an am session -- cannot amend.\"));\n>       ++\t/* In the middle of a revert? */\n>       ++\tif (refs_ref_exists(get_main_ref_store(r), \"REVERT_HEAD\"))\n>       ++\t\treturn ONGOING_REVERT;\n>        +\n>       -+\t\t/* Check middle of rebase specifically stopped for conflicts */\n>       -+\t\tapply_dir = repo_git_path(the_repository,\n>       -+\t\t\t\t\t  \"rebase-apply\");\n>       -+\t\tstopped_sha = repo_git_path(the_repository,\n>       -+\t\t\t\t\t    \"rebase-merge/stopped-sha\");\n>       -+\t\tamend_marker = repo_git_path(the_repository,\n>       -+\t\t\t\t\t     \"rebase-merge/amend\");\n>       -+\t\t/*\n>       -+\t\t * The apply backend only ever stops for conflicts; the\n>       -+\t\t * merge backend writes stopped-sha but omits `amend`,\n>       -+\t\t * which it writes only at a clean edit/reword stop.\n>       -+\t\t */\n>       -+\t\tconflicted_stop =\n>       -+\t\t\tfile_exists(apply_dir) ||\n>       -+\t\t\t(file_exists(stopped_sha) && !file_exists(amend_marker));\n>       ++\t/* In the middle of an `am`? */\n>       ++\tpath = repo_git_path(r, \"rebase-apply/applying\");\n>       ++\tfound = file_exists(path);\n>       ++\tfree(path);\n>       ++\tif (found)\n>       ++\t\treturn ONGOING_AM;\n>       ++\n>       ++\t/*\n>       ++\t * In the middle of a rebase that stopped for conflict resolution?\n>       ++\t * The apply backend only ever stops for conflicts, so the presence\n>       ++\t * of its state directory is enough.  The merge backend writes\n>       ++\t * stopped-sha whenever it hands control back to the user, but omits\n>       ++\t * `amend` unless it stopped with HEAD already pointing at the commit\n>       ++\t * to be amended (a clean edit/reword stop); its absence therefore\n>       ++\t * marks a conflicted stop.\n>       ++\t */\n>       ++\tpath = repo_git_path(r, \"rebase-apply\");\n>       ++\tfound = file_exists(path);\n>       ++\tfree(path);\n>       ++\tif (!found) {\n>       ++\t\tchar *stopped_sha = repo_git_path(r, \"rebase-merge/stopped-sha\");\n>       ++\t\tchar *amend_marker = repo_git_path(r, \"rebase-merge/amend\");\n>        +\n>       -+\t\tfree(apply_dir);\n>       ++\t\tfound = file_exists(stopped_sha) && !file_exists(amend_marker);\n>        +\t\tfree(stopped_sha);\n>        +\t\tfree(amend_marker);\n>       -+\t\tif (conflicted_stop)\n>       -+\t\t\tdie(_(\"You are resolving conflicts during a rebase -- cannot amend.\"));\n>        +\t}\n>       - \tif (fixup_message && squash_message)\n>       - \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--squash\", \"--fixup\");\n>       - \tdie_for_incompatible_opt4(!!use_message, \"-C\",\n>       ++\tif (found)\n>       ++\t\treturn ONGOING_REBASE_CONFLICT;\n>       ++\n>       ++\treturn ONGOING_NONE;\n>       ++}\n>       ++\n>       + int sequencer_get_update_refs_state(const char *wt_dir,\n>       + \t\t\t\t    struct string_list *refs)\n>       + {\n>       +\n>       + ## sequencer.h ##\n>       +@@ sequencer.h: int sequencer_get_last_command(struct repository* r,\n>       + \t\t\t       enum replay_action *action);\n>       + int sequencer_determine_whence(struct repository *r, enum commit_whence *whence);\n>       +\n>       ++/*\n>       ++ * An in-progress operation that records its result (often a conflict\n>       ++ * resolution) as a new commit on top of HEAD, during which amending\n>       ++ * HEAD via \"git commit --amend\" is almost always a mistake.\n>       ++ */\n>       ++enum ongoing_operation {\n>       ++\tONGOING_NONE = 0,\n>       ++\tONGOING_MERGE,\n>       ++\tONGOING_CHERRY_PICK,\n>       ++\tONGOING_REBASE_EMPTY,\n>       ++\tONGOING_REVERT,\n>       ++\tONGOING_AM,\n>       ++\tONGOING_REBASE_CONFLICT\n>       ++};\n>       ++\n>       ++/*\n>       ++ * Return which in-progress operation, if any, is underway; see enum\n>       ++ * ongoing_operation.  'whence' is the origin already computed for the\n>       ++ * pending commit.\n>       ++ */\n>       ++enum ongoing_operation sequencer_ongoing_operation(struct repository *r,\n>       ++\t\t\t\t\t\t   enum commit_whence whence);\n>       ++\n>       + /**\n>       +  * Append the set of ref-OID pairs that are currently stored for the 'git\n>       +  * rebase --update-refs' feature if such a rebase is currently happening.\n>        \n>         ## t/t3404-rebase-interactive.sh ##\n>        @@ t/t3404-rebase-interactive.sh: test_expect_success 'correct error message for commit --amend after empty pick'\n>       - \ttest_grep \"middle of a rebase -- cannot amend.\" err\n>       + \ttest_grep \"resolving a commit that became empty -- cannot amend.\" err\n>         '\n>         \n>        +test_expect_success 'commit --amend is refused at a rebase conflict stop' '\n>   -:  ---------- > 3:  e0be8cdf63 commit: refuse partial commits during conflict resolution\n> \n\n"},{"id":"551369","messageId":"e78b522f-24a2-4913-80a0-4213f421d2bd@gmail.com","threadId":"66221","inReplyTo":"e0be8cdf63446e0535e020abbff2ec209cb518d4.1787792534.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 3/3] commit: refuse partial commits during conflict resolution","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-08-27T15:19:36Z","receivedAt":"2026-08-27T15:27:01Z","isPatch":true,"body":"Hi Elijah\n\nOn 27/08/2026 02:02, Elijah Newren via GitGitGadget wrote:\n> From: Elijah Newren <newren@gmail.com>\n> \n> Similar to the previous commit, just as `git commit --amend` is a\n> foot-gun during conflict resolution, so is a partial commit (`git commit\n> <paths>`).  Recording a conflict resolution is about capturing the state\n> of the entire tree on top of HEAD, not a subset of paths.  For many\n> years we have rejected partial commits in the middle of\n>    - a merge\n>    - a cherry-pick\n>    - a rebase that stopped at a pick\n> \n> but, just like amending, this was never extended to the other\n> operations that can also leave conflicts to resolve:\n>    - an `am` operation\n>    - a revert\n>    - a rebase that stopped for conflict resolution\n> \n> Reuse sequencer_ongoing_operation(), introduced for the analogous\n> `--amend` check, to detect all of these and refuse the partial commit.\n\nGood idea and the changes look good too\n\nThanks\n\nPhillip\n\n\n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>   builtin/commit.c                | 22 ++++++++++++++-------\n>   sequencer.h                     |  5 +++--\n>   t/t3404-rebase-interactive.sh   | 34 +++++++++++++++++++++++++++++++++\n>   t/t3507-cherry-pick-conflict.sh | 11 +++++++++++\n>   t/t4151-am-abort.sh             | 11 +++++++++++\n>   5 files changed, 74 insertions(+), 9 deletions(-)\n> \n> diff --git a/builtin/commit.c b/builtin/commit.c\n> index 4a6054aae0..9da3f1191b 100644\n> --- a/builtin/commit.c\n> +++ b/builtin/commit.c\n> @@ -515,13 +515,21 @@ static const char *prepare_index(const char **argv, const char *prefix,\n>   \t */\n>   \tcommit_style = COMMIT_PARTIAL;\n>   \n> -\tif (whence != FROM_COMMIT) {\n> -\t\tif (whence == FROM_MERGE)\n> -\t\t\tdie(_(\"cannot do a partial commit during a merge.\"));\n> -\t\telse if (is_from_cherry_pick(whence))\n> -\t\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n> -\t\telse if (is_from_rebase(whence))\n> -\t\t\tdie(_(\"cannot do a partial commit while resolving a commit that became empty.\"));\n> +\tswitch (sequencer_ongoing_operation(the_repository, whence)) {\n> +\tcase ONGOING_NONE:\n> +\t\tbreak;\n> +\tcase ONGOING_MERGE:\n> +\t\tdie(_(\"cannot do a partial commit during a merge.\"));\n> +\tcase ONGOING_CHERRY_PICK:\n> +\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n> +\tcase ONGOING_REBASE_EMPTY:\n> +\t\tdie(_(\"cannot do a partial commit while resolving a commit that became empty.\"));\n> +\tcase ONGOING_REVERT:\n> +\t\tdie(_(\"cannot do a partial commit during a revert.\"));\n> +\tcase ONGOING_AM:\n> +\t\tdie(_(\"cannot do a partial commit during an am session.\"));\n> +\tcase ONGOING_REBASE_CONFLICT:\n> +\t\tdie(_(\"cannot do a partial commit while resolving conflicts during a rebase.\"));\n>   \t}\n>   \n>   \tif (list_paths(&partial, !current_head ? NULL : \"HEAD\", &pathspec))\n> diff --git a/sequencer.h b/sequencer.h\n> index 3a4bd97db1..634d1ddcb3 100644\n> --- a/sequencer.h\n> +++ b/sequencer.h\n> @@ -279,8 +279,9 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)\n>   \n>   /*\n>    * An in-progress operation that records its result (often a conflict\n> - * resolution) as a new commit on top of HEAD, during which amending\n> - * HEAD via \"git commit --amend\" is almost always a mistake.\n> + * resolution) as a new commit on top of HEAD.  Some ways of invoking\n> + * \"git commit\" -- amending HEAD, or a partial commit -- are almost\n> + * always a mistake during such an operation.\n>    */\n>   enum ongoing_operation {\n>   \tONGOING_NONE = 0,\n> diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh\n> index 7cf06e5f9a..1314b0fd05 100755\n> --- a/t/t3404-rebase-interactive.sh\n> +++ b/t/t3404-rebase-interactive.sh\n> @@ -1971,6 +1971,40 @@ test_expect_success 'commit --amend is refused at an apply-backend conflict stop\n>   \t)\n>   '\n>   \n> +test_expect_success 'partial commit is refused at a rebase conflict stop' '\n> +\ttest_when_finished \"git rebase --abort\" &&\n> +\tgit checkout --detach conflict-branch &&\n> +\t(\n> +\t\tset_fake_editor &&\n> +\t\tFAKE_LINES=\"1 3\" &&\n> +\t\texport FAKE_LINES &&\n> +\t\ttest_must_fail git rebase -i A\n> +\t) &&\n> +\techo resolved >conflict &&\n> +\tgit add conflict &&\n> +\ttest_must_fail git commit conflict 2>err &&\n> +\ttest_grep \"cannot do a partial commit while resolving conflicts during a rebase.\" err\n> +'\n> +\n> +test_expect_success 'partial commit is refused at an apply-backend conflict stop' '\n> +\ttest_when_finished \"rm -rf apply-backend\" &&\n> +\ttest_create_repo apply-backend &&\n> +\t(\n> +\t\tcd apply-backend &&\n> +\t\ttest_commit base file &&\n> +\t\tgit branch -M mainline &&\n> +\t\ttest_commit upstream file upstream &&\n> +\t\tgit checkout -b side mainline~1 &&\n> +\t\ttest_commit conflicting file side &&\n> +\t\ttest_commit unrelated other &&\n> +\t\ttest_must_fail git rebase --apply mainline &&\n> +\t\techo resolved >file &&\n> +\t\tgit add file &&\n> +\t\ttest_must_fail git commit file 2>err &&\n> +\t\ttest_grep \"cannot do a partial commit while resolving conflicts during a rebase.\" err\n> +\t)\n> +'\n> +\n>   test_expect_success 'todo has correct onto hash' '\n>   \tGIT_SEQUENCE_EDITOR=cat git rebase -i no-conflict-branch~4 no-conflict-branch >actual &&\n>   \tonto=$(git rev-parse --short HEAD~4) &&\n> diff --git a/t/t3507-cherry-pick-conflict.sh b/t/t3507-cherry-pick-conflict.sh\n> index 42de398f76..c3d024c97f 100755\n> --- a/t/t3507-cherry-pick-conflict.sh\n> +++ b/t/t3507-cherry-pick-conflict.sh\n> @@ -375,6 +375,17 @@ test_expect_success 'commit --amend of revert fails' '\n>   \ttest_grep \"in the middle of a revert -- cannot amend.\" err\n>   '\n>   \n> +test_expect_success 'partial commit during a revert fails' '\n> +\tpristine_detach initial &&\n> +\n> +\ttest_must_fail git revert picked &&\n> +\techo resolved >foo &&\n> +\tgit add foo &&\n> +\ttest_must_fail git commit foo 2>err &&\n> +\n> +\ttest_grep \"cannot do a partial commit during a revert.\" err\n> +'\n> +\n>   test_expect_success 'successful revert does not set REVERT_HEAD' '\n>   \tpristine_detach base &&\n>   \tgit revert base &&\n> diff --git a/t/t4151-am-abort.sh b/t/t4151-am-abort.sh\n> index 9313a074b2..c80269e015 100755\n> --- a/t/t4151-am-abort.sh\n> +++ b/t/t4151-am-abort.sh\n> @@ -74,6 +74,17 @@ test_expect_success 'commit --amend during a failed am fails' '\n>   \tgit am --abort\n>   '\n>   \n> +test_expect_success 'partial commit during a failed am fails' '\n> +\tgit reset --hard initial &&\n> +\tcp file-2-expect file-2 &&\n> +\ttest_must_fail git am 000[1245]-*.patch &&\n> +\techo resolved >file-1 &&\n> +\tgit add file-1 &&\n> +\ttest_must_fail git commit file-1 2>err &&\n> +\ttest_grep \"cannot do a partial commit during an am session.\" err &&\n> +\tgit am --abort\n> +'\n> +\n>   test_expect_success 'am -3 --skip removes otherfile-4' '\n>   \tgit reset --hard initial &&\n>   \ttest_must_fail git am -3 0003-*.patch &&\n\n"},{"id":"551372","messageId":"CABPp-BG3O1xB3AhU3NfAwT5S1W5UnikT8U9HW=AqA9inYMS76w@mail.gmail.com","threadId":"66221","inReplyTo":"6a04525c-d9e7-415a-845f-f11ed1c0f0d5@gmail.com","subject":"Re: [PATCH v2 0/3] commit: refuse to amend during conflict resolution","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-08-27T16:28:54Z","receivedAt":"2026-08-27T16:29:07Z","isPatch":true,"body":"Hi Phillip,\n\nOn Thu, Aug 27, 2026 at 8:19 AM Phillip Wood <phillip.wood123@gmail.com> wrote:\n>\n> > The three patches:\n> >\n> >   1. reword the two pre-existing \"empty commit\" rebase messages, which were\n> >      misleadingly generic\n> >   2. refuse git commit --amend during these additional operations\n> >   3. refuse partial commits during the same operations.\n>\n> Thanks for working on this, it is a useful improvement to our UI. I've\n> left a couple of comments but this all looks pretty sound to me.\n\nThanks again for taking a look.  I'll send a v3 incorporating your suggestions.\n"},{"id":"551373","messageId":"xmqq5x0vbjxd.fsf@gitster.g","threadId":"66221","inReplyTo":"65c48ed3cb638cf0be18a3aa6d86d4c4f2cf01a2.1787792534.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 1/3] commit: reword the empty-commit rebase errors","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-27T16:35:42Z","receivedAt":"2026-08-27T16:35:45Z","isPatch":true,"body":"\"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Elijah Newren <newren@gmail.com>\n>\n> When a rebase applies a commit that becomes empty, it stops and asks the\n> user to decide whether to keep it or drop it.  HEAD still points at the\n> previously-applied commit at that point, so either amending or creating\n> a partial commit is refused, with one of the following messages:\n>\n>     You are in the middle of a rebase -- cannot amend.\n>     cannot do a partial commit during a rebase.\n>\n> Neither message hints that the real problem is a commit that became\n> empty, and \"during a rebase\" is overly broad besides -- amending and\n> partial commits are fine at an `edit` or `break` stop.  Reword both to\n> describe the actual situation.\n>\n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>  builtin/commit.c              | 4 ++--\n>  t/t3404-rebase-interactive.sh | 4 ++--\n>  2 files changed, 4 insertions(+), 4 deletions(-)\n>\n> diff --git a/builtin/commit.c b/builtin/commit.c\n> index 28f6174503..0d908d72bb 100644\n> --- a/builtin/commit.c\n> +++ b/builtin/commit.c\n> @@ -521,7 +521,7 @@ static const char *prepare_index(const char **argv, const char *prefix,\n>  \t\telse if (is_from_cherry_pick(whence))\n>  \t\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n>  \t\telse if (is_from_rebase(whence))\n> -\t\t\tdie(_(\"cannot do a partial commit during a rebase.\"));\n> +\t\t\tdie(_(\"cannot do a partial commit while resolving a commit that became empty.\"));\n\nThat is a mouthful.  It also is awkward to say \"while resolving a commit\".\n\nMore importantly, I am not sure if whence == FROM_REBASE_PICK at\nthis point in the code flow is a sufficient sign to tell that we\nwere not just in the middle of a rebase, not just a rebase stopped\nwith _some_ conflict, but the way the rebase stopped was because a\nstep in rebase resulted in a commit that is no-op relative to the\nprevious commit.  What makes us certain that the rebase-pick is\nempty?\n\n> @@ -1334,7 +1334,7 @@ static int parse_and_validate_options(int argc, const char *argv[],\n>  \t\telse if (is_from_cherry_pick(whence))\n>  \t\t\tdie(_(\"You are in the middle of a cherry-pick -- cannot amend.\"));\n>  \t\telse if (whence == FROM_REBASE_PICK)\n> -\t\t\tdie(_(\"You are in the middle of a rebase -- cannot amend.\"));\n> +\t\t\tdie(_(\"You are resolving a commit that became empty -- cannot amend.\"));\n>  \t}\n\nAgain \"resolving a commit\" sounds a bit awkward.  What makes us\ncertain that we aren't seeing an ordinary conflicted \"pick\" step but\nthe one that has become empty?  If \"rebase -i\" stopped for conflict\nwhile applying one step, you edited away conflicts in the working\ntree files, and instead of saying \"rebase --continue\" tried to run\n\"commit --amend\" by mistake, we do want to stop, but wouldn't it\nsurprise us if the message to stop us said something about \"became\nempty\"?\n\n"},{"id":"551376","messageId":"xmqq1pbjbj4x.fsf@gitster.g","threadId":"66221","inReplyTo":"xmqq5x0vbjxd.fsf@gitster.g","subject":"Re: [PATCH v2 1/3] commit: reword the empty-commit rebase errors","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-27T16:52:46Z","receivedAt":"2026-08-27T16:52:48Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n>> +\t\t\tdie(_(\"cannot do a partial commit while resolving a commit that became empty.\"));\n>\n> That is a mouthful.  It also is awkward to say \"while resolving a commit\".\n\nThis still stands, but I haven't come up with a better alternative yet.\n\n> More importantly, I am not sure if whence == FROM_REBASE_PICK at\n> this point in the code flow is a sufficient sign to tell that we\n> were not just in the middle of a rebase, not just a rebase stopped\n> with _some_ conflict, but the way the rebase stopped was because a\n> step in rebase resulted in a commit that is no-op relative to the\n> previous commit.  What makes us certain that the rebase-pick is\n> empty?\n\nThis confusion was because FROM_REBASE_PICK is a misleading name.\n\nsequencer_determine_whence() is the only place that declares the\nwhence is FROM_REBASE_PICK, and it specifically checks if the\nrebase-head and cherry-pick-head are identical before yielding that\nvalue, so by definition we are dealing with an empty-pick situation.\n\nThis came from 430b75f720 (commit: give correct advice for empty\ncommit during a rebase, 2019-12-06); interestingly, the name of\nFROM_REBASE_PICK and is_from_rebase() seem to have confused even the\noriginating commit ;-)  The lines in question\n\n+               else if (is_from_rebase(whence))\n+                       die(_(\"cannot do a partial commit during a rebase.\"));\n\nare from that commit, which wanted to \"give correct advice for empty\ncommit during a rebase\".\n\nWe may want to\n\n * change the code that does whence == FROM_REBASE_PICK to use\n   is_from_rebase(whence) everywhere (other than the implementation\n   of is_from_rebase() itself, of course).\n\n * give FROM_REBASE_PICK and is_from_rebase() better names that\n   contain \"empty\" somewhere.\n\nto unconfuse me.\n"},{"id":"551377","messageId":"xmqqwltba4gt.fsf@gitster.g","threadId":"66221","inReplyTo":"dec05fd6-b99c-4a71-b80e-24e7d6de4b62@gmail.com","subject":"Re: [PATCH v2 1/3] commit: reword the empty-commit rebase errors","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-27T16:54:58Z","receivedAt":"2026-08-27T16:55:00Z","isPatch":true,"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n>> @@ -521,7 +521,7 @@ static const char *prepare_index(const char **argv, const char *prefix,\n>>   \t\telse if (is_from_cherry_pick(whence))\n>>   \t\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n>>   \t\telse if (is_from_rebase(whence))\n>> -\t\t\tdie(_(\"cannot do a partial commit during a rebase.\"));\n>> +\t\t\tdie(_(\"cannot do a partial commit while resolving a commit that became empty.\"));\n>\n> \"while committing a commit that became empty\" would be clearer to me, \n> but I what you have is definitely an improvement on the existing message.\n\nA stupid question, but wouldn't a partial commit of an empty commit\nstill an empty commit?  IOW, why do we need to reject a partial\ncommit while committing a commit that became empty?\n\nThanks.\n"},{"id":"551403","messageId":"CABPp-BEtoN+zA=vyyEAgruNSy5SKWjTdVW=weDjbM8NcenRbGg@mail.gmail.com","threadId":"66221","inReplyTo":"xmqq1pbjbj4x.fsf@gitster.g","subject":"Re: [PATCH v2 1/3] commit: reword the empty-commit rebase errors","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-08-28T07:38:48Z","receivedAt":"2026-08-28T07:39:01Z","isPatch":true,"body":"On Thu, Aug 27, 2026 at 9:52 AM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Junio C Hamano <gitster@pobox.com> writes:\n>\n> >> +                    die(_(\"cannot do a partial commit while resolving a commit that became empty.\"));\n> >\n> > That is a mouthful.  It also is awkward to say \"while resolving a commit\".\n>\n> This still stands, but I haven't come up with a better alternative yet.\n>\n> > More importantly, I am not sure if whence == FROM_REBASE_PICK at\n> > this point in the code flow is a sufficient sign to tell that we\n> > were not just in the middle of a rebase, not just a rebase stopped\n> > with _some_ conflict, but the way the rebase stopped was because a\n> > step in rebase resulted in a commit that is no-op relative to the\n> > previous commit.  What makes us certain that the rebase-pick is\n> > empty?\n>\n> This confusion was because FROM_REBASE_PICK is a misleading name.\n>\n> sequencer_determine_whence() is the only place that declares the\n> whence is FROM_REBASE_PICK, and it specifically checks if the\n> rebase-head and cherry-pick-head are identical before yielding that\n> value, so by definition we are dealing with an empty-pick situation.\n>\n> This came from 430b75f720 (commit: give correct advice for empty\n> commit during a rebase, 2019-12-06); interestingly, the name of\n> FROM_REBASE_PICK and is_from_rebase() seem to have confused even the\n> originating commit ;-)  The lines in question\n>\n> +               else if (is_from_rebase(whence))\n> +                       die(_(\"cannot do a partial commit during a rebase.\"));\n>\n> are from that commit, which wanted to \"give correct advice for empty\n> commit during a rebase\".\n>\n> We may want to\n>\n>  * change the code that does whence == FROM_REBASE_PICK to use\n>    is_from_rebase(whence) everywhere (other than the implementation\n>    of is_from_rebase() itself, of course).\n>\n>  * give FROM_REBASE_PICK and is_from_rebase() better names that\n>    contain \"empty\" somewhere.\n>\n> to unconfuse me.\n\nThat really confused me too.  I figured my series was already growing\ntoo quickly and decided to leave it out, but since it confused you as\nwell, I agree we should fix this up.  I'll add a preparatory patch in\nv3.\n"},{"id":"551404","messageId":"CABPp-BEaPtkT48-wpPmniWghhZ97VELkHAzFMgdckTAosmstgg@mail.gmail.com","threadId":"66221","inReplyTo":"xmqqwltba4gt.fsf@gitster.g","subject":"Re: [PATCH v2 1/3] commit: reword the empty-commit rebase errors","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-08-28T07:38:53Z","receivedAt":"2026-08-28T07:39:05Z","isPatch":true,"body":"On Thu, Aug 27, 2026 at 9:55 AM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Phillip Wood <phillip.wood123@gmail.com> writes:\n>\n> >> @@ -521,7 +521,7 @@ static const char *prepare_index(const char **argv, const char *prefix,\n> >>              else if (is_from_cherry_pick(whence))\n> >>                      die(_(\"cannot do a partial commit during a cherry-pick.\"));\n> >>              else if (is_from_rebase(whence))\n> >> -                    die(_(\"cannot do a partial commit during a rebase.\"));\n> >> +                    die(_(\"cannot do a partial commit while resolving a commit that became empty.\"));\n> >\n> > \"while committing a commit that became empty\" would be clearer to me,\n> > but I what you have is definitely an improvement on the existing message.\n>\n> A stupid question, but wouldn't a partial commit of an empty commit\n> still an empty commit?  IOW, why do we need to reject a partial\n> commit while committing a commit that became empty?\n\nNot stupid at all.  After some digging...\n\nOriginally, we just checked for doing partial commit during merges or\ncherry-picks.  Then in commit 430b75f7209c (commit: give correct\nadvice for empty commit during a rebase, 2019-12-06) it was noted that\nthe \"cannot do a partial commit during a cherry-pick\" message was also\nprinted when rebasing a commit that became empty.  Noting the\nmisleading message, rather than drop the check in that case (likely an\noversight), that commit opted to make the message print the actual\noperation that was in progress.\n\nI can fix it in v3, with another preparatory patch.\n"},{"id":"551405","messageId":"pull.2389.v3.git.git.1787903085.gitgitgadget@gmail.com","threadId":"66221","inReplyTo":"pull.2389.git.git.1787721681893.gitgitgadget@gmail.com","subject":"[PATCH v3 0/5] commit: refuse to amend during conflict resolution","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-08-28T07:44:40Z","receivedAt":"2026-08-28T07:44:50Z","isPatch":true,"body":"Changes since v2:\n\n * Two new preparatory patches:\n   * Rename FROM_REBASE_PICK and is_from_rebase() to point out they are\n     about empty commits\n   * Allow a partial commit when a rebase pick becomes empty\n * Tweaked the error message for attempted amend on now-dropped empty commit\n   (suggestions for further improvements welcome)\n * Used the path accessor functions within sequencer.c to simplify the new\n   helper function\n\nBoth git commit --amend and a partial commit (git commit <paths>) are\nfoot-guns while the user is in the middle of an operation that resolves\nconflicts on top of HEAD: recording a conflict resolution is about capturing\nthe state of the whole tree as a new commit, not about rewriting HEAD or\ncommitting a subset of paths.\n\nHistorically we only rejected these during a merge or a cherry-pick or when\nresolving an empty pick during a rebase. The same hazard exists for am,\nrevert, and rebase conflict stops, none of which were covered. This series\nextends the refusal to all of them.\n\nThe three patches:\n\n 1. reword the two pre-existing \"empty commit\" rebase messages, which were\n    misleadingly generic\n 2. refuse git commit --amend during these additional operations\n 3. refuse partial commits during the same operations.\n\nElijah Newren (5):\n  commit: clarify FROM_REBASE_PICK and is_from_rebase() names\n  commit: allow a partial commit when a rebase pick becomes empty\n  commit: reword the empty-commit rebase amend error\n  commit: refuse to amend during conflict resolution\n  commit: refuse partial commits during conflict resolution\n\n builtin/commit.c                |  65 +++++++++++-----\n sequencer.c                     |  59 ++++++++++++++-\n sequencer.h                     |  24 ++++++\n t/t3404-rebase-interactive.sh   | 128 +++++++++++++++++++++++++++++++-\n t/t3507-cherry-pick-conflict.sh |  22 ++++++\n t/t4151-am-abort.sh             |  22 ++++++\n wt-status.h                     |   6 +-\n 7 files changed, 299 insertions(+), 27 deletions(-)\n\n\nbase-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2389%2Fnewren%2Frefuse-amend-during-conflicts-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2389/newren/refuse-amend-during-conflicts-v3\nPull-Request: https://github.com/git/git/pull/2389\n\nRange-diff vs v2:\n\n -:  ---------- > 1:  7e198a20fa commit: clarify FROM_REBASE_PICK and is_from_rebase() names\n -:  ---------- > 2:  e169303619 commit: allow a partial commit when a rebase pick becomes empty\n 1:  65c48ed3cb ! 3:  0850a999da commit: reword the empty-commit rebase errors\n     @@ Metadata\n      Author: Elijah Newren <newren@gmail.com>\n      \n       ## Commit message ##\n     -    commit: reword the empty-commit rebase errors\n     +    commit: reword the empty-commit rebase amend error\n      \n          When a rebase applies a commit that becomes empty, it stops and asks the\n          user to decide whether to keep it or drop it.  HEAD still points at the\n     -    previously-applied commit at that point, so either amending or creating\n     -    a partial commit is refused, with one of the following messages:\n     +    previously-applied commit at that point, so amending is refused, with:\n      \n              You are in the middle of a rebase -- cannot amend.\n     -        cannot do a partial commit during a rebase.\n      \n     -    Neither message hints that the real problem is a commit that became\n     -    empty, and \"during a rebase\" is overly broad besides -- amending and\n     -    partial commits are fine at an `edit` or `break` stop.  Reword both to\n     -    describe the actual situation.\n     +    That message would suggest that amending is not allowed during an 'edit'\n     +    or 'break' stop, which is misleading, plus it lacks the specificity that\n     +    might help the user know why their particular case is a problem: the\n     +    commit they intended to amend became empty and was dropped, so amending\n     +    would affect the wrong commit.  Reword the error accordingly.\n      \n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n       ## builtin/commit.c ##\n     -@@ builtin/commit.c: static const char *prepare_index(const char **argv, const char *prefix,\n     - \t\telse if (is_from_cherry_pick(whence))\n     - \t\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n     - \t\telse if (is_from_rebase(whence))\n     --\t\t\tdie(_(\"cannot do a partial commit during a rebase.\"));\n     -+\t\t\tdie(_(\"cannot do a partial commit while resolving a commit that became empty.\"));\n     - \t}\n     - \n     - \tif (list_paths(&partial, !current_head ? NULL : \"HEAD\", &pathspec))\n      @@ builtin/commit.c: static int parse_and_validate_options(int argc, const char *argv[],\n       \t\telse if (is_from_cherry_pick(whence))\n       \t\t\tdie(_(\"You are in the middle of a cherry-pick -- cannot amend.\"));\n     - \t\telse if (whence == FROM_REBASE_PICK)\n     + \t\telse if (is_from_rebase_empty(whence))\n      -\t\t\tdie(_(\"You are in the middle of a rebase -- cannot amend.\"));\n     -+\t\t\tdie(_(\"You are resolving a commit that became empty -- cannot amend.\"));\n     ++\t\t\tdie(_(\"The now-empty commit has been dropped -- cannot amend.\"));\n       \t}\n       \tif (fixup_message && squash_message)\n       \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--squash\", \"--fixup\");\n      \n       ## t/t3404-rebase-interactive.sh ##\n     -@@ t/t3404-rebase-interactive.sh: test_expect_success 'correct error message for partial commit after empty pick'\n     - \t) &&\n     - \techo x >file1 &&\n     - \ttest_must_fail git commit file1 2>err &&\n     --\ttest_grep \"cannot do a partial commit during a rebase.\" err\n     -+\ttest_grep \"cannot do a partial commit while resolving a commit that became empty.\" err\n     - '\n     - \n     - test_expect_success 'correct error message for commit --amend after empty pick' '\n      @@ t/t3404-rebase-interactive.sh: test_expect_success 'correct error message for commit --amend after empty pick'\n       \t) &&\n       \techo x>file1 &&\n       \ttest_must_fail git commit -a --amend 2>err &&\n      -\ttest_grep \"middle of a rebase -- cannot amend.\" err\n     -+\ttest_grep \"resolving a commit that became empty -- cannot amend.\" err\n     ++\ttest_grep \"now-empty commit has been dropped -- cannot amend.\" err\n       '\n       \n       test_expect_success 'todo has correct onto hash' '\n 2:  4a1461e527 ! 4:  9f80d8a00d commit: refuse to amend during conflict resolution\n     @@ builtin/commit.c: static int parse_and_validate_options(int argc, const char *ar\n      -\t\telse if (is_from_cherry_pick(whence))\n      +\t\tcase ONGOING_CHERRY_PICK:\n       \t\t\tdie(_(\"You are in the middle of a cherry-pick -- cannot amend.\"));\n     --\t\telse if (whence == FROM_REBASE_PICK)\n     +-\t\telse if (is_from_rebase_empty(whence))\n      +\t\tcase ONGOING_REBASE_EMPTY:\n     - \t\t\tdie(_(\"You are resolving a commit that became empty -- cannot amend.\"));\n     + \t\t\tdie(_(\"The now-empty commit has been dropped -- cannot amend.\"));\n      +\t\tcase ONGOING_REVERT:\n      +\t\t\tdie(_(\"You are in the middle of a revert -- cannot amend.\"));\n      +\t\tcase ONGOING_AM:\n     @@ builtin/commit.c: static int parse_and_validate_options(int argc, const char *ar\n       \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--squash\", \"--fixup\");\n      \n       ## sequencer.c ##\n     +@@ sequencer.c: static GIT_PATH_FUNC(rebase_path_author_script, \"rebase-merge/author-script\")\n     +  * command is processed, this file is deleted.\n     +  */\n     + static GIT_PATH_FUNC(rebase_path_amend, \"rebase-merge/amend\")\n     ++/*\n     ++ * The apply (\"am\") backend keeps its state in the rebase-apply directory;\n     ++ * the \"applying\" file within it marks a plain `git am` (as opposed to an\n     ++ * apply-based rebase).\n     ++ */\n     ++static GIT_PATH_FUNC(apply_dir, \"rebase-apply\")\n     ++static GIT_PATH_FUNC(apply_path_applying, \"rebase-apply/applying\")\n     + /*\n     +  * When we stop at a given patch via the \"edit\" command, this file contains\n     +  * the commit object name of the corresponding patch.\n      @@ sequencer.c: int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)\n       \treturn 0;\n       }\n     @@ sequencer.c: int sequencer_determine_whence(struct repository *r, enum commit_wh\n      +enum ongoing_operation sequencer_ongoing_operation(struct repository *r,\n      +\t\t\t\t\t\t   enum commit_whence whence)\n      +{\n     -+\tchar *path;\n     -+\tint found;\n     -+\n      +\t/*\n      +\t * The merge, cherry-pick, and (empty) rebase-pick stops are already\n      +\t * distinguished by 'whence'.\n     @@ sequencer.c: int sequencer_determine_whence(struct repository *r, enum commit_wh\n      +\tcase FROM_CHERRY_PICK_SINGLE:\n      +\tcase FROM_CHERRY_PICK_MULTI:\n      +\t\treturn ONGOING_CHERRY_PICK;\n     -+\tcase FROM_REBASE_PICK:\n     ++\tcase FROM_REBASE_EMPTY:\n      +\t\treturn ONGOING_REBASE_EMPTY;\n      +\tcase FROM_COMMIT:\n      +\t\tbreak;\n     @@ sequencer.c: int sequencer_determine_whence(struct repository *r, enum commit_wh\n      +\t\treturn ONGOING_REVERT;\n      +\n      +\t/* In the middle of an `am`? */\n     -+\tpath = repo_git_path(r, \"rebase-apply/applying\");\n     -+\tfound = file_exists(path);\n     -+\tfree(path);\n     -+\tif (found)\n     ++\tif (file_exists(apply_path_applying()))\n      +\t\treturn ONGOING_AM;\n      +\n      +\t/*\n     @@ sequencer.c: int sequencer_determine_whence(struct repository *r, enum commit_wh\n      +\t * to be amended (a clean edit/reword stop); its absence therefore\n      +\t * marks a conflicted stop.\n      +\t */\n     -+\tpath = repo_git_path(r, \"rebase-apply\");\n     -+\tfound = file_exists(path);\n     -+\tfree(path);\n     -+\tif (!found) {\n     -+\t\tchar *stopped_sha = repo_git_path(r, \"rebase-merge/stopped-sha\");\n     -+\t\tchar *amend_marker = repo_git_path(r, \"rebase-merge/amend\");\n     -+\n     -+\t\tfound = file_exists(stopped_sha) && !file_exists(amend_marker);\n     -+\t\tfree(stopped_sha);\n     -+\t\tfree(amend_marker);\n     -+\t}\n     -+\tif (found)\n     ++\tif (file_exists(apply_dir()) ||\n     ++\t    (file_exists(rebase_path_stopped_sha()) &&\n     ++\t     !file_exists(rebase_path_amend())))\n      +\t\treturn ONGOING_REBASE_CONFLICT;\n      +\n      +\treturn ONGOING_NONE;\n     @@ sequencer.h: int sequencer_get_last_command(struct repository* r,\n      \n       ## t/t3404-rebase-interactive.sh ##\n      @@ t/t3404-rebase-interactive.sh: test_expect_success 'correct error message for commit --amend after empty pick'\n     - \ttest_grep \"resolving a commit that became empty -- cannot amend.\" err\n     + \ttest_grep \"now-empty commit has been dropped -- cannot amend.\" err\n       '\n       \n      +test_expect_success 'commit --amend is refused at a rebase conflict stop' '\n 3:  e0be8cdf63 ! 5:  050b9e8a52 commit: refuse partial commits during conflict resolution\n     @@ Commit message\n          Similar to the previous commit, just as `git commit --amend` is a\n          foot-gun during conflict resolution, so is a partial commit (`git commit\n          <paths>`).  Recording a conflict resolution is about capturing the state\n     -    of the entire tree on top of HEAD, not a subset of paths.  For many\n     -    years we have rejected partial commits in the middle of\n     +    of the entire tree on top of HEAD, not a subset of paths.  For many years\n     +    we have rejected partial commits in the middle of\n            - a merge\n            - a cherry-pick\n     -      - a rebase that stopped at a pick\n      \n     -    but, just like amending, this was never extended to the other\n     -    operations that can also leave conflicts to resolve:\n     +    but, just like amending, this was never extended to the other operations\n     +    that can also leave conflicts to resolve:\n            - an `am` operation\n            - a revert\n            - a rebase that stopped for conflict resolution\n      \n          Reuse sequencer_ongoing_operation(), introduced for the analogous\n     -    `--amend` check, to detect all of these and refuse the partial commit.\n     +    `--amend` check, to detect these and refuse the partial commit.  A rebase\n     +    that stopped because a pick became empty is not conflict resolution and,\n     +    as an earlier patch established, is deliberately left permitted.\n      \n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n     @@ builtin/commit.c: static const char *prepare_index(const char **argv, const char\n      -\t\t\tdie(_(\"cannot do a partial commit during a merge.\"));\n      -\t\telse if (is_from_cherry_pick(whence))\n      -\t\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n     --\t\telse if (is_from_rebase(whence))\n     --\t\t\tdie(_(\"cannot do a partial commit while resolving a commit that became empty.\"));\n      +\tswitch (sequencer_ongoing_operation(the_repository, whence)) {\n      +\tcase ONGOING_NONE:\n      +\t\tbreak;\n     @@ builtin/commit.c: static const char *prepare_index(const char **argv, const char\n      +\tcase ONGOING_CHERRY_PICK:\n      +\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n      +\tcase ONGOING_REBASE_EMPTY:\n     -+\t\tdie(_(\"cannot do a partial commit while resolving a commit that became empty.\"));\n     ++\t\t/*\n     ++\t\t * A pick that became empty is not a conflict, and creating\n     ++\t\t * a new commit (partial or not) poses no problem.\n     ++\t\t */\n     ++\t\tbreak;\n      +\tcase ONGOING_REVERT:\n      +\t\tdie(_(\"cannot do a partial commit during a revert.\"));\n      +\tcase ONGOING_AM:\n\n-- \ngitgitgadget\n"},{"id":"551406","messageId":"7e198a20fa47f0d5b2c50ffc7046bdfc792b62af.1787903085.git.gitgitgadget@gmail.com","threadId":"66221","inReplyTo":"pull.2389.v3.git.git.1787903085.gitgitgadget@gmail.com","subject":"[PATCH v3 1/5] commit: clarify FROM_REBASE_PICK and is_from_rebase() names","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-08-28T07:44:41Z","receivedAt":"2026-08-28T07:44:51Z","isPatch":true,"body":"From: Elijah Newren <newren@gmail.com>\n\nCommit 430b75f7209c (commit: give correct advice for empty commit during\na rebase, 2019-12-06) introduced a FROM_REBASE_PICK enum value and an\nis_from_rebase() function.  Those names failed to convey that they were\nspecifically about hitting a commit that becomes empty when rebasing.\nClarify their names now.\n\nWhile at it, change `whence == FROM_REBASE_EMPTY` to use\n`is_from_rebase_empty(whence)`.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/commit.c | 14 +++++++-------\n sequencer.c      |  2 +-\n wt-status.h      |  6 +++---\n 3 files changed, 11 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 28f6174503..569e31fb60 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -520,7 +520,7 @@ static const char *prepare_index(const char **argv, const char *prefix,\n \t\t\tdie(_(\"cannot do a partial commit during a merge.\"));\n \t\telse if (is_from_cherry_pick(whence))\n \t\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n-\t\telse if (is_from_rebase(whence))\n+\t\telse if (is_from_rebase_empty(whence))\n \t\t\tdie(_(\"cannot do a partial commit during a rebase.\"));\n \t}\n \n@@ -893,7 +893,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t */\n \telse if (whence == FROM_MERGE)\n \t\thook_arg1 = \"merge\";\n-\telse if (is_from_cherry_pick(whence) || whence == FROM_REBASE_PICK) {\n+\telse if (is_from_cherry_pick(whence) || is_from_rebase_empty(whence)) {\n \t\thook_arg1 = \"commit\";\n \t\thook_arg2 = \"CHERRY_PICK_HEAD\";\n \t}\n@@ -1086,7 +1086,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\tif (amend)\n \t\t\tfputs(_(empty_amend_advice), stderr);\n \t\telse if (is_from_cherry_pick(whence) ||\n-\t\t\t whence == FROM_REBASE_PICK) {\n+\t\t\t is_from_rebase_empty(whence)) {\n \t\t\tfputs(_(empty_cherry_pick_advice), stderr);\n \t\t\tif (whence == FROM_CHERRY_PICK_SINGLE)\n \t\t\t\tfputs(_(empty_cherry_pick_advice_single), stderr);\n@@ -1333,7 +1333,7 @@ static int parse_and_validate_options(int argc, const char *argv[],\n \t\t\tdie(_(\"You are in the middle of a merge -- cannot amend.\"));\n \t\telse if (is_from_cherry_pick(whence))\n \t\t\tdie(_(\"You are in the middle of a cherry-pick -- cannot amend.\"));\n-\t\telse if (whence == FROM_REBASE_PICK)\n+\t\telse if (is_from_rebase_empty(whence))\n \t\t\tdie(_(\"You are in the middle of a rebase -- cannot amend.\"));\n \t}\n \tif (fixup_message && squash_message)\n@@ -1353,7 +1353,7 @@ static int parse_and_validate_options(int argc, const char *argv[],\n \tif (amend && !use_message && !fixup_message)\n \t\tuse_message = \"HEAD\";\n \tif (!use_message && !is_from_cherry_pick(whence) &&\n-\t    !is_from_rebase(whence) && renew_authorship)\n+\t    !is_from_rebase_empty(whence) && renew_authorship)\n \t\tdie(_(\"--reset-author can be used only with -C, -c or --amend.\"));\n \tif (use_message) {\n \t\tuse_message_buffer = read_commit_message(use_message);\n@@ -1362,7 +1362,7 @@ static int parse_and_validate_options(int argc, const char *argv[],\n \t\t\tauthor_message_buffer = use_message_buffer;\n \t\t}\n \t}\n-\tif ((is_from_cherry_pick(whence) || whence == FROM_REBASE_PICK) &&\n+\tif ((is_from_cherry_pick(whence) || is_from_rebase_empty(whence)) &&\n \t    !renew_authorship) {\n \t\tauthor_message = \"CHERRY_PICK_HEAD\";\n \t\tauthor_message_buffer = read_commit_message(author_message);\n@@ -1887,7 +1887,7 @@ int cmd_commit(int argc,\n \t\tif (!reflog_msg)\n \t\t\treflog_msg = is_from_cherry_pick(whence)\n \t\t\t\t\t? \"commit (cherry-pick)\"\n-\t\t\t\t\t: is_from_rebase(whence)\n+\t\t\t\t\t: is_from_rebase_empty(whence)\n \t\t\t\t\t? \"commit (rebase)\"\n \t\t\t\t\t: \"commit\";\n \t\tcommit_list_insert(current_head, &parents);\ndiff --git a/sequencer.c b/sequencer.c\nindex 65afd100d9..0ea730a8dc 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -6956,7 +6956,7 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)\n \t\t    !repo_get_oid(r, \"REBASE_HEAD\", &rebase_head) &&\n \t\t    !repo_get_oid(r, \"CHERRY_PICK_HEAD\", &cherry_pick_head) &&\n \t\t    oideq(&rebase_head, &cherry_pick_head))\n-\t\t\t*whence = FROM_REBASE_PICK;\n+\t\t\t*whence = FROM_REBASE_EMPTY;\n \t\telse\n \t\t\t*whence = FROM_CHERRY_PICK_SINGLE;\n \ndiff --git a/wt-status.h b/wt-status.h\nindex e9fe32e98c..9588097dbe 100644\n--- a/wt-status.h\n+++ b/wt-status.h\n@@ -41,7 +41,7 @@ enum commit_whence {\n \tFROM_MERGE,      /* commit came from merge */\n \tFROM_CHERRY_PICK_SINGLE, /* commit came from cherry-pick */\n \tFROM_CHERRY_PICK_MULTI, /* commit came from a sequence of cherry-picks */\n-\tFROM_REBASE_PICK /* commit came from a pick/reword/edit */\n+\tFROM_REBASE_EMPTY /* rebase applied a pick that became empty */\n };\n \n static inline int is_from_cherry_pick(enum commit_whence whence)\n@@ -50,9 +50,9 @@ static inline int is_from_cherry_pick(enum commit_whence whence)\n \t\twhence == FROM_CHERRY_PICK_MULTI;\n }\n \n-static inline int is_from_rebase(enum commit_whence whence)\n+static inline int is_from_rebase_empty(enum commit_whence whence)\n {\n-\treturn whence == FROM_REBASE_PICK;\n+\treturn whence == FROM_REBASE_EMPTY;\n }\n \n struct wt_status_change_data {\n-- \ngitgitgadget\n\n"},{"id":"551407","messageId":"e16930361978335a6718814cf5d56a8dd8b42f75.1787903085.git.gitgitgadget@gmail.com","threadId":"66221","inReplyTo":"pull.2389.v3.git.git.1787903085.gitgitgadget@gmail.com","subject":"[PATCH v3 2/5] commit: allow a partial commit when a rebase pick becomes empty","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-08-28T07:44:42Z","receivedAt":"2026-08-28T07:44:53Z","isPatch":true,"body":"From: Elijah Newren <newren@gmail.com>\n\nFor years, we disallowed partial commits during merges or cherry-picks.\nIn commit 430b75f7209c (commit: give correct advice for empty commit\nduring a rebase, 2019-12-06) it was noted that the \"cannot do a partial\ncommit during a cherry-pick\" message was also printed when rebasing a\ncommit that became empty, and rather than drop the check in that case,\nthat commit opted to make the message print the actual operation that\nwas in progress.\n\nSince a commit that has become empty comes without conflicts, a new\npartial commit poses no problems; remove the error in that case.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/commit.c              | 2 --\n t/t3404-rebase-interactive.sh | 5 ++---\n 2 files changed, 2 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 569e31fb60..610820c99f 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -520,8 +520,6 @@ static const char *prepare_index(const char **argv, const char *prefix,\n \t\t\tdie(_(\"cannot do a partial commit during a merge.\"));\n \t\telse if (is_from_cherry_pick(whence))\n \t\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n-\t\telse if (is_from_rebase_empty(whence))\n-\t\t\tdie(_(\"cannot do a partial commit during a rebase.\"));\n \t}\n \n \tif (list_paths(&partial, !current_head ? NULL : \"HEAD\", &pathspec))\ndiff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh\nindex ff11abb2f2..3588e16543 100755\n--- a/t/t3404-rebase-interactive.sh\n+++ b/t/t3404-rebase-interactive.sh\n@@ -1858,7 +1858,7 @@ test_expect_success 'post-commit hook is called' '\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'correct error message for partial commit after empty pick' '\n+test_expect_success 'partial commit is allowed when a rebase pick becomes empty' '\n \ttest_when_finished \"git rebase --abort\" &&\n \t(\n \t\tset_fake_editor &&\n@@ -1867,8 +1867,7 @@ test_expect_success 'correct error message for partial commit after empty pick'\n \t\ttest_must_fail git rebase -i A D\n \t) &&\n \techo x >file1 &&\n-\ttest_must_fail git commit file1 2>err &&\n-\ttest_grep \"cannot do a partial commit during a rebase.\" err\n+\tgit commit file1\n '\n \n test_expect_success 'correct error message for commit --amend after empty pick' '\n-- \ngitgitgadget\n\n"},{"id":"551408","messageId":"0850a999dab65345c9efe4b1d5bc6926b2641217.1787903085.git.gitgitgadget@gmail.com","threadId":"66221","inReplyTo":"pull.2389.v3.git.git.1787903085.gitgitgadget@gmail.com","subject":"[PATCH v3 3/5] commit: reword the empty-commit rebase amend error","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-08-28T07:44:43Z","receivedAt":"2026-08-28T07:44:56Z","isPatch":true,"body":"From: Elijah Newren <newren@gmail.com>\n\nWhen a rebase applies a commit that becomes empty, it stops and asks the\nuser to decide whether to keep it or drop it.  HEAD still points at the\npreviously-applied commit at that point, so amending is refused, with:\n\n    You are in the middle of a rebase -- cannot amend.\n\nThat message would suggest that amending is not allowed during an 'edit'\nor 'break' stop, which is misleading, plus it lacks the specificity that\nmight help the user know why their particular case is a problem: the\ncommit they intended to amend became empty and was dropped, so amending\nwould affect the wrong commit.  Reword the error accordingly.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/commit.c              | 2 +-\n t/t3404-rebase-interactive.sh | 2 +-\n 2 files changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 610820c99f..774fb8299d 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -1332,7 +1332,7 @@ static int parse_and_validate_options(int argc, const char *argv[],\n \t\telse if (is_from_cherry_pick(whence))\n \t\t\tdie(_(\"You are in the middle of a cherry-pick -- cannot amend.\"));\n \t\telse if (is_from_rebase_empty(whence))\n-\t\t\tdie(_(\"You are in the middle of a rebase -- cannot amend.\"));\n+\t\t\tdie(_(\"The now-empty commit has been dropped -- cannot amend.\"));\n \t}\n \tif (fixup_message && squash_message)\n \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--squash\", \"--fixup\");\ndiff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh\nindex 3588e16543..81f4844950 100755\n--- a/t/t3404-rebase-interactive.sh\n+++ b/t/t3404-rebase-interactive.sh\n@@ -1880,7 +1880,7 @@ test_expect_success 'correct error message for commit --amend after empty pick'\n \t) &&\n \techo x>file1 &&\n \ttest_must_fail git commit -a --amend 2>err &&\n-\ttest_grep \"middle of a rebase -- cannot amend.\" err\n+\ttest_grep \"now-empty commit has been dropped -- cannot amend.\" err\n '\n \n test_expect_success 'todo has correct onto hash' '\n-- \ngitgitgadget\n\n"},{"id":"551409","messageId":"9f80d8a00d3895e2113d4e7259c93cb899c6cf57.1787903085.git.gitgitgadget@gmail.com","threadId":"66221","inReplyTo":"pull.2389.v3.git.git.1787903085.gitgitgadget@gmail.com","subject":"[PATCH v3 4/5] commit: refuse to amend during conflict resolution","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-08-28T07:44:44Z","receivedAt":"2026-08-28T07:44:58Z","isPatch":true,"body":"From: Elijah Newren <newren@gmail.com>\n\nRunning `git commit --amend` during conflict resolution is an ugly\nfoot-gun.  For many years, we have rejected amending during conflict\nresolution in the middle of\n  - a merge\n  - a cherry-pick\nHowever, this was never extended to other operations that can also\nproduce conflicts:\n  - an `am` operation\n  - a revert\n  - a rebase\n\nExtend it to handle these other cases now.\n\nExtending to `am`, revert, and the apply backend of rebase are fairly\nstraightforward.  However, with the merge backend of rebase we have to\nbe more careful, since it powers interactive rebases and\n  - the interactive machinery internally uses `git commit --amend` for\n    `squash` and `reword` directives\n  - users are expected to `git commit --amend` after hitting an `edit`\n    or `break` directive\nSo, we need to be careful with rebase to only reject amending when doing\nconflict resolution.\n\nA few files under the rebase-merge/ directory provide us the necessary\ninformation:\n\n  - stopped-sha is written only when the rebase stops and hands control\n    back to the user, so its presence marks a genuine stop -- as opposed\n    to the sequencer's own internal `git commit --amend` while applying\n    a squash, fixup, or reword, during which no stopped-sha exists.\n\n  - amend is written only when the rebase stops with HEAD already\n    pointing at the commit the user is meant to amend: a clean `edit`,\n    or a fast-forward `reword`.  Its absence at a stop therefore means\n    the commit did not apply, so HEAD is the previously-applied commit\n    rather than the one being rebased -- exactly the case we refuse.\n\nSo for the merge backend we die when stopped-sha exists and amend does\nnot.  This covers a plain conflicted pick as well as a conflicted `edit`\n(both leave HEAD on the previously-applied commit), while still allowing\na clean `edit` or `reword` stop and a `break` stop (no stopped-sha).\nstopped-sha is unlinked at the start of the resume loop, so a resumed\nsquash's internal amend is unaffected.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/commit.c                | 27 +++++++---\n sequencer.c                     | 57 +++++++++++++++++++++\n sequencer.h                     | 23 +++++++++\n t/t3404-rebase-interactive.sh   | 87 +++++++++++++++++++++++++++++++++\n t/t3507-cherry-pick-conflict.sh | 11 +++++\n t/t4151-am-abort.sh             | 11 +++++\n 6 files changed, 210 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 774fb8299d..83ea8619d6 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -1324,15 +1324,30 @@ static int parse_and_validate_options(int argc, const char *argv[],\n \t\tuse_editor = 0;\n \n \t/* Sanity check options */\n-\tif (amend && !current_head)\n-\t\tdie(_(\"You have nothing to amend.\"));\n-\tif (amend && whence != FROM_COMMIT) {\n-\t\tif (whence == FROM_MERGE)\n+\tif (amend) {\n+\t\tif (!current_head)\n+\t\t\tdie(_(\"You have nothing to amend.\"));\n+\t\t/*\n+\t\t * Refuse to amend in the middle of any operation that is\n+\t\t * meant to record its result as a new commit on top of HEAD\n+\t\t * rather than by rewriting HEAD.\n+\t\t */\n+\t\tswitch (sequencer_ongoing_operation(s->repo, whence)) {\n+\t\tcase ONGOING_NONE:\n+\t\t\tbreak;\n+\t\tcase ONGOING_MERGE:\n \t\t\tdie(_(\"You are in the middle of a merge -- cannot amend.\"));\n-\t\telse if (is_from_cherry_pick(whence))\n+\t\tcase ONGOING_CHERRY_PICK:\n \t\t\tdie(_(\"You are in the middle of a cherry-pick -- cannot amend.\"));\n-\t\telse if (is_from_rebase_empty(whence))\n+\t\tcase ONGOING_REBASE_EMPTY:\n \t\t\tdie(_(\"The now-empty commit has been dropped -- cannot amend.\"));\n+\t\tcase ONGOING_REVERT:\n+\t\t\tdie(_(\"You are in the middle of a revert -- cannot amend.\"));\n+\t\tcase ONGOING_AM:\n+\t\t\tdie(_(\"You are in the middle of an am session -- cannot amend.\"));\n+\t\tcase ONGOING_REBASE_CONFLICT:\n+\t\t\tdie(_(\"You are resolving conflicts during a rebase -- cannot amend.\"));\n+\t\t}\n \t}\n \tif (fixup_message && squash_message)\n \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--squash\", \"--fixup\");\ndiff --git a/sequencer.c b/sequencer.c\nindex 0ea730a8dc..d67896fcd1 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -142,6 +142,13 @@ static GIT_PATH_FUNC(rebase_path_author_script, \"rebase-merge/author-script\")\n  * command is processed, this file is deleted.\n  */\n static GIT_PATH_FUNC(rebase_path_amend, \"rebase-merge/amend\")\n+/*\n+ * The apply (\"am\") backend keeps its state in the rebase-apply directory;\n+ * the \"applying\" file within it marks a plain `git am` (as opposed to an\n+ * apply-based rebase).\n+ */\n+static GIT_PATH_FUNC(apply_dir, \"rebase-apply\")\n+static GIT_PATH_FUNC(apply_path_applying, \"rebase-apply/applying\")\n /*\n  * When we stop at a given patch via the \"edit\" command, this file contains\n  * the commit object name of the corresponding patch.\n@@ -6966,6 +6973,56 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)\n \treturn 0;\n }\n \n+enum ongoing_operation sequencer_ongoing_operation(struct repository *r,\n+\t\t\t\t\t\t   enum commit_whence whence)\n+{\n+\t/*\n+\t * The merge, cherry-pick, and (empty) rebase-pick stops are already\n+\t * distinguished by 'whence'.\n+\t */\n+\tswitch (whence) {\n+\tcase FROM_MERGE:\n+\t\treturn ONGOING_MERGE;\n+\tcase FROM_CHERRY_PICK_SINGLE:\n+\tcase FROM_CHERRY_PICK_MULTI:\n+\t\treturn ONGOING_CHERRY_PICK;\n+\tcase FROM_REBASE_EMPTY:\n+\t\treturn ONGOING_REBASE_EMPTY;\n+\tcase FROM_COMMIT:\n+\t\tbreak;\n+\t}\n+\n+\t/*\n+\t * 'whence' is FROM_COMMIT, but we may still be in the middle of an\n+\t * operation that records its result on top of HEAD; detect those\n+\t * from their on-disk state.\n+\t */\n+\n+\t/* In the middle of a revert? */\n+\tif (refs_ref_exists(get_main_ref_store(r), \"REVERT_HEAD\"))\n+\t\treturn ONGOING_REVERT;\n+\n+\t/* In the middle of an `am`? */\n+\tif (file_exists(apply_path_applying()))\n+\t\treturn ONGOING_AM;\n+\n+\t/*\n+\t * In the middle of a rebase that stopped for conflict resolution?\n+\t * The apply backend only ever stops for conflicts, so the presence\n+\t * of its state directory is enough.  The merge backend writes\n+\t * stopped-sha whenever it hands control back to the user, but omits\n+\t * `amend` unless it stopped with HEAD already pointing at the commit\n+\t * to be amended (a clean edit/reword stop); its absence therefore\n+\t * marks a conflicted stop.\n+\t */\n+\tif (file_exists(apply_dir()) ||\n+\t    (file_exists(rebase_path_stopped_sha()) &&\n+\t     !file_exists(rebase_path_amend())))\n+\t\treturn ONGOING_REBASE_CONFLICT;\n+\n+\treturn ONGOING_NONE;\n+}\n+\n int sequencer_get_update_refs_state(const char *wt_dir,\n \t\t\t\t    struct string_list *refs)\n {\ndiff --git a/sequencer.h b/sequencer.h\nindex 64a9c7fb1b..3a4bd97db1 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -277,6 +277,29 @@ int sequencer_get_last_command(struct repository* r,\n \t\t\t       enum replay_action *action);\n int sequencer_determine_whence(struct repository *r, enum commit_whence *whence);\n \n+/*\n+ * An in-progress operation that records its result (often a conflict\n+ * resolution) as a new commit on top of HEAD, during which amending\n+ * HEAD via \"git commit --amend\" is almost always a mistake.\n+ */\n+enum ongoing_operation {\n+\tONGOING_NONE = 0,\n+\tONGOING_MERGE,\n+\tONGOING_CHERRY_PICK,\n+\tONGOING_REBASE_EMPTY,\n+\tONGOING_REVERT,\n+\tONGOING_AM,\n+\tONGOING_REBASE_CONFLICT\n+};\n+\n+/*\n+ * Return which in-progress operation, if any, is underway; see enum\n+ * ongoing_operation.  'whence' is the origin already computed for the\n+ * pending commit.\n+ */\n+enum ongoing_operation sequencer_ongoing_operation(struct repository *r,\n+\t\t\t\t\t\t   enum commit_whence whence);\n+\n /**\n  * Append the set of ref-OID pairs that are currently stored for the 'git\n  * rebase --update-refs' feature if such a rebase is currently happening.\ndiff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh\nindex 81f4844950..4e6c3e2f19 100755\n--- a/t/t3404-rebase-interactive.sh\n+++ b/t/t3404-rebase-interactive.sh\n@@ -1883,6 +1883,93 @@ test_expect_success 'correct error message for commit --amend after empty pick'\n \ttest_grep \"now-empty commit has been dropped -- cannot amend.\" err\n '\n \n+test_expect_success 'commit --amend is refused at a rebase conflict stop' '\n+\ttest_when_finished \"git rebase --abort\" &&\n+\tgit checkout --detach conflict-branch &&\n+\t(\n+\t\tset_fake_editor &&\n+\t\tFAKE_LINES=\"1 3\" &&\n+\t\texport FAKE_LINES &&\n+\t\ttest_must_fail git rebase -i A\n+\t) &&\n+\ttest_path_is_file .git/rebase-merge/patch &&\n+\ttest_path_is_missing .git/rebase-merge/amend &&\n+\techo resolved >conflict &&\n+\tgit add conflict &&\n+\ttest_must_fail git commit --amend --no-edit 2>err &&\n+\ttest_grep \"You are resolving conflicts during a rebase -- cannot amend\" err\n+'\n+\n+test_expect_success 'commit --amend is refused when an \"edit\" pick conflicts' '\n+\ttest_when_finished \"git rebase --abort\" &&\n+\tgit checkout --detach conflict-branch &&\n+\t(\n+\t\tset_fake_editor &&\n+\t\tFAKE_LINES=\"1 edit 3\" &&\n+\t\texport FAKE_LINES &&\n+\t\ttest_must_fail git rebase -i A\n+\t) &&\n+\ttest_path_is_file .git/rebase-merge/patch &&\n+\ttest_path_is_missing .git/rebase-merge/amend &&\n+\techo resolved >conflict &&\n+\tgit add conflict &&\n+\ttest_must_fail git commit --amend --no-edit 2>err &&\n+\ttest_grep \"You are resolving conflicts during a rebase -- cannot amend\" err\n+'\n+\n+test_expect_success 'commit --amend is allowed at a rebase edit stop' '\n+\ttest_when_finished \"git rebase --abort\" &&\n+\tgit checkout --detach no-conflict-branch &&\n+\t(\n+\t\tset_fake_editor &&\n+\t\tFAKE_LINES=\"edit 1 2 3 4\" &&\n+\t\texport FAKE_LINES &&\n+\t\tgit rebase -i A\n+\t) &&\n+\ttest_path_is_file .git/rebase-merge/amend &&\n+\techo tweak >fileJ &&\n+\tgit add fileJ &&\n+\tgit commit --amend --no-edit\n+'\n+\n+test_expect_success 'commit --amend is allowed at a rebase break stop' '\n+\ttest_when_finished \"git rebase --abort\" &&\n+\tgit checkout --detach no-conflict-branch &&\n+\t(\n+\t\tset_fake_editor &&\n+\t\tFAKE_LINES=\"break 1 2 3 4\" &&\n+\t\texport FAKE_LINES &&\n+\t\tgit rebase -i A\n+\t) &&\n+\ttest_must_fail git rev-parse --verify REBASE_HEAD &&\n+\techo tweak >fileJ &&\n+\tgit add fileJ &&\n+\tgit commit --amend --no-edit\n+'\n+\n+test_expect_success 'commit --amend is refused at an apply-backend conflict stop' '\n+\ttest_when_finished \"rm -rf apply-backend\" &&\n+\ttest_create_repo apply-backend &&\n+\t(\n+\t\tcd apply-backend &&\n+\t\ttest_commit base file &&\n+\t\tgit branch -M mainline &&\n+\t\ttest_commit upstream file upstream &&\n+\t\tgit checkout -b side mainline~1 &&\n+\t\ttest_commit conflicting file side &&\n+\t\ttest_commit unrelated other &&\n+\t\ttest_must_fail git rebase --apply mainline &&\n+\t\t# the apply backend only ever stops for conflicts, and\n+\t\t# leaves HEAD on the previously-applied commit\n+\t\ttest_path_is_dir .git/rebase-apply &&\n+\t\ttest_path_is_missing .git/rebase-apply/applying &&\n+\t\techo resolved >file &&\n+\t\tgit add file &&\n+\t\ttest_must_fail git commit --amend --no-edit 2>err &&\n+\t\ttest_grep \"You are resolving conflicts during a rebase -- cannot amend\" err\n+\t)\n+'\n+\n test_expect_success 'todo has correct onto hash' '\n \tGIT_SEQUENCE_EDITOR=cat git rebase -i no-conflict-branch~4 no-conflict-branch >actual &&\n \tonto=$(git rev-parse --short HEAD~4) &&\ndiff --git a/t/t3507-cherry-pick-conflict.sh b/t/t3507-cherry-pick-conflict.sh\nindex 44596cb1e8..42de398f76 100755\n--- a/t/t3507-cherry-pick-conflict.sh\n+++ b/t/t3507-cherry-pick-conflict.sh\n@@ -364,6 +364,17 @@ test_expect_success 'failed revert sets REVERT_HEAD' '\n \ttest_cmp_rev picked REVERT_HEAD\n '\n \n+test_expect_success 'commit --amend of revert fails' '\n+\tpristine_detach initial &&\n+\n+\ttest_must_fail git revert picked &&\n+\techo resolved >foo &&\n+\tgit add foo &&\n+\ttest_must_fail git commit --amend 2>err &&\n+\n+\ttest_grep \"in the middle of a revert -- cannot amend.\" err\n+'\n+\n test_expect_success 'successful revert does not set REVERT_HEAD' '\n \tpristine_detach base &&\n \tgit revert base &&\ndiff --git a/t/t4151-am-abort.sh b/t/t4151-am-abort.sh\nindex 8e1ecf8a68..9313a074b2 100755\n--- a/t/t4151-am-abort.sh\n+++ b/t/t4151-am-abort.sh\n@@ -63,6 +63,17 @@ do\n \n done\n \n+test_expect_success 'commit --amend during a failed am fails' '\n+\tgit reset --hard initial &&\n+\tcp file-2-expect file-2 &&\n+\ttest_must_fail git am 000[1245]-*.patch &&\n+\techo resolved >file-1 &&\n+\tgit add file-1 &&\n+\ttest_must_fail git commit --amend 2>err &&\n+\ttest_grep \"in the middle of an am session -- cannot amend.\" err &&\n+\tgit am --abort\n+'\n+\n test_expect_success 'am -3 --skip removes otherfile-4' '\n \tgit reset --hard initial &&\n \ttest_must_fail git am -3 0003-*.patch &&\n-- \ngitgitgadget\n\n"},{"id":"551410","messageId":"050b9e8a52e531bcb17f483d0d574aaca3875adf.1787903085.git.gitgitgadget@gmail.com","threadId":"66221","inReplyTo":"pull.2389.v3.git.git.1787903085.gitgitgadget@gmail.com","subject":"[PATCH v3 5/5] commit: refuse partial commits during conflict resolution","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-08-28T07:44:45Z","receivedAt":"2026-08-28T07:45:00Z","isPatch":true,"body":"From: Elijah Newren <newren@gmail.com>\n\nSimilar to the previous commit, just as `git commit --amend` is a\nfoot-gun during conflict resolution, so is a partial commit (`git commit\n<paths>`).  Recording a conflict resolution is about capturing the state\nof the entire tree on top of HEAD, not a subset of paths.  For many years\nwe have rejected partial commits in the middle of\n  - a merge\n  - a cherry-pick\n\nbut, just like amending, this was never extended to the other operations\nthat can also leave conflicts to resolve:\n  - an `am` operation\n  - a revert\n  - a rebase that stopped for conflict resolution\n\nReuse sequencer_ongoing_operation(), introduced for the analogous\n`--amend` check, to detect these and refuse the partial commit.  A rebase\nthat stopped because a pick became empty is not conflict resolution and,\nas an earlier patch established, is deliberately left permitted.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/commit.c                | 24 ++++++++++++++++++-----\n sequencer.h                     |  5 +++--\n t/t3404-rebase-interactive.sh   | 34 +++++++++++++++++++++++++++++++++\n t/t3507-cherry-pick-conflict.sh | 11 +++++++++++\n t/t4151-am-abort.sh             | 11 +++++++++++\n 5 files changed, 78 insertions(+), 7 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 83ea8619d6..e96c663bd5 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -515,11 +515,25 @@ static const char *prepare_index(const char **argv, const char *prefix,\n \t */\n \tcommit_style = COMMIT_PARTIAL;\n \n-\tif (whence != FROM_COMMIT) {\n-\t\tif (whence == FROM_MERGE)\n-\t\t\tdie(_(\"cannot do a partial commit during a merge.\"));\n-\t\telse if (is_from_cherry_pick(whence))\n-\t\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n+\tswitch (sequencer_ongoing_operation(the_repository, whence)) {\n+\tcase ONGOING_NONE:\n+\t\tbreak;\n+\tcase ONGOING_MERGE:\n+\t\tdie(_(\"cannot do a partial commit during a merge.\"));\n+\tcase ONGOING_CHERRY_PICK:\n+\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n+\tcase ONGOING_REBASE_EMPTY:\n+\t\t/*\n+\t\t * A pick that became empty is not a conflict, and creating\n+\t\t * a new commit (partial or not) poses no problem.\n+\t\t */\n+\t\tbreak;\n+\tcase ONGOING_REVERT:\n+\t\tdie(_(\"cannot do a partial commit during a revert.\"));\n+\tcase ONGOING_AM:\n+\t\tdie(_(\"cannot do a partial commit during an am session.\"));\n+\tcase ONGOING_REBASE_CONFLICT:\n+\t\tdie(_(\"cannot do a partial commit while resolving conflicts during a rebase.\"));\n \t}\n \n \tif (list_paths(&partial, !current_head ? NULL : \"HEAD\", &pathspec))\ndiff --git a/sequencer.h b/sequencer.h\nindex 3a4bd97db1..634d1ddcb3 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -279,8 +279,9 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)\n \n /*\n  * An in-progress operation that records its result (often a conflict\n- * resolution) as a new commit on top of HEAD, during which amending\n- * HEAD via \"git commit --amend\" is almost always a mistake.\n+ * resolution) as a new commit on top of HEAD.  Some ways of invoking\n+ * \"git commit\" -- amending HEAD, or a partial commit -- are almost\n+ * always a mistake during such an operation.\n  */\n enum ongoing_operation {\n \tONGOING_NONE = 0,\ndiff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh\nindex 4e6c3e2f19..8c63682b7f 100755\n--- a/t/t3404-rebase-interactive.sh\n+++ b/t/t3404-rebase-interactive.sh\n@@ -1970,6 +1970,40 @@ test_expect_success 'commit --amend is refused at an apply-backend conflict stop\n \t)\n '\n \n+test_expect_success 'partial commit is refused at a rebase conflict stop' '\n+\ttest_when_finished \"git rebase --abort\" &&\n+\tgit checkout --detach conflict-branch &&\n+\t(\n+\t\tset_fake_editor &&\n+\t\tFAKE_LINES=\"1 3\" &&\n+\t\texport FAKE_LINES &&\n+\t\ttest_must_fail git rebase -i A\n+\t) &&\n+\techo resolved >conflict &&\n+\tgit add conflict &&\n+\ttest_must_fail git commit conflict 2>err &&\n+\ttest_grep \"cannot do a partial commit while resolving conflicts during a rebase.\" err\n+'\n+\n+test_expect_success 'partial commit is refused at an apply-backend conflict stop' '\n+\ttest_when_finished \"rm -rf apply-backend\" &&\n+\ttest_create_repo apply-backend &&\n+\t(\n+\t\tcd apply-backend &&\n+\t\ttest_commit base file &&\n+\t\tgit branch -M mainline &&\n+\t\ttest_commit upstream file upstream &&\n+\t\tgit checkout -b side mainline~1 &&\n+\t\ttest_commit conflicting file side &&\n+\t\ttest_commit unrelated other &&\n+\t\ttest_must_fail git rebase --apply mainline &&\n+\t\techo resolved >file &&\n+\t\tgit add file &&\n+\t\ttest_must_fail git commit file 2>err &&\n+\t\ttest_grep \"cannot do a partial commit while resolving conflicts during a rebase.\" err\n+\t)\n+'\n+\n test_expect_success 'todo has correct onto hash' '\n \tGIT_SEQUENCE_EDITOR=cat git rebase -i no-conflict-branch~4 no-conflict-branch >actual &&\n \tonto=$(git rev-parse --short HEAD~4) &&\ndiff --git a/t/t3507-cherry-pick-conflict.sh b/t/t3507-cherry-pick-conflict.sh\nindex 42de398f76..c3d024c97f 100755\n--- a/t/t3507-cherry-pick-conflict.sh\n+++ b/t/t3507-cherry-pick-conflict.sh\n@@ -375,6 +375,17 @@ test_expect_success 'commit --amend of revert fails' '\n \ttest_grep \"in the middle of a revert -- cannot amend.\" err\n '\n \n+test_expect_success 'partial commit during a revert fails' '\n+\tpristine_detach initial &&\n+\n+\ttest_must_fail git revert picked &&\n+\techo resolved >foo &&\n+\tgit add foo &&\n+\ttest_must_fail git commit foo 2>err &&\n+\n+\ttest_grep \"cannot do a partial commit during a revert.\" err\n+'\n+\n test_expect_success 'successful revert does not set REVERT_HEAD' '\n \tpristine_detach base &&\n \tgit revert base &&\ndiff --git a/t/t4151-am-abort.sh b/t/t4151-am-abort.sh\nindex 9313a074b2..c80269e015 100755\n--- a/t/t4151-am-abort.sh\n+++ b/t/t4151-am-abort.sh\n@@ -74,6 +74,17 @@ test_expect_success 'commit --amend during a failed am fails' '\n \tgit am --abort\n '\n \n+test_expect_success 'partial commit during a failed am fails' '\n+\tgit reset --hard initial &&\n+\tcp file-2-expect file-2 &&\n+\ttest_must_fail git am 000[1245]-*.patch &&\n+\techo resolved >file-1 &&\n+\tgit add file-1 &&\n+\ttest_must_fail git commit file-1 2>err &&\n+\ttest_grep \"cannot do a partial commit during an am session.\" err &&\n+\tgit am --abort\n+'\n+\n test_expect_success 'am -3 --skip removes otherfile-4' '\n \tgit reset --hard initial &&\n \ttest_must_fail git am -3 0003-*.patch &&\n-- \ngitgitgadget\n"},{"id":"551423","messageId":"xmqq7bla6ymz.fsf@gitster.g","threadId":"66221","inReplyTo":"7e198a20fa47f0d5b2c50ffc7046bdfc792b62af.1787903085.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 1/5] commit: clarify FROM_REBASE_PICK and is_from_rebase() names","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-28T15:41:24Z","receivedAt":"2026-08-28T15:41:27Z","isPatch":true,"body":"\"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Elijah Newren <newren@gmail.com>\n>\n> Commit 430b75f7209c (commit: give correct advice for empty commit during\n> a rebase, 2019-12-06) introduced a FROM_REBASE_PICK enum value and an\n> is_from_rebase() function.  Those names failed to convey that they were\n> specifically about hitting a commit that becomes empty when rebasing.\n> Clarify their names now.\n\nBecomes empty is different from picking an empty commit, right.  I\nam not sure if \"is_from_rebase_empty()\" conveys the difference and\nmore importantly, I am afraid it hints the latter.  I have a feeling\nthat EMPTY_REBASE (instead of REBASE_EMPTY) may match what we want\nto express slightly better, but not by a large margin to make a\ndifference.  Perhaps Phillip has a better idea?\n\n> While at it, change `whence == FROM_REBASE_EMPTY` to use\n> `is_from_rebase_empty(whence)`.\n\nVery much appreciated.\n"},{"id":"551424","messageId":"xmqq33vy6ydu.fsf@gitster.g","threadId":"66221","inReplyTo":"e16930361978335a6718814cf5d56a8dd8b42f75.1787903085.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 2/5] commit: allow a partial commit when a rebase pick becomes empty","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-28T15:46:53Z","receivedAt":"2026-08-28T15:46:59Z","isPatch":true,"body":"\"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Elijah Newren <newren@gmail.com>\n>\n> For years, we disallowed partial commits during merges or cherry-picks.\n> In commit 430b75f7209c (commit: give correct advice for empty commit\n> during a rebase, 2019-12-06) it was noted that the \"cannot do a partial\n> commit during a cherry-pick\" message was also printed when rebasing a\n> commit that became empty, and rather than drop the check in that case,\n> that commit opted to make the message print the actual operation that\n> was in progress.\n>\n> Since a commit that has become empty comes without conflicts, a new\n> partial commit poses no problems; remove the error in that case.\n>\n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>  builtin/commit.c              | 2 --\n>  t/t3404-rebase-interactive.sh | 5 ++---\n>  2 files changed, 2 insertions(+), 5 deletions(-)\n\nOK.  Looking good.\n\n>\n> diff --git a/builtin/commit.c b/builtin/commit.c\n> index 569e31fb60..610820c99f 100644\n> --- a/builtin/commit.c\n> +++ b/builtin/commit.c\n> @@ -520,8 +520,6 @@ static const char *prepare_index(const char **argv, const char *prefix,\n>  \t\t\tdie(_(\"cannot do a partial commit during a merge.\"));\n>  \t\telse if (is_from_cherry_pick(whence))\n>  \t\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n> -\t\telse if (is_from_rebase_empty(whence))\n> -\t\t\tdie(_(\"cannot do a partial commit during a rebase.\"));\n>  \t}\n>  \n>  \tif (list_paths(&partial, !current_head ? NULL : \"HEAD\", &pathspec))\n> diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh\n> index ff11abb2f2..3588e16543 100755\n> --- a/t/t3404-rebase-interactive.sh\n> +++ b/t/t3404-rebase-interactive.sh\n> @@ -1858,7 +1858,7 @@ test_expect_success 'post-commit hook is called' '\n>  \ttest_cmp expect actual\n>  '\n>  \n> -test_expect_success 'correct error message for partial commit after empty pick' '\n> +test_expect_success 'partial commit is allowed when a rebase pick becomes empty' '\n>  \ttest_when_finished \"git rebase --abort\" &&\n>  \t(\n>  \t\tset_fake_editor &&\n> @@ -1867,8 +1867,7 @@ test_expect_success 'correct error message for partial commit after empty pick'\n>  \t\ttest_must_fail git rebase -i A D\n>  \t) &&\n>  \techo x >file1 &&\n> -\ttest_must_fail git commit file1 2>err &&\n> -\ttest_grep \"cannot do a partial commit during a rebase.\" err\n> +\tgit commit file1\n>  '\n>  \n>  test_expect_success 'correct error message for commit --amend after empty pick' '\n"},{"id":"551425","messageId":"xmqqy0dq5jpi.fsf@gitster.g","threadId":"66221","inReplyTo":"0850a999dab65345c9efe4b1d5bc6926b2641217.1787903085.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 3/5] commit: reword the empty-commit rebase amend error","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-28T15:49:13Z","receivedAt":"2026-08-28T15:49:15Z","isPatch":true,"body":"\"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Elijah Newren <newren@gmail.com>\n>\n> When a rebase applies a commit that becomes empty, it stops and asks the\n> user to decide whether to keep it or drop it.  HEAD still points at the\n> previously-applied commit at that point, so amending is refused, with:\n>\n>     You are in the middle of a rebase -- cannot amend.\n>\n> That message would suggest that amending is not allowed during an 'edit'\n> or 'break' stop, which is misleading, plus it lacks the specificity that\n> might help the user know why their particular case is a problem: the\n> commit they intended to amend became empty and was dropped, so amending\n> would affect the wrong commit.  Reword the error accordingly.\n\n\n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>  builtin/commit.c              | 2 +-\n>  t/t3404-rebase-interactive.sh | 2 +-\n>  2 files changed, 2 insertions(+), 2 deletions(-)\n>\n> diff --git a/builtin/commit.c b/builtin/commit.c\n> index 610820c99f..774fb8299d 100644\n> --- a/builtin/commit.c\n> +++ b/builtin/commit.c\n> @@ -1332,7 +1332,7 @@ static int parse_and_validate_options(int argc, const char *argv[],\n>  \t\telse if (is_from_cherry_pick(whence))\n>  \t\t\tdie(_(\"You are in the middle of a cherry-pick -- cannot amend.\"));\n>  \t\telse if (is_from_rebase_empty(whence))\n> -\t\t\tdie(_(\"You are in the middle of a rebase -- cannot amend.\"));\n> +\t\t\tdie(_(\"The now-empty commit has been dropped -- cannot amend.\"));\n\nOK.  Much less awkward than the previous round.\n\n>  \t}\n>  \tif (fixup_message && squash_message)\n>  \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--squash\", \"--fixup\");\n> diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh\n> index 3588e16543..81f4844950 100755\n> --- a/t/t3404-rebase-interactive.sh\n> +++ b/t/t3404-rebase-interactive.sh\n> @@ -1880,7 +1880,7 @@ test_expect_success 'correct error message for commit --amend after empty pick'\n>  \t) &&\n>  \techo x>file1 &&\n>  \ttest_must_fail git commit -a --amend 2>err &&\n> -\ttest_grep \"middle of a rebase -- cannot amend.\" err\n> +\ttest_grep \"now-empty commit has been dropped -- cannot amend.\" err\n>  '\n>  \n>  test_expect_success 'todo has correct onto hash' '\n"},{"id":"551426","messageId":"xmqqqzji5id2.fsf@gitster.g","threadId":"66221","inReplyTo":"050b9e8a52e531bcb17f483d0d574aaca3875adf.1787903085.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 5/5] commit: refuse partial commits during conflict resolution","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-28T16:18:17Z","receivedAt":"2026-08-28T16:18:20Z","isPatch":true,"body":"\"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> -\tif (whence != FROM_COMMIT) {\n> -\t\tif (whence == FROM_MERGE)\n> -\t\t\tdie(_(\"cannot do a partial commit during a merge.\"));\n> -\t\telse if (is_from_cherry_pick(whence))\n> -\t\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n> +\tswitch (sequencer_ongoing_operation(the_repository, whence)) {\n> +\tcase ONGOING_NONE:\n> +\t\tbreak;\n> +\tcase ONGOING_MERGE:\n> +\t\tdie(_(\"cannot do a partial commit during a merge.\"));\n> +\tcase ONGOING_CHERRY_PICK:\n> +\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n> +\tcase ONGOING_REBASE_EMPTY:\n> +\t\t/*\n> +\t\t * A pick that became empty is not a conflict, and creating\n> +\t\t * a new commit (partial or not) poses no problem.\n> +\t\t */\n> +\t\tbreak;\n> +\tcase ONGOING_REVERT:\n> +\t\tdie(_(\"cannot do a partial commit during a revert.\"));\n> +\tcase ONGOING_AM:\n> +\t\tdie(_(\"cannot do a partial commit during an am session.\"));\n> +\tcase ONGOING_REBASE_CONFLICT:\n> +\t\tdie(_(\"cannot do a partial commit while resolving conflicts during a rebase.\"));\n>  \t}\n\nLooks quite thorough.  \nDeliberate ommission of \"default:\" is a plus ;-)\n\n"},{"id":"551429","messageId":"CABPp-BG0AmOgfT1CrS+hTo8sRBd4J1gT0_KN8TRBHSFo86hMvw@mail.gmail.com","threadId":"66221","inReplyTo":"xmqq7bla6ymz.fsf@gitster.g","subject":"Re: [PATCH v3 1/5] commit: clarify FROM_REBASE_PICK and is_from_rebase() names","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-08-28T17:27:38Z","receivedAt":"2026-08-28T17:27:51Z","isPatch":true,"body":"On Fri, Aug 28, 2026 at 8:41 AM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> \"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n> > From: Elijah Newren <newren@gmail.com>\n> >\n> > Commit 430b75f7209c (commit: give correct advice for empty commit during\n> > a rebase, 2019-12-06) introduced a FROM_REBASE_PICK enum value and an\n> > is_from_rebase() function.  Those names failed to convey that they were\n> > specifically about hitting a commit that becomes empty when rebasing.\n> > Clarify their names now.\n>\n> Becomes empty is different from picking an empty commit, right.  I\n> am not sure if \"is_from_rebase_empty()\" conveys the difference and\n> more importantly, I am afraid it hints the latter.  I have a feeling\n> that EMPTY_REBASE (instead of REBASE_EMPTY) may match what we want\n> to express slightly better, but not by a large margin to make a\n> difference.  Perhaps Phillip has a better idea?\n\nor maybe FROM_REBASE_NOW_EMPTY and is_from_rebase_now_empty() ?\n\n>\n> > While at it, change `whence == FROM_REBASE_EMPTY` to use\n> > `is_from_rebase_empty(whence)`.\n>\n> Very much appreciated.\n"},{"id":"551706","messageId":"pull.2389.v4.git.git.1788301481.gitgitgadget@gmail.com","threadId":"66221","inReplyTo":"pull.2389.git.git.1787721681893.gitgitgadget@gmail.com","subject":"[PATCH v4 0/5] commit: refuse to amend during conflict resolution","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-01T22:24:36Z","receivedAt":"2026-09-01T22:24:43Z","isPatch":true,"body":"Changes since v3:\n\n * Just a clarifying rename, but applied in 3 places:\n   * FROM_REBASE_EMPTY -> FROM_REBASE_NOW_EMPTY\n   * is_from_rebase_empty() -> is_from_rebase_now_empty()\n   * ONGOING_REBASE_EMPTY -> ONGOING_REBASE_NOW_EMPTY\n\nChanges since v2:\n\n * Two new preparatory patches:\n   * Rename FROM_REBASE_PICK and is_from_rebase() to point out they are\n     about empty commits\n   * Allow a partial commit when a rebase pick becomes empty\n * Tweaked the error message for attempted amend on now-dropped empty commit\n   (suggestions for further improvements welcome)\n * Used the path accessor functions within sequencer.c to simplify the new\n   helper function\n\nBoth git commit --amend and a partial commit (git commit <paths>) are\nfoot-guns while the user is in the middle of an operation that resolves\nconflicts on top of HEAD: recording a conflict resolution is about capturing\nthe state of the whole tree as a new commit, not about rewriting HEAD or\ncommitting a subset of paths.\n\nHistorically we only rejected these during a merge or a cherry-pick or when\nresolving an empty pick during a rebase. The same hazard exists for am,\nrevert, and rebase conflict stops, none of which were covered. This series\nextends the refusal to all of them.\n\nThe three patches:\n\n 1. reword the two pre-existing \"empty commit\" rebase messages, which were\n    misleadingly generic\n 2. refuse git commit --amend during these additional operations\n 3. refuse partial commits during the same operations.\n\nElijah Newren (5):\n  commit: clarify FROM_REBASE_PICK and is_from_rebase() names\n  commit: allow a partial commit when a rebase pick becomes empty\n  commit: reword the empty-commit rebase amend error\n  commit: refuse to amend during conflict resolution\n  commit: refuse partial commits during conflict resolution\n\n builtin/commit.c                |  65 +++++++++++-----\n sequencer.c                     |  59 ++++++++++++++-\n sequencer.h                     |  24 ++++++\n t/t3404-rebase-interactive.sh   | 128 +++++++++++++++++++++++++++++++-\n t/t3507-cherry-pick-conflict.sh |  22 ++++++\n t/t4151-am-abort.sh             |  22 ++++++\n wt-status.h                     |   6 +-\n 7 files changed, 299 insertions(+), 27 deletions(-)\n\n\nbase-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2389%2Fnewren%2Frefuse-amend-during-conflicts-v4\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2389/newren/refuse-amend-during-conflicts-v4\nPull-Request: https://github.com/git/git/pull/2389\n\nRange-diff vs v3:\n\n 1:  7e198a20fa ! 1:  bd361679b9 commit: clarify FROM_REBASE_PICK and is_from_rebase() names\n     @@ Commit message\n          specifically about hitting a commit that becomes empty when rebasing.\n          Clarify their names now.\n      \n     -    While at it, change `whence == FROM_REBASE_EMPTY` to use\n     -    `is_from_rebase_empty(whence)`.\n     +    While at it, change `whence == FROM_REBASE_NOW_EMPTY` to use\n     +    `is_from_rebase_now_empty(whence)`.\n      \n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n     @@ builtin/commit.c: static const char *prepare_index(const char **argv, const char\n       \t\telse if (is_from_cherry_pick(whence))\n       \t\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n      -\t\telse if (is_from_rebase(whence))\n     -+\t\telse if (is_from_rebase_empty(whence))\n     ++\t\telse if (is_from_rebase_now_empty(whence))\n       \t\t\tdie(_(\"cannot do a partial commit during a rebase.\"));\n       \t}\n       \n     @@ builtin/commit.c: static int prepare_to_commit(const char *index_file, const cha\n       \telse if (whence == FROM_MERGE)\n       \t\thook_arg1 = \"merge\";\n      -\telse if (is_from_cherry_pick(whence) || whence == FROM_REBASE_PICK) {\n     -+\telse if (is_from_cherry_pick(whence) || is_from_rebase_empty(whence)) {\n     ++\telse if (is_from_cherry_pick(whence) || is_from_rebase_now_empty(whence)) {\n       \t\thook_arg1 = \"commit\";\n       \t\thook_arg2 = \"CHERRY_PICK_HEAD\";\n       \t}\n     @@ builtin/commit.c: static int prepare_to_commit(const char *index_file, const cha\n       \t\t\tfputs(_(empty_amend_advice), stderr);\n       \t\telse if (is_from_cherry_pick(whence) ||\n      -\t\t\t whence == FROM_REBASE_PICK) {\n     -+\t\t\t is_from_rebase_empty(whence)) {\n     ++\t\t\t is_from_rebase_now_empty(whence)) {\n       \t\t\tfputs(_(empty_cherry_pick_advice), stderr);\n       \t\t\tif (whence == FROM_CHERRY_PICK_SINGLE)\n       \t\t\t\tfputs(_(empty_cherry_pick_advice_single), stderr);\n     @@ builtin/commit.c: static int parse_and_validate_options(int argc, const char *ar\n       \t\telse if (is_from_cherry_pick(whence))\n       \t\t\tdie(_(\"You are in the middle of a cherry-pick -- cannot amend.\"));\n      -\t\telse if (whence == FROM_REBASE_PICK)\n     -+\t\telse if (is_from_rebase_empty(whence))\n     ++\t\telse if (is_from_rebase_now_empty(whence))\n       \t\t\tdie(_(\"You are in the middle of a rebase -- cannot amend.\"));\n       \t}\n       \tif (fixup_message && squash_message)\n     @@ builtin/commit.c: static int parse_and_validate_options(int argc, const char *ar\n       \t\tuse_message = \"HEAD\";\n       \tif (!use_message && !is_from_cherry_pick(whence) &&\n      -\t    !is_from_rebase(whence) && renew_authorship)\n     -+\t    !is_from_rebase_empty(whence) && renew_authorship)\n     ++\t    !is_from_rebase_now_empty(whence) && renew_authorship)\n       \t\tdie(_(\"--reset-author can be used only with -C, -c or --amend.\"));\n       \tif (use_message) {\n       \t\tuse_message_buffer = read_commit_message(use_message);\n     @@ builtin/commit.c: static int parse_and_validate_options(int argc, const char *ar\n       \t\t}\n       \t}\n      -\tif ((is_from_cherry_pick(whence) || whence == FROM_REBASE_PICK) &&\n     -+\tif ((is_from_cherry_pick(whence) || is_from_rebase_empty(whence)) &&\n     ++\tif ((is_from_cherry_pick(whence) || is_from_rebase_now_empty(whence)) &&\n       \t    !renew_authorship) {\n       \t\tauthor_message = \"CHERRY_PICK_HEAD\";\n       \t\tauthor_message_buffer = read_commit_message(author_message);\n     @@ builtin/commit.c: int cmd_commit(int argc,\n       \t\t\treflog_msg = is_from_cherry_pick(whence)\n       \t\t\t\t\t? \"commit (cherry-pick)\"\n      -\t\t\t\t\t: is_from_rebase(whence)\n     -+\t\t\t\t\t: is_from_rebase_empty(whence)\n     ++\t\t\t\t\t: is_from_rebase_now_empty(whence)\n       \t\t\t\t\t? \"commit (rebase)\"\n       \t\t\t\t\t: \"commit\";\n       \t\tcommit_list_insert(current_head, &parents);\n     @@ sequencer.c: int sequencer_determine_whence(struct repository *r, enum commit_wh\n       \t\t    !repo_get_oid(r, \"CHERRY_PICK_HEAD\", &cherry_pick_head) &&\n       \t\t    oideq(&rebase_head, &cherry_pick_head))\n      -\t\t\t*whence = FROM_REBASE_PICK;\n     -+\t\t\t*whence = FROM_REBASE_EMPTY;\n     ++\t\t\t*whence = FROM_REBASE_NOW_EMPTY;\n       \t\telse\n       \t\t\t*whence = FROM_CHERRY_PICK_SINGLE;\n       \n     @@ wt-status.h: enum commit_whence {\n       \tFROM_CHERRY_PICK_SINGLE, /* commit came from cherry-pick */\n       \tFROM_CHERRY_PICK_MULTI, /* commit came from a sequence of cherry-picks */\n      -\tFROM_REBASE_PICK /* commit came from a pick/reword/edit */\n     -+\tFROM_REBASE_EMPTY /* rebase applied a pick that became empty */\n     ++\tFROM_REBASE_NOW_EMPTY /* rebase applied a pick that became empty */\n       };\n       \n       static inline int is_from_cherry_pick(enum commit_whence whence)\n     @@ wt-status.h: static inline int is_from_cherry_pick(enum commit_whence whence)\n       }\n       \n      -static inline int is_from_rebase(enum commit_whence whence)\n     -+static inline int is_from_rebase_empty(enum commit_whence whence)\n     ++static inline int is_from_rebase_now_empty(enum commit_whence whence)\n       {\n      -\treturn whence == FROM_REBASE_PICK;\n     -+\treturn whence == FROM_REBASE_EMPTY;\n     ++\treturn whence == FROM_REBASE_NOW_EMPTY;\n       }\n       \n       struct wt_status_change_data {\n 2:  e169303619 ! 2:  a0b9900437 commit: allow a partial commit when a rebase pick becomes empty\n     @@ builtin/commit.c: static const char *prepare_index(const char **argv, const char\n       \t\t\tdie(_(\"cannot do a partial commit during a merge.\"));\n       \t\telse if (is_from_cherry_pick(whence))\n       \t\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n     --\t\telse if (is_from_rebase_empty(whence))\n     +-\t\telse if (is_from_rebase_now_empty(whence))\n      -\t\t\tdie(_(\"cannot do a partial commit during a rebase.\"));\n       \t}\n       \n 3:  0850a999da ! 3:  c4511a9887 commit: reword the empty-commit rebase amend error\n     @@ builtin/commit.c\n      @@ builtin/commit.c: static int parse_and_validate_options(int argc, const char *argv[],\n       \t\telse if (is_from_cherry_pick(whence))\n       \t\t\tdie(_(\"You are in the middle of a cherry-pick -- cannot amend.\"));\n     - \t\telse if (is_from_rebase_empty(whence))\n     + \t\telse if (is_from_rebase_now_empty(whence))\n      -\t\t\tdie(_(\"You are in the middle of a rebase -- cannot amend.\"));\n      +\t\t\tdie(_(\"The now-empty commit has been dropped -- cannot amend.\"));\n       \t}\n 4:  9f80d8a00d ! 4:  e77b34c1cf commit: refuse to amend during conflict resolution\n     @@ builtin/commit.c: static int parse_and_validate_options(int argc, const char *ar\n      -\t\telse if (is_from_cherry_pick(whence))\n      +\t\tcase ONGOING_CHERRY_PICK:\n       \t\t\tdie(_(\"You are in the middle of a cherry-pick -- cannot amend.\"));\n     --\t\telse if (is_from_rebase_empty(whence))\n     -+\t\tcase ONGOING_REBASE_EMPTY:\n     +-\t\telse if (is_from_rebase_now_empty(whence))\n     ++\t\tcase ONGOING_REBASE_NOW_EMPTY:\n       \t\t\tdie(_(\"The now-empty commit has been dropped -- cannot amend.\"));\n      +\t\tcase ONGOING_REVERT:\n      +\t\t\tdie(_(\"You are in the middle of a revert -- cannot amend.\"));\n     @@ sequencer.c: int sequencer_determine_whence(struct repository *r, enum commit_wh\n      +\tcase FROM_CHERRY_PICK_SINGLE:\n      +\tcase FROM_CHERRY_PICK_MULTI:\n      +\t\treturn ONGOING_CHERRY_PICK;\n     -+\tcase FROM_REBASE_EMPTY:\n     -+\t\treturn ONGOING_REBASE_EMPTY;\n     ++\tcase FROM_REBASE_NOW_EMPTY:\n     ++\t\treturn ONGOING_REBASE_NOW_EMPTY;\n      +\tcase FROM_COMMIT:\n      +\t\tbreak;\n      +\t}\n     @@ sequencer.h: int sequencer_get_last_command(struct repository* r,\n      +\tONGOING_NONE = 0,\n      +\tONGOING_MERGE,\n      +\tONGOING_CHERRY_PICK,\n     -+\tONGOING_REBASE_EMPTY,\n     ++\tONGOING_REBASE_NOW_EMPTY,\n      +\tONGOING_REVERT,\n      +\tONGOING_AM,\n      +\tONGOING_REBASE_CONFLICT\n 5:  050b9e8a52 ! 5:  b93b26ed9f commit: refuse partial commits during conflict resolution\n     @@ builtin/commit.c: static const char *prepare_index(const char **argv, const char\n      +\t\tdie(_(\"cannot do a partial commit during a merge.\"));\n      +\tcase ONGOING_CHERRY_PICK:\n      +\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n     -+\tcase ONGOING_REBASE_EMPTY:\n     ++\tcase ONGOING_REBASE_NOW_EMPTY:\n      +\t\t/*\n      +\t\t * A pick that became empty is not a conflict, and creating\n      +\t\t * a new commit (partial or not) poses no problem.\n\n-- \ngitgitgadget\n"},{"id":"551707","messageId":"bd361679b9144682d664e8cfcf9fc2cbd8511b4d.1788301481.git.gitgitgadget@gmail.com","threadId":"66221","inReplyTo":"pull.2389.v4.git.git.1788301481.gitgitgadget@gmail.com","subject":"[PATCH v4 1/5] commit: clarify FROM_REBASE_PICK and is_from_rebase() names","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-01T22:24:37Z","receivedAt":"2026-09-01T22:24:45Z","isPatch":true,"body":"From: Elijah Newren <newren@gmail.com>\n\nCommit 430b75f7209c (commit: give correct advice for empty commit during\na rebase, 2019-12-06) introduced a FROM_REBASE_PICK enum value and an\nis_from_rebase() function.  Those names failed to convey that they were\nspecifically about hitting a commit that becomes empty when rebasing.\nClarify their names now.\n\nWhile at it, change `whence == FROM_REBASE_NOW_EMPTY` to use\n`is_from_rebase_now_empty(whence)`.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/commit.c | 14 +++++++-------\n sequencer.c      |  2 +-\n wt-status.h      |  6 +++---\n 3 files changed, 11 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 28f6174503..17cc27e53e 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -520,7 +520,7 @@ static const char *prepare_index(const char **argv, const char *prefix,\n \t\t\tdie(_(\"cannot do a partial commit during a merge.\"));\n \t\telse if (is_from_cherry_pick(whence))\n \t\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n-\t\telse if (is_from_rebase(whence))\n+\t\telse if (is_from_rebase_now_empty(whence))\n \t\t\tdie(_(\"cannot do a partial commit during a rebase.\"));\n \t}\n \n@@ -893,7 +893,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t */\n \telse if (whence == FROM_MERGE)\n \t\thook_arg1 = \"merge\";\n-\telse if (is_from_cherry_pick(whence) || whence == FROM_REBASE_PICK) {\n+\telse if (is_from_cherry_pick(whence) || is_from_rebase_now_empty(whence)) {\n \t\thook_arg1 = \"commit\";\n \t\thook_arg2 = \"CHERRY_PICK_HEAD\";\n \t}\n@@ -1086,7 +1086,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\tif (amend)\n \t\t\tfputs(_(empty_amend_advice), stderr);\n \t\telse if (is_from_cherry_pick(whence) ||\n-\t\t\t whence == FROM_REBASE_PICK) {\n+\t\t\t is_from_rebase_now_empty(whence)) {\n \t\t\tfputs(_(empty_cherry_pick_advice), stderr);\n \t\t\tif (whence == FROM_CHERRY_PICK_SINGLE)\n \t\t\t\tfputs(_(empty_cherry_pick_advice_single), stderr);\n@@ -1333,7 +1333,7 @@ static int parse_and_validate_options(int argc, const char *argv[],\n \t\t\tdie(_(\"You are in the middle of a merge -- cannot amend.\"));\n \t\telse if (is_from_cherry_pick(whence))\n \t\t\tdie(_(\"You are in the middle of a cherry-pick -- cannot amend.\"));\n-\t\telse if (whence == FROM_REBASE_PICK)\n+\t\telse if (is_from_rebase_now_empty(whence))\n \t\t\tdie(_(\"You are in the middle of a rebase -- cannot amend.\"));\n \t}\n \tif (fixup_message && squash_message)\n@@ -1353,7 +1353,7 @@ static int parse_and_validate_options(int argc, const char *argv[],\n \tif (amend && !use_message && !fixup_message)\n \t\tuse_message = \"HEAD\";\n \tif (!use_message && !is_from_cherry_pick(whence) &&\n-\t    !is_from_rebase(whence) && renew_authorship)\n+\t    !is_from_rebase_now_empty(whence) && renew_authorship)\n \t\tdie(_(\"--reset-author can be used only with -C, -c or --amend.\"));\n \tif (use_message) {\n \t\tuse_message_buffer = read_commit_message(use_message);\n@@ -1362,7 +1362,7 @@ static int parse_and_validate_options(int argc, const char *argv[],\n \t\t\tauthor_message_buffer = use_message_buffer;\n \t\t}\n \t}\n-\tif ((is_from_cherry_pick(whence) || whence == FROM_REBASE_PICK) &&\n+\tif ((is_from_cherry_pick(whence) || is_from_rebase_now_empty(whence)) &&\n \t    !renew_authorship) {\n \t\tauthor_message = \"CHERRY_PICK_HEAD\";\n \t\tauthor_message_buffer = read_commit_message(author_message);\n@@ -1887,7 +1887,7 @@ int cmd_commit(int argc,\n \t\tif (!reflog_msg)\n \t\t\treflog_msg = is_from_cherry_pick(whence)\n \t\t\t\t\t? \"commit (cherry-pick)\"\n-\t\t\t\t\t: is_from_rebase(whence)\n+\t\t\t\t\t: is_from_rebase_now_empty(whence)\n \t\t\t\t\t? \"commit (rebase)\"\n \t\t\t\t\t: \"commit\";\n \t\tcommit_list_insert(current_head, &parents);\ndiff --git a/sequencer.c b/sequencer.c\nindex 65afd100d9..d336c309ca 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -6956,7 +6956,7 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)\n \t\t    !repo_get_oid(r, \"REBASE_HEAD\", &rebase_head) &&\n \t\t    !repo_get_oid(r, \"CHERRY_PICK_HEAD\", &cherry_pick_head) &&\n \t\t    oideq(&rebase_head, &cherry_pick_head))\n-\t\t\t*whence = FROM_REBASE_PICK;\n+\t\t\t*whence = FROM_REBASE_NOW_EMPTY;\n \t\telse\n \t\t\t*whence = FROM_CHERRY_PICK_SINGLE;\n \ndiff --git a/wt-status.h b/wt-status.h\nindex e9fe32e98c..2143f50b49 100644\n--- a/wt-status.h\n+++ b/wt-status.h\n@@ -41,7 +41,7 @@ enum commit_whence {\n \tFROM_MERGE,      /* commit came from merge */\n \tFROM_CHERRY_PICK_SINGLE, /* commit came from cherry-pick */\n \tFROM_CHERRY_PICK_MULTI, /* commit came from a sequence of cherry-picks */\n-\tFROM_REBASE_PICK /* commit came from a pick/reword/edit */\n+\tFROM_REBASE_NOW_EMPTY /* rebase applied a pick that became empty */\n };\n \n static inline int is_from_cherry_pick(enum commit_whence whence)\n@@ -50,9 +50,9 @@ static inline int is_from_cherry_pick(enum commit_whence whence)\n \t\twhence == FROM_CHERRY_PICK_MULTI;\n }\n \n-static inline int is_from_rebase(enum commit_whence whence)\n+static inline int is_from_rebase_now_empty(enum commit_whence whence)\n {\n-\treturn whence == FROM_REBASE_PICK;\n+\treturn whence == FROM_REBASE_NOW_EMPTY;\n }\n \n struct wt_status_change_data {\n-- \ngitgitgadget\n\n"},{"id":"551708","messageId":"a0b9900437e7c2833960e5046b5acb6703f014b9.1788301481.git.gitgitgadget@gmail.com","threadId":"66221","inReplyTo":"pull.2389.v4.git.git.1788301481.gitgitgadget@gmail.com","subject":"[PATCH v4 2/5] commit: allow a partial commit when a rebase pick becomes empty","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-01T22:24:38Z","receivedAt":"2026-09-01T22:24:46Z","isPatch":true,"body":"From: Elijah Newren <newren@gmail.com>\n\nFor years, we disallowed partial commits during merges or cherry-picks.\nIn commit 430b75f7209c (commit: give correct advice for empty commit\nduring a rebase, 2019-12-06) it was noted that the \"cannot do a partial\ncommit during a cherry-pick\" message was also printed when rebasing a\ncommit that became empty, and rather than drop the check in that case,\nthat commit opted to make the message print the actual operation that\nwas in progress.\n\nSince a commit that has become empty comes without conflicts, a new\npartial commit poses no problems; remove the error in that case.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/commit.c              | 2 --\n t/t3404-rebase-interactive.sh | 5 ++---\n 2 files changed, 2 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 17cc27e53e..01b79185e7 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -520,8 +520,6 @@ static const char *prepare_index(const char **argv, const char *prefix,\n \t\t\tdie(_(\"cannot do a partial commit during a merge.\"));\n \t\telse if (is_from_cherry_pick(whence))\n \t\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n-\t\telse if (is_from_rebase_now_empty(whence))\n-\t\t\tdie(_(\"cannot do a partial commit during a rebase.\"));\n \t}\n \n \tif (list_paths(&partial, !current_head ? NULL : \"HEAD\", &pathspec))\ndiff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh\nindex ff11abb2f2..3588e16543 100755\n--- a/t/t3404-rebase-interactive.sh\n+++ b/t/t3404-rebase-interactive.sh\n@@ -1858,7 +1858,7 @@ test_expect_success 'post-commit hook is called' '\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'correct error message for partial commit after empty pick' '\n+test_expect_success 'partial commit is allowed when a rebase pick becomes empty' '\n \ttest_when_finished \"git rebase --abort\" &&\n \t(\n \t\tset_fake_editor &&\n@@ -1867,8 +1867,7 @@ test_expect_success 'correct error message for partial commit after empty pick'\n \t\ttest_must_fail git rebase -i A D\n \t) &&\n \techo x >file1 &&\n-\ttest_must_fail git commit file1 2>err &&\n-\ttest_grep \"cannot do a partial commit during a rebase.\" err\n+\tgit commit file1\n '\n \n test_expect_success 'correct error message for commit --amend after empty pick' '\n-- \ngitgitgadget\n\n"},{"id":"551709","messageId":"c4511a98875a402a59e3109fcfe3839c2564753c.1788301481.git.gitgitgadget@gmail.com","threadId":"66221","inReplyTo":"pull.2389.v4.git.git.1788301481.gitgitgadget@gmail.com","subject":"[PATCH v4 3/5] commit: reword the empty-commit rebase amend error","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-01T22:24:39Z","receivedAt":"2026-09-01T22:24:47Z","isPatch":true,"body":"From: Elijah Newren <newren@gmail.com>\n\nWhen a rebase applies a commit that becomes empty, it stops and asks the\nuser to decide whether to keep it or drop it.  HEAD still points at the\npreviously-applied commit at that point, so amending is refused, with:\n\n    You are in the middle of a rebase -- cannot amend.\n\nThat message would suggest that amending is not allowed during an 'edit'\nor 'break' stop, which is misleading, plus it lacks the specificity that\nmight help the user know why their particular case is a problem: the\ncommit they intended to amend became empty and was dropped, so amending\nwould affect the wrong commit.  Reword the error accordingly.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/commit.c              | 2 +-\n t/t3404-rebase-interactive.sh | 2 +-\n 2 files changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 01b79185e7..9b6eaa3c72 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -1332,7 +1332,7 @@ static int parse_and_validate_options(int argc, const char *argv[],\n \t\telse if (is_from_cherry_pick(whence))\n \t\t\tdie(_(\"You are in the middle of a cherry-pick -- cannot amend.\"));\n \t\telse if (is_from_rebase_now_empty(whence))\n-\t\t\tdie(_(\"You are in the middle of a rebase -- cannot amend.\"));\n+\t\t\tdie(_(\"The now-empty commit has been dropped -- cannot amend.\"));\n \t}\n \tif (fixup_message && squash_message)\n \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--squash\", \"--fixup\");\ndiff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh\nindex 3588e16543..81f4844950 100755\n--- a/t/t3404-rebase-interactive.sh\n+++ b/t/t3404-rebase-interactive.sh\n@@ -1880,7 +1880,7 @@ test_expect_success 'correct error message for commit --amend after empty pick'\n \t) &&\n \techo x>file1 &&\n \ttest_must_fail git commit -a --amend 2>err &&\n-\ttest_grep \"middle of a rebase -- cannot amend.\" err\n+\ttest_grep \"now-empty commit has been dropped -- cannot amend.\" err\n '\n \n test_expect_success 'todo has correct onto hash' '\n-- \ngitgitgadget\n\n"},{"id":"551710","messageId":"e77b34c1cf414839d922d0ce252d4700a313c7bb.1788301481.git.gitgitgadget@gmail.com","threadId":"66221","inReplyTo":"pull.2389.v4.git.git.1788301481.gitgitgadget@gmail.com","subject":"[PATCH v4 4/5] commit: refuse to amend during conflict resolution","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-01T22:24:40Z","receivedAt":"2026-09-01T22:24:49Z","isPatch":true,"body":"From: Elijah Newren <newren@gmail.com>\n\nRunning `git commit --amend` during conflict resolution is an ugly\nfoot-gun.  For many years, we have rejected amending during conflict\nresolution in the middle of\n  - a merge\n  - a cherry-pick\nHowever, this was never extended to other operations that can also\nproduce conflicts:\n  - an `am` operation\n  - a revert\n  - a rebase\n\nExtend it to handle these other cases now.\n\nExtending to `am`, revert, and the apply backend of rebase are fairly\nstraightforward.  However, with the merge backend of rebase we have to\nbe more careful, since it powers interactive rebases and\n  - the interactive machinery internally uses `git commit --amend` for\n    `squash` and `reword` directives\n  - users are expected to `git commit --amend` after hitting an `edit`\n    or `break` directive\nSo, we need to be careful with rebase to only reject amending when doing\nconflict resolution.\n\nA few files under the rebase-merge/ directory provide us the necessary\ninformation:\n\n  - stopped-sha is written only when the rebase stops and hands control\n    back to the user, so its presence marks a genuine stop -- as opposed\n    to the sequencer's own internal `git commit --amend` while applying\n    a squash, fixup, or reword, during which no stopped-sha exists.\n\n  - amend is written only when the rebase stops with HEAD already\n    pointing at the commit the user is meant to amend: a clean `edit`,\n    or a fast-forward `reword`.  Its absence at a stop therefore means\n    the commit did not apply, so HEAD is the previously-applied commit\n    rather than the one being rebased -- exactly the case we refuse.\n\nSo for the merge backend we die when stopped-sha exists and amend does\nnot.  This covers a plain conflicted pick as well as a conflicted `edit`\n(both leave HEAD on the previously-applied commit), while still allowing\na clean `edit` or `reword` stop and a `break` stop (no stopped-sha).\nstopped-sha is unlinked at the start of the resume loop, so a resumed\nsquash's internal amend is unaffected.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/commit.c                | 27 +++++++---\n sequencer.c                     | 57 +++++++++++++++++++++\n sequencer.h                     | 23 +++++++++\n t/t3404-rebase-interactive.sh   | 87 +++++++++++++++++++++++++++++++++\n t/t3507-cherry-pick-conflict.sh | 11 +++++\n t/t4151-am-abort.sh             | 11 +++++\n 6 files changed, 210 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 9b6eaa3c72..284fc7fdc6 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -1324,15 +1324,30 @@ static int parse_and_validate_options(int argc, const char *argv[],\n \t\tuse_editor = 0;\n \n \t/* Sanity check options */\n-\tif (amend && !current_head)\n-\t\tdie(_(\"You have nothing to amend.\"));\n-\tif (amend && whence != FROM_COMMIT) {\n-\t\tif (whence == FROM_MERGE)\n+\tif (amend) {\n+\t\tif (!current_head)\n+\t\t\tdie(_(\"You have nothing to amend.\"));\n+\t\t/*\n+\t\t * Refuse to amend in the middle of any operation that is\n+\t\t * meant to record its result as a new commit on top of HEAD\n+\t\t * rather than by rewriting HEAD.\n+\t\t */\n+\t\tswitch (sequencer_ongoing_operation(s->repo, whence)) {\n+\t\tcase ONGOING_NONE:\n+\t\t\tbreak;\n+\t\tcase ONGOING_MERGE:\n \t\t\tdie(_(\"You are in the middle of a merge -- cannot amend.\"));\n-\t\telse if (is_from_cherry_pick(whence))\n+\t\tcase ONGOING_CHERRY_PICK:\n \t\t\tdie(_(\"You are in the middle of a cherry-pick -- cannot amend.\"));\n-\t\telse if (is_from_rebase_now_empty(whence))\n+\t\tcase ONGOING_REBASE_NOW_EMPTY:\n \t\t\tdie(_(\"The now-empty commit has been dropped -- cannot amend.\"));\n+\t\tcase ONGOING_REVERT:\n+\t\t\tdie(_(\"You are in the middle of a revert -- cannot amend.\"));\n+\t\tcase ONGOING_AM:\n+\t\t\tdie(_(\"You are in the middle of an am session -- cannot amend.\"));\n+\t\tcase ONGOING_REBASE_CONFLICT:\n+\t\t\tdie(_(\"You are resolving conflicts during a rebase -- cannot amend.\"));\n+\t\t}\n \t}\n \tif (fixup_message && squash_message)\n \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--squash\", \"--fixup\");\ndiff --git a/sequencer.c b/sequencer.c\nindex d336c309ca..6a28e0dd9a 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -142,6 +142,13 @@ static GIT_PATH_FUNC(rebase_path_author_script, \"rebase-merge/author-script\")\n  * command is processed, this file is deleted.\n  */\n static GIT_PATH_FUNC(rebase_path_amend, \"rebase-merge/amend\")\n+/*\n+ * The apply (\"am\") backend keeps its state in the rebase-apply directory;\n+ * the \"applying\" file within it marks a plain `git am` (as opposed to an\n+ * apply-based rebase).\n+ */\n+static GIT_PATH_FUNC(apply_dir, \"rebase-apply\")\n+static GIT_PATH_FUNC(apply_path_applying, \"rebase-apply/applying\")\n /*\n  * When we stop at a given patch via the \"edit\" command, this file contains\n  * the commit object name of the corresponding patch.\n@@ -6966,6 +6973,56 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)\n \treturn 0;\n }\n \n+enum ongoing_operation sequencer_ongoing_operation(struct repository *r,\n+\t\t\t\t\t\t   enum commit_whence whence)\n+{\n+\t/*\n+\t * The merge, cherry-pick, and (empty) rebase-pick stops are already\n+\t * distinguished by 'whence'.\n+\t */\n+\tswitch (whence) {\n+\tcase FROM_MERGE:\n+\t\treturn ONGOING_MERGE;\n+\tcase FROM_CHERRY_PICK_SINGLE:\n+\tcase FROM_CHERRY_PICK_MULTI:\n+\t\treturn ONGOING_CHERRY_PICK;\n+\tcase FROM_REBASE_NOW_EMPTY:\n+\t\treturn ONGOING_REBASE_NOW_EMPTY;\n+\tcase FROM_COMMIT:\n+\t\tbreak;\n+\t}\n+\n+\t/*\n+\t * 'whence' is FROM_COMMIT, but we may still be in the middle of an\n+\t * operation that records its result on top of HEAD; detect those\n+\t * from their on-disk state.\n+\t */\n+\n+\t/* In the middle of a revert? */\n+\tif (refs_ref_exists(get_main_ref_store(r), \"REVERT_HEAD\"))\n+\t\treturn ONGOING_REVERT;\n+\n+\t/* In the middle of an `am`? */\n+\tif (file_exists(apply_path_applying()))\n+\t\treturn ONGOING_AM;\n+\n+\t/*\n+\t * In the middle of a rebase that stopped for conflict resolution?\n+\t * The apply backend only ever stops for conflicts, so the presence\n+\t * of its state directory is enough.  The merge backend writes\n+\t * stopped-sha whenever it hands control back to the user, but omits\n+\t * `amend` unless it stopped with HEAD already pointing at the commit\n+\t * to be amended (a clean edit/reword stop); its absence therefore\n+\t * marks a conflicted stop.\n+\t */\n+\tif (file_exists(apply_dir()) ||\n+\t    (file_exists(rebase_path_stopped_sha()) &&\n+\t     !file_exists(rebase_path_amend())))\n+\t\treturn ONGOING_REBASE_CONFLICT;\n+\n+\treturn ONGOING_NONE;\n+}\n+\n int sequencer_get_update_refs_state(const char *wt_dir,\n \t\t\t\t    struct string_list *refs)\n {\ndiff --git a/sequencer.h b/sequencer.h\nindex 64a9c7fb1b..fb4a744208 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -277,6 +277,29 @@ int sequencer_get_last_command(struct repository* r,\n \t\t\t       enum replay_action *action);\n int sequencer_determine_whence(struct repository *r, enum commit_whence *whence);\n \n+/*\n+ * An in-progress operation that records its result (often a conflict\n+ * resolution) as a new commit on top of HEAD, during which amending\n+ * HEAD via \"git commit --amend\" is almost always a mistake.\n+ */\n+enum ongoing_operation {\n+\tONGOING_NONE = 0,\n+\tONGOING_MERGE,\n+\tONGOING_CHERRY_PICK,\n+\tONGOING_REBASE_NOW_EMPTY,\n+\tONGOING_REVERT,\n+\tONGOING_AM,\n+\tONGOING_REBASE_CONFLICT\n+};\n+\n+/*\n+ * Return which in-progress operation, if any, is underway; see enum\n+ * ongoing_operation.  'whence' is the origin already computed for the\n+ * pending commit.\n+ */\n+enum ongoing_operation sequencer_ongoing_operation(struct repository *r,\n+\t\t\t\t\t\t   enum commit_whence whence);\n+\n /**\n  * Append the set of ref-OID pairs that are currently stored for the 'git\n  * rebase --update-refs' feature if such a rebase is currently happening.\ndiff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh\nindex 81f4844950..4e6c3e2f19 100755\n--- a/t/t3404-rebase-interactive.sh\n+++ b/t/t3404-rebase-interactive.sh\n@@ -1883,6 +1883,93 @@ test_expect_success 'correct error message for commit --amend after empty pick'\n \ttest_grep \"now-empty commit has been dropped -- cannot amend.\" err\n '\n \n+test_expect_success 'commit --amend is refused at a rebase conflict stop' '\n+\ttest_when_finished \"git rebase --abort\" &&\n+\tgit checkout --detach conflict-branch &&\n+\t(\n+\t\tset_fake_editor &&\n+\t\tFAKE_LINES=\"1 3\" &&\n+\t\texport FAKE_LINES &&\n+\t\ttest_must_fail git rebase -i A\n+\t) &&\n+\ttest_path_is_file .git/rebase-merge/patch &&\n+\ttest_path_is_missing .git/rebase-merge/amend &&\n+\techo resolved >conflict &&\n+\tgit add conflict &&\n+\ttest_must_fail git commit --amend --no-edit 2>err &&\n+\ttest_grep \"You are resolving conflicts during a rebase -- cannot amend\" err\n+'\n+\n+test_expect_success 'commit --amend is refused when an \"edit\" pick conflicts' '\n+\ttest_when_finished \"git rebase --abort\" &&\n+\tgit checkout --detach conflict-branch &&\n+\t(\n+\t\tset_fake_editor &&\n+\t\tFAKE_LINES=\"1 edit 3\" &&\n+\t\texport FAKE_LINES &&\n+\t\ttest_must_fail git rebase -i A\n+\t) &&\n+\ttest_path_is_file .git/rebase-merge/patch &&\n+\ttest_path_is_missing .git/rebase-merge/amend &&\n+\techo resolved >conflict &&\n+\tgit add conflict &&\n+\ttest_must_fail git commit --amend --no-edit 2>err &&\n+\ttest_grep \"You are resolving conflicts during a rebase -- cannot amend\" err\n+'\n+\n+test_expect_success 'commit --amend is allowed at a rebase edit stop' '\n+\ttest_when_finished \"git rebase --abort\" &&\n+\tgit checkout --detach no-conflict-branch &&\n+\t(\n+\t\tset_fake_editor &&\n+\t\tFAKE_LINES=\"edit 1 2 3 4\" &&\n+\t\texport FAKE_LINES &&\n+\t\tgit rebase -i A\n+\t) &&\n+\ttest_path_is_file .git/rebase-merge/amend &&\n+\techo tweak >fileJ &&\n+\tgit add fileJ &&\n+\tgit commit --amend --no-edit\n+'\n+\n+test_expect_success 'commit --amend is allowed at a rebase break stop' '\n+\ttest_when_finished \"git rebase --abort\" &&\n+\tgit checkout --detach no-conflict-branch &&\n+\t(\n+\t\tset_fake_editor &&\n+\t\tFAKE_LINES=\"break 1 2 3 4\" &&\n+\t\texport FAKE_LINES &&\n+\t\tgit rebase -i A\n+\t) &&\n+\ttest_must_fail git rev-parse --verify REBASE_HEAD &&\n+\techo tweak >fileJ &&\n+\tgit add fileJ &&\n+\tgit commit --amend --no-edit\n+'\n+\n+test_expect_success 'commit --amend is refused at an apply-backend conflict stop' '\n+\ttest_when_finished \"rm -rf apply-backend\" &&\n+\ttest_create_repo apply-backend &&\n+\t(\n+\t\tcd apply-backend &&\n+\t\ttest_commit base file &&\n+\t\tgit branch -M mainline &&\n+\t\ttest_commit upstream file upstream &&\n+\t\tgit checkout -b side mainline~1 &&\n+\t\ttest_commit conflicting file side &&\n+\t\ttest_commit unrelated other &&\n+\t\ttest_must_fail git rebase --apply mainline &&\n+\t\t# the apply backend only ever stops for conflicts, and\n+\t\t# leaves HEAD on the previously-applied commit\n+\t\ttest_path_is_dir .git/rebase-apply &&\n+\t\ttest_path_is_missing .git/rebase-apply/applying &&\n+\t\techo resolved >file &&\n+\t\tgit add file &&\n+\t\ttest_must_fail git commit --amend --no-edit 2>err &&\n+\t\ttest_grep \"You are resolving conflicts during a rebase -- cannot amend\" err\n+\t)\n+'\n+\n test_expect_success 'todo has correct onto hash' '\n \tGIT_SEQUENCE_EDITOR=cat git rebase -i no-conflict-branch~4 no-conflict-branch >actual &&\n \tonto=$(git rev-parse --short HEAD~4) &&\ndiff --git a/t/t3507-cherry-pick-conflict.sh b/t/t3507-cherry-pick-conflict.sh\nindex 44596cb1e8..42de398f76 100755\n--- a/t/t3507-cherry-pick-conflict.sh\n+++ b/t/t3507-cherry-pick-conflict.sh\n@@ -364,6 +364,17 @@ test_expect_success 'failed revert sets REVERT_HEAD' '\n \ttest_cmp_rev picked REVERT_HEAD\n '\n \n+test_expect_success 'commit --amend of revert fails' '\n+\tpristine_detach initial &&\n+\n+\ttest_must_fail git revert picked &&\n+\techo resolved >foo &&\n+\tgit add foo &&\n+\ttest_must_fail git commit --amend 2>err &&\n+\n+\ttest_grep \"in the middle of a revert -- cannot amend.\" err\n+'\n+\n test_expect_success 'successful revert does not set REVERT_HEAD' '\n \tpristine_detach base &&\n \tgit revert base &&\ndiff --git a/t/t4151-am-abort.sh b/t/t4151-am-abort.sh\nindex 8e1ecf8a68..9313a074b2 100755\n--- a/t/t4151-am-abort.sh\n+++ b/t/t4151-am-abort.sh\n@@ -63,6 +63,17 @@ do\n \n done\n \n+test_expect_success 'commit --amend during a failed am fails' '\n+\tgit reset --hard initial &&\n+\tcp file-2-expect file-2 &&\n+\ttest_must_fail git am 000[1245]-*.patch &&\n+\techo resolved >file-1 &&\n+\tgit add file-1 &&\n+\ttest_must_fail git commit --amend 2>err &&\n+\ttest_grep \"in the middle of an am session -- cannot amend.\" err &&\n+\tgit am --abort\n+'\n+\n test_expect_success 'am -3 --skip removes otherfile-4' '\n \tgit reset --hard initial &&\n \ttest_must_fail git am -3 0003-*.patch &&\n-- \ngitgitgadget\n\n"},{"id":"551711","messageId":"b93b26ed9ff6fd37a7533885efb90df9982610c6.1788301481.git.gitgitgadget@gmail.com","threadId":"66221","inReplyTo":"pull.2389.v4.git.git.1788301481.gitgitgadget@gmail.com","subject":"[PATCH v4 5/5] commit: refuse partial commits during conflict resolution","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-01T22:24:41Z","receivedAt":"2026-09-01T22:24:50Z","isPatch":true,"body":"From: Elijah Newren <newren@gmail.com>\n\nSimilar to the previous commit, just as `git commit --amend` is a\nfoot-gun during conflict resolution, so is a partial commit (`git commit\n<paths>`).  Recording a conflict resolution is about capturing the state\nof the entire tree on top of HEAD, not a subset of paths.  For many years\nwe have rejected partial commits in the middle of\n  - a merge\n  - a cherry-pick\n\nbut, just like amending, this was never extended to the other operations\nthat can also leave conflicts to resolve:\n  - an `am` operation\n  - a revert\n  - a rebase that stopped for conflict resolution\n\nReuse sequencer_ongoing_operation(), introduced for the analogous\n`--amend` check, to detect these and refuse the partial commit.  A rebase\nthat stopped because a pick became empty is not conflict resolution and,\nas an earlier patch established, is deliberately left permitted.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/commit.c                | 24 ++++++++++++++++++-----\n sequencer.h                     |  5 +++--\n t/t3404-rebase-interactive.sh   | 34 +++++++++++++++++++++++++++++++++\n t/t3507-cherry-pick-conflict.sh | 11 +++++++++++\n t/t4151-am-abort.sh             | 11 +++++++++++\n 5 files changed, 78 insertions(+), 7 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 284fc7fdc6..4e0fd58f0a 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -515,11 +515,25 @@ static const char *prepare_index(const char **argv, const char *prefix,\n \t */\n \tcommit_style = COMMIT_PARTIAL;\n \n-\tif (whence != FROM_COMMIT) {\n-\t\tif (whence == FROM_MERGE)\n-\t\t\tdie(_(\"cannot do a partial commit during a merge.\"));\n-\t\telse if (is_from_cherry_pick(whence))\n-\t\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n+\tswitch (sequencer_ongoing_operation(the_repository, whence)) {\n+\tcase ONGOING_NONE:\n+\t\tbreak;\n+\tcase ONGOING_MERGE:\n+\t\tdie(_(\"cannot do a partial commit during a merge.\"));\n+\tcase ONGOING_CHERRY_PICK:\n+\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n+\tcase ONGOING_REBASE_NOW_EMPTY:\n+\t\t/*\n+\t\t * A pick that became empty is not a conflict, and creating\n+\t\t * a new commit (partial or not) poses no problem.\n+\t\t */\n+\t\tbreak;\n+\tcase ONGOING_REVERT:\n+\t\tdie(_(\"cannot do a partial commit during a revert.\"));\n+\tcase ONGOING_AM:\n+\t\tdie(_(\"cannot do a partial commit during an am session.\"));\n+\tcase ONGOING_REBASE_CONFLICT:\n+\t\tdie(_(\"cannot do a partial commit while resolving conflicts during a rebase.\"));\n \t}\n \n \tif (list_paths(&partial, !current_head ? NULL : \"HEAD\", &pathspec))\ndiff --git a/sequencer.h b/sequencer.h\nindex fb4a744208..61ebc2ca40 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -279,8 +279,9 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)\n \n /*\n  * An in-progress operation that records its result (often a conflict\n- * resolution) as a new commit on top of HEAD, during which amending\n- * HEAD via \"git commit --amend\" is almost always a mistake.\n+ * resolution) as a new commit on top of HEAD.  Some ways of invoking\n+ * \"git commit\" -- amending HEAD, or a partial commit -- are almost\n+ * always a mistake during such an operation.\n  */\n enum ongoing_operation {\n \tONGOING_NONE = 0,\ndiff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh\nindex 4e6c3e2f19..8c63682b7f 100755\n--- a/t/t3404-rebase-interactive.sh\n+++ b/t/t3404-rebase-interactive.sh\n@@ -1970,6 +1970,40 @@ test_expect_success 'commit --amend is refused at an apply-backend conflict stop\n \t)\n '\n \n+test_expect_success 'partial commit is refused at a rebase conflict stop' '\n+\ttest_when_finished \"git rebase --abort\" &&\n+\tgit checkout --detach conflict-branch &&\n+\t(\n+\t\tset_fake_editor &&\n+\t\tFAKE_LINES=\"1 3\" &&\n+\t\texport FAKE_LINES &&\n+\t\ttest_must_fail git rebase -i A\n+\t) &&\n+\techo resolved >conflict &&\n+\tgit add conflict &&\n+\ttest_must_fail git commit conflict 2>err &&\n+\ttest_grep \"cannot do a partial commit while resolving conflicts during a rebase.\" err\n+'\n+\n+test_expect_success 'partial commit is refused at an apply-backend conflict stop' '\n+\ttest_when_finished \"rm -rf apply-backend\" &&\n+\ttest_create_repo apply-backend &&\n+\t(\n+\t\tcd apply-backend &&\n+\t\ttest_commit base file &&\n+\t\tgit branch -M mainline &&\n+\t\ttest_commit upstream file upstream &&\n+\t\tgit checkout -b side mainline~1 &&\n+\t\ttest_commit conflicting file side &&\n+\t\ttest_commit unrelated other &&\n+\t\ttest_must_fail git rebase --apply mainline &&\n+\t\techo resolved >file &&\n+\t\tgit add file &&\n+\t\ttest_must_fail git commit file 2>err &&\n+\t\ttest_grep \"cannot do a partial commit while resolving conflicts during a rebase.\" err\n+\t)\n+'\n+\n test_expect_success 'todo has correct onto hash' '\n \tGIT_SEQUENCE_EDITOR=cat git rebase -i no-conflict-branch~4 no-conflict-branch >actual &&\n \tonto=$(git rev-parse --short HEAD~4) &&\ndiff --git a/t/t3507-cherry-pick-conflict.sh b/t/t3507-cherry-pick-conflict.sh\nindex 42de398f76..c3d024c97f 100755\n--- a/t/t3507-cherry-pick-conflict.sh\n+++ b/t/t3507-cherry-pick-conflict.sh\n@@ -375,6 +375,17 @@ test_expect_success 'commit --amend of revert fails' '\n \ttest_grep \"in the middle of a revert -- cannot amend.\" err\n '\n \n+test_expect_success 'partial commit during a revert fails' '\n+\tpristine_detach initial &&\n+\n+\ttest_must_fail git revert picked &&\n+\techo resolved >foo &&\n+\tgit add foo &&\n+\ttest_must_fail git commit foo 2>err &&\n+\n+\ttest_grep \"cannot do a partial commit during a revert.\" err\n+'\n+\n test_expect_success 'successful revert does not set REVERT_HEAD' '\n \tpristine_detach base &&\n \tgit revert base &&\ndiff --git a/t/t4151-am-abort.sh b/t/t4151-am-abort.sh\nindex 9313a074b2..c80269e015 100755\n--- a/t/t4151-am-abort.sh\n+++ b/t/t4151-am-abort.sh\n@@ -74,6 +74,17 @@ test_expect_success 'commit --amend during a failed am fails' '\n \tgit am --abort\n '\n \n+test_expect_success 'partial commit during a failed am fails' '\n+\tgit reset --hard initial &&\n+\tcp file-2-expect file-2 &&\n+\ttest_must_fail git am 000[1245]-*.patch &&\n+\techo resolved >file-1 &&\n+\tgit add file-1 &&\n+\ttest_must_fail git commit file-1 2>err &&\n+\ttest_grep \"cannot do a partial commit during an am session.\" err &&\n+\tgit am --abort\n+'\n+\n test_expect_success 'am -3 --skip removes otherfile-4' '\n \tgit reset --hard initial &&\n \ttest_must_fail git am -3 0003-*.patch &&\n-- \ngitgitgadget\n"},{"id":"551774","messageId":"1c3f07e0-63c0-483d-8e46-e4edbdd6991a@gmail.com","threadId":"66221","inReplyTo":"bd361679b9144682d664e8cfcf9fc2cbd8511b4d.1788301481.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 1/5] commit: clarify FROM_REBASE_PICK and is_from_rebase() names","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-02T15:39:09Z","receivedAt":"2026-09-02T15:39:12Z","isPatch":true,"body":"Hi Elijah\n\nOn 01/09/2026 23:24, Elijah Newren via GitGitGadget wrote:\n> From: Elijah Newren <newren@gmail.com>\n> \n> Commit 430b75f7209c (commit: give correct advice for empty commit during\n> a rebase, 2019-12-06) introduced a FROM_REBASE_PICK enum value and an\n> is_from_rebase() function.  Those names failed to convey that they were\n> specifically about hitting a commit that becomes empty when rebasing.\n> Clarify their names now.\n> \n> While at it, change `whence == FROM_REBASE_NOW_EMPTY` to use\n> `is_from_rebase_now_empty(whence)`.\n\nThis looks good, the new names are much clearer\n\nThanks\n\nPhillip\n\n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>   builtin/commit.c | 14 +++++++-------\n>   sequencer.c      |  2 +-\n>   wt-status.h      |  6 +++---\n>   3 files changed, 11 insertions(+), 11 deletions(-)\n> \n> diff --git a/builtin/commit.c b/builtin/commit.c\n> index 28f6174503..17cc27e53e 100644\n> --- a/builtin/commit.c\n> +++ b/builtin/commit.c\n> @@ -520,7 +520,7 @@ static const char *prepare_index(const char **argv, const char *prefix,\n>   \t\t\tdie(_(\"cannot do a partial commit during a merge.\"));\n>   \t\telse if (is_from_cherry_pick(whence))\n>   \t\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n> -\t\telse if (is_from_rebase(whence))\n> +\t\telse if (is_from_rebase_now_empty(whence))\n>   \t\t\tdie(_(\"cannot do a partial commit during a rebase.\"));\n>   \t}\n>   \n> @@ -893,7 +893,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n>   \t */\n>   \telse if (whence == FROM_MERGE)\n>   \t\thook_arg1 = \"merge\";\n> -\telse if (is_from_cherry_pick(whence) || whence == FROM_REBASE_PICK) {\n> +\telse if (is_from_cherry_pick(whence) || is_from_rebase_now_empty(whence)) {\n>   \t\thook_arg1 = \"commit\";\n>   \t\thook_arg2 = \"CHERRY_PICK_HEAD\";\n>   \t}\n> @@ -1086,7 +1086,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n>   \t\tif (amend)\n>   \t\t\tfputs(_(empty_amend_advice), stderr);\n>   \t\telse if (is_from_cherry_pick(whence) ||\n> -\t\t\t whence == FROM_REBASE_PICK) {\n> +\t\t\t is_from_rebase_now_empty(whence)) {\n>   \t\t\tfputs(_(empty_cherry_pick_advice), stderr);\n>   \t\t\tif (whence == FROM_CHERRY_PICK_SINGLE)\n>   \t\t\t\tfputs(_(empty_cherry_pick_advice_single), stderr);\n> @@ -1333,7 +1333,7 @@ static int parse_and_validate_options(int argc, const char *argv[],\n>   \t\t\tdie(_(\"You are in the middle of a merge -- cannot amend.\"));\n>   \t\telse if (is_from_cherry_pick(whence))\n>   \t\t\tdie(_(\"You are in the middle of a cherry-pick -- cannot amend.\"));\n> -\t\telse if (whence == FROM_REBASE_PICK)\n> +\t\telse if (is_from_rebase_now_empty(whence))\n>   \t\t\tdie(_(\"You are in the middle of a rebase -- cannot amend.\"));\n>   \t}\n>   \tif (fixup_message && squash_message)\n> @@ -1353,7 +1353,7 @@ static int parse_and_validate_options(int argc, const char *argv[],\n>   \tif (amend && !use_message && !fixup_message)\n>   \t\tuse_message = \"HEAD\";\n>   \tif (!use_message && !is_from_cherry_pick(whence) &&\n> -\t    !is_from_rebase(whence) && renew_authorship)\n> +\t    !is_from_rebase_now_empty(whence) && renew_authorship)\n>   \t\tdie(_(\"--reset-author can be used only with -C, -c or --amend.\"));\n>   \tif (use_message) {\n>   \t\tuse_message_buffer = read_commit_message(use_message);\n> @@ -1362,7 +1362,7 @@ static int parse_and_validate_options(int argc, const char *argv[],\n>   \t\t\tauthor_message_buffer = use_message_buffer;\n>   \t\t}\n>   \t}\n> -\tif ((is_from_cherry_pick(whence) || whence == FROM_REBASE_PICK) &&\n> +\tif ((is_from_cherry_pick(whence) || is_from_rebase_now_empty(whence)) &&\n>   \t    !renew_authorship) {\n>   \t\tauthor_message = \"CHERRY_PICK_HEAD\";\n>   \t\tauthor_message_buffer = read_commit_message(author_message);\n> @@ -1887,7 +1887,7 @@ int cmd_commit(int argc,\n>   \t\tif (!reflog_msg)\n>   \t\t\treflog_msg = is_from_cherry_pick(whence)\n>   \t\t\t\t\t? \"commit (cherry-pick)\"\n> -\t\t\t\t\t: is_from_rebase(whence)\n> +\t\t\t\t\t: is_from_rebase_now_empty(whence)\n>   \t\t\t\t\t? \"commit (rebase)\"\n>   \t\t\t\t\t: \"commit\";\n>   \t\tcommit_list_insert(current_head, &parents);\n> diff --git a/sequencer.c b/sequencer.c\n> index 65afd100d9..d336c309ca 100644\n> --- a/sequencer.c\n> +++ b/sequencer.c\n> @@ -6956,7 +6956,7 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)\n>   \t\t    !repo_get_oid(r, \"REBASE_HEAD\", &rebase_head) &&\n>   \t\t    !repo_get_oid(r, \"CHERRY_PICK_HEAD\", &cherry_pick_head) &&\n>   \t\t    oideq(&rebase_head, &cherry_pick_head))\n> -\t\t\t*whence = FROM_REBASE_PICK;\n> +\t\t\t*whence = FROM_REBASE_NOW_EMPTY;\n>   \t\telse\n>   \t\t\t*whence = FROM_CHERRY_PICK_SINGLE;\n>   \n> diff --git a/wt-status.h b/wt-status.h\n> index e9fe32e98c..2143f50b49 100644\n> --- a/wt-status.h\n> +++ b/wt-status.h\n> @@ -41,7 +41,7 @@ enum commit_whence {\n>   \tFROM_MERGE,      /* commit came from merge */\n>   \tFROM_CHERRY_PICK_SINGLE, /* commit came from cherry-pick */\n>   \tFROM_CHERRY_PICK_MULTI, /* commit came from a sequence of cherry-picks */\n> -\tFROM_REBASE_PICK /* commit came from a pick/reword/edit */\n> +\tFROM_REBASE_NOW_EMPTY /* rebase applied a pick that became empty */\n>   };\n>   \n>   static inline int is_from_cherry_pick(enum commit_whence whence)\n> @@ -50,9 +50,9 @@ static inline int is_from_cherry_pick(enum commit_whence whence)\n>   \t\twhence == FROM_CHERRY_PICK_MULTI;\n>   }\n>   \n> -static inline int is_from_rebase(enum commit_whence whence)\n> +static inline int is_from_rebase_now_empty(enum commit_whence whence)\n>   {\n> -\treturn whence == FROM_REBASE_PICK;\n> +\treturn whence == FROM_REBASE_NOW_EMPTY;\n>   }\n>   \n>   struct wt_status_change_data {\n\n"},{"id":"551775","messageId":"9950415d-ca3b-453b-9b0f-28c09a3d7f23@gmail.com","threadId":"66221","inReplyTo":"a0b9900437e7c2833960e5046b5acb6703f014b9.1788301481.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 2/5] commit: allow a partial commit when a rebase pick becomes empty","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-02T15:39:15Z","receivedAt":"2026-09-02T15:39:19Z","isPatch":true,"body":"Hi Elijah\n\nOn 01/09/2026 23:24, Elijah Newren via GitGitGadget wrote:\n> From: Elijah Newren <newren@gmail.com>\n> \n> For years, we disallowed partial commits during merges or cherry-picks.\n> In commit 430b75f7209c (commit: give correct advice for empty commit\n> during a rebase, 2019-12-06) it was noted that the \"cannot do a partial\n> commit during a cherry-pick\" message was also printed when rebasing a\n> commit that became empty, and rather than drop the check in that case,\n> that commit opted to make the message print the actual operation that\n> was in progress.\n> \n> Since a commit that has become empty comes without conflicts, a new\n> partial commit poses no problems; remove the error in that case.\n\nI'm not quite sure what I think about this. When we stop for a commit \nthat becomes empty, we write CHERRY_PICK_HEAD and .git/MERGE_MSG so the \nuser can preserve the commit by running \"git commit --allow-empty\". That \nmakes me think we should complain about a partial commit. It also seems \ninconsistent with \"git cherry-pick\" where we still disallow a partial \ncommit when we stop for a commit that becomes empty.\n\nOn the other hand, if the user has asked to edit the commit then \nallowing a partial commit would probably make sense as we know they \nwanted to modify it in some way. As I can't make up my mind I think its \nfair to say I don't have a strong opinion either way.\n\nThanks\n\nPhillip\n\n> \n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>   builtin/commit.c              | 2 --\n>   t/t3404-rebase-interactive.sh | 5 ++---\n>   2 files changed, 2 insertions(+), 5 deletions(-)\n> \n> diff --git a/builtin/commit.c b/builtin/commit.c\n> index 17cc27e53e..01b79185e7 100644\n> --- a/builtin/commit.c\n> +++ b/builtin/commit.c\n> @@ -520,8 +520,6 @@ static const char *prepare_index(const char **argv, const char *prefix,\n>   \t\t\tdie(_(\"cannot do a partial commit during a merge.\"));\n>   \t\telse if (is_from_cherry_pick(whence))\n>   \t\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n> -\t\telse if (is_from_rebase_now_empty(whence))\n> -\t\t\tdie(_(\"cannot do a partial commit during a rebase.\"));\n>   \t}\n>   \n>   \tif (list_paths(&partial, !current_head ? NULL : \"HEAD\", &pathspec))\n> diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh\n> index ff11abb2f2..3588e16543 100755\n> --- a/t/t3404-rebase-interactive.sh\n> +++ b/t/t3404-rebase-interactive.sh\n> @@ -1858,7 +1858,7 @@ test_expect_success 'post-commit hook is called' '\n>   \ttest_cmp expect actual\n>   '\n>   \n> -test_expect_success 'correct error message for partial commit after empty pick' '\n> +test_expect_success 'partial commit is allowed when a rebase pick becomes empty' '\n>   \ttest_when_finished \"git rebase --abort\" &&\n>   \t(\n>   \t\tset_fake_editor &&\n> @@ -1867,8 +1867,7 @@ test_expect_success 'correct error message for partial commit after empty pick'\n>   \t\ttest_must_fail git rebase -i A D\n>   \t) &&\n>   \techo x >file1 &&\n> -\ttest_must_fail git commit file1 2>err &&\n> -\ttest_grep \"cannot do a partial commit during a rebase.\" err\n> +\tgit commit file1\n>   '\n>   \n>   test_expect_success 'correct error message for commit --amend after empty pick' '\n\n"},{"id":"551776","messageId":"050c4aee-8e90-4063-ae70-551ad4f53746@gmail.com","threadId":"66221","inReplyTo":"c4511a98875a402a59e3109fcfe3839c2564753c.1788301481.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 3/5] commit: reword the empty-commit rebase amend error","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-02T15:39:23Z","receivedAt":"2026-09-02T15:39:26Z","isPatch":true,"body":"Hi Elijah\n\nOn 01/09/2026 23:24, Elijah Newren via GitGitGadget wrote:\n> From: Elijah Newren <newren@gmail.com>\n> \n> When a rebase applies a commit that becomes empty, it stops and asks the\n> user to decide whether to keep it or drop it.  HEAD still points at the\n> previously-applied commit at that point, so amending is refused, with:\n> \n>      You are in the middle of a rebase -- cannot amend.\n> \n> That message would suggest that amending is not allowed during an 'edit'\n> or 'break' stop, which is misleading, plus it lacks the specificity that\n> might help the user know why their particular case is a problem: the\n> commit they intended to amend became empty and was dropped, so amending\n> would affect the wrong commit.  Reword the error accordingly.\n\nI agree the new message is clearer\n\nThanks\n\nPhillip\n\n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>   builtin/commit.c              | 2 +-\n>   t/t3404-rebase-interactive.sh | 2 +-\n>   2 files changed, 2 insertions(+), 2 deletions(-)\n> \n> diff --git a/builtin/commit.c b/builtin/commit.c\n> index 01b79185e7..9b6eaa3c72 100644\n> --- a/builtin/commit.c\n> +++ b/builtin/commit.c\n> @@ -1332,7 +1332,7 @@ static int parse_and_validate_options(int argc, const char *argv[],\n>   \t\telse if (is_from_cherry_pick(whence))\n>   \t\t\tdie(_(\"You are in the middle of a cherry-pick -- cannot amend.\"));\n>   \t\telse if (is_from_rebase_now_empty(whence))\n> -\t\t\tdie(_(\"You are in the middle of a rebase -- cannot amend.\"));\n> +\t\t\tdie(_(\"The now-empty commit has been dropped -- cannot amend.\"));\n>   \t}\n>   \tif (fixup_message && squash_message)\n>   \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--squash\", \"--fixup\");\n> diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh\n> index 3588e16543..81f4844950 100755\n> --- a/t/t3404-rebase-interactive.sh\n> +++ b/t/t3404-rebase-interactive.sh\n> @@ -1880,7 +1880,7 @@ test_expect_success 'correct error message for commit --amend after empty pick'\n>   \t) &&\n>   \techo x>file1 &&\n>   \ttest_must_fail git commit -a --amend 2>err &&\n> -\ttest_grep \"middle of a rebase -- cannot amend.\" err\n> +\ttest_grep \"now-empty commit has been dropped -- cannot amend.\" err\n>   '\n>   \n>   test_expect_success 'todo has correct onto hash' '\n\n"},{"id":"551777","messageId":"4ed77ebd-e4ba-4d37-9c92-d987b70135a6@gmail.com","threadId":"66221","inReplyTo":"e77b34c1cf414839d922d0ce252d4700a313c7bb.1788301481.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 4/5] commit: refuse to amend during conflict resolution","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-02T15:39:41Z","receivedAt":"2026-09-02T15:39:45Z","isPatch":true,"body":"Hi Elijah\n\nOn 01/09/2026 23:24, Elijah Newren via GitGitGadget wrote:\n> From: Elijah Newren <newren@gmail.com>\n> \n> Running `git commit --amend` during conflict resolution is an ugly\n> foot-gun.  For many years, we have rejected amending during conflict\n> resolution in the middle of\n>    - a merge\n>    - a cherry-pick\n> However, this was never extended to other operations that can also\n> produce conflicts:\n>    - an `am` operation\n>    - a revert\n>    - a rebase\n> \n> Extend it to handle these other cases now.\n> \n> Extending to `am`, revert, and the apply backend of rebase are fairly\n> straightforward.  However, with the merge backend of rebase we have to\n> be more careful, since it powers interactive rebases and\n>    - the interactive machinery internally uses `git commit --amend` for\n>      `squash` and `reword` directives\n>    - users are expected to `git commit --amend` after hitting an `edit`\n>      or `break` directive\n> So, we need to be careful with rebase to only reject amending when doing\n> conflict resolution.\n> \n> A few files under the rebase-merge/ directory provide us the necessary\n> information:\n> \n>    - stopped-sha is written only when the rebase stops and hands control\n>      back to the user, so its presence marks a genuine stop -- as opposed\n>      to the sequencer's own internal `git commit --amend` while applying\n>      a squash, fixup, or reword, during which no stopped-sha exists.\n> \n>    - amend is written only when the rebase stops with HEAD already\n>      pointing at the commit the user is meant to amend: a clean `edit`,\n>      or a fast-forward `reword`.  Its absence at a stop therefore means\n>      the commit did not apply, so HEAD is the previously-applied commit\n>      rather than the one being rebased -- exactly the case we refuse.\n> \n> So for the merge backend we die when stopped-sha exists and amend does\n> not.  This covers a plain conflicted pick as well as a conflicted `edit`\n> (both leave HEAD on the previously-applied commit), while still allowing\n> a clean `edit` or `reword` stop and a `break` stop (no stopped-sha).\n> stopped-sha is unlinked at the start of the resume loop, so a resumed\n> squash's internal amend is unaffected.\n\nThis looks good now that it is using rebase_path_*() rather than hard \ncoding the paths.\n\nThanks\n\nPhillip\n\n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>   builtin/commit.c                | 27 +++++++---\n>   sequencer.c                     | 57 +++++++++++++++++++++\n>   sequencer.h                     | 23 +++++++++\n>   t/t3404-rebase-interactive.sh   | 87 +++++++++++++++++++++++++++++++++\n>   t/t3507-cherry-pick-conflict.sh | 11 +++++\n>   t/t4151-am-abort.sh             | 11 +++++\n>   6 files changed, 210 insertions(+), 6 deletions(-)\n> \n> diff --git a/builtin/commit.c b/builtin/commit.c\n> index 9b6eaa3c72..284fc7fdc6 100644\n> --- a/builtin/commit.c\n> +++ b/builtin/commit.c\n> @@ -1324,15 +1324,30 @@ static int parse_and_validate_options(int argc, const char *argv[],\n>   \t\tuse_editor = 0;\n>   \n>   \t/* Sanity check options */\n> -\tif (amend && !current_head)\n> -\t\tdie(_(\"You have nothing to amend.\"));\n> -\tif (amend && whence != FROM_COMMIT) {\n> -\t\tif (whence == FROM_MERGE)\n> +\tif (amend) {\n> +\t\tif (!current_head)\n> +\t\t\tdie(_(\"You have nothing to amend.\"));\n> +\t\t/*\n> +\t\t * Refuse to amend in the middle of any operation that is\n> +\t\t * meant to record its result as a new commit on top of HEAD\n> +\t\t * rather than by rewriting HEAD.\n> +\t\t */\n> +\t\tswitch (sequencer_ongoing_operation(s->repo, whence)) {\n> +\t\tcase ONGOING_NONE:\n> +\t\t\tbreak;\n> +\t\tcase ONGOING_MERGE:\n>   \t\t\tdie(_(\"You are in the middle of a merge -- cannot amend.\"));\n> -\t\telse if (is_from_cherry_pick(whence))\n> +\t\tcase ONGOING_CHERRY_PICK:\n>   \t\t\tdie(_(\"You are in the middle of a cherry-pick -- cannot amend.\"));\n> -\t\telse if (is_from_rebase_now_empty(whence))\n> +\t\tcase ONGOING_REBASE_NOW_EMPTY:\n>   \t\t\tdie(_(\"The now-empty commit has been dropped -- cannot amend.\"));\n> +\t\tcase ONGOING_REVERT:\n> +\t\t\tdie(_(\"You are in the middle of a revert -- cannot amend.\"));\n> +\t\tcase ONGOING_AM:\n> +\t\t\tdie(_(\"You are in the middle of an am session -- cannot amend.\"));\n> +\t\tcase ONGOING_REBASE_CONFLICT:\n> +\t\t\tdie(_(\"You are resolving conflicts during a rebase -- cannot amend.\"));\n> +\t\t}\n>   \t}\n>   \tif (fixup_message && squash_message)\n>   \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--squash\", \"--fixup\");\n> diff --git a/sequencer.c b/sequencer.c\n> index d336c309ca..6a28e0dd9a 100644\n> --- a/sequencer.c\n> +++ b/sequencer.c\n> @@ -142,6 +142,13 @@ static GIT_PATH_FUNC(rebase_path_author_script, \"rebase-merge/author-script\")\n>    * command is processed, this file is deleted.\n>    */\n>   static GIT_PATH_FUNC(rebase_path_amend, \"rebase-merge/amend\")\n> +/*\n> + * The apply (\"am\") backend keeps its state in the rebase-apply directory;\n> + * the \"applying\" file within it marks a plain `git am` (as opposed to an\n> + * apply-based rebase).\n> + */\n> +static GIT_PATH_FUNC(apply_dir, \"rebase-apply\")\n> +static GIT_PATH_FUNC(apply_path_applying, \"rebase-apply/applying\")\n>   /*\n>    * When we stop at a given patch via the \"edit\" command, this file contains\n>    * the commit object name of the corresponding patch.\n> @@ -6966,6 +6973,56 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)\n>   \treturn 0;\n>   }\n>   \n> +enum ongoing_operation sequencer_ongoing_operation(struct repository *r,\n> +\t\t\t\t\t\t   enum commit_whence whence)\n> +{\n> +\t/*\n> +\t * The merge, cherry-pick, and (empty) rebase-pick stops are already\n> +\t * distinguished by 'whence'.\n> +\t */\n> +\tswitch (whence) {\n> +\tcase FROM_MERGE:\n> +\t\treturn ONGOING_MERGE;\n> +\tcase FROM_CHERRY_PICK_SINGLE:\n> +\tcase FROM_CHERRY_PICK_MULTI:\n> +\t\treturn ONGOING_CHERRY_PICK;\n> +\tcase FROM_REBASE_NOW_EMPTY:\n> +\t\treturn ONGOING_REBASE_NOW_EMPTY;\n> +\tcase FROM_COMMIT:\n> +\t\tbreak;\n> +\t}\n> +\n> +\t/*\n> +\t * 'whence' is FROM_COMMIT, but we may still be in the middle of an\n> +\t * operation that records its result on top of HEAD; detect those\n> +\t * from their on-disk state.\n> +\t */\n> +\n> +\t/* In the middle of a revert? */\n> +\tif (refs_ref_exists(get_main_ref_store(r), \"REVERT_HEAD\"))\n> +\t\treturn ONGOING_REVERT;\n> +\n> +\t/* In the middle of an `am`? */\n> +\tif (file_exists(apply_path_applying()))\n> +\t\treturn ONGOING_AM;\n> +\n> +\t/*\n> +\t * In the middle of a rebase that stopped for conflict resolution?\n> +\t * The apply backend only ever stops for conflicts, so the presence\n> +\t * of its state directory is enough.  The merge backend writes\n> +\t * stopped-sha whenever it hands control back to the user, but omits\n> +\t * `amend` unless it stopped with HEAD already pointing at the commit\n> +\t * to be amended (a clean edit/reword stop); its absence therefore\n> +\t * marks a conflicted stop.\n> +\t */\n> +\tif (file_exists(apply_dir()) ||\n> +\t    (file_exists(rebase_path_stopped_sha()) &&\n> +\t     !file_exists(rebase_path_amend())))\n> +\t\treturn ONGOING_REBASE_CONFLICT;\n> +\n> +\treturn ONGOING_NONE;\n> +}\n> +\n>   int sequencer_get_update_refs_state(const char *wt_dir,\n>   \t\t\t\t    struct string_list *refs)\n>   {\n> diff --git a/sequencer.h b/sequencer.h\n> index 64a9c7fb1b..fb4a744208 100644\n> --- a/sequencer.h\n> +++ b/sequencer.h\n> @@ -277,6 +277,29 @@ int sequencer_get_last_command(struct repository* r,\n>   \t\t\t       enum replay_action *action);\n>   int sequencer_determine_whence(struct repository *r, enum commit_whence *whence);\n>   \n> +/*\n> + * An in-progress operation that records its result (often a conflict\n> + * resolution) as a new commit on top of HEAD, during which amending\n> + * HEAD via \"git commit --amend\" is almost always a mistake.\n> + */\n> +enum ongoing_operation {\n> +\tONGOING_NONE = 0,\n> +\tONGOING_MERGE,\n> +\tONGOING_CHERRY_PICK,\n> +\tONGOING_REBASE_NOW_EMPTY,\n> +\tONGOING_REVERT,\n> +\tONGOING_AM,\n> +\tONGOING_REBASE_CONFLICT\n> +};\n> +\n> +/*\n> + * Return which in-progress operation, if any, is underway; see enum\n> + * ongoing_operation.  'whence' is the origin already computed for the\n> + * pending commit.\n> + */\n> +enum ongoing_operation sequencer_ongoing_operation(struct repository *r,\n> +\t\t\t\t\t\t   enum commit_whence whence);\n> +\n>   /**\n>    * Append the set of ref-OID pairs that are currently stored for the 'git\n>    * rebase --update-refs' feature if such a rebase is currently happening.\n> diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh\n> index 81f4844950..4e6c3e2f19 100755\n> --- a/t/t3404-rebase-interactive.sh\n> +++ b/t/t3404-rebase-interactive.sh\n> @@ -1883,6 +1883,93 @@ test_expect_success 'correct error message for commit --amend after empty pick'\n>   \ttest_grep \"now-empty commit has been dropped -- cannot amend.\" err\n>   '\n>   \n> +test_expect_success 'commit --amend is refused at a rebase conflict stop' '\n> +\ttest_when_finished \"git rebase --abort\" &&\n> +\tgit checkout --detach conflict-branch &&\n> +\t(\n> +\t\tset_fake_editor &&\n> +\t\tFAKE_LINES=\"1 3\" &&\n> +\t\texport FAKE_LINES &&\n> +\t\ttest_must_fail git rebase -i A\n> +\t) &&\n> +\ttest_path_is_file .git/rebase-merge/patch &&\n> +\ttest_path_is_missing .git/rebase-merge/amend &&\n> +\techo resolved >conflict &&\n> +\tgit add conflict &&\n> +\ttest_must_fail git commit --amend --no-edit 2>err &&\n> +\ttest_grep \"You are resolving conflicts during a rebase -- cannot amend\" err\n> +'\n> +\n> +test_expect_success 'commit --amend is refused when an \"edit\" pick conflicts' '\n> +\ttest_when_finished \"git rebase --abort\" &&\n> +\tgit checkout --detach conflict-branch &&\n> +\t(\n> +\t\tset_fake_editor &&\n> +\t\tFAKE_LINES=\"1 edit 3\" &&\n> +\t\texport FAKE_LINES &&\n> +\t\ttest_must_fail git rebase -i A\n> +\t) &&\n> +\ttest_path_is_file .git/rebase-merge/patch &&\n> +\ttest_path_is_missing .git/rebase-merge/amend &&\n> +\techo resolved >conflict &&\n> +\tgit add conflict &&\n> +\ttest_must_fail git commit --amend --no-edit 2>err &&\n> +\ttest_grep \"You are resolving conflicts during a rebase -- cannot amend\" err\n> +'\n> +\n> +test_expect_success 'commit --amend is allowed at a rebase edit stop' '\n> +\ttest_when_finished \"git rebase --abort\" &&\n> +\tgit checkout --detach no-conflict-branch &&\n> +\t(\n> +\t\tset_fake_editor &&\n> +\t\tFAKE_LINES=\"edit 1 2 3 4\" &&\n> +\t\texport FAKE_LINES &&\n> +\t\tgit rebase -i A\n> +\t) &&\n> +\ttest_path_is_file .git/rebase-merge/amend &&\n> +\techo tweak >fileJ &&\n> +\tgit add fileJ &&\n> +\tgit commit --amend --no-edit\n> +'\n> +\n> +test_expect_success 'commit --amend is allowed at a rebase break stop' '\n> +\ttest_when_finished \"git rebase --abort\" &&\n> +\tgit checkout --detach no-conflict-branch &&\n> +\t(\n> +\t\tset_fake_editor &&\n> +\t\tFAKE_LINES=\"break 1 2 3 4\" &&\n> +\t\texport FAKE_LINES &&\n> +\t\tgit rebase -i A\n> +\t) &&\n> +\ttest_must_fail git rev-parse --verify REBASE_HEAD &&\n> +\techo tweak >fileJ &&\n> +\tgit add fileJ &&\n> +\tgit commit --amend --no-edit\n> +'\n> +\n> +test_expect_success 'commit --amend is refused at an apply-backend conflict stop' '\n> +\ttest_when_finished \"rm -rf apply-backend\" &&\n> +\ttest_create_repo apply-backend &&\n> +\t(\n> +\t\tcd apply-backend &&\n> +\t\ttest_commit base file &&\n> +\t\tgit branch -M mainline &&\n> +\t\ttest_commit upstream file upstream &&\n> +\t\tgit checkout -b side mainline~1 &&\n> +\t\ttest_commit conflicting file side &&\n> +\t\ttest_commit unrelated other &&\n> +\t\ttest_must_fail git rebase --apply mainline &&\n> +\t\t# the apply backend only ever stops for conflicts, and\n> +\t\t# leaves HEAD on the previously-applied commit\n> +\t\ttest_path_is_dir .git/rebase-apply &&\n> +\t\ttest_path_is_missing .git/rebase-apply/applying &&\n> +\t\techo resolved >file &&\n> +\t\tgit add file &&\n> +\t\ttest_must_fail git commit --amend --no-edit 2>err &&\n> +\t\ttest_grep \"You are resolving conflicts during a rebase -- cannot amend\" err\n> +\t)\n> +'\n> +\n>   test_expect_success 'todo has correct onto hash' '\n>   \tGIT_SEQUENCE_EDITOR=cat git rebase -i no-conflict-branch~4 no-conflict-branch >actual &&\n>   \tonto=$(git rev-parse --short HEAD~4) &&\n> diff --git a/t/t3507-cherry-pick-conflict.sh b/t/t3507-cherry-pick-conflict.sh\n> index 44596cb1e8..42de398f76 100755\n> --- a/t/t3507-cherry-pick-conflict.sh\n> +++ b/t/t3507-cherry-pick-conflict.sh\n> @@ -364,6 +364,17 @@ test_expect_success 'failed revert sets REVERT_HEAD' '\n>   \ttest_cmp_rev picked REVERT_HEAD\n>   '\n>   \n> +test_expect_success 'commit --amend of revert fails' '\n> +\tpristine_detach initial &&\n> +\n> +\ttest_must_fail git revert picked &&\n> +\techo resolved >foo &&\n> +\tgit add foo &&\n> +\ttest_must_fail git commit --amend 2>err &&\n> +\n> +\ttest_grep \"in the middle of a revert -- cannot amend.\" err\n> +'\n> +\n>   test_expect_success 'successful revert does not set REVERT_HEAD' '\n>   \tpristine_detach base &&\n>   \tgit revert base &&\n> diff --git a/t/t4151-am-abort.sh b/t/t4151-am-abort.sh\n> index 8e1ecf8a68..9313a074b2 100755\n> --- a/t/t4151-am-abort.sh\n> +++ b/t/t4151-am-abort.sh\n> @@ -63,6 +63,17 @@ do\n>   \n>   done\n>   \n> +test_expect_success 'commit --amend during a failed am fails' '\n> +\tgit reset --hard initial &&\n> +\tcp file-2-expect file-2 &&\n> +\ttest_must_fail git am 000[1245]-*.patch &&\n> +\techo resolved >file-1 &&\n> +\tgit add file-1 &&\n> +\ttest_must_fail git commit --amend 2>err &&\n> +\ttest_grep \"in the middle of an am session -- cannot amend.\" err &&\n> +\tgit am --abort\n> +'\n> +\n>   test_expect_success 'am -3 --skip removes otherfile-4' '\n>   \tgit reset --hard initial &&\n>   \ttest_must_fail git am -3 0003-*.patch &&\n\n"},{"id":"551778","messageId":"03b9f094-c969-4309-b2cc-f7cd01959d49@gmail.com","threadId":"66221","inReplyTo":"pull.2389.v4.git.git.1788301481.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 0/5] commit: refuse to amend during conflict resolution","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-02T15:41:36Z","receivedAt":"2026-09-02T15:41:39Z","isPatch":true,"body":"Hi Elijah\n\nThe changes from v2 look good, I've got mixed feelings about the new \npatch 2 but I'm happy with everything else. Thanks for working on it\n\nPhillip\n\n\nOn 01/09/2026 23:24, Elijah Newren via GitGitGadget wrote:\n> Changes since v3:\n> \n>   * Just a clarifying rename, but applied in 3 places:\n>     * FROM_REBASE_EMPTY -> FROM_REBASE_NOW_EMPTY\n>     * is_from_rebase_empty() -> is_from_rebase_now_empty()\n>     * ONGOING_REBASE_EMPTY -> ONGOING_REBASE_NOW_EMPTY\n> \n> Changes since v2:\n> \n>   * Two new preparatory patches:\n>     * Rename FROM_REBASE_PICK and is_from_rebase() to point out they are\n>       about empty commits\n>     * Allow a partial commit when a rebase pick becomes empty\n>   * Tweaked the error message for attempted amend on now-dropped empty commit\n>     (suggestions for further improvements welcome)\n>   * Used the path accessor functions within sequencer.c to simplify the new\n>     helper function\n> \n> Both git commit --amend and a partial commit (git commit <paths>) are\n> foot-guns while the user is in the middle of an operation that resolves\n> conflicts on top of HEAD: recording a conflict resolution is about capturing\n> the state of the whole tree as a new commit, not about rewriting HEAD or\n> committing a subset of paths.\n> \n> Historically we only rejected these during a merge or a cherry-pick or when\n> resolving an empty pick during a rebase. The same hazard exists for am,\n> revert, and rebase conflict stops, none of which were covered. This series\n> extends the refusal to all of them.\n> \n> The three patches:\n> \n>   1. reword the two pre-existing \"empty commit\" rebase messages, which were\n>      misleadingly generic\n>   2. refuse git commit --amend during these additional operations\n>   3. refuse partial commits during the same operations.\n> \n> Elijah Newren (5):\n>    commit: clarify FROM_REBASE_PICK and is_from_rebase() names\n>    commit: allow a partial commit when a rebase pick becomes empty\n>    commit: reword the empty-commit rebase amend error\n>    commit: refuse to amend during conflict resolution\n>    commit: refuse partial commits during conflict resolution\n> \n>   builtin/commit.c                |  65 +++++++++++-----\n>   sequencer.c                     |  59 ++++++++++++++-\n>   sequencer.h                     |  24 ++++++\n>   t/t3404-rebase-interactive.sh   | 128 +++++++++++++++++++++++++++++++-\n>   t/t3507-cherry-pick-conflict.sh |  22 ++++++\n>   t/t4151-am-abort.sh             |  22 ++++++\n>   wt-status.h                     |   6 +-\n>   7 files changed, 299 insertions(+), 27 deletions(-)\n> \n> \n> base-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e\n> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2389%2Fnewren%2Frefuse-amend-during-conflicts-v4\n> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2389/newren/refuse-amend-during-conflicts-v4\n> Pull-Request: https://github.com/git/git/pull/2389\n> \n> Range-diff vs v3:\n> \n>   1:  7e198a20fa ! 1:  bd361679b9 commit: clarify FROM_REBASE_PICK and is_from_rebase() names\n>       @@ Commit message\n>            specifically about hitting a commit that becomes empty when rebasing.\n>            Clarify their names now.\n>        \n>       -    While at it, change `whence == FROM_REBASE_EMPTY` to use\n>       -    `is_from_rebase_empty(whence)`.\n>       +    While at it, change `whence == FROM_REBASE_NOW_EMPTY` to use\n>       +    `is_from_rebase_now_empty(whence)`.\n>        \n>            Signed-off-by: Elijah Newren <newren@gmail.com>\n>        \n>       @@ builtin/commit.c: static const char *prepare_index(const char **argv, const char\n>         \t\telse if (is_from_cherry_pick(whence))\n>         \t\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n>        -\t\telse if (is_from_rebase(whence))\n>       -+\t\telse if (is_from_rebase_empty(whence))\n>       ++\t\telse if (is_from_rebase_now_empty(whence))\n>         \t\t\tdie(_(\"cannot do a partial commit during a rebase.\"));\n>         \t}\n>         \n>       @@ builtin/commit.c: static int prepare_to_commit(const char *index_file, const cha\n>         \telse if (whence == FROM_MERGE)\n>         \t\thook_arg1 = \"merge\";\n>        -\telse if (is_from_cherry_pick(whence) || whence == FROM_REBASE_PICK) {\n>       -+\telse if (is_from_cherry_pick(whence) || is_from_rebase_empty(whence)) {\n>       ++\telse if (is_from_cherry_pick(whence) || is_from_rebase_now_empty(whence)) {\n>         \t\thook_arg1 = \"commit\";\n>         \t\thook_arg2 = \"CHERRY_PICK_HEAD\";\n>         \t}\n>       @@ builtin/commit.c: static int prepare_to_commit(const char *index_file, const cha\n>         \t\t\tfputs(_(empty_amend_advice), stderr);\n>         \t\telse if (is_from_cherry_pick(whence) ||\n>        -\t\t\t whence == FROM_REBASE_PICK) {\n>       -+\t\t\t is_from_rebase_empty(whence)) {\n>       ++\t\t\t is_from_rebase_now_empty(whence)) {\n>         \t\t\tfputs(_(empty_cherry_pick_advice), stderr);\n>         \t\t\tif (whence == FROM_CHERRY_PICK_SINGLE)\n>         \t\t\t\tfputs(_(empty_cherry_pick_advice_single), stderr);\n>       @@ builtin/commit.c: static int parse_and_validate_options(int argc, const char *ar\n>         \t\telse if (is_from_cherry_pick(whence))\n>         \t\t\tdie(_(\"You are in the middle of a cherry-pick -- cannot amend.\"));\n>        -\t\telse if (whence == FROM_REBASE_PICK)\n>       -+\t\telse if (is_from_rebase_empty(whence))\n>       ++\t\telse if (is_from_rebase_now_empty(whence))\n>         \t\t\tdie(_(\"You are in the middle of a rebase -- cannot amend.\"));\n>         \t}\n>         \tif (fixup_message && squash_message)\n>       @@ builtin/commit.c: static int parse_and_validate_options(int argc, const char *ar\n>         \t\tuse_message = \"HEAD\";\n>         \tif (!use_message && !is_from_cherry_pick(whence) &&\n>        -\t    !is_from_rebase(whence) && renew_authorship)\n>       -+\t    !is_from_rebase_empty(whence) && renew_authorship)\n>       ++\t    !is_from_rebase_now_empty(whence) && renew_authorship)\n>         \t\tdie(_(\"--reset-author can be used only with -C, -c or --amend.\"));\n>         \tif (use_message) {\n>         \t\tuse_message_buffer = read_commit_message(use_message);\n>       @@ builtin/commit.c: static int parse_and_validate_options(int argc, const char *ar\n>         \t\t}\n>         \t}\n>        -\tif ((is_from_cherry_pick(whence) || whence == FROM_REBASE_PICK) &&\n>       -+\tif ((is_from_cherry_pick(whence) || is_from_rebase_empty(whence)) &&\n>       ++\tif ((is_from_cherry_pick(whence) || is_from_rebase_now_empty(whence)) &&\n>         \t    !renew_authorship) {\n>         \t\tauthor_message = \"CHERRY_PICK_HEAD\";\n>         \t\tauthor_message_buffer = read_commit_message(author_message);\n>       @@ builtin/commit.c: int cmd_commit(int argc,\n>         \t\t\treflog_msg = is_from_cherry_pick(whence)\n>         \t\t\t\t\t? \"commit (cherry-pick)\"\n>        -\t\t\t\t\t: is_from_rebase(whence)\n>       -+\t\t\t\t\t: is_from_rebase_empty(whence)\n>       ++\t\t\t\t\t: is_from_rebase_now_empty(whence)\n>         \t\t\t\t\t? \"commit (rebase)\"\n>         \t\t\t\t\t: \"commit\";\n>         \t\tcommit_list_insert(current_head, &parents);\n>       @@ sequencer.c: int sequencer_determine_whence(struct repository *r, enum commit_wh\n>         \t\t    !repo_get_oid(r, \"CHERRY_PICK_HEAD\", &cherry_pick_head) &&\n>         \t\t    oideq(&rebase_head, &cherry_pick_head))\n>        -\t\t\t*whence = FROM_REBASE_PICK;\n>       -+\t\t\t*whence = FROM_REBASE_EMPTY;\n>       ++\t\t\t*whence = FROM_REBASE_NOW_EMPTY;\n>         \t\telse\n>         \t\t\t*whence = FROM_CHERRY_PICK_SINGLE;\n>         \n>       @@ wt-status.h: enum commit_whence {\n>         \tFROM_CHERRY_PICK_SINGLE, /* commit came from cherry-pick */\n>         \tFROM_CHERRY_PICK_MULTI, /* commit came from a sequence of cherry-picks */\n>        -\tFROM_REBASE_PICK /* commit came from a pick/reword/edit */\n>       -+\tFROM_REBASE_EMPTY /* rebase applied a pick that became empty */\n>       ++\tFROM_REBASE_NOW_EMPTY /* rebase applied a pick that became empty */\n>         };\n>         \n>         static inline int is_from_cherry_pick(enum commit_whence whence)\n>       @@ wt-status.h: static inline int is_from_cherry_pick(enum commit_whence whence)\n>         }\n>         \n>        -static inline int is_from_rebase(enum commit_whence whence)\n>       -+static inline int is_from_rebase_empty(enum commit_whence whence)\n>       ++static inline int is_from_rebase_now_empty(enum commit_whence whence)\n>         {\n>        -\treturn whence == FROM_REBASE_PICK;\n>       -+\treturn whence == FROM_REBASE_EMPTY;\n>       ++\treturn whence == FROM_REBASE_NOW_EMPTY;\n>         }\n>         \n>         struct wt_status_change_data {\n>   2:  e169303619 ! 2:  a0b9900437 commit: allow a partial commit when a rebase pick becomes empty\n>       @@ builtin/commit.c: static const char *prepare_index(const char **argv, const char\n>         \t\t\tdie(_(\"cannot do a partial commit during a merge.\"));\n>         \t\telse if (is_from_cherry_pick(whence))\n>         \t\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n>       --\t\telse if (is_from_rebase_empty(whence))\n>       +-\t\telse if (is_from_rebase_now_empty(whence))\n>        -\t\t\tdie(_(\"cannot do a partial commit during a rebase.\"));\n>         \t}\n>         \n>   3:  0850a999da ! 3:  c4511a9887 commit: reword the empty-commit rebase amend error\n>       @@ builtin/commit.c\n>        @@ builtin/commit.c: static int parse_and_validate_options(int argc, const char *argv[],\n>         \t\telse if (is_from_cherry_pick(whence))\n>         \t\t\tdie(_(\"You are in the middle of a cherry-pick -- cannot amend.\"));\n>       - \t\telse if (is_from_rebase_empty(whence))\n>       + \t\telse if (is_from_rebase_now_empty(whence))\n>        -\t\t\tdie(_(\"You are in the middle of a rebase -- cannot amend.\"));\n>        +\t\t\tdie(_(\"The now-empty commit has been dropped -- cannot amend.\"));\n>         \t}\n>   4:  9f80d8a00d ! 4:  e77b34c1cf commit: refuse to amend during conflict resolution\n>       @@ builtin/commit.c: static int parse_and_validate_options(int argc, const char *ar\n>        -\t\telse if (is_from_cherry_pick(whence))\n>        +\t\tcase ONGOING_CHERRY_PICK:\n>         \t\t\tdie(_(\"You are in the middle of a cherry-pick -- cannot amend.\"));\n>       --\t\telse if (is_from_rebase_empty(whence))\n>       -+\t\tcase ONGOING_REBASE_EMPTY:\n>       +-\t\telse if (is_from_rebase_now_empty(whence))\n>       ++\t\tcase ONGOING_REBASE_NOW_EMPTY:\n>         \t\t\tdie(_(\"The now-empty commit has been dropped -- cannot amend.\"));\n>        +\t\tcase ONGOING_REVERT:\n>        +\t\t\tdie(_(\"You are in the middle of a revert -- cannot amend.\"));\n>       @@ sequencer.c: int sequencer_determine_whence(struct repository *r, enum commit_wh\n>        +\tcase FROM_CHERRY_PICK_SINGLE:\n>        +\tcase FROM_CHERRY_PICK_MULTI:\n>        +\t\treturn ONGOING_CHERRY_PICK;\n>       -+\tcase FROM_REBASE_EMPTY:\n>       -+\t\treturn ONGOING_REBASE_EMPTY;\n>       ++\tcase FROM_REBASE_NOW_EMPTY:\n>       ++\t\treturn ONGOING_REBASE_NOW_EMPTY;\n>        +\tcase FROM_COMMIT:\n>        +\t\tbreak;\n>        +\t}\n>       @@ sequencer.h: int sequencer_get_last_command(struct repository* r,\n>        +\tONGOING_NONE = 0,\n>        +\tONGOING_MERGE,\n>        +\tONGOING_CHERRY_PICK,\n>       -+\tONGOING_REBASE_EMPTY,\n>       ++\tONGOING_REBASE_NOW_EMPTY,\n>        +\tONGOING_REVERT,\n>        +\tONGOING_AM,\n>        +\tONGOING_REBASE_CONFLICT\n>   5:  050b9e8a52 ! 5:  b93b26ed9f commit: refuse partial commits during conflict resolution\n>       @@ builtin/commit.c: static const char *prepare_index(const char **argv, const char\n>        +\t\tdie(_(\"cannot do a partial commit during a merge.\"));\n>        +\tcase ONGOING_CHERRY_PICK:\n>        +\t\tdie(_(\"cannot do a partial commit during a cherry-pick.\"));\n>       -+\tcase ONGOING_REBASE_EMPTY:\n>       ++\tcase ONGOING_REBASE_NOW_EMPTY:\n>        +\t\t/*\n>        +\t\t * A pick that became empty is not a conflict, and creating\n>        +\t\t * a new commit (partial or not) poses no problem.\n> \n\n"}]}