stash: reuse cached index entries in --patch temporary index

`git stash -p` prepares the interactive selection by creating a temporary index at HEAD, switching `GIT_INDEX_FILE` to it, and then running the `add -p` machinery. That temporary index was created by running `git read-tree HEAD`. The resulting index had no useful cached stat data or fsmonitor-valid bits from the real index. When `run_add_p()` refreshed that temporary index before showing the first prompt, it could end up lstat(2)-ing every tracked file, even in a repository where `git diff` and `git restore -p` can use fsmonitor to avoid that work. Create the temporary index in-process instead. Use `unpack_trees()` to reset the real index contents to HEAD while writing the result to the temporary index path. For paths whose index entries already match HEAD, `oneway_merge()` reuses the existing cache entries, preserving their cached stat data and `CE_FSMONITOR_VALID` state. This makes the refresh performed by `run_add_p()` behave like the one used by `git restore -p`: unchanged paths can be skipped via fsmonitor instead of being scanned again. In a 206k file repository with `core.fsmonitor` enabled and a one-line change in one file, time to first prompt dropped from 34.774 seconds to 0.659 seconds. The new perf test file demonstrates similar improvements, with maen times for without- and with-fsmonitor cases dropping from 6.90 and 6.83 seconds to 0.55 and 0.28 seconds, respectively. Signed-off-by: Adam Johnson <me@adamj.eu> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Adam Johnson committed May 22, 2026 at 23:12 UTC 48513e05e2f226c85a9b88893630c8ae28409772
2 files changed +107 -6
builtin/stash.c
+64 -6
@@ -372,6 +372,56 @@ static int reset_tree(struct object_id *i_tree, int update, int reset)
372 return 0;
373 }
374
375 +static int create_index_from_tree(const struct object_id *tree_id,
376 + const char *index_path)
377 +{
378 + int nr_trees = 1;
379 + int ret = 0;
380 + struct unpack_trees_options opts;
381 + struct tree_desc t[MAX_UNPACK_TREES];
382 + struct tree *tree;
383 + struct index_state dst_istate = INDEX_STATE_INIT(the_repository);
384 + struct lock_file lock_file = LOCK_INIT;
385 +
386 + repo_read_index_preload(the_repository, NULL, 0);
387 + refresh_index(the_repository->index, REFRESH_QUIET, NULL, NULL, NULL);
388 +
389 + hold_lock_file_for_update(&lock_file, index_path, LOCK_DIE_ON_ERROR);
390 +
391 + memset(&opts, 0, sizeof(opts));
392 +
393 + tree = repo_parse_tree_indirect(the_repository, tree_id);
394 + if (!tree || repo_parse_tree(the_repository, tree)) {
395 + ret = -1;
396 + goto done;
397 + }
398 +
399 + init_tree_desc(t, &tree->object.oid, tree->buffer, tree->size);
400 +
401 + opts.head_idx = 1;
402 + opts.src_index = the_repository->index;
403 + opts.dst_index = &dst_istate;
404 + opts.merge = 1;
405 + opts.reset = UNPACK_RESET_PROTECT_UNTRACKED;
406 + opts.fn = oneway_merge;
407 +
408 + if (unpack_trees(nr_trees, t, &opts)) {
409 + ret = -1;
410 + goto done;
411 + }
412 +
413 + if (write_locked_index(&dst_istate, &lock_file, COMMIT_LOCK)) {
414 + ret = error(_("unable to write new index file"));
415 + goto done;
416 + }
417 +
418 +done:
419 + release_index(&dst_istate);
420 + if (ret)
421 + rollback_lock_file(&lock_file);
422 + return ret;
423 +}
424 +
425 static int diff_tree_binary(struct strbuf *out, struct object_id *w_commit)
426 {
427 struct child_process cp = CHILD_PROCESS_INIT;
@@ -1309,18 +1359,26 @@ static int stash_patch(struct stash_info *info, const struct pathspec *ps,
1359 struct interactive_options *interactive_opts)
1360 {
1361 int ret = 0;
1312 - struct child_process cp_read_tree = CHILD_PROCESS_INIT;
1362 struct child_process cp_diff_tree = CHILD_PROCESS_INIT;
1363 + struct commit *head_commit;
1364 + const struct object_id *head_tree;
1365 struct index_state istate = INDEX_STATE_INIT(the_repository);
1366 char *old_index_env = NULL, *old_repo_index_file;
1367
1368 remove_path(stash_index_path.buf);
1369
1319 - cp_read_tree.git_cmd = 1;
1320 - strvec_pushl(&cp_read_tree.args, "read-tree", "HEAD", NULL);
1321 - strvec_pushf(&cp_read_tree.env, "GIT_INDEX_FILE=%s",
1322 - stash_index_path.buf);
1323 - if (run_command(&cp_read_tree)) {
1370 + head_commit = lookup_commit(the_repository, &info->b_commit);
1371 + if (!head_commit || repo_parse_commit(the_repository, head_commit)) {
1372 + ret = -1;
1373 + goto done;
1374 + }
1375 + head_tree = get_commit_tree_oid(head_commit);
1376 + if (!head_tree) {
1377 + ret = -1;
1378 + goto done;
1379 + }
1380 +
1381 + if (create_index_from_tree(head_tree, stash_index_path.buf)) {
1382 ret = -1;
1383 goto done;
1384 }
t/perf/p3904-stash-patch.sh new
+43
@@ -0,0 +1,43 @@
1 +#!/bin/sh
2 +
3 +test_description="Performance tests for git stash -p"
4 +
5 +. ./perf-lib.sh
6 +
7 +test_perf_fresh_repo
8 +
9 +test_expect_success "setup" '
10 + mkdir files &&
11 + test_seq 1 100000 | while read i; do
12 + echo "content $i" >files/$i.txt || return 1
13 + done &&
14 + git add files/ &&
15 + git commit -q -m "add tracked files" &&
16 + echo modified >files/1.txt
17 +'
18 +
19 +test_perf "stash -p, no fsmonitor" \
20 + --setup 'echo modified >files/1.txt' '
21 + printf "q\n" | git stash -p >/dev/null 2>&1 || true
22 +'
23 +
24 +if test_have_prereq FSMONITOR_DAEMON
25 +then
26 + test_expect_success "enable builtin fsmonitor" '
27 + git config core.fsmonitor true &&
28 + git fsmonitor--daemon start &&
29 + git update-index --fsmonitor &&
30 + git status >/dev/null 2>&1
31 + '
32 +
33 + test_perf "stash -p, builtin fsmonitor" \
34 + --setup 'echo modified >files/1.txt && git status >/dev/null 2>&1' '
35 + printf "q\n" | git stash -p >/dev/null 2>&1 || true
36 + '
37 +
38 + test_expect_success "stop builtin fsmonitor" '
39 + git fsmonitor--daemon stop
40 + '
41 +fi
42 +
43 +test_done