replay: allow callers to control what happens with empty commits

When replaying commits it may happen that some of the commits become empty relative to their parent. Such commits are for now automatically dropped by the replay subsystem without much control from the user. Introduce a new enum that allows the caller to drop, keep or abort in this case. Signed-off-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrick Steinhardt committed Apr 27, 2026 at 07:53 UTC 0cf4ad7cf555ac2ed6556a3169d30b2eabb68d41
2 files changed +43 -5
replay.c
+24 -5
@@ -269,7 +269,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
269 struct commit *onto,
270 struct merge_options *merge_opt,
271 struct merge_result *result,
272 - enum replay_mode mode)
272 + enum replay_mode mode,
273 + enum replay_empty_commit_action empty)
274 {
275 struct commit *base, *replayed_base;
276 struct tree *pickme_tree, *base_tree, *replayed_base_tree;
@@ -321,12 +322,25 @@ static struct commit *pick_regular_commit(struct repository *repo,
322 }
323 merge_opt->ancestor = NULL;
324 merge_opt->branch2 = NULL;
325 +
326 if (!result->clean)
327 return NULL;
326 - /* Drop commits that become empty */
328 +
329 + /* Handle commits that become empty */
330 if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&
328 - !oideq(&pickme_tree->object.oid, &base_tree->object.oid))
329 - return replayed_base;
331 + !oideq(&pickme_tree->object.oid, &base_tree->object.oid)) {
332 + switch (empty) {
333 + case REPLAY_EMPTY_COMMIT_DROP:
334 + return replayed_base;
335 + case REPLAY_EMPTY_COMMIT_KEEP:
336 + break;
337 + case REPLAY_EMPTY_COMMIT_ABORT:
338 + result->clean = error(_("commit %s became empty after replay"),
339 + oid_to_hex(&pickme->object.oid));
340 + return NULL;
341 + }
342 + }
343 +
344 return create_commit(repo, result->tree, pickme, replayed_base, mode);
345 }
346
@@ -417,7 +431,7 @@ int replay_revisions(struct rev_info *revs,
431
432 last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
433 mode == REPLAY_MODE_REVERT ? last_commit : onto,
420 - &merge_opt, &result, mode);
434 + &merge_opt, &result, mode, opts->empty);
435 if (!last_commit)
436 break;
437
@@ -458,6 +472,11 @@ int replay_revisions(struct rev_info *revs,
472 }
473 }
474
475 + if (result.clean < 0) {
476 + ret = -1;
477 + goto out;
478 + }
479 +
480 if (!result.clean) {
481 ret = 1;
482 goto out;
replay.h
+19
@@ -6,6 +6,19 @@
6 struct repository;
7 struct rev_info;
8
9 +/*
10 + * Controls what happens when a replayed commit becomes empty (i.e. its tree
11 + * is identical to its parent's tree after the replay).
12 + */
13 +enum replay_empty_commit_action {
14 + /* Silently discard the empty commit. */
15 + REPLAY_EMPTY_COMMIT_DROP,
16 + /* Keep the empty commit as-is. */
17 + REPLAY_EMPTY_COMMIT_KEEP,
18 + /* Abort with an error. */
19 + REPLAY_EMPTY_COMMIT_ABORT,
20 +};
21 +
22 /*
23 * A set of options that can be passed to `replay_revisions()`.
24 */
@@ -43,6 +56,12 @@ struct replay_revisions_options {
56 * Requires `onto` to be set.
57 */
58 int contained;
59 +
60 + /*
61 + * Controls what to do when a replayed commit becomes empty.
62 + * Defaults to REPLAY_EMPTY_COMMIT_DROP.
63 + */
64 + enum replay_empty_commit_action empty;
65 };
66
67 /* This struct is used as an out-parameter by `replay_revisions()`. */