Git development
 help / color / mirror / Atom feed
From: "D. Ben Knoble" <ben.knoble@gmail.com>
To: git@vger.kernel.org
Cc: "D. Ben Knoble" <ben.knoble@gmail.com>,
	"Eli Barzilay" <eli@barzilay.org>,
	"Phillip Wood" <phillip.wood@dunelm.org.uk>,
	"Junio C Hamano" <gitster@pobox.com>,
	"Elijah Newren" <newren@gmail.com>,
	"Patrick Steinhardt" <ps@pks.im>,
	"Ævar Arnfjörð Bjarmason" <avarab@gmail.com>,
	"Victoria Dye" <vdye@github.com>, "Adam Johnson" <me@adamj.eu>,
	"Jeff King" <peff@peff.net>
Subject: [PATCH v3 5/5] builtin/stash: merge index in-core
Date: Sat, 26 Sep 2026 08:16:48 -0400	[thread overview]
Message-ID: <fde7fb7988b695707c6f2776adc18eec7fe4696a.1790425008.git.ben.knoble@gmail.com> (raw)
In-Reply-To: <cover.1790425008.git.ben.knoble@gmail.com>

"git stash apply --index" does a 2-step dance to report index conflicts
before carrying out the main unstash: first, attempt to merge the index
(and remember the name of the resulting tree). If that succeeds, reset
the index and carry on unstashing the working tree, then use the
remembered index tree to unstash the index.

The "merge the index" step is performed on the actual index by a
combination of git-diff-tree(1) and git-apply(1), which incurs an extra
cost to git-reset(1) to cleanup. This also introduces an autostash bug
when stash.index is true: "git reset" eventually wants to
remove_merge_branch_state(), which calls save_autostash() due to
a03b55530a (merge: teach --autostash option, 2020-04-07). This can
happen from a "git merge --autostash", which itself calls
save_autostash(). Operating on the file-system in this way is not
re-entrant, so we end up trying to lock a now-deleted MERGE_AUTOSTASH
ref [1]. This bug has lurked for a while, but it would have been
impossible to trigger without the availability of stash.index to force
the autostash apply into index mode.

[1]: https://lore.kernel.org/git/CALO-guvbk2TcrVwzdNQ3yRpzHr0HHZ3h1wite0Xp0sUyAT4otA@mail.gmail.com/

Fortunately, we can achieve 2 goals at once: avoid round-tripping to the
file-system (and invoking expensive subprocesses) by performing the
merge in-core. If there are conflicts, we discard the resulting tree, so
we don't see the usual branch and ancestor labels, but the merge
subroutines insist on their presence, so use something simple.

We *could* swap just the git-reset(1) subprocess with our internal
reset_tree() and refresh_index(), which would fix the bug. We'd much
prefer to clean up these vestiges of the shell-based git-stash, though.

Reported-by: Eli Barzilay <eli@barzilay.org>
Helped-by: Phillip Wood <phillip.wood@dunelm.org.uk>
Helped-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: D. Ben Knoble <ben.knoble@gmail.com>
---
 builtin/stash.c  | 80 ++++++++++--------------------------------------
 t/t7600-merge.sh |  9 ++++++
 2 files changed, 26 insertions(+), 63 deletions(-)

diff --git a/builtin/stash.c b/builtin/stash.c
index 043a38cc6d..ac3b3cf84d 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -422,50 +422,6 @@ static int create_index_from_tree(const struct object_id *tree_id,
 	return ret;
 }
 
