Skip to content

Commit 1d637ef

Browse files
committed
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>
1 parent e586f99 commit 1d637ef

3 files changed

Lines changed: 67 additions & 1 deletion

File tree

entry.c

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,23 @@ static void create_directories(const char *path, int path_len,
4949
*/
5050
if (mkdir(buf, 0777)) {
5151
if (errno == EEXIST && state->force &&
52-
!unlink_or_warn(buf) && !mkdir(buf, 0777))
52+
!unlink_or_warn(buf) && !mkdir(buf, 0777)) {
53+
flush_fscache();
5354
continue;
55+
}
5456
die_errno("cannot create directory at '%s'", buf);
5557
}
58+
59+
/*
60+
* Flush the lstat cache of directory listings so that
61+
* subsequent has_dirs_only_path() calls see the
62+
* just-created directory. Without this, the Windows
63+
* fscache returns stale ENOENT for the new directory,
64+
* causing the next entry sharing this parent to
65+
* incorrectly hit the mkdir/unlink recovery path
66+
* above, which then fails with "Directory not empty".
67+
*/
68+
flush_fscache();
5669
}
5770
free(buf);
5871
}

parallel-checkout.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,13 @@ void write_pc_item(struct parallel_checkout_item *pc_item,
395395
goto out;
396396
}
397397

398+
/*
399+
* Flush the Windows fscache so that the lstat() below sees the
400+
* file we just wrote. Without this, the cached parent directory
401+
* listing may not yet include the new file entry.
402+
*/
403+
flush_fscache();
404+
398405
if (state->refresh_cache && !fstat_done && lstat(path.buf, &pc_item->st) < 0) {
399406
error_errno("unable to stat just-written file '%s'", path.buf);
400407
pc_item->status = PC_ITEM_FAILED;

t/t2080-parallel-checkout-basics.sh

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,4 +274,50 @@ test_expect_success '"git checkout ." report should not include failed entries'
274274
)
275275
'
276276

277+
# Regression test: parallel checkout + fscache stale directory listing.
278+
#
279+
# When checkout.workers > 1, checkout_entry_ca() enqueues files for deferred
280+
# writing instead of writing them inline. The inline write_entry() path calls
281+
# flush_fscache() after each file, keeping the Windows fscache in sync with
282+
# newly-created directories. The deferred path skips this flush, so
283+
# has_dirs_only_path() sees stale ENOENT for directories that mkdir() just
284+
# created. The recovery path in create_directories() then tries to unlink+
285+
# recreate the directory, which fails because it already has children.
286+
#
287+
# The trigger is: two files sharing a parent directory that does not yet exist
288+
# on disk when `git checkout <tree> -- <pathspec>` runs.
289+
test_expect_success MINGW 'parallel checkout with fscache does not fail on new directories' '
290+
git init fscache-pc &&
291+
(
292+
cd fscache-pc &&
293+
git config core.fscache true &&
294+
295+
# Commit B1: files in a nested directory
296+
mkdir -p sub/deep/dir &&
297+
echo one >sub/deep/dir/file1.txt &&
298+
echo two >sub/deep/dir/file2.txt &&
299+
git add sub &&
300+
git commit -m "B1: with sub/deep/dir" &&
301+
git tag B1 &&
302+
303+
# Commit B2: the directory is gone
304+
git rm -rf sub &&
305+
git commit -m "B2: without sub" &&
306+
307+
# Now restore both files from B1 with parallel checkout.
308+
# This is the pathspec checkout path (checkout_worktree in
309+
# builtin/checkout.c), which defers writes via enqueue_checkout
310+
# when workers > 1 and does not flush fscache between entries.
311+
git -c checkout.workers=2 \
312+
-c checkout.thresholdForParallelism=0 \
313+
checkout B1 -- sub/deep/dir/file1.txt sub/deep/dir/file2.txt &&
314+
315+
# Verify both files are correctly restored
316+
echo one >expect1 &&
317+
echo two >expect2 &&
318+
test_cmp expect1 sub/deep/dir/file1.txt &&
319+
test_cmp expect2 sub/deep/dir/file2.txt
320+
)
321+
'
322+
277323
test_done

0 commit comments

Comments
 (0)