builtin/replay: extract core logic to replay revisions

We're about to move the core logic used to replay revisions onto a new base into the "libgit.a" library. Prepare for this by pulling out the logic into a new function `replay_revisions()` that: 1. Takes a set of revisions to replay and some options that tell it how it ought to replay the revisions. 2. Replays the commits. 3. Records any reference updates that would be caused by replaying the commits in a structure that is owned by the caller. The logic itself will be moved into a separate file in the next commit. This change is not expected to cause user-visible change in behaviour. Signed-off-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrick Steinhardt committed Jan 13, 2026 at 10:54 UTC 1454743eb84eea1f7e4ea23e0556ed5ef9888894
1 file changed +162 -107
builtin/replay.c
+162 -107
@@ -177,8 +177,9 @@ static void set_up_replay_mode(struct repository *repo,
177 if (!rinfo.positive_refexprs)
178 die(_("need some commits to replay"));
179
180 - die_for_incompatible_opt2(!!onto_name, "--onto",
181 - !!*advance_name, "--advance");
180 + if (!onto_name == !*advance_name)
181 + BUG("one and only one of onto_name and *advance_name must be given");
182 +
183 if (onto_name) {
184 *onto = peel_committish(repo, onto_name, "--onto");
185 if (rinfo.positive_refexprs <
@@ -253,6 +254,134 @@ static struct commit *pick_regular_commit(struct repository *repo,
254 return create_commit(repo, result->tree, pickme, replayed_base);
255 }
256
257 +struct replay_revisions_options {
258 + const char *advance;
259 + const char *onto;
260 + int contained;
261 +};
262 +
263 +struct replay_result {
264 + struct replay_ref_update {
265 + char *refname;
266 + struct object_id old_oid;
267 + struct object_id new_oid;
268 + } *updates;
269 + size_t updates_nr, updates_alloc;
270 +};
271 +
272 +static void replay_result_release(struct replay_result *result)
273 +{
274 + for (size_t i = 0; i < result->updates_nr; i++)
275 + free(result->updates[i].refname);
276 + free(result->updates);
277 +}
278 +
279 +static void replay_result_queue_update(struct replay_result *result,
280 + const char *refname,
281 + const struct object_id *old_oid,
282 + const struct object_id *new_oid)
283 +{
284 + ALLOC_GROW(result->updates, result->updates_nr + 1, result->updates_alloc);
285 + result->updates[result->updates_nr].refname = xstrdup(refname);
286 + result->updates[result->updates_nr].old_oid = *old_oid;
287 + result->updates[result->updates_nr].new_oid = *new_oid;
288 + result->updates_nr++;
289 +}
290 +
291 +static int replay_revisions(struct rev_info *revs,
292 + struct replay_revisions_options *opts,
293 + struct replay_result *out)
294 +{
295 + kh_oid_map_t *replayed_commits = NULL;
296 + struct strset *update_refs = NULL;
297 + struct commit *last_commit = NULL;
298 + struct commit *commit;
299 + struct commit *onto = NULL;
300 + struct merge_options merge_opt;
301 + struct merge_result result;
302 + char *advance;
303 + int ret;
304 +
305 + advance = xstrdup_or_null(opts->advance);
306 + set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto, &advance,
307 + &onto, &update_refs);
308 +
309 + /* FIXME: Should allow replaying commits with the first as a root commit */
310 +
311 + if (prepare_revision_walk(revs) < 0) {
312 + ret = error(_("error preparing revisions"));
313 + goto out;
314 + }
315 +
316 + init_basic_merge_options(&merge_opt, revs->repo);
317 + memset(&result, 0, sizeof(result));
318 + merge_opt.show_rename_progress = 0;
319 + last_commit = onto;
320 + replayed_commits = kh_init_oid_map();
321 + while ((commit = get_revision(revs))) {
322 + const struct name_decoration *decoration;
323 + khint_t pos;
324 + int hr;
325 +
326 + if (!commit->parents)
327 + die(_("replaying down from root commit is not supported yet!"));
328 + if (commit->parents->next)
329 + die(_("replaying merge commits is not supported yet!"));
330 +
331 + last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
332 + onto, &merge_opt, &result);
333 + if (!last_commit)
334 + break;
335 +
336 + /* Record commit -> last_commit mapping */
337 + pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);
338 + if (hr == 0)
339 + BUG("Duplicate rewritten commit: %s\n",
340 + oid_to_hex(&commit->object.oid));
341 + kh_value(replayed_commits, pos) = last_commit;
342 +
343 + /* Update any necessary branches */
344 + if (advance)
345 + continue;
346 + decoration = get_name_decoration(&commit->object);
347 + if (!decoration)
348 + continue;
349 + while (decoration) {
350 + if (decoration->type == DECORATION_REF_LOCAL &&
351 + (opts->contained || strset_contains(update_refs,
352 + decoration->name))) {
353 + replay_result_queue_update(out, decoration->name,
354 + &commit->object.oid,
355 + &last_commit->object.oid);
356 + }
357 + decoration = decoration->next;
358 + }
359 + }
360 +
361 + if (!result.clean) {
362 + ret = 1;
363 + goto out;
364 + }
365 +
366 + /* In --advance mode, advance the target ref */
367 + if (advance)
368 + replay_result_queue_update(out, advance,
369 + &onto->object.oid,
370 + &last_commit->object.oid);
371 +
372 + ret = 0;
373 +
374 +out:
375 + if (update_refs) {
376 + strset_clear(update_refs);
377 + free(update_refs);
378 + }
379 + kh_destroy_oid_map(replayed_commits);
380 + merge_finalize(&merge_opt, &result);
381 + free(advance);
382 + return ret;
383 +}
384 +
385 static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)
386 {
387 if (!ref_action || !strcmp(ref_action, "update"))
@@ -306,21 +435,11 @@ int cmd_replay(int argc,
435 const char *prefix,
436 struct repository *repo)
437 {
309 - const char *advance_name_opt = NULL;
310 - char *advance_name = NULL;
311 - struct commit *onto = NULL;
312 - const char *onto_name = NULL;
313 - int contained = 0;
438 + struct replay_revisions_options opts = { 0 };
439 + struct replay_result result = { 0 };
440 const char *ref_action = NULL;
441 enum ref_action_mode ref_mode;
316 -
442 struct rev_info revs;
318 - struct commit *last_commit = NULL;
319 - struct commit *commit;
320 - struct merge_options merge_opt;
321 - struct merge_result result;
322 - struct strset *update_refs = NULL;
323 - kh_oid_map_t *replayed_commits;
443 struct ref_transaction *transaction = NULL;
444 struct strbuf transaction_err = STRBUF_INIT;
445 struct strbuf reflog_msg = STRBUF_INIT;
@@ -333,13 +452,13 @@ int cmd_replay(int argc,
452 NULL
453 };
454 struct option replay_options[] = {
336 - OPT_STRING(0, "advance", &advance_name_opt,
455 + OPT_STRING(0, "advance", &opts.advance,
456 N_("branch"),
457 N_("make replay advance given branch")),
339 - OPT_STRING(0, "onto", &onto_name,
458 + OPT_STRING(0, "onto", &opts.onto,
459 N_("revision"),
460 N_("replay onto given commit")),
342 - OPT_BOOL(0, "contained", &contained,
461 + OPT_BOOL(0, "contained", &opts.contained,
462 N_("update all branches that point at commits in <revision-range>")),
463 OPT_STRING(0, "ref-action", &ref_action,
464 N_("mode"),
@@ -350,19 +469,19 @@ int cmd_replay(int argc,
469 argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
470 PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
471
353 - if (!onto_name && !advance_name_opt) {
472 + if (!opts.onto && !opts.advance) {
473 error(_("option --onto or --advance is mandatory"));
474 usage_with_options(replay_usage, replay_options);
475 }
476
358 - die_for_incompatible_opt2(!!advance_name_opt, "--advance",
359 - contained, "--contained");
477 + die_for_incompatible_opt2(!!opts.advance, "--advance",
478 + opts.contained, "--contained");
479 + die_for_incompatible_opt2(!!opts.advance, "--advance",
480 + !!opts.onto, "--onto");
481
482 /* Parse ref action mode from command line or config */
483 ref_mode = get_ref_action_mode(repo, ref_action);
484
364 - advance_name = xstrdup_or_null(advance_name_opt);
365 -
485 repo_init_revisions(repo, &revs, prefix);
486
487 /*
@@ -414,18 +533,19 @@ int cmd_replay(int argc,
533 revs.simplify_history = 0;
534 }
535
417 - set_up_replay_mode(repo, &revs.cmdline,
418 - onto_name, &advance_name,
419 - &onto, &update_refs);
420 -
421 - /* FIXME: Should allow replaying commits with the first as a root commit */
536 + ret = replay_revisions(&revs, &opts, &result);
537 + if (ret)
538 + goto cleanup;
539
540 /* Build reflog message */
424 - if (advance_name_opt)
425 - strbuf_addf(&reflog_msg, "replay --advance %s", advance_name_opt);
426 - else
427 - strbuf_addf(&reflog_msg, "replay --onto %s",
428 - oid_to_hex(&onto->object.oid));
541 + if (opts.advance) {
542 + strbuf_addf(&reflog_msg, "replay --advance %s", opts.advance);
543 + } else {
544 + struct object_id oid;
545 + if (repo_get_oid_committish(repo, opts.onto, &oid))
546 + BUG("--onto commit should have been resolved beforehand already");
547 + strbuf_addf(&reflog_msg, "replay --onto %s", oid_to_hex(&oid));
548 + }
549
550 /* Initialize ref transaction if using update mode */
551 if (ref_mode == REF_ACTION_UPDATE) {
@@ -438,78 +558,19 @@ int cmd_replay(int argc,
558 }
559 }
560
441 - if (prepare_revision_walk(&revs) < 0) {
442 - ret = error(_("error preparing revisions"));
443 - goto cleanup;
444 - }
445 -
446 - init_basic_merge_options(&merge_opt, repo);
447 - memset(&result, 0, sizeof(result));
448 - merge_opt.show_rename_progress = 0;
449 - last_commit = onto;
450 - replayed_commits = kh_init_oid_map();
451 - while ((commit = get_revision(&revs))) {
452 - const struct name_decoration *decoration;
453 - khint_t pos;
454 - int hr;
455 -
456 - if (!commit->parents)
457 - die(_("replaying down from root commit is not supported yet!"));
458 - if (commit->parents->next)
459 - die(_("replaying merge commits is not supported yet!"));
460 -
461 - last_commit = pick_regular_commit(repo, commit, replayed_commits,
462 - onto, &merge_opt, &result);
463 - if (!last_commit)
464 - break;
465 -
466 - /* Record commit -> last_commit mapping */
467 - pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);
468 - if (hr == 0)
469 - BUG("Duplicate rewritten commit: %s\n",
470 - oid_to_hex(&commit->object.oid));
471 - kh_value(replayed_commits, pos) = last_commit;
472 -
473 - /* Update any necessary branches */
474 - if (advance_name)
475 - continue;
476 - decoration = get_name_decoration(&commit->object);
477 - if (!decoration)
478 - continue;
479 - while (decoration) {
480 - if (decoration->type == DECORATION_REF_LOCAL &&
481 - (contained || strset_contains(update_refs,
482 - decoration->name))) {
483 - if (handle_ref_update(ref_mode, transaction,
484 - decoration->name,
485 - &last_commit->object.oid,
486 - &commit->object.oid,
487 - reflog_msg.buf,
488 - &transaction_err) < 0) {
489 - ret = error(_("failed to update ref '%s': %s"),
490 - decoration->name, transaction_err.buf);
491 - goto cleanup;
492 - }
493 - }
494 - decoration = decoration->next;
495 - }
496 - }
497 -
498 - /* In --advance mode, advance the target ref */
499 - if (result.clean == 1 && advance_name) {
500 - if (handle_ref_update(ref_mode, transaction, advance_name,
501 - &last_commit->object.oid,
502 - &onto->object.oid,
503 - reflog_msg.buf,
504 - &transaction_err) < 0) {
561 + for (size_t i = 0; i < result.updates_nr; i++) {
562 + ret = handle_ref_update(ref_mode, transaction, result.updates[i].refname,
563 + &result.updates[i].new_oid, &result.updates[i].old_oid,
564 + reflog_msg.buf, &transaction_err);
565 + if (ret) {
566 ret = error(_("failed to update ref '%s': %s"),
506 - advance_name, transaction_err.buf);
567 + result.updates[i].refname, transaction_err.buf);
568 goto cleanup;
569 }
570 }
571
572 /* Commit the ref transaction if we have one */
512 - if (transaction && result.clean == 1) {
573 + if (transaction) {
574 if (ref_transaction_commit(transaction, &transaction_err)) {
575 ret = error(_("failed to commit ref transaction: %s"),
576 transaction_err.buf);
@@ -517,24 +578,18 @@ int cmd_replay(int argc,
578 }
579 }
580
520 - merge_finalize(&merge_opt, &result);
521 - kh_destroy_oid_map(replayed_commits);
522 - if (update_refs) {
523 - strset_clear(update_refs);
524 - free(update_refs);
525 - }
526 - ret = result.clean;
581 + ret = 0;
582
583 cleanup:
584 if (transaction)
585 ref_transaction_free(transaction);
586 + replay_result_release(&result);
587 strbuf_release(&transaction_err);
588 strbuf_release(&reflog_msg);
589 release_revisions(&revs);
534 - free(advance_name);
590
591 /* Return */
592 if (ret < 0)
593 exit(128);
539 - return ret ? 0 : 1;
594 + return ret;
595 }