mirror of
https://github.com/git-for-windows/git.git
synced 2026-06-16 13:04:57 -05:00
entry: flush fscache after creating directories and writing files (#6250)
## Problem `git checkout <tree> -- <pathspec>` with `checkout.workers > 1` and `core.fscache=true` fails when restoring files into directories that do not yet exist on disk. Two failure modes: 1. `fatal: cannot create directory at '...': Directory not empty` (exit 128) 2. `error: unable to stat just-written file '...'` (exit 255) 100% reproducible when two or more files share a not-yet-created parent directory. ## Root Cause The Windows fscache caches directory listings that become stale when `create_directories()` creates new parent directories via `mkdir()` or when `write_pc_item()` writes new files. 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 Add `flush_fscache()` calls: - In `create_directories()` after each successful `mkdir()`, so `has_dirs_only_path()` sees the new directory - In `write_pc_item()` before `lstat()` of the just-written file On non-Windows platforms `flush_fscache()` is a no-op. ## Test Adds a regression test to `t2080-parallel-checkout-basics.sh` (`MINGW` prereq) that deterministically reproduces the bug: two files sharing a nested parent directory, deleted in a second commit, then restored via `git checkout <tree> -- <pathspec>` with `workers=2`.
This commit is contained in:
committed by
Git for Windows Build Agent
commit
077e1e1d6c
15
entry.c
15
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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user