diff --git a/builtin/replay.c b/builtin/replay.c index e14e33bcc5..f37e511d8e 100644 --- a/builtin/replay.c +++ b/builtin/replay.c @@ -223,20 +223,33 @@ static void determine_replay_mode(struct rev_cmdline_info *cmd_info, strset_clear(&rinfo.positive_refs); } +static struct commit *mapped_commit(kh_oid_map_t *replayed_commits, + struct commit *commit, + struct commit *fallback) +{ + khint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid); + if (pos == kh_end(replayed_commits)) + return fallback; + return kh_value(replayed_commits, pos); +} + static struct commit *pick_regular_commit(struct commit *pickme, - struct commit *last_commit, + kh_oid_map_t *replayed_commits, + struct commit *onto, struct merge_options *merge_opt, struct merge_result *result) { - struct commit *base; + struct commit *base, *replayed_base; struct tree *pickme_tree, *base_tree; base = pickme->parents->item; + replayed_base = mapped_commit(replayed_commits, base, onto); + result->tree = repo_get_commit_tree(the_repository, replayed_base); pickme_tree = repo_get_commit_tree(the_repository, pickme); base_tree = repo_get_commit_tree(the_repository, base); - merge_opt->branch1 = short_commit_name(last_commit); + merge_opt->branch1 = short_commit_name(replayed_base); merge_opt->branch2 = short_commit_name(pickme); merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); @@ -250,7 +263,7 @@ static struct commit *pick_regular_commit(struct commit *pickme, merge_opt->ancestor = NULL; if (!result->clean) return NULL; - return create_commit(result->tree, pickme, last_commit); + return create_commit(result->tree, pickme, replayed_base); } int cmd_replay(int argc, const char **argv, const char *prefix) @@ -266,6 +279,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix) struct merge_options merge_opt; struct merge_result result; struct strset *update_refs = NULL; + kh_oid_map_t *replayed_commits; int ret = 0; const char * const replay_usage[] = { @@ -362,21 +376,30 @@ int cmd_replay(int argc, const char **argv, const char *prefix) init_merge_options(&merge_opt, the_repository); memset(&result, 0, sizeof(result)); merge_opt.show_rename_progress = 0; - - result.tree = repo_get_commit_tree(the_repository, onto); last_commit = onto; + replayed_commits = kh_init_oid_map(); while ((commit = get_revision(&revs))) { const struct name_decoration *decoration; + khint_t pos; + int hr; if (!commit->parents) die(_("replaying down to root commit is not supported yet!")); if (commit->parents->next) die(_("replaying merge commits is not supported yet!")); - last_commit = pick_regular_commit(commit, last_commit, &merge_opt, &result); + last_commit = pick_regular_commit(commit, replayed_commits, onto, + &merge_opt, &result); if (!last_commit) break; + /* Record commit -> last_commit mapping */ + pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr); + if (hr == 0) + BUG("Duplicate rewritten commit: %s\n", + oid_to_hex(&commit->object.oid)); + kh_value(replayed_commits, pos) = last_commit; + /* Update any necessary branches */ if (advance_name) continue; @@ -405,13 +428,14 @@ int cmd_replay(int argc, const char **argv, const char *prefix) } merge_finalize(&merge_opt, &result); - ret = result.clean; - -cleanup: + kh_destroy_oid_map(replayed_commits); if (update_refs) { strset_clear(update_refs); free(update_refs); } + ret = result.clean; + +cleanup: release_revisions(&revs); /* Return */ diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh index d6286f9580..389670262e 100755 --- a/t/t3650-replay-basics.sh +++ b/t/t3650-replay-basics.sh @@ -143,4 +143,56 @@ test_expect_success 'using replay on bare repo to also rebase a contained branch test_cmp expect result-bare ' +test_expect_success 'using replay to rebase multiple divergent branches' ' + git replay --onto main ^topic1 topic2 topic4 >result && + + test_line_count = 2 result && + cut -f 3 -d " " result >new-branch-tips && + + git log --format=%s $(head -n 1 new-branch-tips) >actual && + test_write_lines E D M L B A >expect && + test_cmp expect actual && + + git log --format=%s $(tail -n 1 new-branch-tips) >actual && + test_write_lines J I M L B A >expect && + test_cmp expect actual && + + printf "update refs/heads/topic2 " >expect && + printf "%s " $(head -n 1 new-branch-tips) >>expect && + git rev-parse topic2 >>expect && + printf "update refs/heads/topic4 " >>expect && + printf "%s " $(tail -n 1 new-branch-tips) >>expect && + git rev-parse topic4 >>expect && + + test_cmp expect result +' + +test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' ' + git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result && + + test_line_count = 4 result && + cut -f 3 -d " " result >new-branch-tips && + + >expect && + for i in 2 1 3 4 + do + printf "update refs/heads/topic$i " >>expect && + printf "%s " $(grep topic$i result | cut -f 3 -d " ") >>expect && + git -C bare rev-parse topic$i >>expect || return 1 + done && + + test_cmp expect result && + + test_write_lines F C M L B A >expect1 && + test_write_lines E D C M L B A >expect2 && + test_write_lines H G F C M L B A >expect3 && + test_write_lines J I M L B A >expect4 && + + for i in 1 2 3 4 + do + git -C bare log --format=%s $(grep topic$i result | cut -f 3 -d " ") >actual && + test_cmp expect$i actual || return 1 + done +' + test_done