-static int diff_tree_binary(struct strbuf *out, struct object_id *w_commit)
-{
-	struct child_process cp = CHILD_PROCESS_INIT;
-	const char *w_commit_hex = oid_to_hex(w_commit);
-
-	/*
-	 * Diff-tree would not be very hard to replace with a native function,
-	 * however it should be done together with apply_cached.
-	 */
-	cp.git_cmd = 1;
-	strvec_pushl(&cp.args, "diff-tree", "--binary", "--no-color", NULL);
-	strvec_pushf(&cp.args, "%s^2^..%s^2", w_commit_hex, w_commit_hex);
-
-	return pipe_command(&cp, NULL, 0, out, 0, NULL, 0);
-}
-
-static int apply_cached(struct strbuf *out)
-{
-	struct child_process cp = CHILD_PROCESS_INIT;
-
-	/*
-	 * Apply currently only reads either from stdin or a file, thus
-	 * apply_all_patches would have to be updated to optionally take a
-	 * buffer.
-	 */
-	cp.git_cmd = 1;
-	strvec_pushl(&cp.args, "apply", "--cached", NULL);
-	return pipe_command(&cp, out->buf, out->len, NULL, 0, NULL, 0);
-}
-
-static int reset_head(void)
-{
-	struct child_process cp = CHILD_PROCESS_INIT;
-
-	/*
-	 * Reset is overall quite simple, however there is no current public
-	 * API for resetting.
-	 */
-	cp.git_cmd = 1;
-	strvec_pushl(&cp.args, "reset", "--quiet", "--refresh", NULL);
-
-	return run_command(&cp);
-}
-
 static int is_path_a_directory(const char *path)
 {
 	/*
@@ -671,29 +627,27 @@ static enum stash_apply_result do_apply_stash(const char *prefix,
 		    oideq(&c_tree, &info->i_tree)) {
 			has_index = 0;
 		} else {
-			struct strbuf out = STRBUF_INIT;
+			struct merge_result result = { 0 };
 
-			if (diff_tree_binary(&out, &info->w_commit)) {
-				strbuf_release(&out);
-				return error(_("could not generate diff %s^!."),
-					     oid_to_hex(&info->w_commit));
-			}
+			o.branch1 = "Current index";
+			o.branch2 = "Stashed index changes";
+			o.ancestor = "Stash base";
 
-			ret = apply_cached(&out);
-			strbuf_release(&out);
-			if (ret)
+			o.verbosity = 0;
+
+			head = lookup_tree(o.repo, &c_tree);
+			merge = lookup_tree(o.repo, &info->i_tree);
+			merge_base = lookup_tree(o.repo, &info->b_tree);
+
+			merge_incore_nonrecursive(&o, merge_base, head, merge,
+						  &result);
+
+			oidcpy(&index_tree, &result.tree->object.oid);
+			merge_finalize(&o, &result);
+
+			if (!result.clean)
 				return error(_("conflicts in index. "
 					       "Try without --index."));
-
-			discard_index(the_repository->index);
-			repo_read_index(the_repository);
-			if (write_index_as_tree(&index_tree, the_repository->index,
-						repo_get_index_file(the_repository), 0, NULL))
-				return error(_("could not save index tree"));
-
-			reset_head();
-			discard_index(the_repository->index);
-			repo_read_index(the_repository);
 		}
 	}
 
diff --git a/t/t7600-merge.sh b/t/t7600-merge.sh
index 64fe21717d..8f6109fb91 100755
--- a/t/t7600-merge.sh
+++ b/t/t7600-merge.sh
@@ -801,6 +801,15 @@ verify_no_mergehead () {
 	test_cmp result.1-5 file
 '
 
+test_expect_success 'fast-forward merge with --autostash, stash.index' '
+	git reset --hard c0 &&
+	git stash clear &&
+	echo staged >>z && git add z &&
+	git -c stash.index=true merge --autostash c1 2>err &&
+	test_grep "Applied autostash." err &&
+	test_stdout_line_count = 0 git stash list
+'
+
 test_expect_success 'failed fast-forward merge with --autostash' '
 	git reset --hard c0 &&
 	git merge-file file file.orig file.5 &&
-- 
2.56.0.rc1.315.gc6ed9934b7.dirty


  parent reply	other threads:[~2026-09-26 12:17 UTC|newest]

Thread overview: 78+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-19 21:26 [PATCH 0/2] Hi all, D. Ben Knoble
2026-09-19 21:26 ` [PATCH 1/2] builtin/stash: remove unused header D. Ben Knoble
2026-09-21 15:10   ` Junio C Hamano
2026-09-19 21:26 ` [PATCH 2/2] builtin/stash: merge index in-core D. Ben Knoble
2026-09-21 13:17   ` Phillip Wood
2026-09-22 12:43     ` D. Ben Knoble
2026-09-22 12:51       ` D. Ben Knoble
2026-09-22 13:57       ` Phillip Wood
2026-09-22 20:34         ` D. Ben Knoble
2026-09-19 21:32 ` [PATCH 0/2] Hi all, D. Ben Knoble
2026-09-23 12:58 ` [PATCH v2 0/4] stash: clean up index-mode test merge D. Ben Knoble
2026-09-23 12:58   ` [PATCH v2 1/4] builtin/stash: remove unused header D. Ben Knoble
2026-09-23 12:58   ` [PATCH v2 2/4] stash: prepare merge options earlier D. Ben Knoble
2026-09-23 12:58   ` [PATCH v2 3/4] t: test failed "stash apply --index" D. Ben Knoble
2026-09-24  9:42     ` Phillip Wood
2026-09-25 13:36       ` D. Ben Knoble
2026-09-25 15:45         ` Phillip Wood
2026-09-26  9:53           ` Phillip Wood
2026-09-26 12:07             ` D. Ben Knoble
2026-09-23 12:58   ` [PATCH v2 4/4] builtin/stash: merge index in-core D. Ben Knoble
2026-09-24  9:42     ` Phillip Wood
2026-09-25 12:55       ` D. Ben Knoble
2026-09-25 15:58         ` Phillip Wood
2026-09-25 16:16           ` D. Ben Knoble
2026-09-24 21:59     ` Junio C Hamano
2026-09-25  4:12       ` Junio C Hamano
2026-09-25 13:00       ` D. Ben Knoble
2026-09-25 16:24         ` Junio C Hamano
2026-09-26  9:51           ` Phillip Wood
2026-09-26 12:04             ` D. Ben Knoble
2026-09-25 16:04       ` Phillip Wood
2026-09-25 16:17         ` D. Ben Knoble
2026-09-25 16:49           ` Junio C Hamano
2026-09-26 12:16   ` [PATCH v3 0/5] stash: clean up index-mode test merge D. Ben Knoble
2026-09-26 12:16     ` [PATCH v3 1/5] builtin/stash: remove unused header D. Ben Knoble
2026-09-26 12:16     ` [PATCH v3 2/5] stash: prepare merge options earlier D. Ben Knoble
2026-09-26 12:16     ` [PATCH v3 3/5] t3903: test stash --index merges D. Ben Knoble
2026-09-28 15:44       ` Phillip Wood
2026-09-28 15:55         ` D. Ben Knoble
2026-09-29  9:41           ` Phillip Wood
2026-09-26 12:16     ` [PATCH v3 4/5] t3903: test failed "stash apply --index" D. Ben Knoble
2026-09-26 12:16     ` D. Ben Knoble [this message]
2026-09-27 18:59       ` [PATCH v3 5/5] builtin/stash: merge index in-core Junio C Hamano
2026-09-28 12:02         ` D. Ben Knoble
2026-09-28  9:40       ` Junio C Hamano
2026-09-28 12:03         ` D. Ben Knoble
2026-09-28 15:32           ` Junio C Hamano
2026-09-26 12:20     ` [PATCH v3 0/5] stash: clean up index-mode test merge D. Ben Knoble
2026-09-27 19:21     ` Junio C Hamano
2026-09-28  9:50       ` Phillip Wood
2026-09-28 12:05         ` D. Ben Knoble
2026-09-28 12:33           ` D. Ben Knoble
2026-09-28 13:00             ` D. Ben Knoble
2026-09-28 13:45               ` Phillip Wood
2026-09-28 14:50                 ` Thomas Bachem
2026-09-28 15:36                   ` D. Ben Knoble
2026-09-29 11:38                     ` D. Ben Knoble
2026-09-29 15:54                     ` Phillip Wood
2026-09-28 15:40                   ` Phillip Wood
2026-09-29 12:18 ` [PATCH v4 " D. Ben Knoble
2026-09-29 12:18   ` [PATCH v4 1/5] builtin/stash: remove unused header D. Ben Knoble
2026-09-29 12:18   ` [PATCH v4 2/5] stash: prepare merge options earlier D. Ben Knoble
2026-09-29 12:18   ` [PATCH v4 3/5] t3903: test failed "stash apply --index" D. Ben Knoble
2026-09-29 12:18   ` [PATCH v4 4/5] t5520: don't expire reflogs where it matters D. Ben Knoble
2026-09-29 15:46     ` Phillip Wood
2026-09-29 12:18   ` [PATCH v4 5/5] builtin/stash: merge index in-core D. Ben Knoble
2026-09-29 20:07     ` Junio C Hamano
2026-09-30  1:29       ` D. Ben Knoble
2026-09-29 15:48   ` [PATCH v4 0/5] stash: clean up index-mode test merge Phillip Wood
2026-09-29 17:31     ` Ben Knoble
2026-09-30 21:26       ` D. Ben Knoble
2026-09-30 21:24 ` [PATCH v5 0/4] " D. Ben Knoble
2026-09-30 21:24   ` [PATCH v5 1/4] builtin/stash: remove unused header D. Ben Knoble
2026-09-30 21:24   ` [PATCH v5 2/4] stash: prepare merge options earlier D. Ben Knoble
2026-09-30 21:24   ` [PATCH v5 3/4] t3903: test failed "stash apply --index" D. Ben Knoble
2026-09-30 21:24   ` [PATCH v5 4/4] builtin/stash: merge index in-core D. Ben Knoble
2026-10-01 15:52   ` [PATCH v5 0/4] stash: clean up index-mode test merge Phillip Wood
2026-10-01 17:47     ` Junio C Hamano

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=fde7fb7988b695707c6f2776adc18eec7fe4696a.1790425008.git.ben.knoble@gmail.com \
    --to=ben.knoble@gmail.com \
    --cc=avarab@gmail.com \
    --cc=eli@barzilay.org \
    --cc=git@vger.kernel.org \
    --cc=gitster@pobox.com \
    --cc=me@adamj.eu \
    --cc=newren@gmail.com \
    --cc=peff@peff.net \
    --cc=phillip.wood@dunelm.org.uk \
    --cc=ps@pks.im \
    --cc=vdye@github.com \
    /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