replay: stop assuming replayed branches do not diverge

The replay command is able to replay multiple branches but when some of them are based on other replayed branches, their commit should be replayed onto already replayed commits. For this purpose, let's store the replayed commit and its original commit in a key value store, so that we can easily find and reuse a replayed commit instead of the original one. Co-authored-by: Christian Couder <chriscool@tuxfamily.org> Signed-off-by: Elijah Newren <newren@gmail.com> Signed-off-by: Christian Couder <chriscool@tuxfamily.org> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Elijah Newren committed Nov 24, 2023 at 12:10 UTC e928c11e29966b0a8f4340465b8238501c994eb1
2 files changed +86 -10
builtin/replay.c
+34 -10
@@ -223,20 +223,33 @@ static void determine_replay_mode(struct rev_cmdline_info *cmd_info,
223 strset_clear(&rinfo.positive_refs);
224 }
225
226 +static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,
227 + struct commit *commit,
228 + struct commit *fallback)
229 +{
230 + khint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);
231 + if (pos == kh_end(replayed_commits))
232 + return fallback;
233 + return kh_value(replayed_commits, pos);
234 +}
235 +
236 static struct commit *pick_regular_commit(struct commit *pickme,
227 - struct commit *last_commit,
237 + kh_oid_map_t *replayed_commits,
238 + struct commit *onto,
239 struct merge_options *merge_opt,
240 struct merge_result *result)
241 {
231 - struct commit *base;
242 + struct commit *base, *replayed_base;
243 struct tree *pickme_tree, *base_tree;
244
245 base = pickme->parents->item;
246 + replayed_base = mapped_commit(replayed_commits, base, onto);
247
248 + result->tree = repo_get_commit_tree(the_repository, replayed_base);
249 pickme_tree = repo_get_commit_tree(the_repository, pickme);
250 base_tree = repo_get_commit_tree(the_repository, base);
251
239 - merge_opt->branch1 = short_commit_name(last_commit);
252 + merge_opt->branch1 = short_commit_name(replayed_base);
253 merge_opt->branch2 = short_commit_name(pickme);
254 merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
255
@@ -250,7 +263,7 @@ static struct commit *pick_regular_commit(struct commit *pickme,
263 merge_opt->ancestor = NULL;
264 if (!result->clean)
265 return NULL;
253 - return create_commit(result->tree, pickme, last_commit);
266 + return create_commit(result->tree, pickme, replayed_base);
267 }
268
269 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)
279 struct merge_options merge_opt;
280 struct merge_result result;
281 struct strset *update_refs = NULL;
282 + kh_oid_map_t *replayed_commits;
283 int ret = 0;
284
285 const char * const replay_usage[] = {
@@ -363,21 +377,30 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
377 init_merge_options(&merge_opt, the_repository);
378 memset(&result, 0, sizeof(result));
379 merge_opt.show_rename_progress = 0;
366 -
367 - result.tree = repo_get_commit_tree(the_repository, onto);
380 last_commit = onto;
381 + replayed_commits = kh_init_oid_map();
382 while ((commit = get_revision(&revs))) {
383 const struct name_decoration *decoration;
384 + khint_t pos;
385 + int hr;
386
387 if (!commit->parents)
388 die(_("replaying down to root commit is not supported yet!"));
389 if (commit->parents->next)
390 die(_("replaying merge commits is not supported yet!"));
391
377 - last_commit = pick_regular_commit(commit, last_commit, &merge_opt, &result);
392 + last_commit = pick_regular_commit(commit, replayed_commits, onto,
393 + &merge_opt, &result);
394 if (!last_commit)
395 break;
396
397 + /* Record commit -> last_commit mapping */
398 + pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);
399 + if (hr == 0)
400 + BUG("Duplicate rewritten commit: %s\n",
401 + oid_to_hex(&commit->object.oid));
402 + kh_value(replayed_commits, pos) = last_commit;
403 +
404 /* Update any necessary branches */
405 if (advance_name)
406 continue;
@@ -406,13 +429,14 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
429 }
430
431 merge_finalize(&merge_opt, &result);
409 - ret = result.clean;
410 -
411 -cleanup:
432 + kh_destroy_oid_map(replayed_commits);
433 if (update_refs) {
434 strset_clear(update_refs);
435 free(update_refs);
436 }
437 + ret = result.clean;
438 +
439 +cleanup:
440 release_revisions(&revs);
441
442 /* Return */
t/t3650-replay-basics.sh
+52
@@ -143,4 +143,56 @@ test_expect_success 'using replay on bare repo to also rebase a contained branch
143 test_cmp expect result-bare
144 '
145
146 +test_expect_success 'using replay to rebase multiple divergent branches' '
147 + git replay --onto main ^topic1 topic2 topic4 >result &&
148 +
149 + test_line_count = 2 result &&
150 + cut -f 3 -d " " result >new-branch-tips &&
151 +
152 + git log --format=%s $(head -n 1 new-branch-tips) >actual &&
153 + test_write_lines E D M L B A >expect &&
154 + test_cmp expect actual &&
155 +
156 + git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
157 + test_write_lines J I M L B A >expect &&
158 + test_cmp expect actual &&
159 +
160 + printf "update refs/heads/topic2 " >expect &&
161 + printf "%s " $(head -n 1 new-branch-tips) >>expect &&
162 + git rev-parse topic2 >>expect &&
163 + printf "update refs/heads/topic4 " >>expect &&
164 + printf "%s " $(tail -n 1 new-branch-tips) >>expect &&
165 + git rev-parse topic4 >>expect &&
166 +
167 + test_cmp expect result
168 +'
169 +
170 +test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '
171 + git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&
172 +
173 + test_line_count = 4 result &&
174 + cut -f 3 -d " " result >new-branch-tips &&
175 +
176 + >expect &&
177 + for i in 2 1 3 4
178 + do
179 + printf "update refs/heads/topic$i " >>expect &&
180 + printf "%s " $(grep topic$i result | cut -f 3 -d " ") >>expect &&
181 + git -C bare rev-parse topic$i >>expect || return 1
182 + done &&
183 +
184 + test_cmp expect result &&
185 +
186 + test_write_lines F C M L B A >expect1 &&
187 + test_write_lines E D C M L B A >expect2 &&
188 + test_write_lines H G F C M L B A >expect3 &&
189 + test_write_lines J I M L B A >expect4 &&
190 +
191 + for i in 1 2 3 4
192 + do
193 + git -C bare log --format=%s $(grep topic$i result | cut -f 3 -d " ") >actual &&
194 + test_cmp expect$i actual || return 1
195 + done
196 +'
197 +
198 test_done