aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhillip Wood <phillip.wood@dunelm.org.uk>2025-12-18 16:50:26 +0000
committerJunio C Hamano <gitster@pobox.com>2026-01-13 06:13:37 -0800
commit0ee71f4bd035db61342c2c5a25984e4545347c11 (patch)
treefcd0a547f000500708633f2dbfbbd935de8a7f6d
parent9e4a786c3db10205fbc8c6a0aa1f14c4ca325760 (diff)
downloadgit-0ee71f4bd035db61342c2c5a25984e4545347c11.tar.xz
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>
-rw-r--r--Documentation/git-replay.adoc4
-rw-r--r--replay.c10
-rwxr-xr-xt/t3650-replay-basics.sh21
3 files changed, 31 insertions, 4 deletions
diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index 4c61f3aa1f..dc966486ca 100644
--- a/Documentation/git-replay.adoc
+++ b/Documentation/git-replay.adoc
@@ -62,7 +62,9 @@ The default mode can be configured via the `replay.refAction` configuration vari
Range of commits to replay; see "Specifying Ranges" in
linkgit:git-rev-parse[1]. In `--advance <branch>` mode, the
range should have a single tip, so that it's clear to which tip the
- advanced <branch> should point.
+ advanced <branch> should point. 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 94fb76384b..f97d652f33 100644
--- a/replay.c
+++ b/replay.c
@@ -217,12 +217,12 @@ static struct commit *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);
@@ -232,7 +232,7 @@ static struct commit *pick_regular_commit(struct repository *repo,
merge_incore_nonrecursive(merge_opt,
base_tree,
- result->tree,
+ replayed_base_tree,
pickme_tree,
result);
@@ -240,6 +240,10 @@ static struct commit *pick_regular_commit(struct repository *repo,
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 create_commit(repo, result->tree, pickme, replayed_base);
}
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index c862aa39f3..a03f8f9293 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 &&
@@ -160,6 +162,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
'