entry: flush fscache after creating directories and writing files

When checkout.workers > 1 and core.fscache is enabled on Windows,
'git checkout <tree> -- <pathspec>' fails when restoring files into
directories that do not yet exist on disk. Two failure modes occur:

1. create_directories(): the fscache returns a stale directory listing
   that does not include a just-created directory. has_dirs_only_path()
   reports it as non-existent, triggering the unlink+mkdir recovery
   path which fails with 'cannot create directory: Directory not empty'.

2. write_pc_item(): after writing and closing a file, lstat() cannot
   see it through the stale fscache, failing with 'unable to stat
   just-written file'.

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 subsequent
entries.

Fix both by adding flush_fscache() calls after mkdir() in
create_directories() and before lstat() in write_pc_item(). On
non-Windows platforms flush_fscache() is a no-op.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
This commit is contained in:
Tyrie Vella
2026-05-18 09:50:39 -07:00
committed by Git for Windows Build Agent
parent 699534ae07
commit bd08683f99
3 changed files with 67 additions and 1 deletions

15
entry.c
View File

@@ -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);
}

View File

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

View File

@@ -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 <tree> -- <pathspec>` 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