replay: drop commits that become empty

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> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Phillip Wood committed Dec 18, 2025 at 16:50 UTC 0ee71f4bd035db61342c2c5a25984e4545347c11
3 files changed +31 -4
Documentation/git-replay.adoc
+3 -1
@@ -62,7 +62,9 @@ The default mode can be configured via the `replay.refAction` configuration vari
62 Range of commits to replay; see "Specifying Ranges" in
63 linkgit:git-rev-parse[1]. In `--advance <branch>` mode, the
64 range should have a single tip, so that it's clear to which tip the
65 - advanced <branch> should point.
65 + advanced <branch> should point. Any commits in the range whose
66 + changes are already present in the branch the commits are being
67 + replayed onto will be dropped.
68
69 include::rev-list-options.adoc[]
70
replay.c
+7 -3
@@ -217,12 +217,12 @@ static struct commit *pick_regular_commit(struct repository *repo,
217 struct merge_result *result)
218 {
219 struct commit *base, *replayed_base;
220 - struct tree *pickme_tree, *base_tree;
220 + struct tree *pickme_tree, *base_tree, *replayed_base_tree;
221
222 base = pickme->parents->item;
223 replayed_base = mapped_commit(replayed_commits, base, onto);
224
225 - result->tree = repo_get_commit_tree(repo, replayed_base);
225 + replayed_base_tree = repo_get_commit_tree(repo, replayed_base);
226 pickme_tree = repo_get_commit_tree(repo, pickme);
227 base_tree = repo_get_commit_tree(repo, base);
228
@@ -232,7 +232,7 @@ static struct commit *pick_regular_commit(struct repository *repo,
232
233 merge_incore_nonrecursive(merge_opt,
234 base_tree,
235 - result->tree,
235 + replayed_base_tree,
236 pickme_tree,
237 result);
238
@@ -240,6 +240,10 @@ static struct commit *pick_regular_commit(struct repository *repo,
240 merge_opt->ancestor = NULL;
241 if (!result->clean)
242 return NULL;
243 + /* Drop commits that become empty */
244 + if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&
245 + !oideq(&pickme_tree->object.oid, &base_tree->object.oid))
246 + return replayed_base;
247 return create_commit(repo, result->tree, pickme, replayed_base);
248 }
249
t/t3650-replay-basics.sh
+21
@@ -25,6 +25,8 @@ test_expect_success 'setup' '
25 git switch -c topic3 &&
26 test_commit G &&
27 test_commit H &&
28 + git switch -c empty &&
29 + git commit --allow-empty -m empty &&
30 git switch -c topic4 main &&
31 test_commit I &&
32 test_commit J &&
@@ -160,6 +162,25 @@ test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
162 test_cmp expect result-bare
163 '
164
165 +test_expect_success 'commits that become empty are dropped' '
166 + # Save original branches
167 + git for-each-ref --format="update %(refname) %(objectname)" \
168 + refs/heads/ >original-branches &&
169 + test_when_finished "git update-ref --stdin <original-branches &&
170 + rm original-branches" &&
171 + # Cherry-pick tip of topic1 ("F"), from the middle of A..empty, to main
172 + git replay --advance main topic1^! &&
173 +
174 + # Replay all of A..empty onto main (which includes topic1 & thus F
175 + # in the middle)
176 + git replay --onto main --branches --ancestry-path=empty ^A \
177 + >result &&
178 + git log --format="%s%d" L..empty >actual &&
179 + test_write_lines >expect \
180 + "empty (empty)" "H (topic3)" G "C (topic1)" "F (main)" "M (tag: M)" &&
181 + test_cmp expect actual
182 +'
183 +
184 test_expect_success 'replay on bare repo fails with both --advance and --onto' '
185 test_must_fail git -C bare replay --advance main --onto main topic1..topic2 >result-bare
186 '