{"thread":{"id":"64540","subject":"[PATCH] replay: drop commits that become empty","startedAt":"2025-11-27T16:16:05Z","lastAt":"2025-12-19T04:44:39Z","messageCount":16,"participants":["Phillip Wood","Junio C Hamano","Elijah Newren"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"531377","messageId":"8a2a1215306452147cc7b803530ab2429bf57f15.1764260150.git.phillip.wood@dunelm.org.uk","threadId":"64540","inReplyTo":null,"subject":"[PATCH] replay: drop commits that become empty","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-11-27T16:15:54Z","receivedAt":"2025-11-27T16:16:05Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"From: Phillip Wood <phillip.wood@dunelm.org.uk>\n\nIf the changes in a commit being replayed are already in the branch\nthat the commits are being replayed onto then \"git replay\" creates an\nempty commit. This is confusing because the commit message no longer\nmatches the contents of the commit. Drop the commit instead. Commits\nthat start off empty are not dropped. This matches the behavior of\n\"git rebase --reapply-cherry-pick --empty=drop\" and \"git cherry-pick\n--empty-drop\". If a branch points to a commit that is dropped it will\nbe updated to point to the last commit that was not dropped. This can\nbeen seen in the new test where \"topic1\" is updated to point to the\nrebased \"C\" as \"F\" is dropped because it is already upstream. While\nthis is a breaking change \"git replay\" is marked as experimental to\nallow improvements like this that change the behavior.\n\nSigned-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\n---\nElijah - I'm not really clear why we were setting result->tree before\ncalling merge_incore_nonrecursive(), was it just for convenience to\navoid declaring a local variable or have I missed something?\n\nThis patch is based on ps/history\n\nI think dropping commits that become empty is the sensible default,\nif it turns out that some users are relying on the current behavior\nwe can add an option to retain the empty commits.\n\nBase-Commit: 4ac8283def34401e50908903b89fa22498bb23a2\nPublished-As: https://github.com/phillipwood/git/releases/tag/pw%2Freplay-drop-commits-that-become-empty%2Fv1\nView-Changes-At: https://github.com/phillipwood/git/compare/4ac8283de...8a2a12153\nFetch-It-Via: git fetch https://github.com/phillipwood/git pw/replay-drop-commits-that-become-empty/v1\n\n Documentation/git-replay.adoc |  4 +++-\n replay.c                      | 10 +++++++---\n t/t3650-replay-basics.sh      | 25 +++++++++++++++++++++++++\n 3 files changed, 35 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex dcb26e8a8e8..96a3a557bf3 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -59,7 +59,9 @@ The default mode can be configured via the `replay.refAction` configuration vari\n \tbe passed, but in `--advance <branch>` mode, they should have\n \ta single tip, so that it's clear where <branch> should point\n \tto. See \"Specifying Ranges\" in linkgit:git-rev-parse[1] and the\n-\t\"Commit Limiting\" options below.\n+\t\"Commit Limiting\" options below. Any commits in the range whose\n+\tchanges are already present in the branch the commits are being\n+\treplayed onto will be dropped.\n \n include::rev-list-options.adoc[]\n \ndiff --git a/replay.c b/replay.c\nindex 58fdc20140b..7cd7206eee5 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -88,12 +88,12 @@ struct commit *replay_pick_regular_commit(struct repository *repo,\n \t\t\t\t\t  struct merge_result *result)\n {\n \tstruct commit *base, *replayed_base;\n-\tstruct tree *pickme_tree, *base_tree;\n+\tstruct tree *pickme_tree, *base_tree, *replayed_base_tree;\n \n \tbase = pickme->parents->item;\n \treplayed_base = mapped_commit(replayed_commits, base, onto);\n \n-\tresult->tree = repo_get_commit_tree(repo, replayed_base);\n+\treplayed_base_tree = repo_get_commit_tree(repo, replayed_base);\n \tpickme_tree = repo_get_commit_tree(repo, pickme);\n \tbase_tree = repo_get_commit_tree(repo, base);\n \n@@ -103,13 +103,17 @@ struct commit *replay_pick_regular_commit(struct repository *repo,\n \n \tmerge_incore_nonrecursive(merge_opt,\n \t\t\t\t  base_tree,\n-\t\t\t\t  result->tree,\n+\t\t\t\t  replayed_base_tree,\n \t\t\t\t  pickme_tree,\n \t\t\t\t  result);\n \n \tfree((char*)merge_opt->ancestor);\n \tmerge_opt->ancestor = NULL;\n \tif (!result->clean)\n \t\treturn NULL;\n+\t/* Drop commits that become empty */\n+\tif (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&\n+\t    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))\n+\t\treturn replayed_base;\n \treturn replay_create_commit(repo, result->tree, pickme, replayed_base);\n }\ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex cf3aacf3551..d73ab16908a 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -25,6 +25,8 @@ test_expect_success 'setup' '\n \tgit switch -c topic3 &&\n \ttest_commit G &&\n \ttest_commit H &&\n+\tgit switch -c empty &&\n+\tgit commit --allow-empty --only -m empty &&\n \tgit switch -c topic4 main &&\n \ttest_commit I &&\n \ttest_commit J &&\n@@ -106,6 +108,29 @@ test_expect_success 'using replay on bare repo to perform basic cherry-pick' '\n \ttest_cmp expect result-bare\n '\n \n+test_expect_success 'commits that become empty are dropped' '\n+\tgit replay --ref-action=print --advance main topic1^! >result &&\n+\tONTO=$(cut -f 3 -d \" \" result) &&\n+\tgit replay --ref-action=print --onto $ONTO \\\n+\t\t--branches --ancestry-path=empty ^A >result &&\n+\t# Write the new value of refs/heads/empty to \"new-empty\" and\n+\t# generate a sed script that annotates the output of\n+\t# `git log --format=\"%H %s\"` with the updated branches\n+\tSCRIPT=\"$(sed -e \"\n+\t\t/empty/{\n+\t\t\th\n+\t\t\ts|^.*empty \\([^ ]*\\) .*|\\1|wnew-empty\n+\t\t\tg\n+\t\t}\n+\t\ts|^.*/\\([^/ ]*\\) \\([^ ]*\\).*|/^\\2/s/\\\\\\$/ (\\1)/|\n+\t\t\\$s|\\$|;s/^[^ ]* //|\" result)\" &&\n+\tgit log --format=\"%H %s\" --stdin <new-empty >actual.raw &&\n+\tsed -e \"$SCRIPT\" actual.raw >actual &&\n+\ttest_write_lines >expect \\\n+\t\t\"empty (empty)\" \"H (topic3)\" G \"C (topic1)\" F M L B A &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'replay on bare repo fails with both --advance and --onto' '\n \ttest_must_fail git -C bare replay --advance main --onto main topic1..topic2 >result-bare\n '\n-- \n2.52.0.362.g884e03848a9\n\n"},{"id":"531393","messageId":"xmqqbjkmk431.fsf@gitster.g","threadId":"64540","inReplyTo":"8a2a1215306452147cc7b803530ab2429bf57f15.1764260150.git.phillip.wood@dunelm.org.uk","subject":"Re: [PATCH] replay: drop commits that become empty","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-28T07:29:22Z","receivedAt":"2025-11-28T07:29:25Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> From: Phillip Wood <phillip.wood@dunelm.org.uk>\n>\n> If the changes in a commit being replayed are already in the branch\n> that the commits are being replayed onto then \"git replay\" creates an\n> empty commit. This is confusing because the commit message no longer\n> matches the contents of the commit. Drop the commit instead.\n\nIf a commit that originally did two or more things is replayed on a\ndestination that already has only part of it, then the extent of the\nchange the replayed commit makes will shrink, and the commit message\nno longer matches it, either.  It is a lot harder to notice the\nsituation to prompt the user to rewrite the resulting commit\nmessage, but in the degenerated case where the entire changes go\naway, the rewrite of the resulting commit message is very simple,\nwhich is to remove the commit altogether.\n\nMakes sense.\n\n> Commits\n> that start off empty are not dropped.\n\nMakes perfect sense, too.\n\n> This matches the behavior of\n> \"git rebase --reapply-cherry-pick --empty=drop\" and \"git cherry-pick\n> --empty-drop\". If a branch points to a commit that is dropped it will\n> be updated to point to the last commit that was not dropped. This can\n> been seen in the new test where \"topic1\" is updated to point to the\n> rebased \"C\" as \"F\" is dropped because it is already upstream. While\n> this is a breaking change \"git replay\" is marked as experimental to\n> allow improvements like this that change the behavior.\n>\n> Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\n> ---\n> Elijah - I'm not really clear why we were setting result->tree before\n> calling merge_incore_nonrecursive(), was it just for convenience to\n> avoid declaring a local variable or have I missed something?\n>\n> This patch is based on ps/history\n\nAs I take this more as a rfc/rfh than finalized version, it is OK to\ndepend on the topic that is known to be rerolled soonish.\n\n> I think dropping commits that become empty is the sensible default,\n> if it turns out that some users are relying on the current behavior\n> we can add an option to retain the empty commits.\n\nI think it would be a good default to drop what becomes empty.\n"},{"id":"531394","messageId":"CABPp-BEZFPmLnEtnD0WaNbkZ5uE7q5T6uKJQRUvtq+L=C1o9wg@mail.gmail.com","threadId":"64540","inReplyTo":"8a2a1215306452147cc7b803530ab2429bf57f15.1764260150.git.phillip.wood@dunelm.org.uk","subject":"Re: [PATCH] replay: drop commits that become empty","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-11-28T08:06:07Z","receivedAt":"2025-11-28T08:06:19Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Thu, Nov 27, 2025 at 8:16 AM Phillip Wood <phillip.wood123@gmail.com> wrote:\n>\n> From: Phillip Wood <phillip.wood@dunelm.org.uk>\n>\n> If the changes in a commit being replayed are already in the branch\n> that the commits are being replayed onto then \"git replay\" creates an\n> empty commit. This is confusing because the commit message no longer\n> matches the contents of the commit. Drop the commit instead. Commits\n> that start off empty are not dropped.\n\nYeah, I've got a commit in my local branch that does the same thing.\n\nIt feels like there should be a paragraph break in here somewhere, but\nmaybe that's just me?  Pretty minor either way.\n\n> This matches the behavior of\n> \"git rebase --reapply-cherry-pick --empty=drop\" and \"git cherry-pick\n> --empty-drop\". If a branch points to a commit that is dropped it will\n> be updated to point to the last commit that was not dropped. This can\n> been seen in the new test where \"topic1\" is updated to point to the\n> rebased \"C\" as \"F\" is dropped because it is already upstream. While\n> this is a breaking change \"git replay\" is marked as experimental to\n> allow improvements like this that change the behavior.\n\nYep.\n\n>\n> Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\n> ---\n> Elijah - I'm not really clear why we were setting result->tree before\n> calling merge_incore_nonrecursive(), was it just for convenience to\n> avoid declaring a local variable or have I missed something?\n\nI don't know the reason.  That traces back to a commit with\nChristian's Co-authored-by, so it may have been either him or me that\nintroduced it.  My original work on replay was on a branch that I long\nago rebased on top of the version Christian submitted, and the old\nhistory is no longer reachable from my local reflog, so I don't have a\nway to narrow down who of us did it.  If it was him, he may be able to\nanswer.  If it was me, I've long since forgotten.  I think using a\ntemporary, as you've done, is better.\n\n> This patch is based on ps/history\n>\n> I think dropping commits that become empty is the sensible default,\n> if it turns out that some users are relying on the current behavior\n> we can add an option to retain the empty commits.\n\nI fully agree.\n\n> Base-Commit: 4ac8283def34401e50908903b89fa22498bb23a2\n> Published-As: https://github.com/phillipwood/git/releases/tag/pw%2Freplay-drop-commits-that-become-empty%2Fv1\n> View-Changes-At: https://github.com/phillipwood/git/compare/4ac8283de...8a2a12153\n> Fetch-It-Via: git fetch https://github.com/phillipwood/git pw/replay-drop-commits-that-become-empty/v1\n>\n>  Documentation/git-replay.adoc |  4 +++-\n>  replay.c                      | 10 +++++++---\n>  t/t3650-replay-basics.sh      | 25 +++++++++++++++++++++++++\n>  3 files changed, 35 insertions(+), 4 deletions(-)\n>\n> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\n> index dcb26e8a8e8..96a3a557bf3 100644\n> --- a/Documentation/git-replay.adoc\n> +++ b/Documentation/git-replay.adoc\n> @@ -59,7 +59,9 @@ The default mode can be configured via the `replay.refAction` configuration vari\n>         be passed, but in `--advance <branch>` mode, they should have\n>         a single tip, so that it's clear where <branch> should point\n>         to. See \"Specifying Ranges\" in linkgit:git-rev-parse[1] and the\n> -       \"Commit Limiting\" options below.\n> +       \"Commit Limiting\" options below. Any commits in the range whose\n> +       changes are already present in the branch the commits are being\n> +       replayed onto will be dropped.\n>\n>  include::rev-list-options.adoc[]\n>\n> diff --git a/replay.c b/replay.c\n> index 58fdc20140b..7cd7206eee5 100644\n> --- a/replay.c\n> +++ b/replay.c\n> @@ -88,12 +88,12 @@ struct commit *replay_pick_regular_commit(struct repository *repo,\n>                                           struct merge_result *result)\n>  {\n>         struct commit *base, *replayed_base;\n> -       struct tree *pickme_tree, *base_tree;\n> +       struct tree *pickme_tree, *base_tree, *replayed_base_tree;\n>\n>         base = pickme->parents->item;\n>         replayed_base = mapped_commit(replayed_commits, base, onto);\n>\n> -       result->tree = repo_get_commit_tree(repo, replayed_base);\n> +       replayed_base_tree = repo_get_commit_tree(repo, replayed_base);\n>         pickme_tree = repo_get_commit_tree(repo, pickme);\n>         base_tree = repo_get_commit_tree(repo, base);\n>\n> @@ -103,13 +103,17 @@ struct commit *replay_pick_regular_commit(struct repository *repo,\n>\n>         merge_incore_nonrecursive(merge_opt,\n>                                   base_tree,\n> -                                 result->tree,\n> +                                 replayed_base_tree,\n>                                   pickme_tree,\n>                                   result);\n>\n>         free((char*)merge_opt->ancestor);\n>         merge_opt->ancestor = NULL;\n>         if (!result->clean)\n>                 return NULL;\n> +       /* Drop commits that become empty */\n> +       if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&\n> +           !oideq(&pickme_tree->object.oid, &base_tree->object.oid))\n> +               return replayed_base;\n>         return replay_create_commit(repo, result->tree, pickme, replayed_base);\n>  }\n\nMakes sense; your version is similar but slightly cleaner than my\nlocal implementation of the same thing.  Plus you have a test, which I\nhadn't added yet.\n\n> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\n> index cf3aacf3551..d73ab16908a 100755\n> --- a/t/t3650-replay-basics.sh\n> +++ b/t/t3650-replay-basics.sh\n> @@ -25,6 +25,8 @@ test_expect_success 'setup' '\n>         git switch -c topic3 &&\n>         test_commit G &&\n>         test_commit H &&\n> +       git switch -c empty &&\n> +       git commit --allow-empty --only -m empty &&\n>         git switch -c topic4 main &&\n>         test_commit I &&\n>         test_commit J &&\n> @@ -106,6 +108,29 @@ test_expect_success 'using replay on bare repo to perform basic cherry-pick' '\n>         test_cmp expect result-bare\n>  '\n>\n> +test_expect_success 'commits that become empty are dropped' '\n\nThis test is a bit more complicated than normal, and might benefit\nfrom a comment or two.\n\n> +       git replay --ref-action=print --advance main topic1^! >result &&\n> +       ONTO=$(cut -f 3 -d \" \" result) &&\n\nYou're basically cherry-picking one commit from the middle of A..empty\n(namely the tip of topic1) onto main, without updating any refs...\n\n> +       git replay --ref-action=print --onto $ONTO \\\n> +               --branches --ancestry-path=empty ^A >result &&\n\n...and here you replay the range A..empty onto what would have been\nthe new main, but since one of those commits were already\ncherry-picked, you expect that one to be dropped.\n\nSince \"empty\" has no descendant commits or branches, the flags\n    --branches --ancestry-path=empty ^A\nfeel like a more complicated way of saying\n   --contained A..empty\n\n> +       # Write the new value of refs/heads/empty to \"new-empty\" and\n> +       # generate a sed script that annotates the output of\n> +       # `git log --format=\"%H %s\"` with the updated branches\n> +       SCRIPT=\"$(sed -e \"\n> +               /empty/{\n> +                       h\n> +                       s|^.*empty \\([^ ]*\\) .*|\\1|wnew-empty\n> +                       g\n> +               }\n> +               s|^.*/\\([^/ ]*\\) \\([^ ]*\\).*|/^\\2/s/\\\\\\$/ (\\1)/|\n> +               \\$s|\\$|;s/^[^ ]* //|\" result)\" &&\n> +       git log --format=\"%H %s\" --stdin <new-empty >actual.raw &&\n> +       sed -e \"$SCRIPT\" actual.raw >actual &&\n> +       test_write_lines >expect \\\n> +               \"empty (empty)\" \"H (topic3)\" G \"C (topic1)\" F M L B A &&\n> +       test_cmp expect actual\n\nAfter digging around for a while (my sed-fu is far weaker than yours),\nthis feels like you are going out of your way to avoid changing any\nbranches, but then trying to figure out what the branch changes would\nhave been.  Would it be simpler to remove the --ref-action=print\nflags, check directly what changes were made, and use a\ntest_when_finished to reset the branches back to their starting point\nat the end?  That'd change this test to something like:\n\ntest_expect_success 'commits that become empty are dropped' '\n    # Save original branches\n    git for-each-ref --format=\"update %(refname) %(objectname)\"\nrefs/heads/ >original-branches &&\n    test_when_finished \"git update-ref --stdin <original-branches &&\nrm original-branches\" &&\n\n    # Cherry-pick tip of topic1 (\"F\"), from the middle of A..empty, to main\n    git replay --advance main topic1^! &&\n\n    # Replay all of A..empty onto main (which includes topic1 & thus F\nin the middle)\n    git replay --onto main --contained A..empty &&\n\n    # Check that \"F\" was applied first, then \"C\", and that \"F\" wasn't\napplied twice.  Also, that topic1 now points to \"C\".\n    git log --format=\"%s%d\" L..empty >actual &&\n    test_write_lines >expect \\\n        \"empty (empty)\" \"H (topic3)\" G \"C (topic1)\" F \"M (main)\" &&\n    test_cmp expect actual\n'\n"},{"id":"531658","messageId":"10f9afd8-6ac2-4e17-979f-2222bd0a2fda@gmail.com","threadId":"64540","inReplyTo":"CABPp-BEZFPmLnEtnD0WaNbkZ5uE7q5T6uKJQRUvtq+L=C1o9wg@mail.gmail.com","subject":"Re: [PATCH] replay: drop commits that become empty","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-12-04T14:06:51Z","receivedAt":"2025-12-04T14:07:01Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 28/11/2025 08:06, Elijah Newren wrote:\n> On Thu, Nov 27, 2025 at 8:16 AM Phillip Wood <phillip.wood123@gmail.com> wrote:\n>>\n>> From: Phillip Wood <phillip.wood@dunelm.org.uk>\n>>\n>> If the changes in a commit being replayed are already in the branch\n>> that the commits are being replayed onto then \"git replay\" creates an\n>> empty commit. This is confusing because the commit message no longer\n>> matches the contents of the commit. Drop the commit instead. Commits\n>> that start off empty are not dropped.\n> \n> Yeah, I've got a commit in my local branch that does the same thing.\n> \n> It feels like there should be a paragraph break in here somewhere, but\n> maybe that's just me?  Pretty minor either way.\n\nYes it could do with a paragraph break, I'll add one\n\n>> This matches the behavior of\n>> \"git rebase --reapply-cherry-pick --empty=drop\" and \"git cherry-pick\n>> --empty-drop\". If a branch points to a commit that is dropped it will\n>> be updated to point to the last commit that was not dropped. This can\n>> been seen in the new test where \"topic1\" is updated to point to the\n>> rebased \"C\" as \"F\" is dropped because it is already upstream. While\n>> this is a breaking change \"git replay\" is marked as experimental to\n>> allow improvements like this that change the behavior.\n> \n> Yep.\n> \n>>\n>> Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\n>> ---\n>> Elijah - I'm not really clear why we were setting result->tree before\n>> calling merge_incore_nonrecursive(), was it just for convenience to\n>> avoid declaring a local variable or have I missed something?\n> \n> I don't know the reason.  That traces back to a commit with\n> Christian's Co-authored-by, so it may have been either him or me that\n> introduced it.  My original work on replay was on a branch that I long\n> ago rebased on top of the version Christian submitted, and the old\n> history is no longer reachable from my local reflog, so I don't have a\n> way to narrow down who of us did it.  If it was him, he may be able to\n> answer.  If it was me, I've long since forgotten.  I think using a\n> temporary, as you've done, is better.\n\nThanks, I was worried I might have missed some subtlety and \ninadvertently broken a corner case.\n\n>> +       # Write the new value of refs/heads/empty to \"new-empty\" and\n>> +       # generate a sed script that annotates the output of\n>> +       # `git log --format=\"%H %s\"` with the updated branches\n>> +       SCRIPT=\"$(sed -e \"\n>> +               /empty/{\n>> +                       h\n>> +                       s|^.*empty \\([^ ]*\\) .*|\\1|wnew-empty\n>> +                       g\n>> +               }\n>> +               s|^.*/\\([^/ ]*\\) \\([^ ]*\\).*|/^\\2/s/\\\\\\$/ (\\1)/|\n>> +               \\$s|\\$|;s/^[^ ]* //|\" result)\" &&\n>> +       git log --format=\"%H %s\" --stdin <new-empty >actual.raw &&\n>> +       sed -e \"$SCRIPT\" actual.raw >actual &&\n>> +       test_write_lines >expect \\\n>> +               \"empty (empty)\" \"H (topic3)\" G \"C (topic1)\" F M L B A &&\n>> +       test_cmp expect actual\n> \n> After digging around for a while (my sed-fu is far weaker than yours),\n> this feels like you are going out of your way to avoid changing any\n> branches, but then trying to figure out what the branch changes would\n> have been.  Would it be simpler to remove the --ref-action=print\n> flags, check directly what changes were made, and use a\n> test_when_finished to reset the branches back to their starting point\n> at the end?  That'd change this test to something like:\n\nI used --ref-action=print to match the existing tests, but it would be \nmuch simpler to drop it. Your suggestion below looks good.\n\nThanks\n\nPhillip\n\n\n> test_expect_success 'commits that become empty are dropped' '\n>      # Save original branches\n>      git for-each-ref --format=\"update %(refname) %(objectname)\"\n> refs/heads/ >original-branches &&\n>      test_when_finished \"git update-ref --stdin <original-branches &&\n> rm original-branches\" &&\n> \n>      # Cherry-pick tip of topic1 (\"F\"), from the middle of A..empty, to main\n>      git replay --advance main topic1^! &&\n> \n>      # Replay all of A..empty onto main (which includes topic1 & thus F\n> in the middle)\n>      git replay --onto main --contained A..empty &&\n> \n>      # Check that \"F\" was applied first, then \"C\", and that \"F\" wasn't\n> applied twice.  Also, that topic1 now points to \"C\".\n>      git log --format=\"%s%d\" L..empty >actual &&\n>      test_write_lines >expect \\\n>          \"empty (empty)\" \"H (topic3)\" G \"C (topic1)\" F \"M (main)\" &&\n>      test_cmp expect actual\n> '\n> \n\n"},{"id":"531659","messageId":"007cd7f3-0876-4912-9a86-e549876ec9db@gmail.com","threadId":"64540","inReplyTo":"xmqqbjkmk431.fsf@gitster.g","subject":"Re: [PATCH] replay: drop commits that become empty","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-12-04T14:08:43Z","receivedAt":"2025-12-04T14:08:54Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 28/11/2025 07:29, Junio C Hamano wrote:\n> Phillip Wood <phillip.wood123@gmail.com> writes:\n>>\n>> This patch is based on ps/history\n> \n> As I take this more as a rfc/rfh than finalized version, it is OK to\n> depend on the topic that is known to be rerolled soonish.\n\nWould you rather I rebased onto master when I re-roll?\n\nThanks\n\nPhillip\n\n"},{"id":"532179","messageId":"9a81644a0ec670261a85c155fa32e5a1f4576ef4.1765793254.git.phillip.wood@dunelm.org.uk","threadId":"64540","inReplyTo":"8a2a1215306452147cc7b803530ab2429bf57f15.1764260150.git.phillip.wood@dunelm.org.uk","subject":"[PATCH v2] replay: drop commits that become empty","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-12-15T10:07:37Z","receivedAt":"2025-12-15T10:07:54Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"From: Phillip Wood <phillip.wood@dunelm.org.uk>\n\nIf the changes in a commit being replayed are already in the branch\nthat the commits are being replayed onto then \"git replay\" creates an\nempty commit. This is confusing because the commit message no longer\nmatches the contents of the commit. Drop the commit instead. Commits\nthat start off empty are not dropped. This matches the behavior of\n\"git rebase --reapply-cherry-pick --empty=drop\" and \"git cherry-pick\n--empty-drop\".\n\nIf a branch points to a commit that is dropped it will be updated to\npoint to the last commit that was not dropped. This can been seen\nin the new test where \"topic1\" is updated to point to the rebased\n\"C\" as \"F\" is dropped because it is already upstream. While this is\na breaking change \"git replay\" is marked as experimental to allow\nimprovements like this that change the behavior.\n\nHelped-by: Elijah Newren <newren@gmail.com>\nSigned-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\n---\nChanges since v1:\n\n - modified test to update refs as suggested by Elijah. I've kept\n   --ancestry-path --branches rather than switching to --contained as\n   I think it is useful to have test coverage for those options and it\n   means we can check that empty commits are dropped with out replying\n   on --contained working.\n\nThis patch is based on ps/history\n\nI think dropping commits that become empty is the sensible default,\nif it turns out that some users are relying on the current behavior\nwe can add an option to retain the empty commits.\n\nBase-Commit: d37c42ea661434c347d2047f01b338341099fa60\nPublished-As: https://github.com/phillipwood/git/releases/tag/pw%2Freplay-drop-commits-that-become-empty%2Fv2\nView-Changes-At: https://github.com/phillipwood/git/compare/d37c42ea6...9a81644a0\nFetch-It-Via: git fetch https://github.com/phillipwood/git pw/replay-drop-commits-that-become-empty/v2\n\n Documentation/git-replay.adoc |  4 +++-\n replay.c                      | 10 +++++++---\n t/t3650-replay-basics.sh      | 21 +++++++++++++++++++++\n 3 files changed, 31 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex dcb26e8a8e8..96a3a557bf3 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -59,7 +59,9 @@ The default mode can be configured via the `replay.refAction` configuration vari\n \tbe passed, but in `--advance <branch>` mode, they should have\n \ta single tip, so that it's clear where <branch> should point\n \tto. See \"Specifying Ranges\" in linkgit:git-rev-parse[1] and the\n-\t\"Commit Limiting\" options below.\n+\t\"Commit Limiting\" options below. Any commits in the range whose\n+\tchanges are already present in the branch the commits are being\n+\treplayed onto will be dropped.\n \n include::rev-list-options.adoc[]\n \ndiff --git a/replay.c b/replay.c\nindex 13983dbc566..2864c213993 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -88,12 +88,12 @@ struct commit *replay_pick_regular_commit(struct repository *repo,\n \t\t\t\t\t  struct merge_result *result)\n {\n \tstruct commit *base, *replayed_base;\n-\tstruct tree *pickme_tree, *base_tree;\n+\tstruct tree *pickme_tree, *base_tree, *replayed_base_tree;\n \n \tbase = pickme->parents->item;\n \treplayed_base = mapped_commit(replayed_commits, base, onto);\n \n-\tresult->tree = repo_get_commit_tree(repo, replayed_base);\n+\treplayed_base_tree = repo_get_commit_tree(repo, replayed_base);\n \tpickme_tree = repo_get_commit_tree(repo, pickme);\n \tbase_tree = repo_get_commit_tree(repo, base);\n \n@@ -103,13 +103,17 @@ struct commit *replay_pick_regular_commit(struct repository *repo,\n \n \tmerge_incore_nonrecursive(merge_opt,\n \t\t\t\t  base_tree,\n-\t\t\t\t  result->tree,\n+\t\t\t\t  replayed_base_tree,\n \t\t\t\t  pickme_tree,\n \t\t\t\t  result);\n \n \tfree((char*)merge_opt->ancestor);\n \tmerge_opt->ancestor = NULL;\n \tif (!result->clean)\n \t\treturn NULL;\n+\t/* Drop commits that become empty */\n+\tif (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&\n+\t    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))\n+\t\treturn replayed_base;\n \treturn replay_create_commit(repo, result->tree, pickme, replayed_base);\n }\ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex cf3aacf3551..9d4b0dd1a77 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -25,6 +25,8 @@ test_expect_success 'setup' '\n \tgit switch -c topic3 &&\n \ttest_commit G &&\n \ttest_commit H &&\n+\tgit switch -c empty &&\n+\tgit commit --allow-empty --only -m empty &&\n \tgit switch -c topic4 main &&\n \ttest_commit I &&\n \ttest_commit J &&\n@@ -106,6 +108,25 @@ test_expect_success 'using replay on bare repo to perform basic cherry-pick' '\n \ttest_cmp expect result-bare\n '\n \n+test_expect_success 'commits that become empty are dropped' '\n+\t# Save original branches\n+\tgit for-each-ref --format=\"update %(refname) %(objectname)\" \\\n+\t\trefs/heads/ >original-branches &&\n+\ttest_when_finished \"git update-ref --stdin <original-branches &&\n+\t\trm original-branches\" &&\n+\t# Cherry-pick tip of topic1 (\"F\"), from the middle of A..empty, to main\n+\tgit replay --advance main topic1^! &&\n+\n+\t# Replay all of A..empty onto main (which includes topic1 & thus F\n+\t# in the middle)\n+\tgit replay --onto main --branches --ancestry-path=empty ^A \\\n+\t\t>result &&\n+\tgit log --format=\"%s%d\" L..empty >actual &&\n+\ttest_write_lines >expect \\\n+\t\t\"empty (empty)\" \"H (topic3)\" G \"C (topic1)\" \"F (main)\" \"M (tag: M)\" &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'replay on bare repo fails with both --advance and --onto' '\n \ttest_must_fail git -C bare replay --advance main --onto main topic1..topic2 >result-bare\n '\n\nRange-diff against v1:\n1:  8a2a1215306 ! 1:  9a81644a0ec replay: drop commits that become empty\n    @@ Commit message\n         matches the contents of the commit. Drop the commit instead. Commits\n         that start off empty are not dropped. This matches the behavior of\n         \"git rebase --reapply-cherry-pick --empty=drop\" and \"git cherry-pick\n    -    --empty-drop\". If a branch points to a commit that is dropped it will\n    -    be updated to point to the last commit that was not dropped. This can\n    -    been seen in the new test where \"topic1\" is updated to point to the\n    -    rebased \"C\" as \"F\" is dropped because it is already upstream. While\n    -    this is a breaking change \"git replay\" is marked as experimental to\n    -    allow improvements like this that change the behavior.\n    -\n    +    --empty-drop\".\n    +\n    +    If a branch points to a commit that is dropped it will be updated to\n    +    point to the last commit that was not dropped. This can been seen\n    +    in the new test where \"topic1\" is updated to point to the rebased\n    +    \"C\" as \"F\" is dropped because it is already upstream. While this is\n    +    a breaking change \"git replay\" is marked as experimental to allow\n    +    improvements like this that change the behavior.\n    +\n    +    Helped-by: Elijah Newren <newren@gmail.com>\n         Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\n     \n      ## Documentation/git-replay.adoc ##\n    @@ t/t3650-replay-basics.sh: test_expect_success 'using replay on bare repo to perf\n      '\n      \n     +test_expect_success 'commits that become empty are dropped' '\n    -+\tgit replay --ref-action=print --advance main topic1^! >result &&\n    -+\tONTO=$(cut -f 3 -d \" \" result) &&\n    -+\tgit replay --ref-action=print --onto $ONTO \\\n    -+\t\t--branches --ancestry-path=empty ^A >result &&\n    -+\t# Write the new value of refs/heads/empty to \"new-empty\" and\n    -+\t# generate a sed script that annotates the output of\n    -+\t# `git log --format=\"%H %s\"` with the updated branches\n    -+\tSCRIPT=\"$(sed -e \"\n    -+\t\t/empty/{\n    -+\t\t\th\n    -+\t\t\ts|^.*empty \\([^ ]*\\) .*|\\1|wnew-empty\n    -+\t\t\tg\n    -+\t\t}\n    -+\t\ts|^.*/\\([^/ ]*\\) \\([^ ]*\\).*|/^\\2/s/\\\\\\$/ (\\1)/|\n    -+\t\t\\$s|\\$|;s/^[^ ]* //|\" result)\" &&\n    -+\tgit log --format=\"%H %s\" --stdin <new-empty >actual.raw &&\n    -+\tsed -e \"$SCRIPT\" actual.raw >actual &&\n    ++\t# Save original branches\n    ++\tgit for-each-ref --format=\"update %(refname) %(objectname)\" \\\n    ++\t\trefs/heads/ >original-branches &&\n    ++\ttest_when_finished \"git update-ref --stdin <original-branches &&\n    ++\t\trm original-branches\" &&\n    ++\t# Cherry-pick tip of topic1 (\"F\"), from the middle of A..empty, to main\n    ++\tgit replay --advance main topic1^! &&\n    ++\n    ++\t# Replay all of A..empty onto main (which includes topic1 & thus F\n    ++\t# in the middle)\n    ++\tgit replay --onto main --branches --ancestry-path=empty ^A \\\n    ++\t\t>result &&\n    ++\tgit log --format=\"%s%d\" L..empty >actual &&\n     +\ttest_write_lines >expect \\\n    -+\t\t\"empty (empty)\" \"H (topic3)\" G \"C (topic1)\" F M L B A &&\n    ++\t\t\"empty (empty)\" \"H (topic3)\" G \"C (topic1)\" \"F (main)\" \"M (tag: M)\" &&\n     +\ttest_cmp expect actual\n     +'\n     +\n-- \n2.52.0.362.g884e03848a9\n\n"},{"id":"532220","messageId":"xmqqpl8f719x.fsf@gitster.g","threadId":"64540","inReplyTo":"9a81644a0ec670261a85c155fa32e5a1f4576ef4.1765793254.git.phillip.wood@dunelm.org.uk","subject":"Re: [PATCH v2] replay: drop commits that become empty","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-15T23:50:34Z","receivedAt":"2025-12-15T23:50:36Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> From: Phillip Wood <phillip.wood@dunelm.org.uk>\n>\n> If the changes in a commit being replayed are already in the branch\n> that the commits are being replayed onto then \"git replay\" creates an\n> empty commit. This is confusing because the commit message no longer\n> matches the contents of the commit. Drop the commit instead. Commits\n> that start off empty are not dropped. This matches the behavior of\n> \"git rebase --reapply-cherry-pick --empty=drop\" and \"git cherry-pick\n> --empty-drop\".\n\nOK.  Maybe it is just me but \"onto then\" -> \"onto,\" would flow the\nsentence better?\n\n> If a branch points to a commit that is dropped it will be updated to\n> point to the last commit that was not dropped. This can been seen\n\nIf one thinks about it, it is the only natural behaviour to use the\nlast surviving commit to point the branch at.  Thanks for spelling\nit out so clearly.\n\nBTW, \"can been seen\" -> \"can be seen\" (will amend locally).\n\n> in the new test where \"topic1\" is updated to point to the rebased\n> \"C\" as \"F\" is dropped because it is already upstream. While this is\n> a breaking change \"git replay\" is marked as experimental to allow\n> improvements like this that change the behavior.\n\nAgain maybe it is just me, but I'd prefer to see a comma after \"a\nbreaking change\" to flow the sentence better.\n\n> Helped-by: Elijah Newren <newren@gmail.com>\n> Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\n> ---\n> ...\n> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\n> index dcb26e8a8e8..96a3a557bf3 100644\n> --- a/Documentation/git-replay.adoc\n> +++ b/Documentation/git-replay.adoc\n> @@ -59,7 +59,9 @@ The default mode can be configured via the `replay.refAction` configuration vari\n>  \tbe passed, but in `--advance <branch>` mode, they should have\n>  \ta single tip, so that it's clear where <branch> should point\n>  \tto. See \"Specifying Ranges\" in linkgit:git-rev-parse[1] and the\n> -\t\"Commit Limiting\" options below.\n> +\t\"Commit Limiting\" options below. Any commits in the range whose\n> +\tchanges are already present in the branch the commits are being\n> +\treplayed onto will be dropped.\n\nOK.\n\n> diff --git a/replay.c b/replay.c\n> index 13983dbc566..2864c213993 100644\n> --- a/replay.c\n> +++ b/replay.c\n> @@ -88,12 +88,12 @@ struct commit *replay_pick_regular_commit(struct repository *repo,\n>  \t\t\t\t\t  struct merge_result *result)\n>  {\n>  \tstruct commit *base, *replayed_base;\n> -\tstruct tree *pickme_tree, *base_tree;\n> +\tstruct tree *pickme_tree, *base_tree, *replayed_base_tree;\n>  \n>  \tbase = pickme->parents->item;\n>  \treplayed_base = mapped_commit(replayed_commits, base, onto);\n>  \n> -\tresult->tree = repo_get_commit_tree(repo, replayed_base);\n> +\treplayed_base_tree = repo_get_commit_tree(repo, replayed_base);\n>  \tpickme_tree = repo_get_commit_tree(repo, pickme);\n>  \tbase_tree = repo_get_commit_tree(repo, base);\n>  \n> @@ -103,13 +103,17 @@ struct commit *replay_pick_regular_commit(struct repository *repo,\n>  \n>  \tmerge_incore_nonrecursive(merge_opt,\n>  \t\t\t\t  base_tree,\n> -\t\t\t\t  result->tree,\n> +\t\t\t\t  replayed_base_tree,\n>  \t\t\t\t  pickme_tree,\n>  \t\t\t\t  result);\n>  \n>  \tfree((char*)merge_opt->ancestor);\n>  \tmerge_opt->ancestor = NULL;\n>  \tif (!result->clean)\n>  \t\treturn NULL;\n> +\t/* Drop commits that become empty */\n> +\tif (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&\n> +\t    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))\n> +\t\treturn replayed_base;\n>  \treturn replay_create_commit(repo, result->tree, pickme, replayed_base);\n>  }\n\nOK, that is straight-forward.  Instead of overriding the\nresult->tree upfront, we try the same using a temporary\nreplayed_base_tree, and that allows us to see if the resulting tree\ncomputed by merge_incore matches.  Only when it made a non-empty\nchange, we proceed to create a new commit.\n\n> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\n> index cf3aacf3551..9d4b0dd1a77 100755\n> --- a/t/t3650-replay-basics.sh\n> +++ b/t/t3650-replay-basics.sh\n> @@ -25,6 +25,8 @@ test_expect_success 'setup' '\n>  \tgit switch -c topic3 &&\n>  \ttest_commit G &&\n>  \ttest_commit H &&\n> +\tgit switch -c empty &&\n> +\tgit commit --allow-empty --only -m empty &&\n\nThe use of \"--only\" here is a bit curious.  As there is no change\nbetween the index and the commit our \"empty\" branch points at,\nwouldn't it be unnecessary?  The option, together with --allow-empty,\nwould only matter if you did\n\n\tgit switch -c empty &&\n\tmodify blah &&\n\tgit add blah &&\n\tgit commit --allow-empty --only -m empty\n\nbecause without --only, the changes to blah will be taken.\n"},{"id":"532222","messageId":"CABPp-BEDB5y7WnHj_omETTbp+Eim+k8u12cv_9zEj1gB4Dw=jA@mail.gmail.com","threadId":"64540","inReplyTo":"9a81644a0ec670261a85c155fa32e5a1f4576ef4.1765793254.git.phillip.wood@dunelm.org.uk","subject":"Re: [PATCH v2] replay: drop commits that become empty","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-12-16T00:21:16Z","receivedAt":"2025-12-16T00:21:28Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Mon, Dec 15, 2025 at 2:07 AM Phillip Wood <phillip.wood123@gmail.com> wrote:\n>\n> From: Phillip Wood <phillip.wood@dunelm.org.uk>\n>\n> If the changes in a commit being replayed are already in the branch\n> that the commits are being replayed onto then \"git replay\" creates an\n> empty commit. This is confusing because the commit message no longer\n> matches the contents of the commit. Drop the commit instead. Commits\n> that start off empty are not dropped. This matches the behavior of\n> \"git rebase --reapply-cherry-pick --empty=drop\" and \"git cherry-pick\n> --empty-drop\".\n>\n> If a branch points to a commit that is dropped it will be updated to\n> point to the last commit that was not dropped. This can been seen\n> in the new test where \"topic1\" is updated to point to the rebased\n> \"C\" as \"F\" is dropped because it is already upstream. While this is\n> a breaking change \"git replay\" is marked as experimental to allow\n> improvements like this that change the behavior.\n>\n> Helped-by: Elijah Newren <newren@gmail.com>\n> Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\n> ---\n> Changes since v1:\n>\n>  - modified test to update refs as suggested by Elijah. I've kept\n>    --ancestry-path --branches rather than switching to --contained as\n>    I think it is useful to have test coverage for those options and it\n>    means we can check that empty commits are dropped with out replying\n>    on --contained working.\n\nFair enough.\n\n> This patch is based on ps/history\n>\n> I think dropping commits that become empty is the sensible default,\n> if it turns out that some users are relying on the current behavior\n> we can add an option to retain the empty commits.\n>\n> Base-Commit: d37c42ea661434c347d2047f01b338341099fa60\n> Published-As: https://github.com/phillipwood/git/releases/tag/pw%2Freplay-drop-commits-that-become-empty%2Fv2\n> View-Changes-At: https://github.com/phillipwood/git/compare/d37c42ea6...9a81644a0\n> Fetch-It-Via: git fetch https://github.com/phillipwood/git pw/replay-drop-commits-that-become-empty/v2\n>\n>  Documentation/git-replay.adoc |  4 +++-\n>  replay.c                      | 10 +++++++---\n>  t/t3650-replay-basics.sh      | 21 +++++++++++++++++++++\n>  3 files changed, 31 insertions(+), 4 deletions(-)\n>\n> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\n> index dcb26e8a8e8..96a3a557bf3 100644\n> --- a/Documentation/git-replay.adoc\n> +++ b/Documentation/git-replay.adoc\n> @@ -59,7 +59,9 @@ The default mode can be configured via the `replay.refAction` configuration vari\n>         be passed, but in `--advance <branch>` mode, they should have\n>         a single tip, so that it's clear where <branch> should point\n>         to. See \"Specifying Ranges\" in linkgit:git-rev-parse[1] and the\n> -       \"Commit Limiting\" options below.\n> +       \"Commit Limiting\" options below. Any commits in the range whose\n> +       changes are already present in the branch the commits are being\n> +       replayed onto will be dropped.\n>\n>  include::rev-list-options.adoc[]\n>\n> diff --git a/replay.c b/replay.c\n> index 13983dbc566..2864c213993 100644\n> --- a/replay.c\n> +++ b/replay.c\n> @@ -88,12 +88,12 @@ struct commit *replay_pick_regular_commit(struct repository *repo,\n>                                           struct merge_result *result)\n>  {\n>         struct commit *base, *replayed_base;\n> -       struct tree *pickme_tree, *base_tree;\n> +       struct tree *pickme_tree, *base_tree, *replayed_base_tree;\n>\n>         base = pickme->parents->item;\n>         replayed_base = mapped_commit(replayed_commits, base, onto);\n>\n> -       result->tree = repo_get_commit_tree(repo, replayed_base);\n> +       replayed_base_tree = repo_get_commit_tree(repo, replayed_base);\n>         pickme_tree = repo_get_commit_tree(repo, pickme);\n>         base_tree = repo_get_commit_tree(repo, base);\n>\n> @@ -103,13 +103,17 @@ struct commit *replay_pick_regular_commit(struct repository *repo,\n>\n>         merge_incore_nonrecursive(merge_opt,\n>                                   base_tree,\n> -                                 result->tree,\n> +                                 replayed_base_tree,\n>                                   pickme_tree,\n>                                   result);\n>\n>         free((char*)merge_opt->ancestor);\n>         merge_opt->ancestor = NULL;\n>         if (!result->clean)\n>                 return NULL;\n> +       /* Drop commits that become empty */\n> +       if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&\n> +           !oideq(&pickme_tree->object.oid, &base_tree->object.oid))\n> +               return replayed_base;\n>         return replay_create_commit(repo, result->tree, pickme, replayed_base);\n>  }\n> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\n> index cf3aacf3551..9d4b0dd1a77 100755\n> --- a/t/t3650-replay-basics.sh\n> +++ b/t/t3650-replay-basics.sh\n> @@ -25,6 +25,8 @@ test_expect_success 'setup' '\n>         git switch -c topic3 &&\n>         test_commit G &&\n>         test_commit H &&\n> +       git switch -c empty &&\n> +       git commit --allow-empty --only -m empty &&\n>         git switch -c topic4 main &&\n>         test_commit I &&\n>         test_commit J &&\n> @@ -106,6 +108,25 @@ test_expect_success 'using replay on bare repo to perform basic cherry-pick' '\n>         test_cmp expect result-bare\n>  '\n>\n> +test_expect_success 'commits that become empty are dropped' '\n> +       # Save original branches\n> +       git for-each-ref --format=\"update %(refname) %(objectname)\" \\\n> +               refs/heads/ >original-branches &&\n> +       test_when_finished \"git update-ref --stdin <original-branches &&\n> +               rm original-branches\" &&\n> +       # Cherry-pick tip of topic1 (\"F\"), from the middle of A..empty, to main\n> +       git replay --advance main topic1^! &&\n> +\n> +       # Replay all of A..empty onto main (which includes topic1 & thus F\n> +       # in the middle)\n> +       git replay --onto main --branches --ancestry-path=empty ^A \\\n> +               >result &&\n> +       git log --format=\"%s%d\" L..empty >actual &&\n> +       test_write_lines >expect \\\n> +               \"empty (empty)\" \"H (topic3)\" G \"C (topic1)\" \"F (main)\" \"M (tag: M)\" &&\n> +       test_cmp expect actual\n> +'\n> +\n>  test_expect_success 'replay on bare repo fails with both --advance and --onto' '\n>         test_must_fail git -C bare replay --advance main --onto main topic1..topic2 >result-bare\n>  '\n\nI like the minor edits Junio suggested, but otherwise this version\nlooks good to me.  Thanks!\n"},{"id":"532272","messageId":"80477d23-eed3-4a99-be97-f692bc36095e@gmail.com","threadId":"64540","inReplyTo":"xmqqpl8f719x.fsf@gitster.g","subject":"Re: [PATCH v2] replay: drop commits that become empty","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-12-16T14:19:11Z","receivedAt":"2025-12-16T14:19:15Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 15/12/2025 23:50, Junio C Hamano wrote:\n> Phillip Wood <phillip.wood123@gmail.com> writes:\n> \n>> From: Phillip Wood <phillip.wood@dunelm.org.uk>\n>>\n>> If the changes in a commit being replayed are already in the branch\n>> that the commits are being replayed onto then \"git replay\" creates an\n>> empty commit. This is confusing because the commit message no longer\n>> matches the contents of the commit. Drop the commit instead. Commits\n>> that start off empty are not dropped. This matches the behavior of\n>> \"git rebase --reapply-cherry-pick --empty=drop\" and \"git cherry-pick\n>> --empty-drop\".\n> \n> OK.  Maybe it is just me but \"onto then\" -> \"onto,\" would flow the\n> sentence better?\n\nI agree it reads better with a comma here and in the second paragraph, \nI'll re-roll\n\nThanks\n\nPhillip\n\n>> If a branch points to a commit that is dropped it will be updated to\n>> point to the last commit that was not dropped. This can been seen\n> \n> If one thinks about it, it is the only natural behaviour to use the\n> last surviving commit to point the branch at.  Thanks for spelling\n> it out so clearly.\n> \n> BTW, \"can been seen\" -> \"can be seen\" (will amend locally).\n> \n>> in the new test where \"topic1\" is updated to point to the rebased\n>> \"C\" as \"F\" is dropped because it is already upstream. While this is\n>> a breaking change \"git replay\" is marked as experimental to allow\n>> improvements like this that change the behavior.\n> \n> Again maybe it is just me, but I'd prefer to see a comma after \"a\n> breaking change\" to flow the sentence better.\n> \n>> Helped-by: Elijah Newren <newren@gmail.com>\n>> Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\n>> ---\n>> ...\n>> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\n>> index dcb26e8a8e8..96a3a557bf3 100644\n>> --- a/Documentation/git-replay.adoc\n>> +++ b/Documentation/git-replay.adoc\n>> @@ -59,7 +59,9 @@ The default mode can be configured via the `replay.refAction` configuration vari\n>>   \tbe passed, but in `--advance <branch>` mode, they should have\n>>   \ta single tip, so that it's clear where <branch> should point\n>>   \tto. See \"Specifying Ranges\" in linkgit:git-rev-parse[1] and the\n>> -\t\"Commit Limiting\" options below.\n>> +\t\"Commit Limiting\" options below. Any commits in the range whose\n>> +\tchanges are already present in the branch the commits are being\n>> +\treplayed onto will be dropped.\n> \n> OK.\n> \n>> diff --git a/replay.c b/replay.c\n>> index 13983dbc566..2864c213993 100644\n>> --- a/replay.c\n>> +++ b/replay.c\n>> @@ -88,12 +88,12 @@ struct commit *replay_pick_regular_commit(struct repository *repo,\n>>   \t\t\t\t\t  struct merge_result *result)\n>>   {\n>>   \tstruct commit *base, *replayed_base;\n>> -\tstruct tree *pickme_tree, *base_tree;\n>> +\tstruct tree *pickme_tree, *base_tree, *replayed_base_tree;\n>>   \n>>   \tbase = pickme->parents->item;\n>>   \treplayed_base = mapped_commit(replayed_commits, base, onto);\n>>   \n>> -\tresult->tree = repo_get_commit_tree(repo, replayed_base);\n>> +\treplayed_base_tree = repo_get_commit_tree(repo, replayed_base);\n>>   \tpickme_tree = repo_get_commit_tree(repo, pickme);\n>>   \tbase_tree = repo_get_commit_tree(repo, base);\n>>   \n>> @@ -103,13 +103,17 @@ struct commit *replay_pick_regular_commit(struct repository *repo,\n>>   \n>>   \tmerge_incore_nonrecursive(merge_opt,\n>>   \t\t\t\t  base_tree,\n>> -\t\t\t\t  result->tree,\n>> +\t\t\t\t  replayed_base_tree,\n>>   \t\t\t\t  pickme_tree,\n>>   \t\t\t\t  result);\n>>   \n>>   \tfree((char*)merge_opt->ancestor);\n>>   \tmerge_opt->ancestor = NULL;\n>>   \tif (!result->clean)\n>>   \t\treturn NULL;\n>> +\t/* Drop commits that become empty */\n>> +\tif (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&\n>> +\t    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))\n>> +\t\treturn replayed_base;\n>>   \treturn replay_create_commit(repo, result->tree, pickme, replayed_base);\n>>   }\n> \n> OK, that is straight-forward.  Instead of overriding the\n> result->tree upfront, we try the same using a temporary\n> replayed_base_tree, and that allows us to see if the resulting tree\n> computed by merge_incore matches.  Only when it made a non-empty\n> change, we proceed to create a new commit.\n> \n>> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\n>> index cf3aacf3551..9d4b0dd1a77 100755\n>> --- a/t/t3650-replay-basics.sh\n>> +++ b/t/t3650-replay-basics.sh\n>> @@ -25,6 +25,8 @@ test_expect_success 'setup' '\n>>   \tgit switch -c topic3 &&\n>>   \ttest_commit G &&\n>>   \ttest_commit H &&\n>> +\tgit switch -c empty &&\n>> +\tgit commit --allow-empty --only -m empty &&\n> \n> The use of \"--only\" here is a bit curious.  As there is no change\n> between the index and the commit our \"empty\" branch points at,\n> wouldn't it be unnecessary?  The option, together with --allow-empty,\n> would only matter if you did\n> \n> \tgit switch -c empty &&\n> \tmodify blah &&\n> \tgit add blah &&\n> \tgit commit --allow-empty --only -m empty\n> \n> because without --only, the changes to blah will be taken.\n\n"},{"id":"532273","messageId":"73ba74b8a2e7aaa625e6f0689a9f900ceebaaa03.1765894781.git.phillip.wood@dunelm.org.uk","threadId":"64540","inReplyTo":"8a2a1215306452147cc7b803530ab2429bf57f15.1764260150.git.phillip.wood@dunelm.org.uk","subject":"[PATCH v3] replay: drop commits that become empty","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-12-16T14:19:43Z","receivedAt":"2025-12-16T14:19:59Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"From: Phillip Wood <phillip.wood@dunelm.org.uk>\n\nIf the changes in a commit being replayed are already in the branch\nthat the commits are being replayed onto, then \"git replay\" creates an\nempty commit. This is confusing because the commit message no longer\nmatches the contents of the commit. Drop the commit instead. Commits\nthat start off empty are not dropped. This matches the behavior of\n\"git rebase --reapply-cherry-pick --empty=drop\" and \"git cherry-pick\n--empty-drop\".\n\nIf a branch points to a commit that is dropped it will be updated\nto point to the last commit that was not dropped. This can be seen\nin the new test where \"topic1\" is updated to point to the rebased\n\"C\" as \"F\" is dropped because it is already upstream. While this is\na breaking change, \"git replay\" is marked as experimental to allow\nimprovements like this that change the behavior.\n\nHelped-by: Elijah Newren <newren@gmail.com>\nSigned-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\n---\nChanges since v2:\n\n - added a couple of commas to the commit message as suggested by Junio\n\nChanges since v1:\n\n - modified test to update refs as suggested by Elijah. I've kept\n   --ancestry-path --branches rather than switching to --contained as\n   I think it is useful to have test coverage for those options and it\n   means we can check that empty commits are dropped with out replying\n   on --contained working.\n\nThis patch is based on ps/history\n\nI think dropping commits that become empty is the sensible default,\nif it turns out that some users are relying on the current behavior\nwe can add an option to retain the empty commits.\n\nBase-Commit: d37c42ea661434c347d2047f01b338341099fa60\nPublished-As: https://github.com/phillipwood/git/releases/tag/pw%2Freplay-drop-commits-that-become-empty%2Fv3\nView-Changes-At: https://github.com/phillipwood/git/compare/d37c42ea6...73ba74b8a\nFetch-It-Via: git fetch https://github.com/phillipwood/git pw/replay-drop-commits-that-become-empty/v3\n\n Documentation/git-replay.adoc |  4 +++-\n replay.c                      | 10 +++++++---\n t/t3650-replay-basics.sh      | 21 +++++++++++++++++++++\n 3 files changed, 31 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex dcb26e8a8e8..96a3a557bf3 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -59,7 +59,9 @@ The default mode can be configured via the `replay.refAction` configuration vari\n \tbe passed, but in `--advance <branch>` mode, they should have\n \ta single tip, so that it's clear where <branch> should point\n \tto. See \"Specifying Ranges\" in linkgit:git-rev-parse[1] and the\n-\t\"Commit Limiting\" options below.\n+\t\"Commit Limiting\" options below. Any commits in the range whose\n+\tchanges are already present in the branch the commits are being\n+\treplayed onto will be dropped.\n \n include::rev-list-options.adoc[]\n \ndiff --git a/replay.c b/replay.c\nindex 13983dbc566..2864c213993 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -88,12 +88,12 @@ struct commit *replay_pick_regular_commit(struct repository *repo,\n \t\t\t\t\t  struct merge_result *result)\n {\n \tstruct commit *base, *replayed_base;\n-\tstruct tree *pickme_tree, *base_tree;\n+\tstruct tree *pickme_tree, *base_tree, *replayed_base_tree;\n \n \tbase = pickme->parents->item;\n \treplayed_base = mapped_commit(replayed_commits, base, onto);\n \n-\tresult->tree = repo_get_commit_tree(repo, replayed_base);\n+\treplayed_base_tree = repo_get_commit_tree(repo, replayed_base);\n \tpickme_tree = repo_get_commit_tree(repo, pickme);\n \tbase_tree = repo_get_commit_tree(repo, base);\n \n@@ -103,13 +103,17 @@ struct commit *replay_pick_regular_commit(struct repository *repo,\n \n \tmerge_incore_nonrecursive(merge_opt,\n \t\t\t\t  base_tree,\n-\t\t\t\t  result->tree,\n+\t\t\t\t  replayed_base_tree,\n \t\t\t\t  pickme_tree,\n \t\t\t\t  result);\n \n \tfree((char*)merge_opt->ancestor);\n \tmerge_opt->ancestor = NULL;\n \tif (!result->clean)\n \t\treturn NULL;\n+\t/* Drop commits that become empty */\n+\tif (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&\n+\t    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))\n+\t\treturn replayed_base;\n \treturn replay_create_commit(repo, result->tree, pickme, replayed_base);\n }\ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex cf3aacf3551..9d4b0dd1a77 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -25,6 +25,8 @@ test_expect_success 'setup' '\n \tgit switch -c topic3 &&\n \ttest_commit G &&\n \ttest_commit H &&\n+\tgit switch -c empty &&\n+\tgit commit --allow-empty --only -m empty &&\n \tgit switch -c topic4 main &&\n \ttest_commit I &&\n \ttest_commit J &&\n@@ -106,6 +108,25 @@ test_expect_success 'using replay on bare repo to perform basic cherry-pick' '\n \ttest_cmp expect result-bare\n '\n \n+test_expect_success 'commits that become empty are dropped' '\n+\t# Save original branches\n+\tgit for-each-ref --format=\"update %(refname) %(objectname)\" \\\n+\t\trefs/heads/ >original-branches &&\n+\ttest_when_finished \"git update-ref --stdin <original-branches &&\n+\t\trm original-branches\" &&\n+\t# Cherry-pick tip of topic1 (\"F\"), from the middle of A..empty, to main\n+\tgit replay --advance main topic1^! &&\n+\n+\t# Replay all of A..empty onto main (which includes topic1 & thus F\n+\t# in the middle)\n+\tgit replay --onto main --branches --ancestry-path=empty ^A \\\n+\t\t>result &&\n+\tgit log --format=\"%s%d\" L..empty >actual &&\n+\ttest_write_lines >expect \\\n+\t\t\"empty (empty)\" \"H (topic3)\" G \"C (topic1)\" \"F (main)\" \"M (tag: M)\" &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'replay on bare repo fails with both --advance and --onto' '\n \ttest_must_fail git -C bare replay --advance main --onto main topic1..topic2 >result-bare\n '\n\nRange-diff against v2:\n1:  9a81644a0ec ! 1:  73ba74b8a2e replay: drop commits that become empty\n    @@ Commit message\n         replay: drop commits that become empty\n     \n         If the changes in a commit being replayed are already in the branch\n    -    that the commits are being replayed onto then \"git replay\" creates an\n    +    that the commits are being replayed onto, then \"git replay\" creates an\n         empty commit. This is confusing because the commit message no longer\n         matches the contents of the commit. Drop the commit instead. Commits\n         that start off empty are not dropped. This matches the behavior of\n         \"git rebase --reapply-cherry-pick --empty=drop\" and \"git cherry-pick\n         --empty-drop\".\n     \n    -    If a branch points to a commit that is dropped it will be updated to\n    -    point to the last commit that was not dropped. This can been seen\n    +    If a branch points to a commit that is dropped it will be updated\n    +    to point to the last commit that was not dropped. This can be seen\n         in the new test where \"topic1\" is updated to point to the rebased\n         \"C\" as \"F\" is dropped because it is already upstream. While this is\n    -    a breaking change \"git replay\" is marked as experimental to allow\n    +    a breaking change, \"git replay\" is marked as experimental to allow\n         improvements like this that change the behavior.\n     \n         Helped-by: Elijah Newren <newren@gmail.com>\n-- \n2.52.0.362.g884e03848a9\n\n"},{"id":"532286","messageId":"CABPp-BHH2NaLc9tFmO1hKcY4O6jZJU05+65viR1T_yBaarCwrA@mail.gmail.com","threadId":"64540","inReplyTo":"73ba74b8a2e7aaa625e6f0689a9f900ceebaaa03.1765894781.git.phillip.wood@dunelm.org.uk","subject":"Re: [PATCH v3] replay: drop commits that become empty","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-12-16T16:36:16Z","receivedAt":"2025-12-16T16:36:28Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Dec 16, 2025 at 6:19 AM Phillip Wood <phillip.wood123@gmail.com> wrote:\n>\n> From: Phillip Wood <phillip.wood@dunelm.org.uk>\n>\n> If the changes in a commit being replayed are already in the branch\n> that the commits are being replayed onto, then \"git replay\" creates an\n> empty commit. This is confusing because the commit message no longer\n> matches the contents of the commit. Drop the commit instead. Commits\n> that start off empty are not dropped. This matches the behavior of\n> \"git rebase --reapply-cherry-pick --empty=drop\" and \"git cherry-pick\n> --empty-drop\".\n>\n> If a branch points to a commit that is dropped it will be updated\n> to point to the last commit that was not dropped. This can be seen\n> in the new test where \"topic1\" is updated to point to the rebased\n> \"C\" as \"F\" is dropped because it is already upstream. While this is\n> a breaking change, \"git replay\" is marked as experimental to allow\n> improvements like this that change the behavior.\n>\n> Helped-by: Elijah Newren <newren@gmail.com>\n> Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\n> ---\n> Changes since v2:\n>\n>  - added a couple of commas to the commit message as suggested by Junio\n\n - also changes \"can been seen\" to \"can be seen\"\n\nI'm also curious if you are keeping the \"--only\" in the testcase\nintentionally, or overlooked that part of Junio's feedback.\n\n\nAnyway, this round looks good to me.\n"},{"id":"532370","messageId":"d54c50ef-9d6c-498c-aca3-ed4461733190@gmail.com","threadId":"64540","inReplyTo":"xmqqpl8f719x.fsf@gitster.g","subject":"Re: [PATCH v2] replay: drop commits that become empty","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-12-17T14:45:54Z","receivedAt":"2025-12-17T14:45:57Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 15/12/2025 23:50, Junio C Hamano wrote:\n> Phillip Wood <phillip.wood123@gmail.com> writes:\n>> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\n>> index cf3aacf3551..9d4b0dd1a77 100755\n>> --- a/t/t3650-replay-basics.sh\n>> +++ b/t/t3650-replay-basics.sh\n>> @@ -25,6 +25,8 @@ test_expect_success 'setup' '\n>>   \tgit switch -c topic3 &&\n>>   \ttest_commit G &&\n>>   \ttest_commit H &&\n>> +\tgit switch -c empty &&\n>> +\tgit commit --allow-empty --only -m empty &&\n> \n> The use of \"--only\" here is a bit curious.  As there is no change\n> between the index and the commit our \"empty\" branch points at,\n> wouldn't it be unnecessary?  The option, together with --allow-empty,\n> would only matter if you did\n> \n> \tgit switch -c empty &&\n> \tmodify blah &&\n> \tgit add blah &&\n> \tgit commit --allow-empty --only -m empty\n> \n> because without --only, the changes to blah will be taken.\n\nI've got into the habit of always adding \"--only\" when I want to create \nan empty commit in case there are staged changes. I don't really like \n\"--allow-empty\" as I've never wanted to create commit that might or \nmight not be empty - either I want to create an empty commit in which \ncase I don't want to commit any staged changes, or I want the commit to \nfail if there are no staged changes). I can remove it if you want.\n\nThanks\n\nPhillip\n\n"},{"id":"532371","messageId":"fe18c90c-ec6a-42e4-a6e6-30623482d7f7@gmail.com","threadId":"64540","inReplyTo":"CABPp-BHH2NaLc9tFmO1hKcY4O6jZJU05+65viR1T_yBaarCwrA@mail.gmail.com","subject":"Re: [PATCH v3] replay: drop commits that become empty","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-12-17T14:47:31Z","receivedAt":"2025-12-17T14:47:33Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 16/12/2025 16:36, Elijah Newren wrote:\n> \n> I'm also curious if you are keeping the \"--only\" in the testcase\n> intentionally, or overlooked that part of Junio's feedback.\n\nOh, well spotted I'd forgotten to reply to that, thanks for pointing it out\n\nThanks\n\nPhillip\n\n"},{"id":"532392","messageId":"xmqqtsxozn2a.fsf@gitster.g","threadId":"64540","inReplyTo":"d54c50ef-9d6c-498c-aca3-ed4461733190@gmail.com","subject":"Re: [PATCH v2] replay: drop commits that become empty","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-17T23:49:17Z","receivedAt":"2025-12-17T23:49:19Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n>> \tgit commit --allow-empty --only -m empty\n>> \n>> because without --only, the changes to blah will be taken.\n>\n> I've got into the habit of always adding \"--only\" when I want to create \n> an empty commit in case there are staged changes. I don't really like \n> \"--allow-empty\" as I've never wanted to create commit that might or \n> might not be empty - either I want to create an empty commit in which \n> case I don't want to commit any staged changes, or I want the commit to \n> fail if there are no staged changes). I can remove it if you want.\n\nBeing explicit when you are unsure is good, but in this script I\nthink we should be very sure that the index matches HEAD, so I would\nconsider that the only effect of the use of the \"--only\" here is to\npuzzle readers.\n\nA comment \"# force an empty commit by including no paths\" before the\ncommand would work to help unpuzzle readers, though ;-)\n\nThanks.\n\n"},{"id":"532478","messageId":"375adc4e941f3bb22a2b12ee26a083951ed724dd.1766076625.git.phillip.wood@dunelm.org.uk","threadId":"64540","inReplyTo":"8a2a1215306452147cc7b803530ab2429bf57f15.1764260150.git.phillip.wood@dunelm.org.uk","subject":"[PATCH v4] replay: drop commits that become empty","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-12-18T16:50:26Z","receivedAt":"2025-12-18T16:50:41Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"From: Phillip Wood <phillip.wood@dunelm.org.uk>\n\nIf the changes in a commit being replayed are already in the branch\nthat the commits are being replayed onto, then \"git replay\" creates an\nempty commit. This is confusing because the commit message no longer\nmatches the contents of the commit. Drop the commit instead. Commits\nthat start off empty are not dropped. This matches the behavior of\n\"git rebase --reapply-cherry-pick --empty=drop\" and \"git cherry-pick\n--empty-drop\".\n\nIf a branch points to a commit that is dropped it will be updated\nto point to the last commit that was not dropped. This can be seen\nin the new test where \"topic1\" is updated to point to the rebased\n\"C\" as \"F\" is dropped because it is already upstream. While this is\na breaking change, \"git replay\" is marked as experimental to allow\nimprovements like this that change the behavior.\n\nHelped-by: Elijah Newren <newren@gmail.com>\nSigned-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\n---\nChanges since v3:\n\n - dropped \"--only\" when creating an empty commit\n\nChanges since v2:\n\n - added a couple of commas to the commit message as suggested by Junio\n\nChanges since v1:\n\n - modified test to update refs as suggested by Elijah. I've kept\n   --ancestry-path --branches rather than switching to --contained as\n   I think it is useful to have test coverage for those options and it\n   means we can check that empty commits are dropped with out replying\n   on --contained working.\n\nThis patch is based on ps/history\n\nI think dropping commits that become empty is the sensible default,\nif it turns out that some users are relying on the current behavior\nwe can add an option to retain the empty commits.\n\nBase-Commit: d37c42ea661434c347d2047f01b338341099fa60\nPublished-As: https://github.com/phillipwood/git/releases/tag/pw%2Freplay-drop-commits-that-become-empty%2Fv4\nView-Changes-At: https://github.com/phillipwood/git/compare/d37c42ea6...375adc4e9\nFetch-It-Via: git fetch https://github.com/phillipwood/git pw/replay-drop-commits-that-become-empty/v4\n\n Documentation/git-replay.adoc |  4 +++-\n replay.c                      | 10 +++++++---\n t/t3650-replay-basics.sh      | 21 +++++++++++++++++++++\n 3 files changed, 31 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex dcb26e8a8e8..96a3a557bf3 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -59,7 +59,9 @@ The default mode can be configured via the `replay.refAction` configuration vari\n \tbe passed, but in `--advance <branch>` mode, they should have\n \ta single tip, so that it's clear where <branch> should point\n \tto. See \"Specifying Ranges\" in linkgit:git-rev-parse[1] and the\n-\t\"Commit Limiting\" options below.\n+\t\"Commit Limiting\" options below. Any commits in the range whose\n+\tchanges are already present in the branch the commits are being\n+\treplayed onto will be dropped.\n \n include::rev-list-options.adoc[]\n \ndiff --git a/replay.c b/replay.c\nindex 13983dbc566..2864c213993 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -88,12 +88,12 @@ struct commit *replay_pick_regular_commit(struct repository *repo,\n \t\t\t\t\t  struct merge_result *result)\n {\n \tstruct commit *base, *replayed_base;\n-\tstruct tree *pickme_tree, *base_tree;\n+\tstruct tree *pickme_tree, *base_tree, *replayed_base_tree;\n \n \tbase = pickme->parents->item;\n \treplayed_base = mapped_commit(replayed_commits, base, onto);\n \n-\tresult->tree = repo_get_commit_tree(repo, replayed_base);\n+\treplayed_base_tree = repo_get_commit_tree(repo, replayed_base);\n \tpickme_tree = repo_get_commit_tree(repo, pickme);\n \tbase_tree = repo_get_commit_tree(repo, base);\n \n@@ -103,13 +103,17 @@ struct commit *replay_pick_regular_commit(struct repository *repo,\n \n \tmerge_incore_nonrecursive(merge_opt,\n \t\t\t\t  base_tree,\n-\t\t\t\t  result->tree,\n+\t\t\t\t  replayed_base_tree,\n \t\t\t\t  pickme_tree,\n \t\t\t\t  result);\n \n \tfree((char*)merge_opt->ancestor);\n \tmerge_opt->ancestor = NULL;\n \tif (!result->clean)\n \t\treturn NULL;\n+\t/* Drop commits that become empty */\n+\tif (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&\n+\t    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))\n+\t\treturn replayed_base;\n \treturn replay_create_commit(repo, result->tree, pickme, replayed_base);\n }\ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex cf3aacf3551..b3fb8869600 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -25,6 +25,8 @@ test_expect_success 'setup' '\n \tgit switch -c topic3 &&\n \ttest_commit G &&\n \ttest_commit H &&\n+\tgit switch -c empty &&\n+\tgit commit --allow-empty -m empty &&\n \tgit switch -c topic4 main &&\n \ttest_commit I &&\n \ttest_commit J &&\n@@ -106,6 +108,25 @@ test_expect_success 'using replay on bare repo to perform basic cherry-pick' '\n \ttest_cmp expect result-bare\n '\n \n+test_expect_success 'commits that become empty are dropped' '\n+\t# Save original branches\n+\tgit for-each-ref --format=\"update %(refname) %(objectname)\" \\\n+\t\trefs/heads/ >original-branches &&\n+\ttest_when_finished \"git update-ref --stdin <original-branches &&\n+\t\trm original-branches\" &&\n+\t# Cherry-pick tip of topic1 (\"F\"), from the middle of A..empty, to main\n+\tgit replay --advance main topic1^! &&\n+\n+\t# Replay all of A..empty onto main (which includes topic1 & thus F\n+\t# in the middle)\n+\tgit replay --onto main --branches --ancestry-path=empty ^A \\\n+\t\t>result &&\n+\tgit log --format=\"%s%d\" L..empty >actual &&\n+\ttest_write_lines >expect \\\n+\t\t\"empty (empty)\" \"H (topic3)\" G \"C (topic1)\" \"F (main)\" \"M (tag: M)\" &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'replay on bare repo fails with both --advance and --onto' '\n \ttest_must_fail git -C bare replay --advance main --onto main topic1..topic2 >result-bare\n '\n\nRange-diff against v3:\n1:  73ba74b8a2e ! 1:  375adc4e941 replay: drop commits that become empty\n    @@ t/t3650-replay-basics.sh: test_expect_success 'setup' '\n      \ttest_commit G &&\n      \ttest_commit H &&\n     +\tgit switch -c empty &&\n    -+\tgit commit --allow-empty --only -m empty &&\n    ++\tgit commit --allow-empty -m empty &&\n      \tgit switch -c topic4 main &&\n      \ttest_commit I &&\n      \ttest_commit J &&\n-- \n2.52.0.362.g884e03848a9\n\n"},{"id":"532515","messageId":"xmqqv7i3w05n.fsf@gitster.g","threadId":"64540","inReplyTo":"375adc4e941f3bb22a2b12ee26a083951ed724dd.1766076625.git.phillip.wood@dunelm.org.uk","subject":"Re: [PATCH v4] replay: drop commits that become empty","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-19T04:44:36Z","receivedAt":"2025-12-19T04:44:39Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> From: Phillip Wood <phillip.wood@dunelm.org.uk>\n> ...\n> Helped-by: Elijah Newren <newren@gmail.com>\n> Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\n> ---\n> Changes since v3:\n>\n>  - dropped \"--only\" when creating an empty commit\n>\n> Changes since v2:\n>\n>  - added a couple of commas to the commit message as suggested by Junio\n>\n> Changes since v1:\n>\n>  - modified test to update refs as suggested by Elijah. I've kept\n>    --ancestry-path --branches rather than switching to --contained as\n>    I think it is useful to have test coverage for those options and it\n>    means we can check that empty commits are dropped with out replying\n>    on --contained working.\n>\n> This patch is based on ps/history\n>\n> I think dropping commits that become empty is the sensible default,\n> if it turns out that some users are relying on the current behavior\n> we can add an option to retain the empty commits.\n\nThanks.  Will replace.\n\nBut I am not sure what the next move for this topic would be, until\nthe base topic ps/history is sorted out.  There was a discussion\nbetween \"it is experimental, the early adopters should be prepared\nthat the behaviour can and will change\" and \"the behaviour being\nquestioned is so fundamental in the workflow, it is impossible to\nfix retrospecitively\".  \n"}]}