Skip to content

feat: Windows support through ReFS block cloning - #53

Draft
nexxeln wants to merge 25 commits into
devfrom
nxl/windows-support
Draft

nexxeln wants to merge 25 commits into
devfrom
nxl/windows-support

Conversation

@nexxeln

@nexxeln nexxeln commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Draft. Adds Windows support: a ReFS block-clone backend (Dev Drive or any ReFS volume), fail-closed init on NTFS, and the lifecycle, CLI, hook, PowerShell, and plugin changes Windows needs.

Work in progress. The description will be rewritten when the branch is ready for review.

nexxeln added 25 commits October 9, 2026 19:41
std::fs::canonicalize returns \\?\ paths on Windows. Those paths break Git, shells, and comparisons.
Windows rename fails while a handle is open. Retry briefly, then return in_use.
Windows cannot rename a directory that is the process cwd. The printed shell path still comes from the directory captured before that move.
raw_arg keeps quotes inside the command, and /d skips AutoRun. Hook tests compare the written lines, not the shell's line endings.
pwsh and powershell both get a rift function that changes directory from the path rift prints, keeps the native exit code, and forces UTF-8 for that call.
CON, trailing dots, and <>:\"|?* are not valid workspace names on Windows. Those names stay valid on other platforms.
The checks return early unless RIFT_REQUIRE_REFS_TESTS is set. When it is set, a failed create or a missing pwsh fails the run.
The RPC child starts in the home directory. Cancel kills the process tree. A bare name resolves to rift.exe, or to the bundled binary next to npm's shim.
A global install puts rift.cmd in the npm prefix and the package in prefix\node_modules\rift-snapshot. A local install still resolves through node_modules\.bin.
The core messages already name the path and the fix. A Dev Drive hint is wrong on Linux and macOS.
Removing a created workspace also trashes descendants, and those live in sibling storage. If the process cwd is inside one, move to the parent first.
Windows now uses RefsStrategy. init and create clone a multi-cluster
probe file with FSCTL_DUPLICATE_EXTENTS_TO_FILE, check the bytes and
write isolation, and delete the probe. On any other volume they fail
with CowUnavailable naming the filesystem and pointing at Dev Drive.
create also requires the source and the destination parent to share a
volume serial number from FILE_ID_INFO.

Directories and regular files are cloned. Reparse points and encrypted
files are unsupported for now.

The NTFS CI job sets RIFT_REQUIRE_UNSUPPORTED_WINDOWS_TESTS.
Adds a ReFS job on windows-2022 (diskpart partition) and a Dev Drive
job on windows-2025 (samypr100/setup-dev-drive, 10 GB, trusted).
scripts/ci/windows-refs.ps1 checks the volume is ReFS, prints its
cluster size and Dev Drive state, and copies the checkout onto it, so
tests create their temporary directories there.

With RIFT_REQUIRE_REFS_TESTS set, the probe must pass on ReFS and leave
nothing behind, and a create must clone a multi-cluster file with
shared extents, isolate writes in both directions, and copy empty and
nested files.

All Windows jobs run with --no-fail-fast and --show-output so every
test binary reports and gated tests show what they observed.
Each walked entry is classified once from its attributes and reparse
tag. Symlinks are recreated from their target text, with a Developer
Mode hint when Windows refuses. Junctions are recreated from their raw
reparse data and never descended into. Other reparse points (cloud
placeholders, dedup, app execution aliases) and encrypted files are
UnsupportedEntry.

Files keep their creation, last-access, and last-write times and their
settable attributes, applied last so read-only lands after every
write. Git for Windows reads creation time as ctime, so a clone keeps
the index's stat data valid. Directories get theirs deepest-first after
the walk. Named streams are copied by value. Hard links are keyed by the
128-bit FILE_ID_INFO id and recreated against the first clone.

The ReFS round trip now covers a Git repository with dirty, staged,
untracked, and ignored files, read-only objects, a hidden file, a
stream, a hard-linked pair, file and directory symlinks, junctions
inside and outside the tree, a Unicode name, and a path past 260
characters.
Clones share what is on disk, so writes still in the cache can come
back zero-filled (git-lfs#6312, pnpm#7186). Each source file is now
flushed through a second, writable handle before it is cloned.
Read-only files, such as Git objects, have the attribute cleared for the
flush and restored on every path. When no writable handle can be opened,
for example because another program holds the file without write
sharing, the clone is compared byte for byte with its source and a
mismatch fails with CowUnavailable naming the file.

New ReFS tests write 64 files of 4 MiB and create immediately, and clone
a file held open without write sharing.
std::fs::remove_dir_all cannot delete read-only files, such as Git
objects, on volumes without POSIX delete semantics, which includes the
plain ReFS volume in CI. RefsStrategy::remove_directory now clears the
read-only attribute across the tree on a permission error and retries
once. It never descends into reparse points and std never follows them,
so junction and symlink targets survive.

The ReFS round trip now removes the child and runs gc, and checks that a
junction target outside the tree survives. A separate test records what
std alone does on each volume next to RefsStrategy's result.
copy_directory must refuse a destination on another volume with
CowUnavailable and create nothing; CI's ReFS checkout and the runner's
temporary directory sit on different volumes. The write-then-clone test
also times a second create of the same, now flushed, files.
A sparse source must stay sparse in its clone, since ReFS only clones a
sparse file into a sparse one. The round trip also counts the child's
entries before removing it, so the printed count is real.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant