From a3c1affbc8ef54934998748084cd0b3c1cc9292d Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Fri, 15 May 2026 10:38:36 -0700 Subject: [PATCH 1/2] t2080: add regression test for fscache + parallel checkout dir creation When checkout.workers > 1 and core.fscache is enabled on Windows, 'git checkout -- ' fails with 'cannot create directory: Directory not empty' when restoring files into directories that do not yet exist on disk. Root cause: create_directories() creates parent directories via mkdir(), but the Windows fscache (which caches directory listings) is not invalidated. Subsequent has_dirs_only_path() calls for the same parent directory return stale ENOENT from the cached listing. The recovery path then tries to unlink+recreate the directory, which fails because mkdir() already populated it with child directories. With workers=1, write_entry() calls flush_fscache() after each file, keeping the cache in sync. With workers>1, enqueue_checkout() defers the write (and the flush), leaving the cache stale for the next entry. Add a test that reproduces this deterministically: create two files sharing a nested parent directory, delete them in a second commit, then restore both via 'git checkout -- ' with workers>1. Bug: https://dev.azure.com/microsoft/OS/_workitems/edit/62260193 Assisted-by: Claude Opus 4.6 Signed-off-by: Tyrie Vella --- t/t2080-parallel-checkout-basics.sh | 46 +++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/t/t2080-parallel-checkout-basics.sh b/t/t2080-parallel-checkout-basics.sh index 5ffe1a41e2cd72..7ad96cd5cd24a3 100755 --- a/t/t2080-parallel-checkout-basics.sh +++ b/t/t2080-parallel-checkout-basics.sh @@ -274,4 +274,50 @@ test_expect_success '"git checkout ." report should not include failed entries' ) ' +# Regression test: parallel checkout + fscache stale directory listing. +# +# When checkout.workers > 1, checkout_entry_ca() enqueues files for deferred +# writing instead of writing them inline. The inline write_entry() path calls +# flush_fscache() after each file, keeping the Windows fscache in sync with +# newly-created directories. The deferred path skips this flush, so +# has_dirs_only_path() sees stale ENOENT for directories that mkdir() just +# created. The recovery path in create_directories() then tries to unlink+ +# recreate the directory, which fails because it already has children. +# +# The trigger is: two files sharing a parent directory that does not yet exist +# on disk when `git checkout -- ` runs. +test_expect_success MINGW 'parallel checkout with fscache does not fail on new directories' ' + git init fscache-pc && + ( + cd fscache-pc && + git config core.fscache true && + + # Commit B1: files in a nested directory + mkdir -p sub/deep/dir && + echo one >sub/deep/dir/file1.txt && + echo two >sub/deep/dir/file2.txt && + git add sub && + git commit -m "B1: with sub/deep/dir" && + git tag B1 && + + # Commit B2: the directory is gone + git rm -rf sub && + git commit -m "B2: without sub" && + + # Now restore both files from B1 with parallel checkout. + # This is the pathspec checkout path (checkout_worktree in + # builtin/checkout.c), which defers writes via enqueue_checkout + # when workers > 1 and does not flush fscache between entries. + git -c checkout.workers=2 \ + -c checkout.thresholdForParallelism=0 \ + checkout B1 -- sub/deep/dir/file1.txt sub/deep/dir/file2.txt && + + # Verify both files are correctly restored + echo one >expect1 && + echo two >expect2 && + test_cmp expect1 sub/deep/dir/file1.txt && + test_cmp expect2 sub/deep/dir/file2.txt + ) +' + test_done From 8b819f9fe79ffd3dcaf3a1d6c5e26d0898f9dd89 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Fri, 15 May 2026 10:58:56 -0700 Subject: [PATCH 2/2] entry: flush fscache after creating directories and writing files When checkout.workers > 1 and core.fscache is enabled on Windows, the fscache caches directory listings that become stale when create_directories() creates new parent directories via mkdir() or when write_pc_item() writes new files. Subsequent lstat() calls through the fscache return ENOENT for these just-created filesystem entries, causing two failure modes: 1. create_directories(): has_dirs_only_path() reports a just-created directory as non-existent, triggering the unlink+mkdir recovery path which fails with 'Directory not empty' because the directory already has children from earlier mkdir() calls. 2. write_pc_item(): after writing and closing a file, lstat() cannot see it through the stale parent directory listing cache, failing with 'unable to stat just-written file'. With workers=1 these do not occur because write_entry() calls flush_fscache() after each file, keeping the cache in sync. With workers>1, enqueue_checkout() defers the write (and the flush), leaving the cache stale for subsequent entries. Fix both by adding flush_fscache() calls: - In create_directories() after each successful mkdir() - In write_pc_item() before lstat() of the just-written file On non-Windows platforms flush_fscache() is a no-op, so there is no behavioral change. Assisted-by: Claude Opus 4.6 Signed-off-by: Tyrie Vella --- entry.c | 15 ++++++++++++++- parallel-checkout.c | 7 +++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/entry.c b/entry.c index 383641fa254997..5a4270698d0a01 100644 --- a/entry.c +++ b/entry.c @@ -49,10 +49,23 @@ static void create_directories(const char *path, int path_len, */ if (mkdir(buf, 0777)) { if (errno == EEXIST && state->force && - !unlink_or_warn(buf) && !mkdir(buf, 0777)) + !unlink_or_warn(buf) && !mkdir(buf, 0777)) { + flush_fscache(); continue; + } die_errno("cannot create directory at '%s'", buf); } + + /* + * Flush the lstat cache of directory listings so that + * subsequent has_dirs_only_path() calls see the + * just-created directory. Without this, the Windows + * fscache returns stale ENOENT for the new directory, + * causing the next entry sharing this parent to + * incorrectly hit the mkdir/unlink recovery path + * above, which then fails with "Directory not empty". + */ + flush_fscache(); } free(buf); } diff --git a/parallel-checkout.c b/parallel-checkout.c index 8fadb7c804bc02..1eb277a0fc0a55 100644 --- a/parallel-checkout.c +++ b/parallel-checkout.c @@ -395,6 +395,13 @@ void write_pc_item(struct parallel_checkout_item *pc_item, goto out; } + /* + * Flush the Windows fscache so that the lstat() below sees the + * file we just wrote. Without this, the cached parent directory + * listing may not yet include the new file entry. + */ + flush_fscache(); + if (state->refresh_cache && !fstat_done && lstat(path.buf, &pc_item->st) < 0) { error_errno("unable to stat just-written file '%s'", path.buf); pc_item->status = PC_ITEM_FAILED;