git/list[1] front-page[2] threads[3] people[4] search[5] about
wed 2026-10-07 17:23 UTC

[PATCH 2/2] builtin/stash: merge index in-core

From
D. Ben Knoble <ben.knoble@gmail.com>
Date
Sep 19, 2026, 21:26 UTC
Message-ID
<782fe91251111fbb28359574d860e4a6d2e45fc0.1789853192.git.ben.knoble@gmail.com>
In-Reply-To
<cover.1789853192.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. Since the results are never seen, we don't need to set the usual branch and ancestor labels.

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>
Signed-off-by: D. Ben Knoble <ben.knoble@gmail.com>
---
Notes (benknoble/commits):
    We *could* leave the asserts in, but then we somewhat uselessly set the
    conflict labels, which I did in the original patch [1]. Phillip
    suggested we don't need them, and I otherwise agree.
    
    [1]: https://lore.kernel.org/git/CALnO6CDfwscMWZktBu3FtXOQVbcBRo76nqK07kMnrzC5cPyZiQ@mail.gmail.com/
    
    In all the versions of 231e2dd49d (merge-ort: add some high-level
    algorithm structure, 2020-12-13) I could find on the mailing list, the
    "assert(opt->ancestor)" is present without explanation or comment, so
    I'm not in a good place to assess the impact of removing it and its
    compatriots.
    
    Cc: Elijah Newren <newren@gmail.com>
 builtin/stash.c  | 76 +++++++++---------------------------------------
 merge-ort.c      |  3 --
 t/t7600-merge.sh |  9 ++++++
 3 files changed, 23 insertions(+), 65 deletions(-)
