replay: support replaying down from root commit

git-replay(1) doesn't allow replaying commits all the way down to the root commit. Fix that. Signed-off-by: Toon Claes <toon@iotcl.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Toon Claes committed Mar 24, 2026 at 20:35 UTC e8b79a96ebaa2113391d14bfcdabe239f6ff8611
2 files changed +24 -13
replay.c
+17 -10
@@ -209,7 +209,10 @@ static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,
209 struct commit *commit,
210 struct commit *fallback)
211 {
212 - khint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);
212 + khint_t pos;
213 + if (!commit)
214 + return fallback;
215 + pos = kh_get_oid_map(replayed_commits, commit->object.oid);
216 if (pos == kh_end(replayed_commits))
217 return fallback;
218 return kh_value(replayed_commits, pos);
@@ -225,16 +228,24 @@ static struct commit *pick_regular_commit(struct repository *repo,
228 struct commit *base, *replayed_base;
229 struct tree *pickme_tree, *base_tree, *replayed_base_tree;
230
228 - base = pickme->parents->item;
229 - replayed_base = mapped_commit(replayed_commits, base, onto);
231 + if (pickme->parents) {
232 + base = pickme->parents->item;
233 + base_tree = repo_get_commit_tree(repo, base);
234 + } else {
235 + base = NULL;
236 + base_tree = lookup_tree(repo, repo->hash_algo->empty_tree);
237 + }
238
239 + replayed_base = mapped_commit(replayed_commits, base, onto);
240 replayed_base_tree = repo_get_commit_tree(repo, replayed_base);
241 pickme_tree = repo_get_commit_tree(repo, pickme);
233 - base_tree = repo_get_commit_tree(repo, base);
242
243 merge_opt->branch1 = short_commit_name(repo, replayed_base);
244 merge_opt->branch2 = short_commit_name(repo, pickme);
237 - merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
245 + if (pickme->parents)
246 + merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
247 + else
248 + merge_opt->ancestor = xstrdup("empty tree");
249
250 merge_incore_nonrecursive(merge_opt,
251 base_tree,
@@ -293,8 +304,6 @@ int replay_revisions(struct rev_info *revs,
304 set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,
305 &detached_head, &advance, &onto, &update_refs);
306
296 - /* FIXME: Should allow replaying commits with the first as a root commit */
297 -
307 if (prepare_revision_walk(revs) < 0) {
308 ret = error(_("error preparing revisions"));
309 goto out;
@@ -309,9 +318,7 @@ int replay_revisions(struct rev_info *revs,
318 khint_t pos;
319 int hr;
320
312 - if (!commit->parents)
313 - die(_("replaying down from root commit is not supported yet!"));
314 - if (commit->parents->next)
321 + if (commit->parents && commit->parents->next)
322 die(_("replaying merge commits is not supported yet!"));
323
324 last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
t/t3650-replay-basics.sh
+7 -3
@@ -81,9 +81,13 @@ test_expect_success 'option --onto or --advance is mandatory' '
81 test_cmp expect actual
82 '
83
84 -test_expect_success 'no base or negative ref gives no-replaying down to root error' '
85 - echo "fatal: replaying down from root commit is not supported yet!" >expect &&
86 - test_must_fail git replay --onto=topic1 topic2 2>actual &&
84 +test_expect_success 'replay down to root onto another branch' '
85 + git replay --ref-action=print --onto main topic2 >result &&
86 +
87 + test_line_count = 1 result &&
88 +
89 + git log --format=%s $(cut -f 3 -d " " result) >actual &&
90 + test_write_lines E D C M L B A >expect &&
91 test_cmp expect actual
92 '
93