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