git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH] am: record blobs of cleanly applied patches when using --3way

From
NLNikita Leshenko <nikita@island.io>
Date
Aug 25, 2026, 08:55 UTC
Message-ID
<20260825085516.66088-1-nikita@island.io>
Make "git am --3way" succeed in an edge case where it currently fails.
First, some background about the case:

Say we have a patch with two commits, A and B, and both of them change the same file. We apply them to a different repo on a different version of the file using --3way.

If both patches apply cleanly, we are done.

If A does not apply cleanly, git am falls back to 3-way merge. To merge, Git uses the preimage hash from the patch:

    A: index 83b2a16..cccad2b 100644
    B: index cccad2b..0ce2f98 100644

git am looks up 83b2a16, applies A to it, and merges that result with the current version of the file. As an important side effect, applying A to 83b2a16 also stores A's postimage, cccad2b, in the repository. This means that if B also doesn't apply cleanly, cccad2b (which is now B's preimage) exists in the repository so we can merge against it as well.

Let's assume instead that A applies cleanly and B fails. Because git am didn't have to 3-way merge A, nothing created cccad2b this time. What we have after applying A is our file plus A's change, which is a different hash. When B doesn't apply cleanly, git am fails because it doesn't know what cccad2b is:

    Applying: A
    Applying: B
    error: sha1 information is lacking or useless (file).
    error: could not build fake ancestor

However, technically we have the information to build the fake ancestor! We have 83b2a16 in the repository, and we have A, so if we apply A we'll get that hash.

So do exactly that: if the user requested --3way, apply the patch on the fake ancestor even after a patch applies cleanly, in order to produce intermediate hashes for later commits. If the preimage is missing, or the patch does not apply, nothing is recorded and git am behaves as it does today.

This does not change the behavior of how patches apply, but when the user requested --3way it does cost one extra "git apply --build-fake-ancestor" process and one extra apply per clean patch.

Signed-off-by: Nikita Leshenko <nikita@island.io>
---
 builtin/am.c  | 58 ++++++++++++++++++++++++++++++++++++++++++++-------
 t/t4150-am.sh | 28 +++++++++++++++++++++++++
 2 files changed, 79 insertions(+), 7 deletions(-)
diff --git a/builtin/am.c b/builtin/am.c
index e9623b8307..37569fed65 100644
--- a/builtin/am.c
+++ b/builtin/am.c
@@ -1488,7 +1488,8 @@ static int parse_mail_rebase(struct am_state *state, const char *mail)
  * Applies current patch with git-apply. Returns 0 on success, -1 otherwise. If
  * `index_file` is not NULL, the patch will be applied to that index.
  */
-static int run_apply(const struct am_state *state, const char *index_file)
+static int run_apply(const struct am_state *state, const char *index_file,
+		     int quiet)
 {
 	struct strvec apply_paths = STRVEC_INIT;
 	struct strvec apply_opts = STRVEC_INIT;
@@ -1528,7 +1529,7 @@ static int run_apply(const struct am_state *state, const char *index_file)
 	 * If we are allowed to fall back on 3-way merge, don't give false
 	 * errors during the initial attempt.
 	 */
-	if (state->threeway && !index_file)
+	if (quiet || (state->threeway && !index_file))
 		apply_state.apply_verbosity = verbosity_silent;
 
 	if (check_apply_state(&apply_state, force_apply))
@@ -1559,11 +1560,13 @@ static int run_apply(const struct am_state *state, const char *index_file)
 /**
  * Builds an index that contains just the blobs needed for a 3way merge.
  */
-static int build_fake_ancestor(const struct am_state *state, const char *index_file)
+static int build_fake_ancestor(const struct am_state *state,
+			       const char *index_file, int quiet)
 {
 	struct child_process cp = CHILD_PROCESS_INIT;
 
 	cp.git_cmd = 1;
+	cp.no_stderr = quiet;
 	strvec_push(&cp.args, "apply");
 	strvec_pushv(&cp.args, state->git_apply_opts.v);
 	strvec_pushf(&cp.args, "--build-fake-ancestor=%s", index_file);
@@ -1589,7 +1592,7 @@ static int fall_back_threeway(const struct am_state *state, const char *index_pa
 	if (repo_get_oid(the_repository, "HEAD", &our_tree) < 0)
 		oidcpy(&our_tree, the_hash_algo->empty_tree);
 
-	if (build_fake_ancestor(state, index_path))
+	if (build_fake_ancestor(state, index_path, 0))
 		return error("could not build fake ancestor");
 
 	discard_index(the_repository->index);
@@ -1617,7 +1620,7 @@ static int fall_back_threeway(const struct am_state *state, const char *index_pa
 		release_revisions(&rev_info);
 	}
 
-	if (run_apply(state, index_path))
+	if (run_apply(state, index_path, 0))
 		return error(_("Did you hand edit your patch?\n"
 				"It does not apply to blobs recorded in its index."));
 
@@ -1658,6 +1661,39 @@ static int fall_back_threeway(const struct am_state *state, const char *index_pa
 	return 0;
 }
 
+/**
+ * Applies the patch on the fake ancestor and stores the postimage in the
+ * repository for future patches to reference.  Best effort, fails quietly.
+ *
+ * Motivation: Say a patch file has two commits, A and B, that both change the
+ * same file.  We apply it with --3way.  If A applies cleanly, nothing stores
+ * A's postimage.  If B then does not apply cleanly, git am cannot 3-way merge
+ * it, because B's preimage is A's postimage.
+ */
+static void try_record_patch_postimage(const struct am_state *state)
+{
+	struct strbuf index_path = STRBUF_INIT;
+
+	strbuf_addstr(&index_path, am_path(state, "patch-postimage-index"));
+
+	if (build_fake_ancestor(state, index_path.buf, 1))
+		goto done;
+
+	/*
+         * Discard index because run_apply() reads `index_path` only if no index
+         * is in core.
+         */
+	discard_index(the_repository->index);
+	run_apply(state, index_path.buf, 1);
+
+	discard_index(the_repository->index);
+	repo_read_index(the_repository);
+
+done:
+	unlink(index_path.buf);
+	strbuf_release(&index_path);
+}
+
 /**
  * Commits the current index with state->msg as the commit message and
  * state->author_name, state->author_email and state->author_date as the author
@@ -1886,9 +1922,17 @@ static void am_run(struct am_state *state, int resume)
 
 		say(state, stdout, _("Applying: %.*s"), linelen(state->msg), state->msg);
 
-		apply_status = run_apply(state, NULL);
+		apply_status = run_apply(state, NULL, 0);
 
-		if (apply_status && state->threeway) {
+		if (!apply_status && state->threeway && state->cur < state->last) {
+			/*
+			 * The patch applied cleanly, so no 3-way was performed.
+			 * A patch later in the series may reference postimage
+			 * hashes this patch would have produced, so record them
+			 * while we still can.
+			 */
+			try_record_patch_postimage(state);
+		} else if (apply_status && state->threeway) {
 			struct strbuf sb = STRBUF_INIT;
 
 			strbuf_addstr(&sb, am_path(state, "patch-merge-index"));
diff --git a/t/t4150-am.sh b/t/t4150-am.sh
index ee96223668..e5ed666c2f 100755
--- a/t/t4150-am.sh
+++ b/t/t4150-am.sh
@@ -641,6 +641,34 @@ test_expect_success 'am with config am.threeWay overridden by --no-3way' '
 	test_path_is_dir .git/rebase-apply
 '
 
+test_expect_success 'am -3 records blobs a later patch needs' '
+	test_when_finished "rm -rf 3way-source 3way-target" &&
+
+	# Two patches touching the same file, the first of which applies
+	# cleanly to the target while the second one does not.
+	git init 3way-source &&
+	test_write_lines 1 2 3 4 5 6 7 8 9 >3way-source/file &&
+	git -C 3way-source add file &&
+	git -C 3way-source commit -m base &&
+	test_write_lines 11 2 3 4 5 6 7 8 9 >3way-source/file &&
+	git -C 3way-source commit -am first &&
+	test_write_lines 11 2 3 4 5 66 7 8 9 >3way-source/file &&
+	git -C 3way-source commit -am second &&
+	git -C 3way-source format-patch --stdout -2 >3way-two.patches &&
+
+	git init 3way-target &&
+	test_write_lines 1 2 3 4 5 6 7 8 9 >3way-target/file &&
+	git -C 3way-target add file &&
+	git -C 3way-target commit -m base &&
+	test_write_lines 1 2 3 4 5 6 7 8 9XXX >3way-target/file &&
+	git -C 3way-target commit -am "change outside the first patch" &&
+
+	git -C 3way-target am -3 ../3way-two.patches &&
+	test_path_is_missing 3way-target/.git/rebase-apply &&
+	test_write_lines 11 2 3 4 5 66 7 8 9XXX >expect &&
+	test_cmp expect 3way-target/file
+'
+
 test_expect_success 'am can rename a file' '
 	test_grep "^rename from" rename.patch &&
 	rm -fr .git/rebase-apply &&
-- 
2.55.0
Next: Junio C Hamano
Message 1 of 3 in “am: record blobs of cleanly applied patches when using --3way”
  1. am: record blobs of cleanly applied patches when using --3wayNikita Leshenko, Aug 25, 2026
  2. Junio C HamanoAug 25, 2026
  3. Nikita LeshenkoAug 25, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.