diff --git a/builtin/stash.c b/builtin/stash.c
index dfea2d2c4c..9fc1a25e3d 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)
 {
 	/*
@@ -669,29 +625,25 @@ 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));
-			}
+			init_basic_merge_options(&o, the_repository);
 
-			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, head, merge, merge_base,
+						  &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);
+			oidcpy(&index_tree, &result.tree->object.oid);
+			clear_merge_options(&o);
 		}
 	}
 
diff --git a/merge-ort.c b/merge-ort.c
index c410a5d353..f69a49d48a 100644
--- a/merge-ort.c
+++ b/merge-ort.c
@@ -5035,8 +5035,6 @@ static void merge_start(struct merge_options *opt, struct merge_result *result)
 	trace2_region_enter("merge", "sanity checks", opt->repo);
 	assert(opt->repo);
 
-	assert(opt->branch1 && opt->branch2);
-
 	assert(opt->detect_directory_renames >= MERGE_DIRECTORY_RENAMES_NONE &&
 	       opt->detect_directory_renames <= MERGE_DIRECTORY_RENAMES_TRUE);
 	assert(opt->rename_limit >= -1);
@@ -5409,7 +5407,6 @@ void merge_incore_nonrecursive(struct merge_options *opt,
 	trace2_region_enter("merge", "incore_nonrecursive", opt->repo);
 
 	trace2_region_enter("merge", "merge_start", opt->repo);
-	assert(opt->ancestor != NULL);
 	merge_check_renames_reusable(opt, result, merge_base, side1, side2);
 	merge_start(opt, result);
 	/*
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
Previous: D. Ben KnobleNext: D. Ben Knoble
Message 3 of 78 in “Hi all,”
  1. 0/2 Hi all,D. Ben Knoble, Sep 19, 2026
  2. 1/2 builtin/stash: remove unused headerD. Ben Knoble, Sep 19, 2026
  3. 2/2 builtin/stash: merge index in-coreD. Ben Knoble, Sep 19, 2026
  4. D. Ben KnobleSep 19, 2026
  5. Phillip WoodSep 21, 2026
  6. Junio C HamanoSep 21, 2026
  7. D. Ben KnobleSep 22, 2026
  8. D. Ben KnobleSep 22, 2026
  9. Phillip WoodSep 22, 2026
  10. D. Ben KnobleSep 22, 2026
  11. 0/4 stash: clean up index-mode test mergeD. Ben Knoble, Sep 23, 2026
  12. 1/4 builtin/stash: remove unused headerD. Ben Knoble, Sep 23, 2026
  13. 2/4 stash: prepare merge options earlierD. Ben Knoble, Sep 23, 2026
  14. 3/4 t: test failed "stash apply --index"D. Ben Knoble, Sep 23, 2026
  15. 4/4 builtin/stash: merge index in-coreD. Ben Knoble, Sep 23, 2026
  16. Phillip WoodSep 24, 2026
  17. Phillip WoodSep 24, 2026
  18. Junio C HamanoSep 24, 2026
  19. Junio C HamanoSep 25, 2026
  20. D. Ben KnobleSep 25, 2026
  21. D. Ben KnobleSep 25, 2026
  22. D. Ben KnobleSep 25, 2026
  23. Phillip WoodSep 25, 2026
  24. Phillip WoodSep 25, 2026
  25. Phillip WoodSep 25, 2026
  26. D. Ben KnobleSep 25, 2026
  27. D. Ben KnobleSep 25, 2026
  28. Junio C HamanoSep 25, 2026
  29. Junio C HamanoSep 25, 2026
  30. Phillip WoodSep 26, 2026
  31. Phillip WoodSep 26, 2026
  32. D. Ben KnobleSep 26, 2026
  33. D. Ben KnobleSep 26, 2026
  34. 0/5 stash: clean up index-mode test mergeD. Ben Knoble, Sep 26, 2026
  35. 1/5 builtin/stash: remove unused headerD. Ben Knoble, Sep 26, 2026
  36. 2/5 stash: prepare merge options earlierD. Ben Knoble, Sep 26, 2026
  37. 3/5 t3903: test stash --index mergesD. Ben Knoble, Sep 26, 2026
  38. 4/5 t3903: test failed "stash apply --index"D. Ben Knoble, Sep 26, 2026
  39. 5/5 builtin/stash: merge index in-coreD. Ben Knoble, Sep 26, 2026
  40. D. Ben KnobleSep 26, 2026
  41. Junio C HamanoSep 27, 2026
  42. Junio C HamanoSep 27, 2026
  43. Junio C HamanoSep 28, 2026
  44. Phillip WoodSep 28, 2026
  45. D. Ben KnobleSep 28, 2026
  46. D. Ben KnobleSep 28, 2026
  47. D. Ben KnobleSep 28, 2026
  48. D. Ben KnobleSep 28, 2026
  49. D. Ben KnobleSep 28, 2026
  50. Phillip WoodSep 28, 2026
  51. Thomas BachemSep 28, 2026
  52. Junio C HamanoSep 28, 2026
  53. D. Ben KnobleSep 28, 2026
  54. Phillip WoodSep 28, 2026
  55. Phillip WoodSep 28, 2026
  56. D. Ben KnobleSep 28, 2026
  57. Phillip WoodSep 29, 2026
  58. D. Ben KnobleSep 29, 2026
  59. 0/5 stash: clean up index-mode test mergeD. Ben Knoble, Sep 29, 2026
  60. 1/5 builtin/stash: remove unused headerD. Ben Knoble, Sep 29, 2026
  61. 2/5 stash: prepare merge options earlierD. Ben Knoble, Sep 29, 2026
  62. 3/5 t3903: test failed "stash apply --index"D. Ben Knoble, Sep 29, 2026
  63. 4/5 t5520: don't expire reflogs where it mattersD. Ben Knoble, Sep 29, 2026
  64. 5/5 builtin/stash: merge index in-coreD. Ben Knoble, Sep 29, 2026
  65. Phillip WoodSep 29, 2026
  66. Phillip WoodSep 29, 2026
  67. Phillip WoodSep 29, 2026
  68. Ben KnobleSep 29, 2026
  69. Junio C HamanoSep 29, 2026
  70. D. Ben KnobleSep 30, 2026
  71. 1/4 builtin/stash: remove unused headerD. Ben Knoble, Sep 30, 2026
  72. 0/4 stash: clean up index-mode test mergeD. Ben Knoble, Sep 30, 2026
  73. 3/4 t3903: test failed "stash apply --index"D. Ben Knoble, Sep 30, 2026
  74. 2/4 stash: prepare merge options earlierD. Ben Knoble, Sep 30, 2026
  75. 4/4 builtin/stash: merge index in-coreD. Ben Knoble, Sep 30, 2026
  76. D. Ben KnobleSep 30, 2026
  77. Phillip WoodOct 1, 2026
  78. Junio C HamanoOct 1, 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.