Skip to contentSkip to conversation

Hi all,

PATCH Superseded D. Ben Knoble 4 participants 78 messages Sep 19 – Oct 1

lore
Share & tools
lore
#0 lore raw

cover Hi all,

This small patch series fixes a bug reported by Eli Barzilay in the interaction between autostashing, staged index entries, and stash.index=true.

The first patch is an incidental cleanup, while the second holds the interesting bits. Preferences on keeping or removing a few assert() calls in merge-ort.c are welcome.

[1/2] builtin/stash: remove unused header
[2/2] builtin/stash: merge index in-core

 builtin/stash.c  | 77 +++++++++---------------------------------------
 merge-ort.c      |  3 --
 t/t7600-merge.sh |  9 ++++++
 3 files changed, 23 insertions(+), 66 deletions(-)

base-commit: 339ab2a8f14c0c304ae2f28df1a859f3d2cf610c

Signature · 1 line
2.56.0.rc1.315.gc6ed9934b7.dirty
#1 lore raw

PATCH 1/2 builtin/stash: remove unused header

Clang complains that oid-array.h is unused. Certainly none of the oid_array* functions, types, etc., are used, and the transitively-included hash.h declarations are used but covered by a pre-existing direct #include of hash.h.

---
 builtin/stash.c | 1 -
 1 file changed, 1 deletion(-)
builtin/stash.c +0 -1
index 7a9843413b..dfea2d2c4c 100644
@@ -31,7 +31,6 @@
 #include "reflog.h"
 #include "reflog-walk.h"
 #include "add-interactive.h"
-#include "oid-array.h"
 #include "commit.h"
 
 #define INCLUDE_ALL_FILES 2
Signature · 1 line
2.56.0.rc1.315.gc6ed9934b7.dirty
#2 lore raw

Re: [PATCH 1/2] builtin/stash: remove unused header

Quoted 6 lines · D. Ben Knoble

Clang complains that oid-array.h is unused. Certainly none of the oid_array* functions, types, etc., are used, and the

transitively-included hash.h declarations are used but covered by a pre-existing direct #include of hash.h.

Good thing to make sure.

And the correctness of the patch can easily be validated, which makes this kind of patch no-brainer to accept ;-)

Thanks.

Quoted 18 lines
---
 builtin/stash.c | 1 -
 1 file changed, 1 deletion(-)
builtin/stash.c +0 -1
index 7a9843413b..dfea2d2c4c 100644
@@ -31,7 +31,6 @@
 #include "reflog.h"
 #include "reflog-walk.h"
 #include "add-interactive.h"
-#include "oid-array.h"
 #include "commit.h"
 
 #define INCLUDE_ALL_FILES 2
#3 lore raw

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

In reply to D. Ben Knoble

"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.

---

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 <[email protected]>
 builtin/stash.c  | 76 +++++++++---------------------------------------
 merge-ort.c      |  3 --
 t/t7600-merge.sh |  9 ++++++
 3 files changed, 23 insertions(+), 65 deletions(-)
builtin/stash.c +14 -62
index dfea2d2c4c..9fc1a25e3d 100644
@@ -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);
 		}
 	}
 
