From: "Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Elijah Newren <newren@gmail.com>, Patrick Steinhardt <ps@pks.im>,
Johannes Schindelin <johannes.schindelin@gmx.de>,
Johannes Schindelin <johannes.schindelin@gmx.de>
Subject: [PATCH/RFC 2/5] replay: short-circuit merge replay when parent and base trees are unchanged
Date: Wed, 06 May 2026 22:43:21 +0000 [thread overview]
Message-ID: <2f3d696104f7f634e2ca3bb51d57d5c57ffb0bbd.1778107405.git.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2106.git.1778107405.gitgitgadget@gmail.com>
From: Johannes Schindelin <johannes.schindelin@gmx.de>
For the common `git history reword` case the rewrite changes only
commit messages, so every commit on the line being replayed has the
same tree as before. When such a rewrite reaches a 2-parent merge
whose rewritten parents AND merge bases all carry the same trees as
the originals, the inner auto-merge of the rewritten parents (N) is
tree-equal to the inner auto-merge of the original parents (R), and
the outer 3-way merge with R as the merge base, the original merge
tree as side 1 and N as side 2 yields the original tree as result.
Detect this in `pick_merge_commit()` before doing any merge work and
write the new merge commit directly with the original tree and the
rewritten parents. This saves two recursive merges and one
non-recursive merge per merge commit on the rewrite path, which
dominates the cost of `git history reword` across histories with
many merges.
The merge-base trees must be checked too, in order. Tree-same
parents over a tree-different base could still produce a different
auto-merge (a conflict region that did not exist before, or vice
versa), and the original resolution would be inappropriate to apply.
To avoid recomputing the merge bases when the fast path does not
apply, both pairs are computed up front and the slow path that
follows reuses them.
Assisted-by: Claude Opus 4.7
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
replay.c | 67 ++++++++++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 58 insertions(+), 9 deletions(-)
diff --git a/replay.c b/replay.c
index 3dbce095f9..5dfdef1447 100644
--- a/replay.c
+++ b/replay.c
@@ -396,6 +396,64 @@ static struct commit *pick_merge_commit(struct repository *repo,
replayed_par1 = mapped_commit(replayed_commits, parent1, parent1);
replayed_par2 = mapped_commit(replayed_commits, parent2, parent2);
+ /*
+ * Compute both pairs of merge bases up front. The fast path below
+ * needs them for the tree-equality check, and the slow path that
+ * follows reuses them to avoid recomputing.
+ */
+ if (repo_get_merge_bases(repo, parent1, parent2, &parent_bases) < 0 ||
+ repo_get_merge_bases(repo, replayed_par1, replayed_par2,
+ &replayed_bases) < 0) {
+ result->clean = -1;
+ goto out;
+ }
+
+ /*
+ * Fast path: when both rewritten parents carry the same trees as
+ * the originals AND every merge base does too (in order), the
+ * auto-merges R and N would be tree-equal (their inputs match
+ * content-wise), so the outer 3-way merge trivially yields the
+ * original merge's tree. Skip the inner merges and write the new
+ * merge commit directly.
+ *
+ * This is the common case for `git history reword`, which only
+ * changes commit messages and so leaves every tree on the line
+ * being replayed unchanged. The merge-base trees must be checked
+ * too: tree-same parents over a tree-different base could still
+ * produce a different auto-merge (a conflict region that did not
+ * exist before, or vice versa), and the original resolution would
+ * be inappropriate.
+ */
+ if (oideq(&repo_get_commit_tree(repo, parent1)->object.oid,
+ &repo_get_commit_tree(repo, replayed_par1)->object.oid) &&
+ oideq(&repo_get_commit_tree(repo, parent2)->object.oid,
+ &repo_get_commit_tree(repo, replayed_par2)->object.oid)) {
+ struct commit_list *bo, *bn;
+ int bases_match = 1;
+
+ for (bo = parent_bases, bn = replayed_bases;
+ bo && bn;
+ bo = bo->next, bn = bn->next) {
+ if (!oideq(&repo_get_commit_tree(repo, bo->item)->object.oid,
+ &repo_get_commit_tree(repo, bn->item)->object.oid)) {
+ bases_match = 0;
+ break;
+ }
+ }
+ if (bo || bn)
+ bases_match = 0;
+
+ if (bases_match) {
+ pickme_tree = repo_get_commit_tree(repo, pickme);
+ parents = NULL;
+ commit_list_insert(replayed_par2, &parents);
+ commit_list_insert(replayed_par1, &parents);
+ picked = create_commit(repo, pickme_tree, pickme,
+ parents, REPLAY_MODE_PICK);
+ goto out;
+ }
+ }
+
/*
* R: auto-remerge of the original parents.
*
@@ -408,10 +466,6 @@ static struct commit *pick_merge_commit(struct repository *repo,
remerge_opt.show_rename_progress = 0;
remerge_opt.branch1 = "ours";
remerge_opt.branch2 = "theirs";
- if (repo_get_merge_bases(repo, parent1, parent2, &parent_bases) < 0) {
- result->clean = -1;
- goto out;
- }
merge_incore_recursive(&remerge_opt, parent_bases,
parent1, parent2, &remerge_res);
parent_bases = NULL; /* consumed by merge_incore_recursive */
@@ -425,11 +479,6 @@ static struct commit *pick_merge_commit(struct repository *repo,
new_merge_opt.show_rename_progress = 0;
new_merge_opt.branch1 = "ours";
new_merge_opt.branch2 = "theirs";
- if (repo_get_merge_bases(repo, replayed_par1, replayed_par2,
- &replayed_bases) < 0) {
- result->clean = -1;
- goto out;
- }
merge_incore_recursive(&new_merge_opt, replayed_bases,
replayed_par1, replayed_par2, &new_merge_res);
replayed_bases = NULL; /* consumed by merge_incore_recursive */
--
gitgitgadget
next prev parent reply other threads:[~2026-05-06 22:43 UTC|newest]
Thread overview: 12+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-05-06 22:43 [PATCH/RFC 0/5] replay: support replaying 2-parent merges Johannes Schindelin via GitGitGadget
2026-05-06 22:43 ` [PATCH/RFC 1/5] " Johannes Schindelin via GitGitGadget
2026-05-08 9:36 ` Phillip Wood
2026-05-08 10:05 ` Phillip Wood
2026-05-06 22:43 ` Johannes Schindelin via GitGitGadget [this message]
2026-05-06 22:43 ` [PATCH/RFC 3/5] history.adoc: describe merge-replay support and its limits Johannes Schindelin via GitGitGadget
2026-05-06 22:43 ` [PATCH/RFC 4/5] test-tool: add a "historian" subcommand for building merge fixtures Johannes Schindelin via GitGitGadget
2026-05-12 10:54 ` Toon Claes
2026-05-06 22:43 ` [PATCH/RFC 5/5] t3454: cover merge-replay scenarios with the historian helper Johannes Schindelin via GitGitGadget
2026-05-07 14:14 ` [PATCH/RFC 0/5] replay: support replaying 2-parent merges D. Ben Knoble
2026-05-07 15:06 ` Johannes Schindelin
2026-05-07 15:39 ` Ben Knoble
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=2f3d696104f7f634e2ca3bb51d57d5c57ffb0bbd.1778107405.git.gitgitgadget@gmail.com \
--to=gitgitgadget@gmail.com \
--cc=git@vger.kernel.org \
--cc=johannes.schindelin@gmx.de \
--cc=newren@gmail.com \
--cc=ps@pks.im \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox