git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH v4] replay: drop commits that become empty

From
PWPhillip Wood <phillip.wood123@gmail.com>
Date
Dec 18, 2025, 16:50 UTC
Message-ID
<375adc4e941f3bb22a2b12ee26a083951ed724dd.1766076625.git.phillip.wood@dunelm.org.uk>
In-Reply-To
<8a2a1215306452147cc7b803530ab2429bf57f15.1764260150.git.phillip.wood@dunelm.org.uk>
From: Phillip Wood <phillip.wood@dunelm.org.uk>

If the changes in a commit being replayed are already in the branch that the commits are being replayed onto, then "git replay" creates an empty commit. This is confusing because the commit message no longer matches the contents of the commit. Drop the commit instead. Commits that start off empty are not dropped. This matches the behavior of "git rebase --reapply-cherry-pick --empty=drop" and "git cherry-pick --empty-drop".

If a branch points to a commit that is dropped it will be updated to point to the last commit that was not dropped. This can be seen in the new test where "topic1" is updated to point to the rebased "C" as "F" is dropped because it is already upstream. While this is a breaking change, "git replay" is marked as experimental to allow improvements like this that change the behavior.

Helped-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>
---
Changes since v3:
 - dropped "--only" when creating an empty commit
Changes since v2:
 - added a couple of commas to the commit message as suggested by Junio
Changes since v1:
 - modified test to update refs as suggested by Elijah. I've kept
   --ancestry-path --branches rather than switching to --contained as
   I think it is useful to have test coverage for those options and it
   means we can check that empty commits are dropped with out replying
   on --contained working.
This patch is based on ps/history

I think dropping commits that become empty is the sensible default, if it turns out that some users are relying on the current behavior we can add an option to retain the empty commits.

Base-Commit: d37c42ea661434c347d2047f01b338341099fa60
Published-As: https://github.com/phillipwood/git/releases/tag/pw%2Freplay-drop-commits-that-become-empty%2Fv4
View-Changes-At: https://github.com/phillipwood/git/compare/d37c42ea6...375adc4e9
Fetch-It-Via: git fetch https://github.com/phillipwood/git pw/replay-drop-commits-that-become-empty/v4
 Documentation/git-replay.adoc |  4 +++-
 replay.c                      | 10 +++++++---
 t/t3650-replay-basics.sh      | 21 +++++++++++++++++++++
 3 files changed, 31 insertions(+), 4 deletions(-)
diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index dcb26e8a8e8..96a3a557bf3 100644
--- a/Documentation/git-replay.adoc
+++ b/Documentation/git-replay.adoc
@@ -59,7 +59,9 @@ The default mode can be configured via the `replay.refAction` configuration vari
 	be passed, but in `--advance <branch>` mode, they should have
 	a single tip, so that it's clear where <branch> should point
 	to. See "Specifying Ranges" in linkgit:git-rev-parse[1] and the
-	"Commit Limiting" options below.
+	"Commit Limiting" options below. Any commits in the range whose
+	changes are already present in the branch the commits are being
+	replayed onto will be dropped.
 
 include::rev-list-options.adoc[]
 
diff --git a/replay.c b/replay.c
index 13983dbc566..2864c213993 100644
--- a/replay.c
+++ b/replay.c
@@ -88,12 +88,12 @@ struct commit *replay_pick_regular_commit(struct repository *repo,
 					  struct merge_result *result)
 {
 	struct commit *base, *replayed_base;
-	struct tree *pickme_tree, *base_tree;
+	struct tree *pickme_tree, *base_tree, *replayed_base_tree;
 
 	base = pickme->parents->item;
 	replayed_base = mapped_commit(replayed_commits, base, onto);
 
-	result->tree = repo_get_commit_tree(repo, replayed_base);
+	replayed_base_tree = repo_get_commit_tree(repo, replayed_base);
 	pickme_tree = repo_get_commit_tree(repo, pickme);
 	base_tree = repo_get_commit_tree(repo, base);
 
@@ -103,13 +103,17 @@ struct commit *replay_pick_regular_commit(struct repository *repo,
 
 	merge_incore_nonrecursive(merge_opt,
 				  base_tree,
-				  result->tree,
+				  replayed_base_tree,
 				  pickme_tree,
 				  result);
 
 	free((char*)merge_opt->ancestor);
 	merge_opt->ancestor = NULL;
 	if (!result->clean)
 		return NULL;
+	/* Drop commits that become empty */
+	if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&
+	    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))
+		return replayed_base;
 	return replay_create_commit(repo, result->tree, pickme, replayed_base);
 }
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index cf3aacf3551..b3fb8869600 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -25,6 +25,8 @@ test_expect_success 'setup' '
 	git switch -c topic3 &&
 	test_commit G &&
 	test_commit H &&
+	git switch -c empty &&
+	git commit --allow-empty -m empty &&
 	git switch -c topic4 main &&
 	test_commit I &&
 	test_commit J &&
@@ -106,6 +108,25 @@ test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
 	test_cmp expect result-bare
 '
 
+test_expect_success 'commits that become empty are dropped' '
+	# Save original branches
+	git for-each-ref --format="update %(refname) %(objectname)" \
+		refs/heads/ >original-branches &&
+	test_when_finished "git update-ref --stdin <original-branches &&
+		rm original-branches" &&
+	# Cherry-pick tip of topic1 ("F"), from the middle of A..empty, to main
+	git replay --advance main topic1^! &&
+
+	# Replay all of A..empty onto main (which includes topic1 & thus F
+	# in the middle)
+	git replay --onto main --branches --ancestry-path=empty ^A \
+		>result &&
+	git log --format="%s%d" L..empty >actual &&
+	test_write_lines >expect \
+		"empty (empty)" "H (topic3)" G "C (topic1)" "F (main)" "M (tag: M)" &&
+	test_cmp expect actual
+'
+
 test_expect_success 'replay on bare repo fails with both --advance and --onto' '
 	test_must_fail git -C bare replay --advance main --onto main topic1..topic2 >result-bare
 '

Range-diff against v3:
1:  73ba74b8a2e ! 1:  375adc4e941 replay: drop commits that become empty
    @@ t/t3650-replay-basics.sh: test_expect_success 'setup' '
      	test_commit G &&
      	test_commit H &&
     +	git switch -c empty &&
    -+	git commit --allow-empty --only -m empty &&
    ++	git commit --allow-empty -m empty &&
      	git switch -c topic4 main &&
      	test_commit I &&
      	test_commit J &&
-- 
2.52.0.362.g884e03848a9
Previous: Phillip WoodNext: Junio C Hamano
Message 15 of 16 in “replay: drop commits that become empty”
  1. replay: drop commits that become emptyPhillip Wood, Nov 27, 2025
  2. Junio C HamanoNov 28, 2025
  3. Phillip WoodDec 4, 2025
  4. Elijah NewrenNov 28, 2025
  5. Phillip WoodDec 4, 2025
  6. replay: drop commits that become emptyPhillip Wood, Dec 15, 2025
  7. Junio C HamanoDec 15, 2025
  8. Phillip WoodDec 16, 2025
  9. Phillip WoodDec 17, 2025
  10. Junio C HamanoDec 17, 2025
  11. Elijah NewrenDec 16, 2025
  12. replay: drop commits that become emptyPhillip Wood, Dec 16, 2025
  13. Elijah NewrenDec 16, 2025
  14. Phillip WoodDec 17, 2025
  15. replay: drop commits that become emptyPhillip Wood, Dec 18, 2025
  16. Junio C HamanoDec 19, 2025

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.