merge-ort.c +0 -3
index c410a5d353..f69a49d48a 100644
@@ -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);
 	/*
t/t7600-merge.sh +9 -0
index 64fe21717d..8f6109fb91 100755
@@ -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 &&
Signature · 1 line
2.56.0.rc1.315.gc6ed9934b7.dirty
#4 lore raw

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

Hi Ben

Quoted 19 lines · D. Ben Knoble

"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.

Quoted 6 lines

[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.

When the merge succeeds without conflicts we use the result so it is seen. It would be clearer to say that "If there are conflicts we discard the result so ...". The rest of the commit message explains the problem nicely.

Quoted 3 lines

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.

Definitely

Quoted 1 line
  builtin/stash.c  | 76 +++++++++---------------------------------------

Nice diffstat!

builtin/stash.c static enum stash_apply_result do_apply_stash(const char *prefix, · line 630

Quoted 13 lines
builtin/stash.c +2 -6
@@ -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);

This means we potentially use different diff algorithms when merging the index and when merging the work tree, let's use the _ui variant here instead.

builtin/stash.c static enum stash_apply_result do_apply_stash(const char *prefix, · line 632

Quoted 5 lines
builtin/stash.c +1 -3
-			ret = apply_cached(&out);
-			strbuf_release(&out);
-			if (ret)
+			o.verbosity = 0;

Looking at the code in merge-ort.c it appears the verbosity option was used by the recursive strategy but isn't used anymore so I think we could drop this.

Quoted 13 lines
builtin/stash.c +9 -1
+
+			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);

builtin/stash.c static enum stash_apply_result do_apply_stash(const char *prefix, · line 646

Quoted 10 lines
builtin/stash.c +2 -8
-			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);

Looking at replay.c:replay_revisions() I think this should be

merge_finalize(&opts, &result);

merge-ort.c static void merge_start(struct merge_options *opt, struct merge_result *result) · line 5038

Quoted 12 lines
(quoted diff) +0 -0
  		}
  	}
 
merge-ort.c +0 -1
index c410a5d353..f69a49d48a 100644
@@ -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);

This, and the hunk below, make me nervous. Normally assertions like this exist because the pointers are unconditionally dereferenced later on. Looking at merge_3way() it asserts opt->ancestor is non-NULL and dereferences all three labels. t3903 does not appear to have test coverage for the index merge failing (if it did I think we'd see a SIGSEV), we should probably add a test that checks the command fails leaving the index and work tree untouched, and verifies the message on stderr.

Lets set some simple, fixed, ancestor and branch names in do_apply_stash() above.

Quoted 10 lines
merge-ort.c +0 -1
  	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);

merge-ort.c void merge_incore_nonrecursive(struct merge_options *opt,

Quoted 10 lines
merge-ort.c +8 -0
  	/*
 
+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
+'

We check the autostash is applied and is not saved - good

Thanks for working on this, it is really good to get rid of those subprocesses.

Phillip

Quoted 3 lines
  test_expect_success 'failed fast-forward merge with --autostash' '
  	git reset --hard c0 &&
  	git merge-file file file.orig file.5 &&
#5 lore raw

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

Quoted 9 lines · Phillip Wood

Hi Ben

Quoted 4 lines · D. Ben Knoble

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.

Quoted 4 lines

When the merge succeeds without conflicts we use the result so it is seen. It would be clearer to say that "If there are conflicts we discard the result so ...". The rest of the commit message explains the problem nicely.

Indeed. This is what I get for (unusually) dashing off the commit message up against the clock. Thanks!

Quoted 7 lines
Quoted 7 lines
builtin/stash.c +1 -1
@@ -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 };
 
Quoted 10 lines

builtin/stash.c static enum stash_apply_result do_apply_stash(const char *prefix, · line 630

Quoted 6 lines
builtin/stash.c +1 -5
-                     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);

This means we potentially use different diff algorithms when merging the index and when merging the work tree, let's use the _ui variant here instead.

Yep, you know I'd spotted that and wasn't expecting it to make a meaningful difference. It's an easy swap, but I thought that (like above, since we don't show the conflict results) the diff algorithm wouldn't matter too much.

Maybe it affects the actual merge-ability, though, in which case I agree using the same is important?

Quoted 5 lines

builtin/stash.c static enum stash_apply_result do_apply_stash(const char *prefix, · line 632

Quoted 1 line
builtin/stash.c +1 -0
+                     o.verbosity = 0;

Looking at the code in merge-ort.c it appears the verbosity option was used by the recursive strategy but isn't used anymore so I think we could drop this.

Intriguing. (Assuming the default "2") There's a "< 5" check in path_msg() that wouldn't be affected by dropping this, and a "> 2" check in checkout() that… also wouldn't be affected?

But it might matter if something is setting the verbosity elsewhere (config, GIT_MERGE_VERBOSITY), and I think we really want this merge to be quiet? I seem to remember reading commits in this area quieting "git reset" and so on to keep the noise down.

So I'm inclined to leave it for now, especially in case it later does get used.

Quoted 6 lines

builtin/stash.c static enum stash_apply_result do_apply_stash(const char *prefix, · line 646

Quoted 2 lines
builtin/stash.c +2 -0
+                     oidcpy(&index_tree, &result.tree->object.oid);
+                     clear_merge_options(&o);

Looking at replay.c:replay_revisions() I think this should be

merge_finalize(&opts, &result);

Hm, possibly. It does look like that does more with the "result," which is probably needed. But it doesn't actually clear the merge options.

On one hand, I thought it could be important not to reuse that struct between merges. But if we do use the "ui" init, it might be ok? replay_revisions() does use the same struct between calls to merge_incore_nonrecursive().

Oh, but one other thing: we unconditionally reinit the merge options later on in do_apply_stash(). We could conditionally initialize there ("if (has_index)"), I suppose?

Quoted 19 lines

merge-ort.c static void merge_start(struct merge_options *opt, struct merge_result *result) · line 5038

Quoted 9 lines
merge-ort.c +0 -1
index c410a5d353..f69a49d48a 100644
@@ -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);

This, and the hunk below, make me nervous. Normally assertions like this exist because the pointers are unconditionally dereferenced later on. Looking at merge_3way() it asserts opt->ancestor is non-NULL and dereferences all three labels. t3903 does not appear to have test coverage for the index merge failing (if it did I think we'd see a SIGSEV), we should probably add a test that checks the command fails leaving the index and work tree untouched, and verifies the message on stderr.

Quoted 2 lines

Lets set some simple, fixed, ancestor and branch names in do_apply_stash() above.

Funny, I was getting aborts before removing the asserts because I hadn't set the labels, aha. Looks like we've come back around to keeping the labels. I'll probably keep a similar structure as the working tree merge uses, I think.

A fail-to-merge test also seems like a good idea. Let me mull on that.

Thanks for the review.

Signature · 1 line
D. Ben Knoble
#6 lore raw

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

Quoted 4 lines · D. Ben Knoble

Oh, but one other thing: we unconditionally reinit the merge options later on in do_apply_stash(). We could conditionally initialize there ("if (has_index)"), I suppose?

er, "!has_index" of course (which is what I originally typed and then, confused, edited).

Signature · 1 line
D. Ben Knoble
#7 lore raw

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

In reply to D. Ben Knoble

Hi Ben

Quoted 33 lines · D. Ben Knoble
Quoted 10 lines · Phillip Wood
Quoted 4 lines · D. Ben Knoble

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.

When the merge succeeds without conflicts we use the result so it is seen. It would be clearer to say that "If there are conflicts we discard the result so ...". The rest of the commit message explains the problem nicely.

Indeed. This is what I get for (unusually) dashing off the commit message up against the clock. Thanks!

Quoted 7 lines
Quoted 7 lines
builtin/stash.c +1 -1
@@ -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 };
 
Quoted 10 lines

builtin/stash.c static enum stash_apply_result do_apply_stash(const char *prefix, · line 630

Quoted 6 lines
builtin/stash.c +1 -5
-                     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);

This means we potentially use different diff algorithms when merging the index and when merging the work tree, let's use the _ui variant here instead.

Quoted 7 lines

Yep, you know I'd spotted that and wasn't expecting it to make a meaningful difference. It's an easy swap, but I thought that (like above, since we don't show the conflict results) the diff algorithm wouldn't matter too much.

Maybe it affects the actual merge-ability, though, in which case I agree using the same is important?

I think there are wierd cases where one diff algorithm results in conflicts and another doesn't because they generate different (but equally valid) diffs so allowing the user to tweak the algorithm we use via init_ui_merge_options() is probably a good idea.

Quoted 10 lines
Quoted 6 lines

builtin/stash.c static enum stash_apply_result do_apply_stash(const char *prefix, · line 632

Quoted 2 lines
builtin/stash.c +1 -0
+                     o.verbosity = 0;

Looking at the code in merge-ort.c it appears the verbosity option was used by the recursive strategy but isn't used anymore so I think we could drop this.

Intriguing. (Assuming the default "2") There's a "< 5" check in path_msg() that wouldn't be affected by dropping this, and a "> 2" check in checkout() that… also wouldn't be affected?

The former is not affected because we're cherry-picking so never have an inner merge from merging multiple merge bases. The latter is not affected because we don't checkout the result!

Quoted 6 lines

But it might matter if something is setting the verbosity elsewhere (config, GIT_MERGE_VERBOSITY), and I think we really want this merge to be quiet? I seem to remember reading commits in this area quieting "git reset" and so on to keep the noise down.

So I'm inclined to leave it for now, especially in case it later does get used.

merge ort does not print anything - it just adds messages to an strmap in struct merge_result() which we ignore here. I guess setting it to zero might avoid a little work generating the messages.

Quoted 10 lines
Quoted 7 lines

builtin/stash.c static enum stash_apply_result do_apply_stash(const char *prefix, · line 646

Quoted 3 lines
builtin/stash.c +2 -0
+                     oidcpy(&index_tree, &result.tree->object.oid);
+                     clear_merge_options(&o);

Looking at replay.c:replay_revisions() I think this should be

merge_finalize(&opts, &result);

Hm, possibly. It does look like that does more with the "result," which is probably needed.

Oh, we definitely want to free the strmap in the merge result.

Quoted 1 line

But it doesn't actually clear the merge options.

Isn't that because there are no allocations in that struct? (obuf is unused - it looks like we could clean up the struct by removing the members that were used by merge-recursive but are ignored by merge-ort)

Quoted 8 lines

On one hand, I thought it could be important not to reuse that struct between merges. But if we do use the "ui" init, it might be ok? replay_revisions() does use the same struct between calls to merge_incore_nonrecursive().

Oh, but one other thing: we unconditionally reinit the merge options later on in do_apply_stash(). We could conditionally initialize there ("if (has_index)"), I suppose?

I'd just move the call to init_ui_merge_options() above "if (index)". As far as I know it should be fine to reuse it - any state is stored in the result

Quoted 20 lines
Quoted 20 lines

merge-ort.c static void merge_start(struct merge_options *opt, struct merge_result *result) · line 5038

Quoted 10 lines
merge-ort.c +0 -1
index c410a5d353..f69a49d48a 100644
@@ -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);

This, and the hunk below, make me nervous. Normally assertions like this exist because the pointers are unconditionally dereferenced later on. Looking at merge_3way() it asserts opt->ancestor is non-NULL and dereferences all three labels. t3903 does not appear to have test coverage for the index merge failing (if it did I think we'd see a SIGSEV), we should probably add a test that checks the command fails leaving the index and work tree untouched, and verifies the message on stderr.

Quoted 6 lines
Quoted 2 lines

Lets set some simple, fixed, ancestor and branch names in do_apply_stash() above.

Funny, I was getting aborts before removing the asserts because I hadn't set the labels, aha. Looks like we've come back around to keeping the labels.

Sorry for that detour

Quoted 2 lines

I'll probably keep a similar structure as the
working tree merge uses, I think.

I'd use fixed names and not bother with all the conditionals around the label text to keep it simple.

Quoted 1 line

A fail-to-merge test also seems like a good idea. Let me mull on that.

That's great

Thanks

Phillip

Quoted 2 lines

Thanks for the review.

#8 lore raw

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

Thanks again, Philip :)

Quoted 8 lines · Phillip Wood

Hi Ben

On 22/09/2026 13:43, D. Ben Knoble wrote:
I think there are wierd cases where one diff algorithm results in conflicts and another doesn't because they generate different (but equally valid) diffs so allowing the user to tweak the algorithm we use via init_ui_merge_options() is probably a good idea.

Gotcha; I've already queued this locally.

Quoted 6 lines
Quoted 6 lines
Quoted 5 lines
Quoted 1 line

+ o.verbosity = 0;

Looking at the code in merge-ort.c it appears the verbosity option was used by the recursive strategy but isn't used anymore so I think we could drop this.

Quoted 7 lines
Quoted 3 lines

Intriguing. (Assuming the default "2") There's a "< 5" check in path_msg() that wouldn't be affected by dropping this, and a "> 2" check in checkout() that… also wouldn't be affected?

The former is not affected because we're cherry-picking so never have an inner merge from merging multiple merge bases. The latter is not affected because we don't checkout the result!

That's very helpful; I find it challenging right now to navigate the various call-graphs here :)

Quoted 10 lines
Quoted 6 lines

But it might matter if something is setting the verbosity elsewhere (config, GIT_MERGE_VERBOSITY), and I think we really want this merge to be quiet? I seem to remember reading commits in this area quieting "git reset" and so on to keep the noise down.

So I'm inclined to leave it for now, especially in case it later does get used.

merge ort does not print anything - it just adds messages to an strmap in struct merge_result() which we ignore here. I guess setting it to zero might avoid a little work generating the messages.

Possibly! I still think it signals our intent to be quiet better this way, too.

Quoted 10 lines
Quoted 9 lines
Quoted 6 lines

builtin/stash.c static enum stash_apply_result do_apply_stash(const char *prefix, · line 646

Quoted 2 lines
builtin/stash.c +2 -0
+                     oidcpy(&index_tree, &result.tree->object.oid);
+                     clear_merge_options(&o);

Looking at replay.c:replay_revisions() I think this should be

merge_finalize(&opts, &result);

Hm, possibly. It does look like that does more with the "result," which is probably needed.

Quoted 7 lines

Oh, we definitely want to free the strmap in the merge result.

Quoted 1 line

But it doesn't actually clear the merge options.

Isn't that because there are no allocations in that struct? (obuf is unused - it looks like we could clean up the struct by removing the members that were used by merge-recursive but are ignored by merge-ort)

Maybe---I was more worried about un-reusable state, but it's true that the clear function is a no-op right now, heh. So it was a bit of "in case one day this is mandatory," perhaps.

Quoted 5 lines
Quoted 5 lines

On one hand, I thought it could be important not to reuse that struct between merges. But if we do use the "ui" init, it might be ok? replay_revisions() does use the same struct between calls to merge_incore_nonrecursive().

Quoted 7 lines
Quoted 3 lines

Oh, but one other thing: we unconditionally reinit the merge options later on in do_apply_stash(). We could conditionally initialize there ("if (has_index)"), I suppose?

I'd just move the call to init_ui_merge_options() above "if (index)". As far as I know it should be fine to reuse it - any state is stored in the result

Yeah, that's smarter. Locally I got tripped by the case where we said --index but skip some work; but it should be fine to unconditionally initialize those options earlier.

Quoted 5 lines
Quoted 3 lines

Funny, I was getting aborts before removing the asserts because I hadn't set the labels, aha. Looks like we've come back around to keeping the labels.

Sorry for that detour

No worries.

Quoted 5 lines
Quoted 2 lines

I'll probably keep a similar structure as the
working tree merge uses, I think.

I'd use fixed names and not bother with all the conditionals around the label text to keep it simple.

That's what I ended up with locally, yeah. I finally decided it was too complicated to do anything else for labels that would be really hard to find.

I'll get v2 out in the morning, probably.

Signature · 1 line
D. Ben Knoble
#10 lore raw

v2 cover stash: clean up index-mode test merge

In reply to D. Ben Knoble

Hi all,

This small patch series fixes a bug reported by Eli Barzilay in the interaction between autostashing, staged index entries, and stash.index=true.

The first patch is an incidental cleanup, and the second re-arranges one line to make the change easier. The third adds a new test, while the fourth holds the interesting bits.

Changes in v2:

• Do give branch labels for the incore merge, although they are never seen (and clarify commit message as a result, also keeping the merge-ort asserts). Phillip was right: without those, we do segfault on conflicts.
• Use the ui merge options to keep the same diff algorithm.
• Use merge_finalize instead of clear_merge_options, and reuse the options between merge calls if they are already initialized.
• Add a new 2/4 to simplify merge options initialization.
• Add a new 3/4 with a test case for conflicted index merges.

v1: <[email protected]>

[1/4] builtin/stash: remove unused header
[2/4] stash: prepare merge options earlier
[3/4] t: test failed "stash apply --index"
[4/4] builtin/stash: merge index in-core

 builtin/stash.c  | 83 +++++++++++-------------------------------------
 t/t3903-stash.sh | 18 +++++++++++
 t/t7600-merge.sh |  9 ++++++
 3 files changed, 45 insertions(+), 65 deletions(-)

Diff-intervalle contre v1 :

1:  b6798c8a25 = 1:  b6798c8a25 builtin/stash: remove unused header
-:  ---------- > 2:  1e2343c7fc stash: prepare merge options earlier
-:  ---------- > 3:  5bd4b78cac t: test failed "stash apply --index"
2:  782fe91251 ! 4:  e49936ee12 builtin/stash: merge index in-core

@@ Commit message

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.
+ 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
@@ Commit message

-
- ## 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 <[email protected]>
-
## builtin/stash.c ##
@@ builtin/stash.c: static int create_index_from_tree(const struct object_id *tree_id,
return ret;
@@ builtin/stash.c: static enum stash_apply_result do_apply_stash(const char *prefi
- return error(_("could not generate diff %s^!."),
- oid_to_hex(&info->w_commit));
- }
-+ init_basic_merge_options(&o, the_repository);
++ o.branch1 = "Upstream index";
++ o.branch2 = "Stashed index changes";
++ o.ancestor = "Stash base";

- ret = apply_cached(&out);
- strbuf_release(&out);
@@ builtin/stash.c: static enum stash_apply_result do_apply_stash(const char *prefi
- discard_index(the_repository->index);
- repo_read_index(the_repository);
+ oidcpy(&index_tree, &result.tree->object.oid);
-+ clear_merge_options(&o);
++ merge_finalize(&o, &result);
}
}

- ## merge-ort.c ##
-@@ merge-ort.c: 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);
-@@ merge-ort.c: 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);
- /*
-
## t/t7600-merge.sh ##
@@ t/t7600-merge.sh: verify_no_mergehead () {
test_cmp result.1-5 file

base-commit: 339ab2a8f14c0c304ae2f28df1a859f3d2cf610c

Signature · 1 line
2.56.0.rc1.315.gc6ed9934b7.dirty
#11 lore raw

PATCH v2 1/4 builtin/stash: remove unused header

Clang complains that oid-array.h is unused. Certainly none of the oid_array* functions, types, etc., are used, and the transitively-included hash.h declarations are used but covered by a pre-existing direct #include of hash.h.

---
 builtin/stash.c | 1 -
 1 file changed, 1 deletion(-)
builtin/stash.c +0 -1
index 7a9843413b..dfea2d2c4c 100644
@@ -31,7 +31,6 @@
 #include "reflog.h"
 #include "reflog-walk.h"
 #include "add-interactive.h"
-#include "oid-array.h"
 #include "commit.h"
 
 #define INCLUDE_ALL_FILES 2
Signature · 1 line
2.56.0.rc1.315.gc6ed9934b7.dirty
#12 lore raw

PATCH v2 2/4 stash: prepare merge options earlier

In reply to D. Ben Knoble

In a future commit, we will reuse these options for the index merge of "apply --index", not just for the worktree.

---
 builtin/stash.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
builtin/stash.c +2 -2
index dfea2d2c4c..043a38cc6d 100644
@@ -664,6 +664,8 @@ static enum stash_apply_result do_apply_stash(const char *prefix,
 				repo_get_index_file(the_repository), 0, NULL))
 		return error(_("cannot apply a stash in the middle of a merge"));
 
+	init_ui_merge_options(&o, the_repository);
+
 	if (index) {
 		if (oideq(&info->b_tree, &info->i_tree) ||
 		    oideq(&c_tree, &info->i_tree)) {
@@ -695,8 +697,6 @@ static enum stash_apply_result do_apply_stash(const char *prefix,
 		}
 	}
 
-	init_ui_merge_options(&o, the_repository);
-
 	o.branch1 = label_ours ? label_ours : "Updated upstream";
 	o.branch2 = label_theirs ? label_theirs : "Stashed changes";
 	o.ancestor = label_base ? label_base : "Stash base";
Signature · 1 line
2.56.0.rc1.315.gc6ed9934b7.dirty
#13 lore raw

PATCH v2 3/4 t: test failed "stash apply --index"

In reply to D. Ben Knoble

The next commit will refactor index handling for applied stashes, so let's make sure we cover conflicted index merging, too.

---
 t/t3903-stash.sh | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)
t/t3903-stash.sh +18 -0
index 721158606f..3958ab3c8d 100755
@@ -374,6 +374,24 @@ setup_stash() {
 	test_cmp expect actual
 '
 
+test_expect_success 'stash apply --index leaves everything untouched on failure' '
+	git reset --hard &&
+	echo test >other-file &&
+	git add other-file &&
+	git stash &&
+	echo unrelated >file &&
+	echo unrelated >another-file &&
+	git add another-file &&
+	git diff-files >expect &&
+
+	echo conflict >other-file &&
+	git add other-file &&
+	test_must_fail git stash apply --index 2>err &&
+	test_grep "conflicts in index. Try without --index" err &&
+	git diff-files >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'stash -k' '
 	echo bar3 >file &&
 	echo bar4 >file2 &&
Signature · 1 line
2.56.0.rc1.315.gc6ed9934b7.dirty
#14 lore raw

Re: [PATCH v2 3/4] t: test failed "stash apply --index"

Hi Ben

Quoted 15 lines · D. Ben Knoble

The next commit will refactor index handling for applied stashes, so let's make sure we cover conflicted index merging, too.

---
  t/t3903-stash.sh | 18 ++++++++++++++++++
  1 file changed, 18 insertions(+)
t/t3903-stash.sh +0 -0
index 721158606f..3958ab3c8d 100755
@@ -374,6 +374,24 @@ setup_stash() {
  	test_cmp expect actual
  '

t/t3903-stash.sh setup_stash() { · line 385

Quoted 10 lines
t/t3903-stash.sh +9 -0
 
+test_expect_success 'stash apply --index leaves everything untouched on failure' '
+	git reset --hard &&
+	echo test >other-file &&
+	git add other-file &&
+	git stash &&
+	echo unrelated >file &&
+	echo unrelated >another-file &&
+	git add another-file &&
+	git diff-files >expect &&

diff-files shows the worktree blobs as null object ids, so comparing this before and after stashing only tells us that the same set of files have unstaged changes, not that the unstaged changes are the same. Adding "-p" would check the worktree files are unchanged.

t/t3903-stash.sh setup_stash() { · line 388

Quoted 2 lines
t/t3903-stash.sh +2 -0
+	echo conflict >other-file &&
+	git add other-file &&

I wonder if we should to add "git diff-index --cached HEAD >expect-index" here so we can check the index is unchanged as well. For the paths that have unstaged changes we're already checking the index object ids via "diff-files", but I think in theory it would be possible to have an identical change in the index and worktree that is not picked up by that.

Thanks for adding this test, it is a useful improvement in our coverage.

Phillip

Quoted 9 lines
t/t3903-stash.sh +6 -0
+	test_must_fail git stash apply --index 2>err &&
+	test_grep "conflicts in index. Try without --index" err &&
+	git diff-files >actual &&
+	test_cmp expect actual
+'
+
  test_expect_success 'stash -k' '
  	echo bar3 >file &&
  	echo bar4 >file2 &&
#15 lore raw

Re: [PATCH v2 3/4] t: test failed "stash apply --index"

Hi Phillip,

Quoted 20 lines · Phillip Wood

Hi Ben

Quoted 16 lines · D. Ben Knoble

The next commit will refactor index handling for applied stashes, so let's make sure we cover conflicted index merging, too.

---
  t/t3903-stash.sh | 18 ++++++++++++++++++
  1 file changed, 18 insertions(+)
t/t3903-stash.sh +0 -0
index 721158606f..3958ab3c8d 100755
@@ -374,6 +374,24 @@ setup_stash() {
      test_cmp expect actual
  '
 
Quoted 14 lines

t/t3903-stash.sh setup_stash() { · line 385

Quoted 9 lines
t/t3903-stash.sh +9 -0
+test_expect_success 'stash apply --index leaves everything untouched on failure' '
+     git reset --hard &&
+     echo test >other-file &&
+     git add other-file &&
+     git stash &&
+     echo unrelated >file &&
+     echo unrelated >another-file &&
+     git add another-file &&
+     git diff-files >expect &&

diff-files shows the worktree blobs as null object ids, so comparing this before and after stashing only tells us that the same set of files have unstaged changes, not that the unstaged changes are the same. Adding "-p" would check the worktree files are unchanged.

I confess I played with diff-files and diff-index manually before trying to construct this test case, and I still don't totally understand how they're being used in the test just prior…

Anway, it looks to me like "diff-files -p" is the same as "diff -p" (albeit without some niceties like color-moved applying automatically from config), so that would make the test quite a bit more complicated, no? (The "index $sha1..$sha2" line would change…)

Since we know what the expected contents are, perhaps we can simply assert on those.

Hm. I spent some time with test_pause in the previous test, and I think my concerns about that line changing are moot. But, asserting on the contents is also a bit silly (as that's what the blob IDs are doing for us in the output).

Quoted 9 lines

t/t3903-stash.sh setup_stash() { · line 388

Quoted 2 lines
t/t3903-stash.sh +2 -0
+     echo conflict >other-file &&
+     git add other-file &&

I wonder if we should to add "git diff-index --cached HEAD

Quoted 1 line

expect-index" here so we can check the index is unchanged as well. For

the paths that have unstaged changes we're already checking the index object ids via "diff-files", but I think in theory it would be possible to have an identical change in the index and worktree that is not picked up by that.

So, this test sets up an intermediate state prior to attempting to unstash where

- another-file is new in the index & working tree (content: "unrelated")
- other-file is modified in the index & working tree (content: from "6" to "conflict")
- file is modified in the working tree (content: from "bar" to "unrelated")

And we should still be there when finished. (I wonder if, like the previous test, we should have a file that differs from itself in the index and working tree?)

So overall, I'm thinking

- (old) diff-files only shows file is changed
- diff-files -p shows us changes for file, better (and won't show the other 2 files unless they've become unstaged)
- diff-index --cached HEAD helps us check all the index changes

Phew! Thanks for reading my rambling thinking aloud :)

Signature · 1 line
D. Ben Knoble
#16 lore raw

Re: [PATCH v2 3/4] t: test failed "stash apply --index"

Hi Ben

Quoted 7 lines · D. Ben Knoble

So overall, I'm thinking

- (old) diff-files only shows file is changed
- diff-files -p shows us changes for file, better (and won't show the other 2 files unless they've become unstaged)
- diff-index --cached HEAD helps us check all the index changes

I think that sounds reasonable, we can delete the index lines from the patch output with sed to make it easier to compare them.

Thanks

Phillip

Quoted 2 lines

Phew! Thanks for reading my rambling thinking aloud :)

#17 lore raw

Re: [PATCH v2 3/4] t: test failed "stash apply --index"

Quoted 2 lines · Phillip Wood

I think that sounds reasonable, we can delete the index lines from the patch output with sed to make it easier to compare them.

I just opened the test file and realized it has a diff_cmp() function to compare diffs ignoring the index lines

Thanks

Phillip

#18 lore raw

Re: [PATCH v2 3/4] t: test failed "stash apply --index"

Quoted 6 lines · Phillip Wood
Quoted 3 lines · Phillip Wood

I think that sounds reasonable, we can delete the index lines from the patch output with sed to make it easier to compare them.

Quoted 6 lines

I just opened the test file and realized it has a diff_cmp() function to compare diffs ignoring the index lines

Thanks

Phillip

Doh!

On the other hand, I don't think we need it. Those lines are showing blob IDs, which would be stable in our case (fixed hash algorithm over fixed contents), and we don't make the test dependent on the actual IDs?

Signature · 1 line
D. Ben Knoble
#19 lore raw

PATCH v2 4/4 builtin/stash: merge index in-core

In reply to D. Ben Knoble

"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.

---
 builtin/stash.c  | 78 ++++++++++--------------------------------------
 t/t7600-merge.sh |  9 ++++++
 2 files changed, 25 insertions(+), 62 deletions(-)
builtin/stash.c +16 -62
index 043a38cc6d..219ca457be 100644
@@ -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 = "Upstream 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, 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);
+			merge_finalize(&o, &result);
 		}
 	}
 
t/t7600-merge.sh +9 -0
index 64fe21717d..8f6109fb91 100755
@@ -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 &&
Signature · 1 line
2.56.0.rc1.315.gc6ed9934b7.dirty
#20 lore raw

Re: [PATCH v2 4/4] builtin/stash: merge index in-core

Hi Ben

I've spotted a memory leak that I missed last time, apart from that this looks good.

builtin/stash.c static enum stash_apply_result do_apply_stash(const char *prefix, · line 632

Quoted 13 lines · D. Ben Knoble
builtin/stash.c +2 -6
@@ -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 = "Upstream index";

This is the current index, calling it "upstream" is a bit confusing to me but that's not worth a re-roll on its own.

Quoted 8 lines
builtin/stash.c +4 -3
+			o.branch2 = "Stashed index changes";
+			o.ancestor = "Stash base";
 
-			ret = apply_cached(&out);
-			strbuf_release(&out);
-			if (ret)
+			o.verbosity = 0;
+

builtin/stash.c static enum stash_apply_result do_apply_stash(const char *prefix, · line 647

Quoted 10 lines
builtin/stash.c +8 -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."));

Sorry, I missed this last time, but we should finalize the merge before returning to ensure the allocations in result are freed. Everything else looks fine.

Thanks

Phillip

Quoted 34 lines
builtin/stash.c +2 -9
-			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);
+			merge_finalize(&o, &result);
  		}
  	}
 
t/t7600-merge.sh +9 -0
index 64fe21717d..8f6109fb91 100755
@@ -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 &&
#21 lore raw

Re: [PATCH v2 4/4] builtin/stash: merge index in-core

Hi Phillip,

Quoted 14 lines · Phillip Wood

Hi Ben

I've spotted a memory leak that I missed last time, apart from that this looks good.

Quoted 7 lines · D. Ben Knoble
builtin/stash.c +1 -1
@@ -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 };
 
Quoted 9 lines

builtin/stash.c static enum stash_apply_result do_apply_stash(const char *prefix, · line 632

Quoted 6 lines
builtin/stash.c +1 -5
-                     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 = "Upstream index";

This is the current index, calling it "upstream" is a bit confusing to me but that's not worth a re-roll on its own.

Will fix. The "upstream" verbiage comes from the working tree labels.

Quoted 13 lines
Quoted 13 lines
builtin/stash.c +9 -3
+                     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, head, merge, merge_base,
Quoted 8 lines

builtin/stash.c static enum stash_apply_result do_apply_stash(const char *prefix, · line 647

Quoted 5 lines
builtin/stash.c +3 -0
+                                               &result);
+
+                     if (!result.clean)
                              return error(_("conflicts in index. "
                                             "Try without --index."));

Sorry, I missed this last time, but we should finalize the merge before returning to ensure the allocations in result are freed.

Yeah, I think CI caught this:
https://github.com/benknoble/git/actions/runs/36033463504/job/107747745741#step:5:31

But I'm not sure I could have understood what it was telling me without your hint, thanks!

Signature · 1 line
D. Ben Knoble
#22 lore raw

Re: [PATCH v2 4/4] builtin/stash: merge index in-core

Hi Ben

Quoted 10 lines · D. Ben Knoble
Quoted 3 lines · Phillip Wood

Sorry, I missed this last time, but we should finalize the merge before returning to ensure the allocations in result are freed.

Yeah, I think CI caught this:
https://github.com/benknoble/git/actions/runs/36033463504/job/107747745741#step:5:31

But I'm not sure I could have understood what it was telling me without your hint, thanks!

Yes, that output is terrible - to see the leaks you have to scroll to line 28282 of "print test failures" which is ridiculous. See https://github.com/benknoble/git/actions/runs/36033463504/job/107747745741#step:10:28282

Thanks

Phillip>

#23 lore raw

Re: [PATCH v2 4/4] builtin/stash: merge index in-core

Quoted 9 lines · Phillip Wood
Quoted 8 lines · D. Ben Knoble
Quoted 3 lines · Phillip Wood

Sorry, I missed this last time, but we should finalize the merge before returning to ensure the allocations in result are freed.

Yeah, I think CI caught this:
https://github.com/benknoble/git/actions/runs/36033463504/job/107747745741#step:5:31

Quoted 6 lines
Quoted 2 lines

But I'm not sure I could have understood what it was telling me without your hint, thanks!

Yes, that output is terrible - to see the leaks you have to scroll to line 28282 of "print test failures" which is ridiculous. See
https://github.com/benknoble/git/actions/runs/36033463504/job/107747745741#step:10:28282

Ah, sorry. My link was sloppy.

I did get that far, but the allocation backtrace doesn't make it obvious that merge_result is what leaked, and that's where I was saying an especial thank you ;)

Signature · 1 line
D. Ben Knoble
#24 lore raw

Re: [PATCH v2 4/4] builtin/stash: merge index in-core

In reply to D. Ben Knoble
Quoted 9 lines · D. Ben Knoble
(quoted diff) +1 -3
@@ -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);

Review static enum stash_apply_result do_apply_stash(const char *prefix, · line 684

Quoted 10 lines
(quoted diff) +3 -6
-				return error(_("could not generate diff %s^!."),
-					     oid_to_hex(&info->w_commit));
-			}
+			o.branch1 = "Upstream index";
+			o.branch2 = "Stashed index changes";
+			o.ancestor = "Stash base";
 
-			ret = apply_cached(&out);
-			strbuf_release(&out);
-			if (ret)

So, we used to take a diff between w_commit^2^ and w_commit^2 and then give the resulting patch to "apply --cached". w_commit is the working tree state, w_commit^1 is the HEAD (i.e. b_tree) when the stash was created (i.e., "diff HEAD w_commit" is the change in the working tree), w_commit^2 is the contents of the index (i.e. i_tree), so we are computing a patch that represents what "diff --cached HEAD" would have shown when we created the stash. And the goal is to reflect this change on the current HEAD to recreate the "staged" changes in the current index.

IOW, we want to three-way merge the change that moves you from b_tree to i_tree into c_tree.

Review static enum stash_apply_result do_apply_stash(const char *prefix, · line 643

Quoted 8 lines
(quoted diff) +8 -0
+			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);

We are using the merge machinery to perform a cherry-pick of the changes to go from b_tree to i_tree into c_tree. but the three trees involved in this cherry-pick is named unnecessarily confusingly.

merge-incore-nonrecursive() takes the common ancestor ("merge_base") and two sides ("side1" and "side2") in this order. It takes the changes to go from common to side1 and computes the result of updating the remainder (side2) with such a change (or vice versa; a merge is symmetric). When cherry-picking, the first tree would be the b_tree, the second tree would be the i_tree, and the target tree would be the c_tree.

- b_tree serves as merge_base
- i_tree serves as side1
- c_tree serves as side2

Am I following what the code should be doing correctly?

I am wondering if the order of the tree trees in the merge_incore_nonrecursive() call is correct. Shouldn't it be

			merge_incore_nonrecursive(&o,
						  merge_base, merge, head,
						  &result);

(or merge and head swapped) if we want to update head (I would call it side2) in such a way that the change to go from it to the resulting tree is similar to the change between merge_base (b_tree) and merge (i_tree)?

Ahh, or perhaps the trees are indeed given in a wrong order, but not in a random wrong order. merge_ort_nonrecursive(), which is *not* the function you are using, takes head, merge, and merge_base in this order, and that order matches what you wrote.

Perhaps the true culprit in this confusion is that the order in which merge_ort_nonrecursive() takes its three trees (head, merge, and common) and the order in which merge_incore_nonrecursive() takes its trees (merge_base, side1, and side2) are different, and if we fix them to match, it would make it easier to work with?

The new test in the attached patch will fail with this step but if we revert the changes to builtin/stash.c in this step, it passes.

 t/t3903-stash.sh | 32 ++++++++++++++++++++++++++++++++
 1 file changed, 32 insertions(+)
t/t3903-stash.sh +32 -0
index 3958ab3c8d..0a87e62b11 100755
@@ -374,6 +374,38 @@ test_expect_success 'stash apply -q --index refreshes the index' '
 	test_cmp expect actual
 '
 
+
+test_expect_success 'stash apply --index does not revert unrelated upstream index changes' '
+	test_when_finished "rm -fr playpen" &&
+	mkdir playpen &&
+	(
+		cd playpen &&
+		git init &&
+		echo "base1" >file1 &&
+		echo "base2" >file2 &&
+		git add file1 file2 &&
+		git commit -m "initial base" &&
+
+		# Make a staged change to file1 and stash it
+		echo "staged1" >file1 &&
+		git add file1 &&
+		git stash &&
+
+		# Upstream advances by modifying unrelated file2
+		echo "upstream2" >file2 &&
+		git add file2 &&
+		git commit -m "upstream change to file2" &&
+
+		# Apply the stash with --index
+		git stash apply --index &&
+
+		# Verify working tree and index state
+		test "$(git show :file1)" = "staged1" &&
+		test "$(git show :file2)" = "upstream2" &&
+		test "$(git show HEAD:file2)" = "upstream2"
+	)
+'
+
 test_expect_success 'stash apply --index leaves everything untouched on failure' '
 	git reset --hard &&
 	echo test >other-file &&
#25 lore raw

Re: [PATCH v2 4/4] builtin/stash: merge index in-core

Quoted 2 lines · Junio C Hamano

The new test in the attached patch will fail with this step but if we revert the changes to builtin/stash.c in this step, it passes.

Oh, and with the change to the code, it passes again.

builtin/stash.c +1 -1
index 219ca457be..44d962cc5d 100644
@@ -639,7 +639,7 @@ static enum stash_apply_result do_apply_stash(const char *prefix,
 			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,
+			merge_incore_nonrecursive(&o, merge_base, merge, head,
 						  &result);
 
 			if (!result.clean)
t/t3903-stash.sh +32 -0
index 3958ab3c8d..0a87e62b11 100755
@@ -374,6 +374,38 @@ test_expect_success 'stash apply -q --index refreshes the index' '
 	test_cmp expect actual
 '
 
+
+test_expect_success 'stash apply --index does not revert unrelated upstream index changes' '
+	test_when_finished "rm -fr playpen" &&
+	mkdir playpen &&
+	(
+		cd playpen &&
+		git init &&
+		echo "base1" >file1 &&
+		echo "base2" >file2 &&
+		git add file1 file2 &&
+		git commit -m "initial base" &&
+
+		# Make a staged change to file1 and stash it
+		echo "staged1" >file1 &&
+		git add file1 &&
+		git stash &&
+
+		# Upstream advances by modifying unrelated file2
+		echo "upstream2" >file2 &&
+		git add file2 &&
+		git commit -m "upstream change to file2" &&
+
+		# Apply the stash with --index
+		git stash apply --index &&
+
+		# Verify working tree and index state
+		test "$(git show :file1)" = "staged1" &&
+		test "$(git show :file2)" = "upstream2" &&
+		test "$(git show HEAD:file2)" = "upstream2"
+	)
+'
+
 test_expect_success 'stash apply --index leaves everything untouched on failure' '
 	git reset --hard &&
 	echo test >other-file &&
#26 lore raw

Re: [PATCH v2 4/4] builtin/stash: merge index in-core

In reply to Junio C Hamano
Quoted 11 lines · Junio C Hamano

Ahh, or perhaps the trees are indeed given in a wrong order, but not in a random wrong order. merge_ort_nonrecursive(), which is *not* the function you are using, takes head, merge, and merge_base in this order, and that order matches what you wrote.

Perhaps the true culprit in this confusion is that the order in which merge_ort_nonrecursive() takes its three trees (head, merge, and common) and the order in which merge_incore_nonrecursive() takes its trees (merge_base, side1, and side2) are different, and if we fix them to match, it would make it easier to work with?

Indeed, the confusion is that simple ;) Shamefully, we don't have enough test coverage to catch that regression, so I'm very glad indeed you spotted it.

Quoted 2 lines

The new test in the attached patch will fail with this step but if we revert the changes to builtin/stash.c in this step, it passes.

Any objection to me adding this test as a preparatory patch? There's no sign-off, so I don't want to mess up the DCO here.

#27 lore raw

Re: [PATCH v2 4/4] builtin/stash: merge index in-core

Quoted 17 lines · D. Ben Knoble
Quoted 11 lines · Junio C Hamano

Ahh, or perhaps the trees are indeed given in a wrong order, but not in a random wrong order. merge_ort_nonrecursive(), which is *not* the function you are using, takes head, merge, and merge_base in this order, and that order matches what you wrote.

Perhaps the true culprit in this confusion is that the order in which merge_ort_nonrecursive() takes its three trees (head, merge, and common) and the order in which merge_incore_nonrecursive() takes its trees (merge_base, side1, and side2) are different, and if we fix them to match, it would make it easier to work with?

Indeed, the confusion is that simple ;) Shamefully, we don't have enough test coverage to catch that regression, so I'm very glad indeed you spotted it.

Quoted 5 lines
Quoted 2 lines

The new test in the attached patch will fail with this step but if we revert the changes to builtin/stash.c in this step, it passes.

Any objection to me adding this test as a preparatory patch? There's no sign-off, so I don't want to mess up the DCO here.

It was written merely as an illustration and is not something I am proud of. For example, creating a totally new playpen repository only for a single piece of test and remove the entire thing when the single test piece is done was done only to make sure the existing test that come later can never be affected. Also the test only uses the most trivial case (a file is added in the stashed change, nobody else involved in the stash application has touched the file so there is nothing to "merge" in the file). It was enough to demonstrate that the order of arguments given to the function was wrong, but we wouldn't catch problems in content-level merge with such a test.

So, I wouldn't mind if you reused that as one in a series of tests, but I'd prefer to see those who are move invested in the topic to come up with a bit more realistic scenario.

Thanks.

#28 lore raw

Re: [PATCH v2 4/4] builtin/stash: merge index in-core

Quoted 36 lines · Junio C Hamano
Quoted 17 lines · D. Ben Knoble
Quoted 11 lines · Junio C Hamano

Ahh, or perhaps the trees are indeed given in a wrong order, but not in a random wrong order. merge_ort_nonrecursive(), which is *not* the function you are using, takes head, merge, and merge_base in this order, and that order matches what you wrote.

Perhaps the true culprit in this confusion is that the order in which merge_ort_nonrecursive() takes its three trees (head, merge, and common) and the order in which merge_incore_nonrecursive() takes its trees (merge_base, side1, and side2) are different, and if we fix them to match, it would make it easier to work with?

Indeed, the confusion is that simple ;) Shamefully, we don't have enough test coverage to catch that regression, so I'm very glad indeed you spotted it.

Quoted 5 lines
Quoted 2 lines

The new test in the attached patch will fail with this step but if we revert the changes to builtin/stash.c in this step, it passes.

Any objection to me adding this test as a preparatory patch? There's no sign-off, so I don't want to mess up the DCO here.

It was written merely as an illustration and is not something I am proud of. For example, creating a totally new playpen repository only for a single piece of test and remove the entire thing when the single test piece is done was done only to make sure the existing test that come later can never be affected. Also the test only uses the most trivial case (a file is added in the stashed change, nobody else involved in the stash application has touched the file so there is nothing to "merge" in the file). It was enough to demonstrate that the order of arguments given to the function was wrong, but we wouldn't catch problems in content-level merge with such a test.

Quoted 3 lines

So, I wouldn't mind if you reused that as one in a series of tests, but I'd prefer to see those who are move invested in the topic to come up with a bit more realistic scenario.

Maybe something like the test below (which I admit I haven't actually tested). That checks we merge the file contents and puts the changes in the file close enough together so that the old code would fail and has different contents for the three merged blobs.

test_write_lines A B C >file &&
git commit -m xxx file &&
test_write_lines A B staged >file &&
git add file &&
test_write_lines A B unstaged >file &&
git stash &&
test_write_lines committed B C >file &&
git commit -m yyy file &&
git stash pop --index &&
git show :file >actual &&
test_write_lines committed B staged >expect &&
text_cmp expect actual &&
test_write_lines committed B unstaged >expect &&
test_cmp expect file

Thanks

Phillip

#29 lore raw

Re: [PATCH v2 4/4] builtin/stash: merge index in-core

Quoted 47 lines · Phillip Wood
Quoted 36 lines · Junio C Hamano
Quoted 17 lines · D. Ben Knoble
Quoted 11 lines · Junio C Hamano

Ahh, or perhaps the trees are indeed given in a wrong order, but not in a random wrong order. merge_ort_nonrecursive(), which is *not* the function you are using, takes head, merge, and merge_base in this order, and that order matches what you wrote.

Perhaps the true culprit in this confusion is that the order in which merge_ort_nonrecursive() takes its three trees (head, merge, and common) and the order in which merge_incore_nonrecursive() takes its trees (merge_base, side1, and side2) are different, and if we fix them to match, it would make it easier to work with?

Indeed, the confusion is that simple ;) Shamefully, we don't have enough test coverage to catch that regression, so I'm very glad indeed you spotted it.

Quoted 5 lines
Quoted 2 lines

The new test in the attached patch will fail with this step but if we revert the changes to builtin/stash.c in this step, it passes.

Any objection to me adding this test as a preparatory patch? There's no sign-off, so I don't want to mess up the DCO here.

It was written merely as an illustration and is not something I am proud of. For example, creating a totally new playpen repository only for a single piece of test and remove the entire thing when the single test piece is done was done only to make sure the existing test that come later can never be affected. Also the test only uses the most trivial case (a file is added in the stashed change, nobody else involved in the stash application has touched the file so there is nothing to "merge" in the file). It was enough to demonstrate that the order of arguments given to the function was wrong, but we wouldn't catch problems in content-level merge with such a test.

Quoted 3 lines

So, I wouldn't mind if you reused that as one in a series of tests, but I'd prefer to see those who are move invested in the topic to come up with a bit more realistic scenario.

Maybe something like the test below (which I admit I haven't actually tested). That checks we merge the file contents and puts the changes in the file close enough together so that the old code would fail and has different contents for the three merged blobs.

Quoted 12 lines

test_write_lines A B C >file &&
git commit -m xxx file &&
test_write_lines A B staged >file &&
git add file &&
test_write_lines A B unstaged >file &&
git stash &&
test_write_lines committed B C >file &&
git commit -m yyy file &&
git stash pop --index &&
git show :file >actual &&
test_write_lines committed B staged >expect &&
text_cmp expect actual &&

s/text/test ;)

Quoted 2 lines

test_write_lines committed B unstaged >expect &&
test_cmp expect file

This does fail on the original code (head, base, merge_base) because the index (git show :file) has "A B staged" lines instead of "committed B staged" lines.

This test does pass on the new code, but needs some arrangement/cleanup for the later "stash -k" test to succeed, so I'll include that in the next round as well.

Signature · 1 line
D. Ben Knoble
#30 lore raw

Re: [PATCH v2 4/4] builtin/stash: merge index in-core

In reply to Junio C Hamano

Hi Junio

Quoted 6 lines · Junio C Hamano

"D. Ben Knoble" <[email protected]> writes:

Ahh, or perhaps the trees are indeed given in a wrong order, but not in a random wrong order. merge_ort_nonrecursive(), which is *not* the function you are using, takes head, merge, and merge_base in this order, and that order matches what you wrote.

Ouch that's nasty. Well spotted, I missed it when I read the code (because the arguments were in the same order as the call to merge_ort_nonrecursive()) and the tests we have use the same version of the file for "base" and "stage2" so do not notice if they'd been transposed. It is rather confusing that two functions that are so closely related take their arguments in a different order.

Quoted 5 lines

Perhaps the true culprit in this confusion is that the order in which merge_ort_nonrecursive() takes its three trees (head, merge, and common) and the order in which merge_incore_nonrecursive() takes its trees (merge_base, side1, and side2) are different, and if we fix them to match, it would make it easier to work with?

I think it is definitely worth fixing them to take the trees in the same order. My preference would be "base", "stage1", "stage2" but so long as they match each other I dont object to "stage1", "stage2", "base".

Thanks

Phillip

Quoted 49 lines

The new test in the attached patch will fail with this step but if we revert the changes to builtin/stash.c in this step, it passes.

  t/t3903-stash.sh | 32 ++++++++++++++++++++++++++++++++
  1 file changed, 32 insertions(+)
t/t3903-stash.sh +32 -0
index 3958ab3c8d..0a87e62b11 100755
@@ -374,6 +374,38 @@ test_expect_success 'stash apply -q --index refreshes the index' '
  	test_cmp expect actual
  '
 
+
+test_expect_success 'stash apply --index does not revert unrelated upstream index changes' '
+	test_when_finished "rm -fr playpen" &&
+	mkdir playpen &&
+	(
+		cd playpen &&
+		git init &&
+		echo "base1" >file1 &&
+		echo "base2" >file2 &&
+		git add file1 file2 &&
+		git commit -m "initial base" &&
+
+		# Make a staged change to file1 and stash it
+		echo "staged1" >file1 &&
+		git add file1 &&
+		git stash &&
+
+		# Upstream advances by modifying unrelated file2
+		echo "upstream2" >file2 &&
+		git add file2 &&
+		git commit -m "upstream change to file2" &&
+
+		# Apply the stash with --index
+		git stash apply --index &&
+
+		# Verify working tree and index state
+		test "$(git show :file1)" = "staged1" &&
+		test "$(git show :file2)" = "upstream2" &&
+		test "$(git show HEAD:file2)" = "upstream2"
+	)
+'
+
  test_expect_success 'stash apply --index leaves everything untouched on failure' '
  	git reset --hard &&
  	echo test >other-file &&
#31 lore raw

Re: [PATCH v2 4/4] builtin/stash: merge index in-core

Quoted 4 lines · Phillip Wood

Hi Junio

On 24/09/2026 22:59, Junio C Hamano wrote:

[snip]

Quoted 6 lines
Quoted 5 lines

Perhaps the true culprit in this confusion is that the order in which merge_ort_nonrecursive() takes its three trees (head, merge, and common) and the order in which merge_incore_nonrecursive() takes its trees (merge_base, side1, and side2) are different, and if we fix them to match, it would make it easier to work with?

Quoted 7 lines

I think it is definitely worth fixing them to take the trees in the same order. My preference would be "base", "stage1", "stage2" but so long as they match each other I dont object to "stage1", "stage2", "base".

Thanks

Phillip

FWIW, I concur with changing them (and Phillip's preference of order), but I'll elect to leave that out of scope for this series.

Signature · 1 line
D. Ben Knoble
#32 lore raw

Re: [PATCH v2 4/4] builtin/stash: merge index in-core

Quoted 10 lines · D. Ben Knoble
Quoted 7 lines

I think it is definitely worth fixing them to take the trees in the same order. My preference would be "base", "stage1", "stage2" but so long as they match each other I dont object to "stage1", "stage2", "base".

Thanks

Phillip

FWIW, I concur with changing them (and Phillip's preference of order), but I'll elect to leave that out of scope for this series.

Oh, absolutely it is out of scope for this series.

#33 lore raw

v3 cover stash: clean up index-mode test merge

In reply to D. Ben Knoble

Hi all,

This small patch series fixes a bug reported by Eli Barzilay in the interaction between autostashing, staged index entries, and stash.index=true.

The first patch is an incidental cleanup, and the second re-arranges one line to make the change easier. The third and fourth add missing test coverage (which catch breakages from prior incorrect rounds of this series), while the last holds the interesting bits.

Changes in v3:

• Change conflict label for current index
• Fix memory leak of merge_result
• Fix order of trees to make the correct merge (cherry-pick)
• New test (3/5) to validate this
• Fix test in 4/5 to assert more details of expected state

Changes in v2:

• Do give branch labels for the incore merge, although they are never seen (and clarify commit message as a result, also keeping the merge-ort asserts). Phillip was right: without those, we do segfault on conflicts.
• Use the ui merge options to keep the same diff algorithm.
• Use merge_finalize instead of clear_merge_options, and reuse the options between merge calls if they are already initialized.
• Add a new 2/4 to simplify merge options initialization.
• Add a new 3/4 with a test case for conflicted index merges.

v1: <[email protected]>
v2: <[email protected]>

[1/5] builtin/stash: remove unused header
[2/5] stash: prepare merge options earlier
[3/5] t3903: test stash --index merges
[4/5] t3903: test failed "stash apply --index"
[5/5] builtin/stash: merge index in-core

 builtin/stash.c  | 85 +++++++++++-------------------------------------
 t/t3903-stash.sh | 42 ++++++++++++++++++++++++
 t/t7600-merge.sh |  9 +++++
 3 files changed, 70 insertions(+), 66 deletions(-)

Diff-intervalle contre v2 :

1:  b6798c8a25 = 1:  6a165c4df4 builtin/stash: remove unused header
2:  1e2343c7fc = 2:  d9a9e18f3a stash: prepare merge options earlier
-:  ---------- > 3:  8b5ea5e6f4 t3903: test stash --index merges
3:  5bd4b78cac ! 4:  d39e16905d t: test failed "stash apply --index"
    @@ Metadata
     Author: D. Ben Knoble <[email protected]>
     
      ## Commit message ##
    -    t: test failed "stash apply --index"
    +    t3903: test failed "stash apply --index"
     
         The next commit will refactor index handling for applied stashes, so
         let's make sure we cover conflicted index merging, too.
     
    +    Helped-by: Phillip Wood <[email protected]>
    +
      ## t/t3903-stash.sh ##
     @@ t/t3903-stash.sh: setup_stash() {
    - 	test_cmp expect actual
    + 	test_cmp expect file
      '
      
     +test_expect_success 'stash apply --index leaves everything untouched on failure' '
    @@ t/t3903-stash.sh: setup_stash() {
     +	echo unrelated >file &&
     +	echo unrelated >another-file &&
     +	git add another-file &&
    -+	git diff-files >expect &&
    -+
     +	echo conflict >other-file &&
     +	git add other-file &&
    ++	git diff-files -p >expect &&
    ++	git diff-index --cached HEAD >expect-index &&
    ++
     +	test_must_fail git stash apply --index 2>err &&
     +	test_grep "conflicts in index. Try without --index" err &&
    -+	git diff-files >actual &&
    -+	test_cmp expect actual
    ++	git diff-files -p >actual &&
    ++	test_cmp expect actual &&
    ++	git diff-index --cached HEAD >actual-index &&
    ++	test_cmp expect-index actual-index
     +'
     +
      test_expect_success 'stash -k' '
4:  e49936ee12 ! 5:  fde7fb7988 builtin/stash: merge index in-core
    @@ Commit message
     
         Reported-by: Eli Barzilay <[email protected]>
         Helped-by: Phillip Wood <[email protected]>
    +    Helped-by: Junio C Hamano <[email protected]>
     
      ## builtin/stash.c ##
     @@ builtin/stash.c: static int create_index_from_tree(const struct object_id *tree_id,
    @@ builtin/stash.c: static enum stash_apply_result do_apply_stash(const char *prefi
     -				return error(_("could not generate diff %s^!."),
     -					     oid_to_hex(&info->w_commit));
     -			}
    -+			o.branch1 = "Upstream index";
    ++			o.branch1 = "Current index";
     +			o.branch2 = "Stashed index changes";
     +			o.ancestor = "Stash base";
      
    @@ builtin/stash.c: static enum stash_apply_result do_apply_stash(const char *prefi
     +			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,
    ++			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,
    @@ builtin/stash.c: static enum stash_apply_result do_apply_stash(const char *prefi
     -			reset_head();
     -			discard_index(the_repository->index);
     -			repo_read_index(the_repository);
    -+			oidcpy(&index_tree, &result.tree->object.oid);
    -+			merge_finalize(&o, &result);
      		}
      	}

base-commit: d38352cd43ab9745686d697872408bc3249a153f

Signature · 1 line
2.56.0.rc1.315.gc6ed9934b7.dirty
#34 lore raw

PATCH v3 1/5 builtin/stash: remove unused header

Clang complains that oid-array.h is unused. Certainly none of the oid_array* functions, types, etc., are used, and the transitively-included hash.h declarations are used but covered by a pre-existing direct #include of hash.h.

---
 builtin/stash.c | 1 -
 1 file changed, 1 deletion(-)
builtin/stash.c +0 -1
index 7a9843413b..dfea2d2c4c 100644
@@ -31,7 +31,6 @@
 #include "reflog.h"
 #include "reflog-walk.h"
 #include "add-interactive.h"
-#include "oid-array.h"
 #include "commit.h"
 
 #define INCLUDE_ALL_FILES 2
Signature · 1 line
2.56.0.rc1.315.gc6ed9934b7.dirty
#35 lore raw

PATCH v3 2/5 stash: prepare merge options earlier

In reply to D. Ben Knoble

In a future commit, we will reuse these options for the index merge of "apply --index", not just for the worktree.

---
 builtin/stash.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
builtin/stash.c +2 -2
index dfea2d2c4c..043a38cc6d 100644
@@ -664,6 +664,8 @@ static enum stash_apply_result do_apply_stash(const char *prefix,
 				repo_get_index_file(the_repository), 0, NULL))
 		return error(_("cannot apply a stash in the middle of a merge"));
 
+	init_ui_merge_options(&o, the_repository);
+
 	if (index) {
 		if (oideq(&info->b_tree, &info->i_tree) ||
 		    oideq(&c_tree, &info->i_tree)) {
@@ -695,8 +697,6 @@ static enum stash_apply_result do_apply_stash(const char *prefix,
 		}
 	}
 
-	init_ui_merge_options(&o, the_repository);
-
 	o.branch1 = label_ours ? label_ours : "Updated upstream";
 	o.branch2 = label_theirs ? label_theirs : "Stashed changes";
 	o.ancestor = label_base ? label_base : "Stash base";
Signature · 1 line
2.56.0.rc1.315.gc6ed9934b7.dirty
#36 lore raw

PATCH v3 3/5 t3903: test stash --index merges

In reply to D. Ben Knoble

A future commit will refactor index handling for applied stashes, and we need to take care to get the order of trees right when merging. Add a test that covers this case.

---
 t/t3903-stash.sh | 21 +++++++++++++++++++++
 1 file changed, 21 insertions(+)
t/t3903-stash.sh +21 -0
index 721158606f..9bc99fa252 100755
@@ -374,6 +374,27 @@ setup_stash() {
 	test_cmp expect actual
 '
 
+# the later "stash -k" test is not expecting us to muck with file so much, so
+# reset when finished
+test_expect_success 'stash apply --index merges the correct trees' '
+	head=$(git rev-parse HEAD) &&
+	test_when_finished "git reset --hard $head" &&
+	test_write_lines A B C >file &&
+	git commit -m setup file &&
+	test_write_lines A B staged >file &&
+	git add file &&
+	test_write_lines A B unstaged >file &&
+	git stash &&
+	test_write_lines committed B C >file &&
+	git commit -m to-be-merged file &&
+	git stash pop --index &&
+	git show :file >actual &&
+	test_write_lines committed B staged >expect &&
+	test_cmp expect actual &&
+	test_write_lines committed B unstaged >expect &&
+	test_cmp expect file
+'
+
 test_expect_success 'stash -k' '
 	echo bar3 >file &&
 	echo bar4 >file2 &&
Signature · 1 line
2.56.0.rc1.315.gc6ed9934b7.dirty
#37 lore raw

Re: [PATCH v3 3/5] t3903: test stash --index merges

Hi Ben

Quoted 3 lines · D. Ben Knoble

A future commit will refactor index handling for applied stashes, and we need to take care to get the order of trees right when merging. Add a test that covers this case.

The test looks good, but without the changes in patch 5 it fails and so adding it here breaks running "git bisect" on this series. I'd squash this into the final patch and I think we can probably replace an existing "stash apply --index" tests that are not so strict with this one, rather than adding a new test.

Thanks

Phillip

Quoted 38 lines
---
  t/t3903-stash.sh | 21 +++++++++++++++++++++
  1 file changed, 21 insertions(+)
t/t3903-stash.sh +21 -0
index 721158606f..9bc99fa252 100755
@@ -374,6 +374,27 @@ setup_stash() {
  	test_cmp expect actual
  '
 
+# the later "stash -k" test is not expecting us to muck with file so much, so
+# reset when finished
+test_expect_success 'stash apply --index merges the correct trees' '
+	head=$(git rev-parse HEAD) &&
+	test_when_finished "git reset --hard $head" &&
+	test_write_lines A B C >file &&
+	git commit -m setup file &&
+	test_write_lines A B staged >file &&
+	git add file &&
+	test_write_lines A B unstaged >file &&
+	git stash &&
+	test_write_lines committed B C >file &&
+	git commit -m to-be-merged file &&
+	git stash pop --index &&
+	git show :file >actual &&
+	test_write_lines committed B staged >expect &&
+	test_cmp expect actual &&
+	test_write_lines committed B unstaged >expect &&
+	test_cmp expect file
+'
+
  test_expect_success 'stash -k' '
  	echo bar3 >file &&
  	echo bar4 >file2 &&
#38 lore raw

Re: [PATCH v3 3/5] t3903: test stash --index merges

Quoted 11 lines · Phillip Wood

Hi Ben

Quoted 3 lines · D. Ben Knoble

A future commit will refactor index handling for applied stashes, and we need to take care to get the order of trees right when merging. Add a test that covers this case.

The test looks good, but without the changes in patch 5 it fails and so adding it here breaks running "git bisect" on this series. I'd squash this into the final patch

Interesting. I thought I checked that the test passed sans patch 5, but I'll double check. I can't think of a reason it wouldn't offhand, but my thoughts on patch 5's changes have become a bit scattered.

Quoted 3 lines

and I think we can probably replace an
existing "stash apply --index" tests that are not so strict with this one, rather than adding a new test.

That's probably a good idea, thanks.

#39 lore raw

Re: [PATCH v3 3/5] t3903: test stash --index merges

Hi Ben

Quoted 9 lines · D. Ben Knoble
Quoted 4 lines · Phillip Wood

The test looks good, but without the changes in patch 5 it fails and so adding it here breaks running "git bisect" on this series. I'd squash this into the final patch

Interesting. I thought I checked that the test passed sans patch 5, but I'll double check. I can't think of a reason it wouldn't offhand, but my thoughts on patch 5's changes have become a bit scattered.

It fails because it tries to apply a patch that looks like

@@ -1,3 +1,3 @@
A
B
-C
+staged

to a file that looks like

committed
B
C

and so the first context line does not match. Because the changes do not overlap the merge machinery is perfectly happy. As an aside when we clear the worktree changes from "git stash push -p" generate the patch with "-U1" to try and avoid problems like this.

Thanks

Phillip

Quoted 6 lines
Quoted 4 lines

and I think we can probably replace an
existing "stash apply --index" tests that are not so strict with this one, rather than adding a new test.

That's probably a good idea, thanks.

#40 lore raw

PATCH v3 4/5 t3903: test failed "stash apply --index"

In reply to D. Ben Knoble

The next commit will refactor index handling for applied stashes, so let's make sure we cover conflicted index merging, too.

---
 t/t3903-stash.sh | 21 +++++++++++++++++++++
 1 file changed, 21 insertions(+)
t/t3903-stash.sh +21 -0
index 9bc99fa252..11942d875b 100755
@@ -395,6 +395,27 @@ setup_stash() {
 	test_cmp expect file
 '
 
+test_expect_success 'stash apply --index leaves everything untouched on failure' '
+	git reset --hard &&
+	echo test >other-file &&
+	git add other-file &&
+	git stash &&
+	echo unrelated >file &&
+	echo unrelated >another-file &&
+	git add another-file &&
+	echo conflict >other-file &&
+	git add other-file &&
+	git diff-files -p >expect &&
+	git diff-index --cached HEAD >expect-index &&
+
+	test_must_fail git stash apply --index 2>err &&
+	test_grep "conflicts in index. Try without --index" err &&
+	git diff-files -p >actual &&
+	test_cmp expect actual &&
+	git diff-index --cached HEAD >actual-index &&
+	test_cmp expect-index actual-index
+'
+
 test_expect_success 'stash -k' '
 	echo bar3 >file &&
 	echo bar4 >file2 &&
Signature · 1 line
2.56.0.rc1.315.gc6ed9934b7.dirty
#41 lore raw

PATCH v3 5/5 builtin/stash: merge index in-core

In reply to D. Ben Knoble

"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.

---
 builtin/stash.c  | 80 ++++++++++--------------------------------------
 t/t7600-merge.sh |  9 ++++++
 2 files changed, 26 insertions(+), 63 deletions(-)
builtin/stash.c +17 -63
index 043a38cc6d..ac3b3cf84d 100644
@@ -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);
 		}
 	}
 
t/t7600-merge.sh +9 -0
index 64fe21717d..8f6109fb91 100755
@@ -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 &&
Signature · 1 line
2.56.0.rc1.315.gc6ed9934b7.dirty
#42 lore raw

Re: [PATCH v3 5/5] builtin/stash: merge index in-core

Quoted 10 lines · D. Ben Knoble
(quoted diff) +1 -4
@@ -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^!."),

Review static enum stash_apply_result do_apply_stash(const char *prefix, · line 636

Quoted 10 lines
(quoted diff) +4 -5
-					     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;

We realize that 'o' is a struct merge_options defined on the stack for this function, initialized with init_ui_merge_options() fairly early on. It would have initialized '.verbosity' to the default verbosity, the merge.verbosity configuration variable, or the GIT_MERGE_VERBOSITY environment variable.

You drop the verbosity here, presumably because you want to match the previous implementation 'diff-tree | apply --cached' (which I guess was fairly quiet, but I do not use 'stash pop --index' myself).

Review static enum stash_apply_result do_apply_stash(const char *prefix, · line 645

Quoted 9 lines
(quoted diff) +9 -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);

And then the (index) merge is quiet, which is nice.

Quoted 6 lines
(quoted diff) +2 -2
+
+			if (!result.clean)
 				return error(_("conflicts in index. "
 					       "Try without --index."));
-
-			discard_index(the_repository->index);

Review static enum stash_apply_result do_apply_stash(const char *prefix, · line 651

Quoted 10 lines
(quoted diff) +0 -8
-			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);
 		}
 	}

But the thing is, this is not the end of the function, or the last call to the merge machinery using 'o'. We then use the same 'o' to drive another three-way merge. Yet nobody restores '.verbosity' that was unconditionally turned off above for that second merge.

It is a bit surprising that the existing test suite did not catch this. Perhaps we do not test --quiet and the merge.verbosity configuration in combination?

Anyway, I think you'd need something like the following (caveat
emptor: written against checked out 'seen' while reading the patch, and not even compile tested).

Thanks.

 builtin/stash.c | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)
builtin/stash.c +3 -5
index 0f10b9c703..a165419d77 100644
@@ -622,6 +622,9 @@ static enum stash_apply_result do_apply_stash(const char *prefix,
 
 	init_ui_merge_options(&o, the_repository);
 
+	if (quiet)
+		o.verbosity = 0;
+
 	if (index) {
 		if (oideq(&info->b_tree, &info->i_tree) ||
 		    oideq(&c_tree, &info->i_tree)) {
@@ -633,8 +636,6 @@ static enum stash_apply_result do_apply_stash(const char *prefix,
 			o.branch2 = "Stashed index changes";
 			o.ancestor = "Stash base";
 
-			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);
@@ -658,9 +659,6 @@ static enum stash_apply_result do_apply_stash(const char *prefix,
 	if (oideq(&info->b_tree, &c_tree))
 		o.branch1 = "Version stash was based on";
 
-	if (quiet)
-		o.verbosity = 0;
-
 	if (o.verbosity >= 3)
 		printf_ln(_("Merging %s with %s"), o.branch1, o.branch2);
 
#43 lore raw

Re: [PATCH v3 5/5] builtin/stash: merge index in-core

Quoted 30 lines · Junio C Hamano
Quoted 10 lines · D. Ben Knoble
(quoted diff) +1 -4
@@ -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^!."),

Review static enum stash_apply_result do_apply_stash(const char *prefix, · line 636

Quoted 10 lines
(quoted diff) +4 -5
-                                          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;

We realize that 'o' is a struct merge_options defined on the stack for this function, initialized with init_ui_merge_options() fairly early on. It would have initialized '.verbosity' to the default verbosity, the merge.verbosity configuration variable, or the GIT_MERGE_VERBOSITY environment variable.

Quoted 4 lines

You drop the verbosity here, presumably because you want to match the previous implementation 'diff-tree | apply --cached' (which I guess was fairly quiet, but I do not use 'stash pop --index' myself).

Yes, the original piped "apply --cached" output to a strbuf and discarded it.

Quoted 14 lines
Quoted 14 lines
(quoted diff) +2 -10
+
+                     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);
Quoted 8 lines

Review static enum stash_apply_result do_apply_stash(const char *prefix, · line 642

Quoted 2 lines
(quoted diff) +0 -0
              }
      }

But the thing is, this is not the end of the function, or the last call to the merge machinery using 'o'. We then use the same 'o' to drive another three-way merge. Yet nobody restores '.verbosity' that was unconditionally turned off above for that second merge.

But you're right, we should restore the verbosity (which is not what the sketch patch does exactly). Will fix.

#44 lore raw

Re: [PATCH v3 5/5] builtin/stash: merge index in-core

In reply to D. Ben Knoble

builtin/stash.c static enum stash_apply_result do_apply_stash(const char *prefix, · line 645

Quoted 4 lines · D. Ben Knoble
builtin/stash.c +4 -0
+			merge_incore_nonrecursive(&o, merge_base, head, merge,
+						  &result);
+
+			oidcpy(&index_tree, &result.tree->object.oid);

This is risky, isn't it?

If there were catastrophic failure (e.g., missing object that were involved in the merge), merge_incore_nonrecursive() may stuff -1 to result.clean and return without populating result.tree, and when that happens, result.tree->object.oid would be dereferencing NULL.

#45 lore raw

Re: [PATCH v3 5/5] builtin/stash: merge index in-core

Quoted 8 lines · Junio C Hamano

builtin/stash.c static enum stash_apply_result do_apply_stash(const char *prefix, · line 645

Quoted 4 lines · D. Ben Knoble
builtin/stash.c +4 -0
+                     merge_incore_nonrecursive(&o, merge_base, head, merge,
+                                               &result);
+
+                     oidcpy(&index_tree, &result.tree->object.oid);
Quoted 6 lines

This is risky, isn't it?

If there were catastrophic failure (e.g., missing object that were involved in the merge), merge_incore_nonrecursive() may stuff -1 to result.clean and return without populating result.tree, and when that happens, result.tree->object.oid would be dereferencing NULL.

Indeed… unfortunate. Thanks for spotting.

Signature · 1 line
D. Ben Knoble
#46 lore raw

Re: [PATCH v3 5/5] builtin/stash: merge index in-core

Quoted 9 lines · D. Ben Knoble
Quoted 8 lines · Junio C Hamano
Quoted 4 lines · D. Ben Knoble
builtin/stash.c +4 -0
+                     merge_incore_nonrecursive(&o, merge_base, head, merge,
+                                               &result);
+
+                     oidcpy(&index_tree, &result.tree->object.oid);
Quoted 8 lines
Quoted 6 lines

This is risky, isn't it?

If there were catastrophic failure (e.g., missing object that were involved in the merge), merge_incore_nonrecursive() may stuff -1 to result.clean and return without populating result.tree, and when that happens, result.tree->object.oid would be dereferencing NULL.

Indeed… unfortunate. Thanks for spotting.

I did

	$ git grep merge_incore_nonrecursive \*.c

and read all the current callers.

They all have code to specifically check for the (result.clean < 0) condition and error out before touching any of the other members of the result structure, so they seem to be safe.

#47 lore raw

Re: [PATCH v3 0/5] stash: clean up index-mode test merge

In reply to D. Ben Knoble
Quoted 7 lines · D. Ben Knoble

Hi all,

This small patch series fixes a bug reported by Eli Barzilay in the interaction between autostashing, staged index entries, and stash.index=true.

Quoted 6 lines

The first patch is an incidental cleanup, and the second re-arranges one line to make the change easier. The third and fourth add missing test coverage (which catch breakages from prior incorrect rounds of this series), while the last holds the interesting bits.

Changes in v3:

Woops. Contrary to my usual practice of late, I sent this in reply to v2 rather than v1. Oh well.

#48 lore raw

Re: [PATCH v3 0/5] stash: clean up index-mode test merge

In reply to D. Ben Knoble
Quoted 10 lines · D. Ben Knoble

Hi all,

This small patch series fixes a bug reported by Eli Barzilay in the interaction between autostashing, staged index entries, and stash.index=true.

The first patch is an incidental cleanup, and the second re-arranges one line to make the change easier. The third and fourth add missing test coverage (which catch breakages from prior incorrect rounds of this series), while the last holds the interesting bits.

I may have reported this on the previous round, too, but 'seen' seems to break t5520 when this topic is merged. I'll eject the topic from my tree for now in the meantime.

#49 lore raw

Re: [PATCH v3 0/5] stash: clean up index-mode test merge

Quoted 8 lines · Junio C Hamano
Quoted 6 lines · D. Ben Knoble

Hi all,

This small patch series fixes a bug reported by Eli Barzilay in the interaction between autostashing, staged index entries, and stash.index=true.

Quoted 8 lines
Quoted 4 lines

The first patch is an incidental cleanup, and the second re-arranges one line to make the change easier. The third and fourth add missing test coverage (which catch breakages from prior incorrect rounds of this series), while the last holds the interesting bits.

I may have reported this on the previous round, too, but 'seen' seems to break t5520 when this topic is merged. I'll eject the topic from my tree for now in the meantime.

I'm a bit stumped by that as the failing test (5520.69 '--rebase -f with rebased upstream') does not stash anything. There seems to be something funny going on with pull's fork-point detection. If I add GIT_TRACE=1 to "git pull --rebase" then on 'seen' I see

trace: built-in: git rebase --no-autostash --onto ae9857430e281d178a3755aecfc5e29c46a02306 f29aa667ce68e4d514557081ca7f54b12e108922

but with this series I see

trace: built-in: git rebase --no-autostash --onto ae9857430e281d178a3755aecfc5e29c46a02306 ae9857430e281d178a3755aecfc5e29c46a02306

so the upstream commit has changed. The previous test also checks the fork-point behavior and the failing test just runs "git reset --hard" at the start rather than re-creating the reflogs which seems a bit iffy to me but I've no idea why this series causes it to fail. I tried a merge of 'master' and 'seen' just in case the failure was caused by the base I'd used for this series but that passes.

Thanks

Phillip

#50 lore raw

Re: [PATCH v3 0/5] stash: clean up index-mode test merge

Quoted 10 lines · Phillip Wood
Quoted 8 lines · Junio C Hamano
Quoted 6 lines · D. Ben Knoble

Hi all,

This small patch series fixes a bug reported by Eli Barzilay in the interaction between autostashing, staged index entries, and stash.index=true.

Quoted 8 lines
Quoted 8 lines
Quoted 4 lines

The first patch is an incidental cleanup, and the second re-arranges one line to make the change easier. The third and fourth add missing test coverage (which catch breakages from prior incorrect rounds of this series), while the last holds the interesting bits.

I may have reported this on the previous round, too, but 'seen' seems to break t5520 when this topic is merged. I'll eject the topic from my tree for now in the meantime.

First I'm hearing about it, but I'll try to bisect seen and see what I can find.

Quoted 2 lines

I'm a bit stumped by that as the failing test (5520.69 '--rebase -f with rebased upstream') does not stash anything.

I wonder if a prior test is affected "silently" and we only find out by .69?

Quoted 14 lines

There seems to be something
funny going on with pull's fork-point detection. If I add GIT_TRACE=1 to "git pull --rebase" then on 'seen' I see

trace: built-in: git rebase --no-autostash --onto
ae9857430e281d178a3755aecfc5e29c46a02306
f29aa667ce68e4d514557081ca7f54b12e108922

but with this series I see

trace: built-in: git rebase --no-autostash --onto
ae9857430e281d178a3755aecfc5e29c46a02306
ae9857430e281d178a3755aecfc5e29c46a02306

Quoted 6 lines

so the upstream commit has changed. The previous test also checks the fork-point behavior and the failing test just runs "git reset --hard" at the start rather than re-creating the reflogs which seems a bit iffy to me but I've no idea why this series causes it to fail. I tried a merge of 'master' and 'seen' just in case the failure was caused by the base I'd used for this series but that passes.

Thanks Phillip!

Signature · 1 line
D. Ben Knoble
#51 lore raw

Re: [PATCH v3 0/5] stash: clean up index-mode test merge

Just leaving some breadcrumb notes…

Quoted 4 lines · D. Ben Knoble
Quoted 2 lines · Phillip Wood

On 27/09/2026 20:21, Junio C Hamano wrote:

From my local version of the branch, the following script points at 4f65642eb0 (Merge branch 'tb/rerere-lock-grace' into jch, 2026-09-27):

#! /bin/zsh
HEAD=$(git rev-parse HEAD) &&
git merge --no-edit bk/autostash-index-reset &&
if ! ninja -C build; then exit 125; fi &&
meson test -C build t5520-pull # no chain! need to keep going no matter what code=$? &&
git reset --hard $HEAD &&
exit $code

(using "git bisect start --first-parent origin/seen @")

[Cc: Thomas Bachem <[email protected]> in case you have any immediate ideas]

I don't think the bisect log will interest anyone, but I've attached it anyway.

Signature · 1 line
D. Ben Knoble
#52 lore raw

Re: [PATCH v3 0/5] stash: clean up index-mode test merge

Quoted 11 lines · D. Ben Knoble

Just leaving some breadcrumb notes…

Quoted 4 lines · D. Ben Knoble
Quoted 2 lines · Phillip Wood

On 27/09/2026 20:21, Junio C Hamano wrote:

From my local version of the branch, the following script points at 4f65642eb0 (Merge branch 'tb/rerere-lock-grace' into jch, 2026-09-27):

And within that topic, bisect points to 2d1fa0323f (rebase, cherry-pick, revert: run auto maintenance when done, 2026-09-17) in t5220.69 as Phillip said.

expecting success of 5520.69 '--rebase -f with rebased upstream':
test_when_finished "test_might_fail git rebase --abort" && git reset --hard to-rebase-orig &&
git pull --rebase -f me copy &&
echo "conflicting modification" >expect &&
test_cmp expect file &&
echo file >expect &&
test_cmp expect file2

++ test_when_finished 'test_might_fail git rebase --abort'
++ test 0 = 0
++ test_cleanup=$'{ test_might_fail git rebase --abort\n\t\t} || eval_ret=$?; :' ++ git reset --hard to-rebase-orig
HEAD is now at cb9bf26 to-rebase
++ git pull --rebase -f me copy
From .
* branch copy -> FETCH_HEAD
Rebasing (1/4)
Auto-merging file
CONFLICT (content): Merge conflict in file
error: could not apply f29aa66... file
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
hint: Disable this message with "git config set advice.mergeConflict false" Could not apply f29aa66... # file
error: last command exited with $?=1

Signature · 1 line
D. Ben Knoble
#53 lore raw

Re: [PATCH v3 0/5] stash: clean up index-mode test merge

Hi Ben

Quoted 8 lines · D. Ben Knoble
Quoted 7 lines · D. Ben Knoble

Just leaving some breadcrumb notes…

Quoted 3 lines · D. Ben Knoble
Quoted 1 line · Phillip Wood
Quoted 7 lines
Quoted 4 lines
Quoted 1 line
Quoted 1 line · Junio C Hamano

From my local version of the branch, the following script points at
4f65642eb0 (Merge branch 'tb/rerere-lock-grace' into jch, 2026-09-27):

And within that topic, bisect points to 2d1fa0323f (rebase, cherry-pick, revert: run auto maintenance when done, 2026-09-17)

Oh, when I was thinking about this over lunch I did wonder if that might be the culprit. Previously we didn't run "git maintenance --auto" after a rebase with the 'merge' backend but with that topic we do, and because we set GIT_COMMITTER_DATE to sometime in 2005, if 'git reflog expire' gets triggered it will expire the reflog entries that 'git pull --rebase' relies on. As you suggested in another mail, I assume this topic has changed something in one of the '--autostash' tests that come before the failing test triggers which the new behavior. What that something is I'm not sure; off the top of my head I'd expect the number of reflog entries in HEAD to be the same but maybe I'm missing something. Adding

git config maintenance.reflog-expire.auto 0

to the 'setup' test fixes the test failure, but it would be good to try and understand why this topic triggers the reflog to be expired in case there is something nasty happening that we've not thought of.

Thanks

Phillip

Quoted 34 lines

in
t5220.69 as Phillip said.

expecting success of 5520.69 '--rebase -f with rebased upstream':
test_when_finished "test_might_fail git rebase --abort" && git reset --hard to-rebase-orig &&
git pull --rebase -f me copy &&
echo "conflicting modification" >expect &&
test_cmp expect file &&
echo file >expect &&
test_cmp expect file2

++ test_when_finished 'test_might_fail git rebase --abort'
++ test 0 = 0
++ test_cleanup=$'{ test_might_fail git rebase --abort\n\t\t} || eval_ret=$?; :' ++ git reset --hard to-rebase-orig
HEAD is now at cb9bf26 to-rebase
++ git pull --rebase -f me copy
From .
* branch copy -> FETCH_HEAD
Rebasing (1/4)
Auto-merging file
CONFLICT (content): Merge conflict in file
error: could not apply f29aa66... file
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
hint: Disable this message with "git config set advice.mergeConflict false" Could not apply f29aa66... # file
error: last command exited with $?=1

#54 lore raw

Re: [PATCH v3 0/5] stash: clean up index-mode test merge

Quoted 6 lines · Phillip Wood

Oh, when I was thinking about this over lunch I did wonder if that might be the culprit. Previously we didn't run "git maintenance --auto" after a rebase with the 'merge' backend but with that topic we do, and because we set GIT_COMMITTER_DATE to sometime in 2005, if 'git reflog expire' gets triggered it will expire the reflog entries that 'git pull --rebase' relies on. As you suggested in another mail, I assume this

That is it. I ran t5520 on 'seen' with and without Ben's series under GIT_TRACE2_EVENT, and the two runs differ in one place: which command's auto maintenance runs "git reflog expire --all".

"git pull --rebase" computes the fork point before it fetches, from the reflog of refs/remotes/me/copy, and test 69 needs the entry that test 68's fetch wrote there, copy-orig (f29aa66) to ae98574. With the reflog empty, "merge-base --fork-point" falls back to the ref itself, ae98574 is no ancestor of to-rebase, and pull hands the merge head to rebase as the upstream. That is your "--onto ae98... ae98...", and the four commits from copy-orig up come back, the first of them conflicting with "conflict".

Quoted 5 lines

topic has changed something in one of the '--autostash' tests that come before the failing test triggers which the new behavior. What that something is I'm not sure; off the top of my head I'd expect the number of reflog entries in HEAD to be the same but maybe I'm missing something. Adding

It is eight entries fewer, and they come from the failed merges, not from the autostash tests. "git merge" restores a dirty tree with "stash apply --index --quiet", and until Ben's series that spawned "git reset --quiet --refresh", which writes "reset: moving to HEAD" to the reflog. That happens eight times in t5520 before test 68.

Auto maintenance expires reflogs once HEAD's reflog holds a hundred entries that the policy would remove, the default of maintenance.reflog-expire.auto, and after the first test_tick that is every entry. Which run crosses the hundred depends on how many entries and maintenance runs came before it. On 'seen' the expiry lands on "git commit -m conflict" in test 68, before the fetch writes the entry. Eight entries fewer move the crossing past that commit, and the maintenance run my topic adds at the end of the rebase in test 68 is the next one: after the fetch, before test 69 reads the reflog. Either change alone leaves it somewhere harmless, and nothing else is going on. The expiry is the usual 90 days applied to entries dated 2005, and the only new thing is one more maintenance run per rebase, the same one "git commit" and "git fetch" run.

Quoted 5 lines

git config maintenance.reflog-expire.auto 0

to the 'setup' test fixes the test failure, but it would be good to try and understand why this topic triggers the reflog to be expired in case there is something nasty happening that we've not thought of.

I'd pin the expiry itself instead, as ea7d894f44 (t34xx: don't expire reflogs where it matters, 2026-02-24) did for the rebase tests:

git config set gc.reflogExpire never && git config set gc.reflogExpireUnreachable never &&

That covers a "git gc" as well, which expires reflogs on its own. With it, 'seen' plus Ben's series passes t5520 here and no expiry runs during the script at all. I sent it as a patch on master:
<[email protected]>

FWIW, any script that reads a reflog after a hundred HEAD updates can fall into the same hole. I have not looked further than t5520.

Thomas

#55 lore raw

Re: [PATCH v3 0/5] stash: clean up index-mode test merge

Let me see if I understand correctly…

Quoted 11 lines · Thomas Bachem
Quoted 6 lines · Phillip Wood

Oh, when I was thinking about this over lunch I did wonder if that might be the culprit. Previously we didn't run "git maintenance --auto" after a rebase with the 'merge' backend but with that topic we do, and because we set GIT_COMMITTER_DATE to sometime in 2005, if 'git reflog expire' gets triggered it will expire the reflog entries that 'git pull --rebase' relies on. As you suggested in another mail, I assume this

"git pull --rebase" computes the fork point before it fetches, from the reflog of refs/remotes/me/copy,

This is described by the manual for git-rebase under --fork-point, which is on unless we have an <upstream> or --keep-base (modulo config). Put a pin in this.

Quoted 20 lines

and test 69 needs the entry that
test 68's fetch wrote there, copy-orig (f29aa66) to ae98574. With the reflog empty, "merge-base --fork-point" falls back to the ref itself, ae98574 is no ancestor of to-rebase, and pull hands the merge head to rebase as the upstream. That is your "--onto ae98... ae98...", and the four commits from copy-orig up come back, the first of them conflicting with "conflict".

Quoted 5 lines

topic has changed something in one of the '--autostash' tests that come before the failing test triggers which the new behavior. What that something is I'm not sure; off the top of my head I'd expect the number of reflog entries in HEAD to be the same but maybe I'm missing something. Adding

It is eight entries fewer, and they come from the failed merges, not from the autostash tests. "git merge" restores a dirty tree with "stash apply --index --quiet", and until Ben's series that spawned "git reset --quiet --refresh", which writes "reset: moving to HEAD" to the reflog. That happens eight times in t5520 before test 68.

Quoted 13 lines

Auto maintenance expires reflogs once HEAD's reflog holds a hundred entries that the policy would remove, the default of maintenance.reflog-expire.auto, and after the first test_tick that is every entry. Which run crosses the hundred depends on how many entries and maintenance runs came before it. On 'seen' the expiry lands on "git commit -m conflict" in test 68, before the fetch writes the entry. Eight entries fewer move the crossing past that commit, and the maintenance run my topic adds at the end of the rebase in test 68 is the next one: after the fetch, before test 69 reads the reflog. Either change alone leaves it somewhere harmless, and nothing else is going on. The expiry is the usual 90 days applied to entries dated 2005, and the only new thing is one more maintenance run per rebase, the same one "git commit" and "git fetch" run.

In short, expiry used to happen prior to .68, so the reflog entry created in that test which is used by "pull --rebase" in .69 is picked up. With fewer reflog entries, expiry happens later, and it just so happens to drop the important entry. Darn!

But here's what I can't figure out, returning to that pin from
earlier: I was a bit surprised to see mention of rebase reading reflogs! When I remembered --fork-point, I was even more curious (but at least it's obvious that rebase will read the reflogs in some scenarios).

What confuses me is that builtin/pull.c:run_rebase() sure looks like it provides an <upstream> to the command invocation, so shouldn't --fork-point and reflog use be disabled????

I'll try tracing that test myself later, I suppose. It's nice to know we have a fix available (thanks for the patch), but it sure feels like a hack :) oh well?

Signature · 1 line
D. Ben Knoble
#56 lore raw

Re: [PATCH v3 0/5] stash: clean up index-mode test merge

Quoted 27 lines · D. Ben Knoble

Let me see if I understand correctly…

Quoted 11 lines · Thomas Bachem
Quoted 6 lines · Phillip Wood

Oh, when I was thinking about this over lunch I did wonder if that might be the culprit. Previously we didn't run "git maintenance --auto" after a rebase with the 'merge' backend but with that topic we do, and because we set GIT_COMMITTER_DATE to sometime in 2005, if 'git reflog expire' gets triggered it will expire the reflog entries that 'git pull --rebase' relies on. As you suggested in another mail, I assume this

"git pull --rebase" computes the fork point before it fetches, from the reflog of refs/remotes/me/copy,

This is described by the manual for git-rebase under --fork-point, which is on unless we have an <upstream> or --keep-base (modulo config). Put a pin in this.

But here's what I can't figure out, returning to that pin from
earlier: I was a bit surprised to see mention of rebase reading reflogs! When I remembered --fork-point, I was even more curious (but at least it's obvious that rebase will read the reflogs in some scenarios).

Quoted 3 lines

What confuses me is that builtin/pull.c:run_rebase() sure looks like it provides an <upstream> to the command invocation, so shouldn't --fork-point and reflog use be disabled????

Indeed, from GIT_TRACE2 output I can see we do run

    git rebase --no-autostash --onto ae98… f29a…

but well before that we run

    git merge-base --fork-point refs/remotes/me/copy to-rebase

which is then presumably fed down to the rebase. Interesting.

Signature · 1 line
D. Ben Knoble
#57 lore raw

Re: [PATCH v3 0/5] stash: clean up index-mode test merge

In reply to D. Ben Knoble

Hi Ben

Quoted 53 lines · D. Ben Knoble

Let me see if I understand correctly…

Quoted 11 lines · Thomas Bachem
Quoted 6 lines · Phillip Wood

Oh, when I was thinking about this over lunch I did wonder if that might be the culprit. Previously we didn't run "git maintenance --auto" after a rebase with the 'merge' backend but with that topic we do, and because we set GIT_COMMITTER_DATE to sometime in 2005, if 'git reflog expire' gets triggered it will expire the reflog entries that 'git pull --rebase' relies on. As you suggested in another mail, I assume this

"git pull --rebase" computes the fork point before it fetches, from the reflog of refs/remotes/me/copy,

This is described by the manual for git-rebase under --fork-point, which is on unless we have an <upstream> or --keep-base (modulo config). Put a pin in this.

Quoted 20 lines

and test 69 needs the entry that
test 68's fetch wrote there, copy-orig (f29aa66) to ae98574. With the reflog empty, "merge-base --fork-point" falls back to the ref itself, ae98574 is no ancestor of to-rebase, and pull hands the merge head to rebase as the upstream. That is your "--onto ae98... ae98...", and the four commits from copy-orig up come back, the first of them conflicting with "conflict".

Quoted 5 lines

topic has changed something in one of the '--autostash' tests that come before the failing test triggers which the new behavior. What that something is I'm not sure; off the top of my head I'd expect the number of reflog entries in HEAD to be the same but maybe I'm missing something. Adding

It is eight entries fewer, and they come from the failed merges, not from the autostash tests. "git merge" restores a dirty tree with "stash apply --index --quiet", and until Ben's series that spawned "git reset --quiet --refresh", which writes "reset: moving to HEAD" to the reflog. That happens eight times in t5520 before test 68.

Quoted 13 lines

Auto maintenance expires reflogs once HEAD's reflog holds a hundred entries that the policy would remove, the default of maintenance.reflog-expire.auto, and after the first test_tick that is every entry. Which run crosses the hundred depends on how many entries and maintenance runs came before it. On 'seen' the expiry lands on "git commit -m conflict" in test 68, before the fetch writes the entry. Eight entries fewer move the crossing past that commit, and the maintenance run my topic adds at the end of the rebase in test 68 is the next one: after the fetch, before test 69 reads the reflog. Either change alone leaves it somewhere harmless, and nothing else is going on. The expiry is the usual 90 days applied to entries dated 2005, and the only new thing is one more maintenance run per rebase, the same one "git commit" and "git fetch" run.

Quoted 4 lines

In short, expiry used to happen prior to .68, so the reflog entry created in that test which is used by "pull --rebase" in .69 is picked up. With fewer reflog entries, expiry happens later, and it just so happens to drop the important entry. Darn!

Yes, it is incredibly bad luck that the test broke, though I guess it is also fortunate as it means we can fix the latent bug in the test.

Quoted 7 lines

But here's what I can't figure out, returning to that pin from
earlier: I was a bit surprised to see mention of rebase reading reflogs! When I remembered --fork-point, I was even more curious (but at least it's obvious that rebase will read the reflogs in some scenarios).

Quoted 7 lines

What confuses me is that builtin/pull.c:run_rebase() sure looks like it provides an <upstream> to the command invocation, so shouldn't --fork-point and reflog use be disabled????

I'll try tracing that test myself later, I suppose. It's nice to know we have a fix available (thanks for the patch), but it sure feels like a hack :) oh well?

It is a bit confusing that "git pull --rebase" does not use "git rebase --fork-point", instead it calls "git merge-base --fork-point" (which is where we read the reflog of the remote branch) itself and then passes that as the upstream revision to "git rebase". I think this is because fork-point handling was added to "git pull" before "--fork-point" existed in "git rebase". "git rebase" only looks for a fork-point if its upstream argument is a ref, so as "git pull" passes an object id, the fork-point detection in rebase is bypassed.

Thanks

Phillip

#58 lore raw

Re: [PATCH v3 0/5] stash: clean up index-mode test merge

In reply to Thomas Bachem

Hi Thomas

Quoted 11 lines · Thomas Bachem
Quoted 6 lines · Phillip Wood

topic has changed something in one of the '--autostash' tests that come before the failing test triggers which the new behavior. What that something is I'm not sure; off the top of my head I'd expect the number of reflog entries in HEAD to be the same but maybe I'm missing something. Adding

It is eight entries fewer, and they come from the failed merges, not from the autostash tests. "git merge" restores a dirty tree with "stash apply --index --quiet",

Thanks for tracking that down, I couldn't see where we'd be calling "git stash apply" with "--index" but builtin/merge.c:restore_state() calls "git stash apply --index --quiet" rather than calling one of the autostash helper functions which do not use "--index".

Quoted 3 lines

and until Ben's series that spawned
"git reset --quiet --refresh", which writes "reset: moving to HEAD" to the reflog. That happens eight times in t5520 before test 68.

That accounts for the difference in the number of reflog entries. It's good to have an explanation for why we're expiring the reflog entries at a slightly different time.

Thanks

Phillip

Quoted 35 lines

Auto maintenance expires reflogs once HEAD's reflog holds a hundred entries that the policy would remove, the default of maintenance.reflog-expire.auto, and after the first test_tick that is every entry. Which run crosses the hundred depends on how many entries and maintenance runs came before it. On 'seen' the expiry lands on "git commit -m conflict" in test 68, before the fetch writes the entry. Eight entries fewer move the crossing past that commit, and the maintenance run my topic adds at the end of the rebase in test 68 is the next one: after the fetch, before test 69 reads the reflog. Either change alone leaves it somewhere harmless, and nothing else is going on. The expiry is the usual 90 days applied to entries dated 2005, and the only new thing is one more maintenance run per rebase, the same one "git commit" and "git fetch" run.

Quoted 5 lines

git config maintenance.reflog-expire.auto 0

to the 'setup' test fixes the test failure, but it would be good to try and understand why this topic triggers the reflog to be expired in case there is something nasty happening that we've not thought of.

I'd pin the expiry itself instead, as ea7d894f44 (t34xx: don't expire reflogs where it matters, 2026-02-24) did for the rebase tests:

git config set gc.reflogExpire never && git config set gc.reflogExpireUnreachable never &&

That covers a "git gc" as well, which expires reflogs on its own. With it, 'seen' plus Ben's series passes t5520 here and no expiry runs during the script at all. I sent it as a patch on master:
<[email protected]>

FWIW, any script that reads a reflog after a hundred HEAD updates can fall into the same hole. I have not looked further than t5520.

Thomas

#59 lore raw

v4 cover stash: clean up index-mode test merge

In reply to D. Ben Knoble

Hi all,

This small patch series fixes a bug reported by Eli Barzilay in the interaction between autostashing, staged index entries, and stash.index=true.

The first patch is an incidental cleanup, and the second re-arranges one line to make the change easier. The third adds missing test coverage (which catch breakages from prior incorrect rounds of this series). The fourth fixes a test interaction with another in-flight topic. The last holds the interesting bits.

Changes in v4:
• Drop merge verbosity changes altogether. I was going to save-and-restore, but when looking at the index-merge test case (more below) closer, I noticed that "git apply --cached" reports conflicts on stderr. That is, "git stash apply --index" would report conflicts, and silencing the merge takes that away. So instead let's leave the configured verbosity alone.
• Only copy resulting index merge tree OID when successful
• Fix interaction with t5520 (new patch 4/5)
• Squash test from 3/5 into 5/5, since it requires actually merging trees. I've elected to keep it a separate test for now (contrary to Phillip's suggestion) since it's written and working. Adapting existing tests requires quite a bit more digging into implicit context assumptions ;)

Changes in v3:

• Change conflict label for current index
• Fix memory leak of merge_result
• Fix order of trees to make the correct merge (cherry-pick)
• New test (3/5) to validate this
• Fix test in 4/5 to assert more details of expected state

Changes in v2:

• Do give branch labels for the incore merge, although they are never seen (and clarify commit message as a result, also keeping the merge-ort asserts). Phillip was right: without those, we do segfault on conflicts.
• Use the ui merge options to keep the same diff algorithm.
• Use merge_finalize instead of clear_merge_options, and reuse the options between merge calls if they are already initialized.
• Add a new 2/4 to simplify merge options initialization.
• Add a new 3/4 with a test case for conflicted index merges.

v1: <[email protected]>
v2: <[email protected]>
v3: <[email protected]>

[1/5] builtin/stash: remove unused header [2/5] stash: prepare merge options earlier [3/5] t3903: test failed "stash apply --index" [4/5] t5520: don't expire reflogs where it matters [5/5] builtin/stash: merge index in-core

 builtin/stash.c  | 91 ++++++++++++------------------------------------
 t/t3903-stash.sh | 42 ++++++++++++++++++++++
 t/t5520-pull.sh  |  6 ++++
 t/t7600-merge.sh |  9 +++++
 4 files changed, 79 insertions(+), 69 deletions(-)

Diff-intervalle contre v3 :

1:  6a165c4df4 = 1:  6a165c4df4 builtin/stash: remove unused header
2:  d9a9e18f3a ! 2:  35b64ae321 stash: prepare merge options earlier
    @@ builtin/stash.c: static enum stash_apply_result do_apply_stash(const char *prefi
      		return error(_("cannot apply a stash in the middle of a merge"));
      
     +	init_ui_merge_options(&o, the_repository);
    ++
    ++	if (quiet)
    ++		o.verbosity = 0;
     +
      	if (index) {
      		if (oideq(&info->b_tree, &info->i_tree) ||
    @@ builtin/stash.c: static enum stash_apply_result do_apply_stash(const char *prefi
      	o.branch1 = label_ours ? label_ours : "Updated upstream";
      	o.branch2 = label_theirs ? label_theirs : "Stashed changes";
      	o.ancestor = label_base ? label_base : "Stash base";
    +@@ builtin/stash.c: static enum stash_apply_result do_apply_stash(const char *prefix,
    + 	if (oideq(&info->b_tree, &c_tree))
    + 		o.branch1 = "Version stash was based on";
    + 
    +-	if (quiet)
    +-		o.verbosity = 0;
    +-
    + 	if (o.verbosity >= 3)
    + 		printf_ln(_("Merging %s with %s"), o.branch1, o.branch2);
    + 

4: d39e16905d ! 3: 7b0b317ce0 t3903: test failed "stash apply --index"
@@ Commit message

## t/t3903-stash.sh ##
@@ t/t3903-stash.sh: setup_stash() {
- test_cmp expect file
+ test_cmp expect actual
'

+test_expect_success 'stash apply --index leaves everything untouched on failure' '
3: 8b5ea5e6f4 ! 4: 2ac371d2dc t3903: test stash --index merges
@@
## Metadata ##
-Author: D. Ben Knoble <[email protected]>
+Author: Thomas Bachem <[email protected]>

## Commit message ##
- t3903: test stash --index merges
+ t5520: don't expire reflogs where it matters

- A future commit will refactor index handling for applied stashes, and we
- need to take care to get the order of trees right when merging. Add a
- test that covers this case.
+ The "--rebase -f with rebased upstream" test computes its fork point
+ from the reflog of refs/remotes/me/copy, and the entry it needs is
+ the one that the fetch of the test before it wrote. Like every reflog
+ entry the suite writes after test_tick, it is dated 2005, so the
+ first "git reflog expire --all" after that fetch removes it. Pull
+ then finds no fork point and rebases onto the merge head with the
+ merge head as the upstream, and the rewound commits come back as a
+ conflict.

- Suggested-by: Phillip Wood <[email protected]>
+ Since 452b12c2e0 (builtin/maintenance: use "geometric" strategy by
+ default, 2026-02-24) auto maintenance runs that expiry once the reflog
+ of HEAD holds a hundred entries it would remove, the default of
+ maintenance.reflog-expire.auto. Which run crosses the threshold
+ depends on the entries and maintenance runs before it, so the script
+ passed by chance: a stash topic that no longer runs "git reset" from
+ "stash apply --index" and a rebase topic that runs auto maintenance
+ at the end of "git rebase" together move the expiry between the two
+ tests.

- ## t/t3903-stash.sh ##
-@@ t/t3903-stash.sh: setup_stash() {
- test_cmp expect actual
- '
+ Pin the expiry as ea7d894f44 (t34xx: don't expire reflogs where it
+ matters, 2026-02-24) did for the rebase tests. That covers a "git gc"
+ as well, which expires reflogs on its own, where turning off the auto
+ trigger of the reflog-expire task alone would not.
+
+ Reported-by: Junio C Hamano <[email protected]>
+ Helped-by: D. Ben Knoble <[email protected]>
+ Helped-by: Phillip Wood <[email protected]>
+ Assisted-by: Claude Fable 5.1
+ Signed-off-by: Thomas Bachem <[email protected]>
+
+ ## t/t5520-pull.sh ##
+@@ t/t5520-pull.sh: test_pull_autostash_fail () {
+ }

-+# the later "stash -k" test is not expecting us to muck with file so much, so -+# reset when finished
-+test_expect_success 'stash apply --index merges the correct trees' '
-+ head=$(git rev-parse HEAD) &&
-+ test_when_finished "git reset --hard $head" &&
-+ test_write_lines A B C >file &&
-+ git commit -m setup file &&
-+ test_write_lines A B staged >file &&
-+ git add file &&
-+ test_write_lines A B unstaged >file &&
-+ git stash &&
-+ test_write_lines committed B C >file &&
-+ git commit -m to-be-merged file &&
-+ git stash pop --index &&
-+ git show :file >actual &&
-+ test_write_lines committed B staged >expect &&
-+ test_cmp expect actual &&
-+ test_write_lines committed B unstaged >expect &&
-+ test_cmp expect file
-+'
+ test_expect_success setup '
++ # Commit dates are hardcoded to 2005, and the reflog entries will have
++ # a matching timestamp. Maintenance may thus immediately expire
++ # reflogs if it was running.
++ git config set gc.reflogExpire never &&
++ git config set gc.reflogExpireUnreachable never &&
+
- test_expect_success 'stash -k' '
- echo bar3 >file &&
- echo bar4 >file2 &&
+ echo file >file &&
+ git add file &&
+ git commit -a -m original
5: fde7fb7988 ! 5: e21b832a6e builtin/stash: merge index in-core

    @@ Commit message
         we don't see the usual branch and ancestor labels, but the merge
         subroutines insist on their presence, so use something simple.
     
    +    We need to take care to get the order of trees right when merging. Add a
    +    test that covers this case.
    +
         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.
    @@ builtin/stash.c: static enum stash_apply_result do_apply_stash(const char *prefi
     -			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);
    @@ builtin/stash.c: static enum stash_apply_result do_apply_stash(const char *prefi
     +			merge_incore_nonrecursive(&o, merge_base, head, merge,
     +						  &result);
     +
    -+			oidcpy(&index_tree, &result.tree->object.oid);
    -+			merge_finalize(&o, &result);
    -+
    -+			if (!result.clean)
    ++			if (!result.clean) {
    ++				merge_finalize(&o, &result);
      				return error(_("conflicts in index. "
      					       "Try without --index."));
     -
    @@ builtin/stash.c: static enum stash_apply_result do_apply_stash(const char *prefi
     -			reset_head();
     -			discard_index(the_repository->index);
     -			repo_read_index(the_repository);
    ++			} else {
    ++				oidcpy(&index_tree, &result.tree->object.oid);
    ++				merge_finalize(&o, &result);
    ++			}
      		}
      	}
      
     
    + ## t/t3903-stash.sh ##
    +@@ t/t3903-stash.sh: setup_stash() {
    + 	test_cmp expect-index actual-index
    + '
    + 
    ++# the later "stash -k" test is not expecting us to muck with file so much, so
    ++# reset when finished
    ++test_expect_success 'stash apply --index merges the correct trees' '
    ++	head=$(git rev-parse HEAD) &&
    ++	test_when_finished "git reset --hard $head" &&
    ++	test_write_lines A B C >file &&
    ++	git commit -m setup file &&
    ++	test_write_lines A B staged >file &&
    ++	git add file &&
    ++	test_write_lines A B unstaged >file &&
    ++	git stash &&
    ++	test_write_lines committed B C >file &&
    ++	git commit -m to-be-merged file &&
    ++	git stash pop --index &&
    ++	git show :file >actual &&
    ++	test_write_lines committed B staged >expect &&
    ++	test_cmp expect actual &&
    ++	test_write_lines committed B unstaged >expect &&
    ++	test_cmp expect file
    ++'
    ++
    + test_expect_success 'stash -k' '
    + 	echo bar3 >file &&
    + 	echo bar4 >file2 &&
    +
      ## t/t7600-merge.sh ##
     @@ t/t7600-merge.sh: verify_no_mergehead () {
      	test_cmp result.1-5 file

base-commit: d38352cd43ab9745686d697872408bc3249a153f

Signature · 1 line
2.56.0.rc1.315.gc6ed9934b7.dirty
#60 lore raw

PATCH v4 1/5 builtin/stash: remove unused header

Clang complains that oid-array.h is unused. Certainly none of the oid_array* functions, types, etc., are used, and the transitively-included hash.h declarations are used but covered by a pre-existing direct #include of hash.h.

---
 builtin/stash.c | 1 -
 1 file changed, 1 deletion(-)
builtin/stash.c +0 -1
index 7a9843413b..dfea2d2c4c 100644
@@ -31,7 +31,6 @@
 #include "reflog.h"
 #include "reflog-walk.h"
 #include "add-interactive.h"
-#include "oid-array.h"
 #include "commit.h"
 
 #define INCLUDE_ALL_FILES 2
Signature · 1 line
2.56.0.rc1.315.gc6ed9934b7.dirty
#61 lore raw

PATCH v4 2/5 stash: prepare merge options earlier

In reply to D. Ben Knoble

In a future commit, we will reuse these options for the index merge of "apply --index", not just for the worktree.

---
 builtin/stash.c | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)
builtin/stash.c +5 -5
index dfea2d2c4c..d2b736d4e6 100644
@@ -664,6 +664,11 @@ static enum stash_apply_result do_apply_stash(const char *prefix,
 				repo_get_index_file(the_repository), 0, NULL))
 		return error(_("cannot apply a stash in the middle of a merge"));
 
+	init_ui_merge_options(&o, the_repository);
+
+	if (quiet)
+		o.verbosity = 0;
+
 	if (index) {
 		if (oideq(&info->b_tree, &info->i_tree) ||
 		    oideq(&c_tree, &info->i_tree)) {
@@ -695,8 +700,6 @@ static enum stash_apply_result do_apply_stash(const char *prefix,
 		}
 	}
 
-	init_ui_merge_options(&o, the_repository);
-
 	o.branch1 = label_ours ? label_ours : "Updated upstream";
 	o.branch2 = label_theirs ? label_theirs : "Stashed changes";
 	o.ancestor = label_base ? label_base : "Stash base";
@@ -704,9 +707,6 @@ static enum stash_apply_result do_apply_stash(const char *prefix,
 	if (oideq(&info->b_tree, &c_tree))
 		o.branch1 = "Version stash was based on";
 
-	if (quiet)
-		o.verbosity = 0;
-
 	if (o.verbosity >= 3)
 		printf_ln(_("Merging %s with %s"), o.branch1, o.branch2);
 
Signature · 1 line
2.56.0.rc1.315.gc6ed9934b7.dirty
#62 lore raw

PATCH v4 3/5 t3903: test failed "stash apply --index"

In reply to D. Ben Knoble

The next commit will refactor index handling for applied stashes, so let's make sure we cover conflicted index merging, too.

---
 t/t3903-stash.sh | 21 +++++++++++++++++++++
 1 file changed, 21 insertions(+)
t/t3903-stash.sh +21 -0
index 721158606f..70af58e161 100755
@@ -374,6 +374,27 @@ setup_stash() {
 	test_cmp expect actual
 '
 
+test_expect_success 'stash apply --index leaves everything untouched on failure' '
+	git reset --hard &&
+	echo test >other-file &&
+	git add other-file &&
+	git stash &&
+	echo unrelated >file &&
+	echo unrelated >another-file &&
+	git add another-file &&
+	echo conflict >other-file &&
+	git add other-file &&
+	git diff-files -p >expect &&
+	git diff-index --cached HEAD >expect-index &&
+
+	test_must_fail git stash apply --index 2>err &&
+	test_grep "conflicts in index. Try without --index" err &&
+	git diff-files -p >actual &&
+	test_cmp expect actual &&
+	git diff-index --cached HEAD >actual-index &&
+	test_cmp expect-index actual-index
+'
+
 test_expect_success 'stash -k' '
 	echo bar3 >file &&
 	echo bar4 >file2 &&
Signature · 1 line
2.56.0.rc1.315.gc6ed9934b7.dirty
#63 lore raw

PATCH v4 4/5 t5520: don't expire reflogs where it matters

In reply to D. Ben Knoble

From: Thomas Bachem <[email protected]>

The "--rebase -f with rebased upstream" test computes its fork point from the reflog of refs/remotes/me/copy, and the entry it needs is the one that the fetch of the test before it wrote. Like every reflog entry the suite writes after test_tick, it is dated 2005, so the first "git reflog expire --all" after that fetch removes it. Pull then finds no fork point and rebases onto the merge head with the merge head as the upstream, and the rewound commits come back as a conflict.

Since 452b12c2e0 (builtin/maintenance: use "geometric" strategy by default, 2026-02-24) auto maintenance runs that expiry once the reflog of HEAD holds a hundred entries it would remove, the default of maintenance.reflog-expire.auto. Which run crosses the threshold depends on the entries and maintenance runs before it, so the script passed by chance: a stash topic that no longer runs "git reset" from "stash apply --index" and a rebase topic that runs auto maintenance at the end of "git rebase" together move the expiry between the two tests.

Pin the expiry as ea7d894f44 (t34xx: don't expire reflogs where it matters, 2026-02-24) did for the rebase tests. That covers a "git gc" as well, which expires reflogs on its own, where turning off the auto trigger of the reflog-expire task alone would not.

---
 t/t5520-pull.sh | 6 ++++++
 1 file changed, 6 insertions(+)
t/t5520-pull.sh +6 -0
index 27f38ab3c8..bc818605a5 100755
@@ -35,6 +35,12 @@ test_pull_autostash_fail () {
 }
 
 test_expect_success setup '
+	# Commit dates are hardcoded to 2005, and the reflog entries will have
+	# a matching timestamp. Maintenance may thus immediately expire
+	# reflogs if it was running.
+	git config set gc.reflogExpire never &&
+	git config set gc.reflogExpireUnreachable never &&
+
 	echo file >file &&
 	git add file &&
 	git commit -a -m original
Signature · 1 line
2.56.0.rc1.315.gc6ed9934b7.dirty
#64 lore raw

Re: [PATCH v4 4/5] t5520: don't expire reflogs where it matters

Quoted 21 lines · D. Ben Knoble

From: Thomas Bachem <[email protected]>

The "--rebase -f with rebased upstream" test computes its fork point from the reflog of refs/remotes/me/copy, and the entry it needs is the one that the fetch of the test before it wrote. Like every reflog entry the suite writes after test_tick, it is dated 2005, so the first "git reflog expire --all" after that fetch removes it. Pull then finds no fork point and rebases onto the merge head with the merge head as the upstream, and the rewound commits come back as a conflict.

Since 452b12c2e0 (builtin/maintenance: use "geometric" strategy by default, 2026-02-24) auto maintenance runs that expiry once the reflog of HEAD holds a hundred entries it would remove, the default of maintenance.reflog-expire.auto. Which run crosses the threshold depends on the entries and maintenance runs before it, so the script passed by chance: a stash topic that no longer runs "git reset" from "stash apply --index" and a rebase topic that runs auto maintenance at the end of "git rebase" together move the expiry between the two tests.

Quoted 4 lines

Pin the expiry as ea7d894f44 (t34xx: don't expire reflogs where it matters, 2026-02-24) did for the rebase tests. That covers a "git gc" as well, which expires reflogs on its own, where turning off the auto trigger of the reflog-expire task alone would not.

I find this commit message quite hard to understand. From my perspective the important points are

- "git merge" uses "git stash" to clear any uncommitted changes from the worktree before it tries each strategy. The stashed changes are popped with "--index".

- switching "git stash pop --index" to use merge_incore_nonrecursive() causes "git merge" to stop writing the reflog entries that came from "git stash pop --index" running "git reset"

- that combined with "git rebase" starting to run "git maintenance --auto" changed when we expire the reflogs which breaks the fork- point detection.

The changes themselves look good

Thanks

Phillip

Quoted 28 lines
---
  t/t5520-pull.sh | 6 ++++++
  1 file changed, 6 insertions(+)
t/t5520-pull.sh +6 -0
index 27f38ab3c8..bc818605a5 100755
@@ -35,6 +35,12 @@ test_pull_autostash_fail () {
  }
 
  test_expect_success setup '
+	# Commit dates are hardcoded to 2005, and the reflog entries will have
+	# a matching timestamp. Maintenance may thus immediately expire
+	# reflogs if it was running.
+	git config set gc.reflogExpire never &&
+	git config set gc.reflogExpireUnreachable never &&
+
  	echo file >file &&
  	git add file &&
  	git commit -a -m original
#65 lore raw

PATCH v4 5/5 builtin/stash: merge index in-core

In reply to D. Ben Knoble

"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 need to take care to get the order of trees right when merging. Add a test that covers this case.

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.

---
 builtin/stash.c  | 80 ++++++++++--------------------------------------
 t/t3903-stash.sh | 21 +++++++++++++
 t/t7600-merge.sh |  9 ++++++
 3 files changed, 47 insertions(+), 63 deletions(-)
builtin/stash.c +17 -63
index d2b736d4e6..ec07547376 100644
@@ -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)
 {
 	/*
@@ -674,29 +630,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)
+			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);
+
+			if (!result.clean) {
+				merge_finalize(&o, &result);
 				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);
+			} else {
+				oidcpy(&index_tree, &result.tree->object.oid);
+				merge_finalize(&o, &result);
+			}
 		}
 	}
 
t/t3903-stash.sh +21 -0
index 70af58e161..70c6031958 100755
@@ -395,6 +395,27 @@ setup_stash() {
 	test_cmp expect-index actual-index
 '
 
+# the later "stash -k" test is not expecting us to muck with file so much, so
+# reset when finished
+test_expect_success 'stash apply --index merges the correct trees' '
+	head=$(git rev-parse HEAD) &&
+	test_when_finished "git reset --hard $head" &&
+	test_write_lines A B C >file &&
+	git commit -m setup file &&
+	test_write_lines A B staged >file &&
+	git add file &&
+	test_write_lines A B unstaged >file &&
+	git stash &&
+	test_write_lines committed B C >file &&
+	git commit -m to-be-merged file &&
+	git stash pop --index &&
+	git show :file >actual &&
+	test_write_lines committed B staged >expect &&
+	test_cmp expect actual &&
+	test_write_lines committed B unstaged >expect &&
+	test_cmp expect file
+'
+
 test_expect_success 'stash -k' '
 	echo bar3 >file &&
 	echo bar4 >file2 &&
t/t7600-merge.sh +9 -0
index 64fe21717d..8f6109fb91 100755
@@ -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 &&
Signature · 1 line
2.56.0.rc1.315.gc6ed9934b7.dirty
#66 lore raw

Re: [PATCH v4 5/5] builtin/stash: merge index in-core

builtin/stash.c static enum stash_apply_result do_apply_stash(const char *prefix, · line 644

Quoted 2 lines · D. Ben Knoble
builtin/stash.c +2 -0
+			merge_incore_nonrecursive(&o, merge_base, head, merge,
+						  &result);

In a hard error from merge_incore_nonrecursive(), result->clean is set to -1, which means that ...

builtin/stash.c static enum stash_apply_result do_apply_stash(const char *prefix, · line 646

Quoted 1 line
builtin/stash.c +1 -0
+			if (!result.clean) {

... "result.clean is false" is not true here, so we will ...

builtin/stash.c static enum stash_apply_result do_apply_stash(const char *prefix, · line 650

Quoted 4 lines
builtin/stash.c +2 -0
+				merge_finalize(&o, &result);
 				return error(_("conflicts in index. "
 					       "Try without --index."));
+			} else {

... come here to access result.tree member, no?

builtin/stash.c static enum stash_apply_result do_apply_stash(const char *prefix, · line 653

Quoted 3 lines
builtin/stash.c +3 -0
+				oidcpy(&index_tree, &result.tree->object.oid);
+				merge_finalize(&o, &result);
+			}

IOW, shouldn't it be more like three-way check,

			if (result.clean < 0) {
				merge_finalize(&o, &result);
                                return error(_("index merge failed."));
			} else if (!result.clean) {
				merge_finalize(&o, &result);
                                return error(_("conflict in index merge."));
			} else {
				... happy path ...
			}

or something like that?

#67 lore raw

Re: [PATCH v4 5/5] builtin/stash: merge index in-core

Quoted 18 lines · Junio C Hamano

builtin/stash.c static enum stash_apply_result do_apply_stash(const char *prefix, · line 644

Quoted 2 lines · D. Ben Knoble
builtin/stash.c +2 -0
+                     merge_incore_nonrecursive(&o, merge_base, head, merge,
+                                               &result);

In a hard error from merge_incore_nonrecursive(), result->clean is set to -1, which means that ...

Quoted 1 line

+ if (!result.clean) {

... "result.clean is false" is not true here, so we will ...

builtin/stash.c static enum stash_apply_result do_apply_stash(const char *prefix, · line 650

Quoted 4 lines
builtin/stash.c +2 -0
+                             merge_finalize(&o, &result);
                              return error(_("conflicts in index. "
                                             "Try without --index."));
+                     } else {
Quoted 7 lines

... come here to access result.tree member, no?

builtin/stash.c static enum stash_apply_result do_apply_stash(const char *prefix, · line 653

Quoted 3 lines
builtin/stash.c +3 -0
+                             oidcpy(&index_tree, &result.tree->object.oid);
+                             merge_finalize(&o, &result);
+                     }

IOW, shouldn't it be more like three-way check,

Yep. Missed that when looking at the result struct. Will fix.

Quoted 12 lines
                        if (result.clean < 0) {
                                merge_finalize(&o, &result);
                                return error(_("index merge failed."));
                        } else if (!result.clean) {
                                merge_finalize(&o, &result);
                                return error(_("conflict in index merge."));
                        } else {
                                ... happy path ...
                        }

or something like that?

Signature · 1 line
D. Ben Knoble
#68 lore raw

Re: [PATCH v4 0/5] stash: clean up index-mode test merge

In reply to D. Ben Knoble

Hi Ben

Quoted 15 lines · D. Ben Knoble

Changes in v4:
• Drop merge verbosity changes altogether. I was going to save-and-restore, but when looking at the index-merge test case (more below) closer, I noticed that "git apply --cached" reports conflicts on stderr. That is, "git stash apply --index" would report conflicts, and silencing the merge takes that away. So instead let's leave the configured verbosity alone.
• Only copy resulting index merge tree OID when successful
• Fix interaction with t5520 (new patch 4/5)
• Squash test from 3/5 into 5/5, since it requires actually merging trees. I've elected to keep it a separate test for now (contrary to Phillip's suggestion) since it's written and working. Adapting existing tests requires quite a bit more digging into implicit context assumptions ;)

I've left a comment on the new patch 4, but everything else in the range-diff looks ready to me.

Thanks

Phillip

Quoted 238 lines

Changes in v3:

• Change conflict label for current index
• Fix memory leak of merge_result
• Fix order of trees to make the correct merge (cherry-pick)
• New test (3/5) to validate this
• Fix test in 4/5 to assert more details of expected state

Changes in v2:

• Do give branch labels for the incore merge, although they are never seen (and clarify commit message as a result, also keeping the merge-ort asserts). Phillip was right: without those, we do segfault on conflicts.
• Use the ui merge options to keep the same diff algorithm.
• Use merge_finalize instead of clear_merge_options, and reuse the options between merge calls if they are already initialized.
• Add a new 2/4 to simplify merge options initialization.
• Add a new 3/4 with a test case for conflicted index merges.

v1: <[email protected]>
v2: <[email protected]>
v3: <[email protected]>

[1/5] builtin/stash: remove unused header [2/5] stash: prepare merge options earlier [3/5] t3903: test failed "stash apply --index" [4/5] t5520: don't expire reflogs where it matters [5/5] builtin/stash: merge index in-core

  builtin/stash.c  | 91 ++++++++++++------------------------------------
  t/t3903-stash.sh | 42 ++++++++++++++++++++++
  t/t5520-pull.sh  |  6 ++++
  t/t7600-merge.sh |  9 +++++
  4 files changed, 79 insertions(+), 69 deletions(-)

Diff-intervalle contre v3 :

1:  6a165c4df4 = 1:  6a165c4df4 builtin/stash: remove unused header
2:  d9a9e18f3a ! 2:  35b64ae321 stash: prepare merge options earlier
     @@ builtin/stash.c: static enum stash_apply_result do_apply_stash(const char *prefi
       		return error(_("cannot apply a stash in the middle of a merge"));
       
      +	init_ui_merge_options(&o, the_repository);
     ++
     ++	if (quiet)
     ++		o.verbosity = 0;
      +
       	if (index) {
       		if (oideq(&info->b_tree, &info->i_tree) ||
     @@ builtin/stash.c: static enum stash_apply_result do_apply_stash(const char *prefi
       	o.branch1 = label_ours ? label_ours : "Updated upstream";
       	o.branch2 = label_theirs ? label_theirs : "Stashed changes";
       	o.ancestor = label_base ? label_base : "Stash base";
     +@@ builtin/stash.c: static enum stash_apply_result do_apply_stash(const char *prefix,
     + 	if (oideq(&info->b_tree, &c_tree))
     + 		o.branch1 = "Version stash was based on";
     +
     +-	if (quiet)
     +-		o.verbosity = 0;
     +-
     + 	if (o.verbosity >= 3)
     + 		printf_ln(_("Merging %s with %s"), o.branch1, o.branch2);
     +

4: d39e16905d ! 3: 7b0b317ce0 t3903: test failed "stash apply --index"
@@ Commit message

## t/t3903-stash.sh ##
@@ t/t3903-stash.sh: setup_stash() {
- test_cmp expect file
+ test_cmp expect actual
'

+test_expect_success 'stash apply --index leaves everything untouched on failure' '
3: 8b5ea5e6f4 ! 4: 2ac371d2dc t3903: test stash --index merges
@@
## Metadata ##
-Author: D. Ben Knoble <[email protected]>
+Author: Thomas Bachem <[email protected]>

## Commit message ##
- t3903: test stash --index merges
+ t5520: don't expire reflogs where it matters

- A future commit will refactor index handling for applied stashes, and we
- need to take care to get the order of trees right when merging. Add a
- test that covers this case.
+ The "--rebase -f with rebased upstream" test computes its fork point
+ from the reflog of refs/remotes/me/copy, and the entry it needs is
+ the one that the fetch of the test before it wrote. Like every reflog
+ entry the suite writes after test_tick, it is dated 2005, so the
+ first "git reflog expire --all" after that fetch removes it. Pull
+ then finds no fork point and rebases onto the merge head with the
+ merge head as the upstream, and the rewound commits come back as a
+ conflict.

- Suggested-by: Phillip Wood <[email protected]>
+ Since 452b12c2e0 (builtin/maintenance: use "geometric" strategy by
+ default, 2026-02-24) auto maintenance runs that expiry once the reflog
+ of HEAD holds a hundred entries it would remove, the default of
+ maintenance.reflog-expire.auto. Which run crosses the threshold
+ depends on the entries and maintenance runs before it, so the script
+ passed by chance: a stash topic that no longer runs "git reset" from
+ "stash apply --index" and a rebase topic that runs auto maintenance
+ at the end of "git rebase" together move the expiry between the two
+ tests.

- ## t/t3903-stash.sh ##
-@@ t/t3903-stash.sh: setup_stash() {
- test_cmp expect actual
- '
+ Pin the expiry as ea7d894f44 (t34xx: don't expire reflogs where it
+ matters, 2026-02-24) did for the rebase tests. That covers a "git gc"
+ as well, which expires reflogs on its own, where turning off the auto
+ trigger of the reflog-expire task alone would not.
+
+ Reported-by: Junio C Hamano <[email protected]>
+ Helped-by: D. Ben Knoble <[email protected]>
+ Helped-by: Phillip Wood <[email protected]>
+ Assisted-by: Claude Fable 5.1
+ Signed-off-by: Thomas Bachem <[email protected]>
+
+ ## t/t5520-pull.sh ##
+@@ t/t5520-pull.sh: test_pull_autostash_fail () {
+ }

-+# the later "stash -k" test is not expecting us to muck with file so much, so -+# reset when finished
-+test_expect_success 'stash apply --index merges the correct trees' '
-+ head=$(git rev-parse HEAD) &&
-+ test_when_finished "git reset --hard $head" &&
-+ test_write_lines A B C >file &&
-+ git commit -m setup file &&
-+ test_write_lines A B staged >file &&
-+ git add file &&
-+ test_write_lines A B unstaged >file &&
-+ git stash &&
-+ test_write_lines committed B C >file &&
-+ git commit -m to-be-merged file &&
-+ git stash pop --index &&
-+ git show :file >actual &&
-+ test_write_lines committed B staged >expect &&
-+ test_cmp expect actual &&
-+ test_write_lines committed B unstaged >expect &&
-+ test_cmp expect file
-+'
+ test_expect_success setup '
++ # Commit dates are hardcoded to 2005, and the reflog entries will have
++ # a matching timestamp. Maintenance may thus immediately expire
++ # reflogs if it was running.
++ git config set gc.reflogExpire never &&
++ git config set gc.reflogExpireUnreachable never &&
+
- test_expect_success 'stash -k' '
- echo bar3 >file &&
- echo bar4 >file2 &&
+ echo file >file &&
+ git add file &&
+ git commit -a -m original
5: fde7fb7988 ! 5: e21b832a6e builtin/stash: merge index in-core

     @@ Commit message
          we don't see the usual branch and ancestor labels, but the merge
          subroutines insist on their presence, so use something simple.
      
     +    We need to take care to get the order of trees right when merging. Add a
     +    test that covers this case.
     +
          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.
     @@ builtin/stash.c: static enum stash_apply_result do_apply_stash(const char *prefi
      -			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);
     @@ builtin/stash.c: static enum stash_apply_result do_apply_stash(const char *prefi
      +			merge_incore_nonrecursive(&o, merge_base, head, merge,
      +						  &result);
      +
     -+			oidcpy(&index_tree, &result.tree->object.oid);
     -+			merge_finalize(&o, &result);
     -+
     -+			if (!result.clean)
     ++			if (!result.clean) {
     ++				merge_finalize(&o, &result);
       				return error(_("conflicts in index. "
       					       "Try without --index."));
      -
     @@ builtin/stash.c: static enum stash_apply_result do_apply_stash(const char *prefi
      -			reset_head();
      -			discard_index(the_repository->index);
      -			repo_read_index(the_repository);
     ++			} else {
     ++				oidcpy(&index_tree, &result.tree->object.oid);
     ++				merge_finalize(&o, &result);
     ++			}
       		}
       	}
       
      
     + ## t/t3903-stash.sh ##
     +@@ t/t3903-stash.sh: setup_stash() {
     + 	test_cmp expect-index actual-index
     + '
     +
     ++# the later "stash -k" test is not expecting us to muck with file so much, so
     ++# reset when finished
     ++test_expect_success 'stash apply --index merges the correct trees' '
     ++	head=$(git rev-parse HEAD) &&
     ++	test_when_finished "git reset --hard $head" &&
     ++	test_write_lines A B C >file &&
     ++	git commit -m setup file &&
     ++	test_write_lines A B staged >file &&
     ++	git add file &&
     ++	test_write_lines A B unstaged >file &&
     ++	git stash &&
     ++	test_write_lines committed B C >file &&
     ++	git commit -m to-be-merged file &&
     ++	git stash pop --index &&
     ++	git show :file >actual &&
     ++	test_write_lines committed B staged >expect &&
     ++	test_cmp expect actual &&
     ++	test_write_lines committed B unstaged >expect &&
     ++	test_cmp expect file
     ++'
     ++
     + test_expect_success 'stash -k' '
     + 	echo bar3 >file &&
     + 	echo bar4 >file2 &&
     +
       ## t/t7600-merge.sh ##
      @@ t/t7600-merge.sh: verify_no_mergehead () {
       	test_cmp result.1-5 file

base-commit: d38352cd43ab9745686d697872408bc3249a153f

author
#69 lore raw

Re: [PATCH v4 0/5] stash: clean up index-mode test merge

Quoted 21 lines · Phillip Wood

Hi Ben

Quoted 7 lines · D. Ben Knoble

Changes in v4:
• Drop merge verbosity changes altogether. I was going to save-and-restore, but when looking at the index-merge test case (more below) closer, I noticed that "git apply --cached" reports conflicts on stderr. That is, "git stash apply --index" would report conflicts, and silencing the merge takes that away. So instead let's leave the

Quoted 8 lines

configured verbosity alone.
• Only copy resulting index merge tree OID when successful
• Fix interaction with t5520 (new patch 4/5)
• Squash test from 3/5 into 5/5, since it requires actually merging trees. I've elected to keep it a separate test for now (contrary to Phillip's suggestion) since it's written and working. Adapting existing tests requires quite a bit more digging into implicit context assumptions ;)

Quoted 5 lines

I've left a comment on the new patch 4, but everything else in the range-diff looks ready to me.

Thanks

Phillip

Thanks Phillip. Pending other positive acks, I’m not sure if I should reroll with Thomas’s new patch, reroll dropping it now there’s a seen topic for it, or just wait ;)

I’ll probably wait a bit and see how the dust settles, but:

Junio if you want to see a reroll hit the list using the new synthetic base to make things nicer for you, I can do so. In particular, I think the last check I made when I saw your mail about the synthetic base had the prior round.

#70 lore raw

Re: [PATCH v4 0/5] stash: clean up index-mode test merge

Quoted 26 lines · Ben Knoble
Quoted 26 lines · Phillip Wood

Hi Ben

Quoted 7 lines · D. Ben Knoble

Changes in v4:
• Drop merge verbosity changes altogether. I was going to save-and-restore, but when looking at the index-merge test case (more below) closer, I noticed that "git apply --cached" reports conflicts on stderr. That is, "git stash apply --index" would report conflicts, and silencing the merge takes that away. So instead let's leave the

Quoted 8 lines

configured verbosity alone.
• Only copy resulting index merge tree OID when successful
• Fix interaction with t5520 (new patch 4/5)
• Squash test from 3/5 into 5/5, since it requires actually merging trees. I've elected to keep it a separate test for now (contrary to Phillip's suggestion) since it's written and working. Adapting existing tests requires quite a bit more digging into implicit context assumptions ;)

I've left a comment on the new patch 4, but everything else in the range-diff looks ready to me.

Thanks

Quoted 7 lines
Quoted 1 line

Phillip

Thanks Phillip. Pending other positive acks, I’m not sure if I should reroll with Thomas’s new patch, reroll dropping it now there’s a seen topic for it, or just wait ;)

I’ll probably wait a bit and see how the dust settles, but:

Junio if you want to see a reroll hit the list using the new synthetic base to make things nicer for you, I can do so. In particular, I think the last check I made when I saw your mail about the synthetic base had the prior round.

I realized Junio wasn't CC'd on the prior mail, but since I re-rolled and the merge base changed, I think I've got it right for v5, which just went out.

Signature · 1 line
D. Ben Knoble
#71 lore raw

v5 cover stash: clean up index-mode test merge

In reply to D. Ben Knoble

Hi all,

This small patch series fixes a bug reported by Eli Barzilay in the interaction between autostashing, staged index entries, and stash.index=true.

The first patch is an incidental cleanup, and the second re-arranges one line to make the change easier. The third adds missing test coverage (which catch breakages from prior incorrect rounds of this series). The fourth fixes a test interaction with another in-flight topic. The last holds the interesting bits.

Changes in v5:
• Rebase on synthetic merge for the test interaction with t5520 (dropping old 4/5) [59d1ce1b6e (Merge branch 'tb/t5520-reflog-expire' into dk/stash-apply-index-incore, 2026-09-29)]
• Fix handling of tri-state merge_result.clean

Changes in v4:
• Drop merge verbosity changes altogether. I was going to save-and-restore, but when looking at the index-merge test case (more below) closer, I noticed that "git apply --cached" reports conflicts on stderr. That is, "git stash apply --index" would report conflicts, and silencing the merge takes that away. So instead let's leave the configured verbosity alone.
• Only copy resulting index merge tree OID when successful
• Fix interaction with t5520 (new patch 4/5)
• Squash test from 3/5 into 5/5, since it requires actually merging trees. I've elected to keep it a separate test for now (contrary to Phillip's suggestion) since it's written and working. Adapting existing tests requires quite a bit more digging into implicit context assumptions ;)

Changes in v3:

• Change conflict label for current index
• Fix memory leak of merge_result
• Fix order of trees to make the correct merge (cherry-pick)
• New test (3/5) to validate this
• Fix test in 4/5 to assert more details of expected state

Changes in v2:

• Do give branch labels for the incore merge, although they are never seen (and clarify commit message as a result, also keeping the merge-ort asserts). Phillip was right: without those, we do segfault on conflicts.
• Use the ui merge options to keep the same diff algorithm.
• Use merge_finalize instead of clear_merge_options, and reuse the options between merge calls if they are already initialized.
• Add a new 2/4 to simplify merge options initialization.
• Add a new 3/4 with a test case for conflicted index merges.

v1: <[email protected]>
v2: <[email protected]>
v3: <[email protected]>
v4: <[email protected]>

[1/4] builtin/stash: remove unused header
[2/4] stash: prepare merge options earlier
[3/4] t3903: test failed "stash apply --index"
[4/4] builtin/stash: merge index in-core

 builtin/stash.c  | 94 +++++++++++++-----------------------------------
 t/t3903-stash.sh | 42 ++++++++++++++++++++++
 t/t7600-merge.sh |  9 +++++
 3 files changed, 76 insertions(+), 69 deletions(-)

Diff-intervalle contre v4 :

1:  6a165c4df4 = 1:  d8f4c36459 builtin/stash: remove unused header
2:  35b64ae321 = 2:  8e99033ef0 stash: prepare merge options earlier
3:  7b0b317ce0 = 3:  ee28d0a840 t3903: test failed "stash apply --index"
4:  2ac371d2dc < -:  ---------- t5520: don't expire reflogs where it matters
5:  e21b832a6e ! 4:  ca3de1d4a3 builtin/stash: merge index in-core
    @@ builtin/stash.c: static enum stash_apply_result do_apply_stash(const char *prefi
     +			merge_incore_nonrecursive(&o, merge_base, head, merge,
     +						  &result);
     +
    -+			if (!result.clean) {
    ++			if (result.clean < 0) {
    ++				merge_finalize(&o, &result);
    ++				return error(_("index merge failed"));
    ++			} else if (!result.clean) {
     +				merge_finalize(&o, &result);
      				return error(_("conflicts in index. "
      					       "Try without --index."));

base-commit: a018953688f1b10bddf91bff8747068f5f4746a4
prerequisite-patch-id: 601853fa5478b0dbfb260ba02632418e90338219

Signature · 1 line
2.56.0.rc1.315.gc6ed9934b7.dirty
#72 lore raw

PATCH v5 1/4 builtin/stash: remove unused header

Clang complains that oid-array.h is unused. Certainly none of the oid_array* functions, types, etc., are used, and the transitively-included hash.h declarations are used but covered by a pre-existing direct #include of hash.h.

---
 builtin/stash.c | 1 -
 1 file changed, 1 deletion(-)
builtin/stash.c +0 -1
index 7a9843413b..dfea2d2c4c 100644
@@ -31,7 +31,6 @@
 #include "reflog.h"
 #include "reflog-walk.h"
 #include "add-interactive.h"
-#include "oid-array.h"
 #include "commit.h"
 
 #define INCLUDE_ALL_FILES 2
Signature · 1 line
2.56.0.rc1.315.gc6ed9934b7.dirty
#73 lore raw

PATCH v5 2/4 stash: prepare merge options earlier

In reply to D. Ben Knoble

In a future commit, we will reuse these options for the index merge of "apply --index", not just for the worktree.

---
 builtin/stash.c | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)
builtin/stash.c +5 -5
index dfea2d2c4c..d2b736d4e6 100644
@@ -664,6 +664,11 @@ static enum stash_apply_result do_apply_stash(const char *prefix,
 				repo_get_index_file(the_repository), 0, NULL))
 		return error(_("cannot apply a stash in the middle of a merge"));
 
+	init_ui_merge_options(&o, the_repository);
+
+	if (quiet)
+		o.verbosity = 0;
+
 	if (index) {
 		if (oideq(&info->b_tree, &info->i_tree) ||
 		    oideq(&c_tree, &info->i_tree)) {
@@ -695,8 +700,6 @@ static enum stash_apply_result do_apply_stash(const char *prefix,
 		}
 	}
 
-	init_ui_merge_options(&o, the_repository);
-
 	o.branch1 = label_ours ? label_ours : "Updated upstream";
 	o.branch2 = label_theirs ? label_theirs : "Stashed changes";
 	o.ancestor = label_base ? label_base : "Stash base";
@@ -704,9 +707,6 @@ static enum stash_apply_result do_apply_stash(const char *prefix,
 	if (oideq(&info->b_tree, &c_tree))
 		o.branch1 = "Version stash was based on";
 
-	if (quiet)
-		o.verbosity = 0;
-
 	if (o.verbosity >= 3)
 		printf_ln(_("Merging %s with %s"), o.branch1, o.branch2);
 
Signature · 1 line
2.56.0.rc1.315.gc6ed9934b7.dirty
#74 lore raw

PATCH v5 3/4 t3903: test failed "stash apply --index"

In reply to D. Ben Knoble

The next commit will refactor index handling for applied stashes, so let's make sure we cover conflicted index merging, too.

---
 t/t3903-stash.sh | 21 +++++++++++++++++++++
 1 file changed, 21 insertions(+)
t/t3903-stash.sh +21 -0
index 721158606f..70af58e161 100755
@@ -374,6 +374,27 @@ setup_stash() {
 	test_cmp expect actual
 '
 
+test_expect_success 'stash apply --index leaves everything untouched on failure' '
+	git reset --hard &&
+	echo test >other-file &&
+	git add other-file &&
+	git stash &&
+	echo unrelated >file &&
+	echo unrelated >another-file &&
+	git add another-file &&
+	echo conflict >other-file &&
+	git add other-file &&
+	git diff-files -p >expect &&
+	git diff-index --cached HEAD >expect-index &&
+
+	test_must_fail git stash apply --index 2>err &&
+	test_grep "conflicts in index. Try without --index" err &&
+	git diff-files -p >actual &&
+	test_cmp expect actual &&
+	git diff-index --cached HEAD >actual-index &&
+	test_cmp expect-index actual-index
+'
+
 test_expect_success 'stash -k' '
 	echo bar3 >file &&
 	echo bar4 >file2 &&
Signature · 1 line
2.56.0.rc1.315.gc6ed9934b7.dirty
#75 lore raw

PATCH v5 4/4 builtin/stash: merge index in-core

In reply to D. Ben Knoble

"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 need to take care to get the order of trees right when merging. Add a test that covers this case.

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.

---
 builtin/stash.c  | 83 ++++++++++++------------------------------------
 t/t3903-stash.sh | 21 ++++++++++++
 t/t7600-merge.sh |  9 ++++++
 3 files changed, 50 insertions(+), 63 deletions(-)
builtin/stash.c +20 -63
index d2b736d4e6..fa3deeecbe 100644
@@ -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)
 {
 	/*
@@ -674,29 +630,30 @@ 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)
+			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);
+
+			if (result.clean < 0) {
+				merge_finalize(&o, &result);
+				return error(_("index merge failed"));
+			} else if (!result.clean) {
+				merge_finalize(&o, &result);
 				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);
+			} else {
+				oidcpy(&index_tree, &result.tree->object.oid);
+				merge_finalize(&o, &result);
+			}
 		}
 	}
 
t/t3903-stash.sh +21 -0
index 70af58e161..70c6031958 100755
@@ -395,6 +395,27 @@ setup_stash() {
 	test_cmp expect-index actual-index
 '
 
+# the later "stash -k" test is not expecting us to muck with file so much, so
+# reset when finished
+test_expect_success 'stash apply --index merges the correct trees' '
+	head=$(git rev-parse HEAD) &&
+	test_when_finished "git reset --hard $head" &&
+	test_write_lines A B C >file &&
+	git commit -m setup file &&
+	test_write_lines A B staged >file &&
+	git add file &&
+	test_write_lines A B unstaged >file &&
+	git stash &&
+	test_write_lines committed B C >file &&
+	git commit -m to-be-merged file &&
+	git stash pop --index &&
+	git show :file >actual &&
+	test_write_lines committed B staged >expect &&
+	test_cmp expect actual &&
+	test_write_lines committed B unstaged >expect &&
+	test_cmp expect file
+'
+
 test_expect_success 'stash -k' '
 	echo bar3 >file &&
 	echo bar4 >file2 &&
t/t7600-merge.sh +9 -0
index 64fe21717d..8f6109fb91 100755
@@ -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 &&
Signature · 1 line
2.56.0.rc1.315.gc6ed9934b7.dirty
#76 lore raw

Re: [PATCH v5 0/4] stash: clean up index-mode test merge

In reply to D. Ben Knoble

Hi Ben

Quoted 6 lines · D. Ben Knoble

Changes in v5:
• Rebase on synthetic merge for the test interaction with t5520 (dropping old 4/5) [59d1ce1b6e (Merge branch 'tb/t5520-reflog-expire' into dk/stash-apply-index-incore, 2026-09-29)]
• Fix handling of tri-state merge_result.clean

The range-diff below looks as expected, thanks for working on this, I'm really pleased to see us removing some subprocesses from "git stash".

Thanks

Phillip

Quoted 71 lines

Changes in v4:
• Drop merge verbosity changes altogether. I was going to save-and-restore, but when looking at the index-merge test case (more below) closer, I noticed that "git apply --cached" reports conflicts on stderr. That is, "git stash apply --index" would report conflicts, and silencing the merge takes that away. So instead let's leave the configured verbosity alone.
• Only copy resulting index merge tree OID when successful
• Fix interaction with t5520 (new patch 4/5)
• Squash test from 3/5 into 5/5, since it requires actually merging trees. I've elected to keep it a separate test for now (contrary to Phillip's suggestion) since it's written and working. Adapting existing tests requires quite a bit more digging into implicit context assumptions ;)

Changes in v3:

• Change conflict label for current index
• Fix memory leak of merge_result
• Fix order of trees to make the correct merge (cherry-pick)
• New test (3/5) to validate this
• Fix test in 4/5 to assert more details of expected state

Changes in v2:

• Do give branch labels for the incore merge, although they are never seen (and clarify commit message as a result, also keeping the merge-ort asserts). Phillip was right: without those, we do segfault on conflicts.
• Use the ui merge options to keep the same diff algorithm.
• Use merge_finalize instead of clear_merge_options, and reuse the options between merge calls if they are already initialized.
• Add a new 2/4 to simplify merge options initialization.
• Add a new 3/4 with a test case for conflicted index merges.

v1: <[email protected]>
v2: <[email protected]>
v3: <[email protected]>
v4: <[email protected]>

[1/4] builtin/stash: remove unused header
[2/4] stash: prepare merge options earlier
[3/4] t3903: test failed "stash apply --index"
[4/4] builtin/stash: merge index in-core

  builtin/stash.c  | 94 +++++++++++++-----------------------------------
  t/t3903-stash.sh | 42 ++++++++++++++++++++++
  t/t7600-merge.sh |  9 +++++
  3 files changed, 76 insertions(+), 69 deletions(-)

Diff-intervalle contre v4 :

1:  6a165c4df4 = 1:  d8f4c36459 builtin/stash: remove unused header
2:  35b64ae321 = 2:  8e99033ef0 stash: prepare merge options earlier
3:  7b0b317ce0 = 3:  ee28d0a840 t3903: test failed "stash apply --index"
4:  2ac371d2dc < -:  ---------- t5520: don't expire reflogs where it matters
5:  e21b832a6e ! 4:  ca3de1d4a3 builtin/stash: merge index in-core
     @@ builtin/stash.c: static enum stash_apply_result do_apply_stash(const char *prefi
      +			merge_incore_nonrecursive(&o, merge_base, head, merge,
      +						  &result);
      +
     -+			if (!result.clean) {
     ++			if (result.clean < 0) {
     ++				merge_finalize(&o, &result);
     ++				return error(_("index merge failed"));
     ++			} else if (!result.clean) {
      +				merge_finalize(&o, &result);
       				return error(_("conflicts in index. "
       					       "Try without --index."));

base-commit: a018953688f1b10bddf91bff8747068f5f4746a4
prerequisite-patch-id: 601853fa5478b0dbfb260ba02632418e90338219

#77 lore raw

Re: [PATCH v5 0/4] stash: clean up index-mode test merge

Quoted 4 lines · Phillip Wood

Hi Ben

Quoted 1 line · D. Ben Knoble
Quoted 8 lines
Quoted 5 lines

Changes in v5:
• Rebase on synthetic merge for the test interaction with t5520 (dropping old 4/5) [59d1ce1b6e (Merge branch 'tb/t5520-reflog-expire' into dk/stash-apply-index-incore, 2026-09-29)]
• Fix handling of tri-state merge_result.clean

The range-diff below looks as expected, thanks for working on this, I'm really pleased to see us removing some subprocesses from "git stash".

Thanks for writing and reviewing. These now look very good to me too.

Let me mark them for 'next'.