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; 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