bug(prep): on Windows, rt prep --dry-run refuses when another program holds an ignored file open — the scratch copy reads every ignored file #1543
Labels
No labels
bump
major
bump
minor
bump
patch
kind
bug
kind
chore
kind
docs
kind
epic
kind
feature
loop/paused
mirror-drift
priority
critical
priority
high
priority
low
priority
medium
size
L
size
M
size
S
size
XL
status
blocked
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
frankenbit/release-toolkit#1543
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
On Windows,
rt prep --dry-runrefuses in a checkout where a running program holds an ignored file open without read sharing. A Unity editor with the project open does that. The scratch copy opens every file under the checkout, ignored ones included, and a sharing violation on any of them ends the preview. The preview needs none of those files.Measured (2026-10-04)
Setup.
rtv0.65.0, built natively from the tag: Go 1.27.1 windows/amd64, git 2.55.0.windows.3, Git Bash.fd3e3ec(tag-is-version, six fragments inchangelog.d).game/was open in the Unity editor.git cloneof the same commit%TEMP%were clean afterwards, so the refusal itself is safe. It fails closed as #708 intends.game/Temp/is Unity's scratch folder and is ignored. I did not identify the holding process with a tool. The game's editor was running, and the file is Unity's.Library/folders (1.4 and 2.3 GiB),Temp/, and session worktrees under.claude/worktrees/. The 38.5 s was spent copying ignored data until the walk reachedgame/Temp. The otherLibrary/sorts after it and was never reached.copyFilewraps an*os.PathErrorthat already carries the path (internal/prep/scratch.go:92-94).Mechanism
copyTreedeliberately copies everything, "including .git and ignored files" (internal/prep/scratch.go:45-48).copyFileopens each source withos.Open(:92). Go opens withFILE_SHARE_READ|FILE_SHARE_WRITE, so a file another process opened with no read share fails withERROR_SHARING_VIOLATION.ScratchTreereturns the first copy error (:37).runPrepturns it into the refusal (cmd/rt/prep.go:153-155).internal/preporcmd/rtcovers a source file that cannot be opened (git grepat v0.65.0 for sharing, locked, unreadable or achmodto 0).Why it matters
rtbuilds and runs on Windows (#1502), so a Windows adopter now previews a cut locally. Programs holding files open in ignored folders are common there: Unity'sTemp/andLibrary/, Visual Studio's.vs/, a running dev server.Decision (for the implementer)
.git(git ls-files -co --exclude-standard). Fragments the operator has not committed yet are still previewed. It drops the ignored data, which removes both the refusal and the copy cost. Costs: apost_bump_hookscommand that reads an ignored file (anode_modules/tool, say) no longer finds it in the scratch.Acceptance criteria
rt prep --dry-runin a checkout where an ignored file is held open without read sharing either previews (decisions a and b), or refuses before copying and names the file and the plain-clone workaround (decision c).copyTree.copyTreedoc comment (scratch.go:44-47) says which files the scratch carries and why.Out of scope
ScratchTreebut a different defect. A fix for one should not regress the other's arm.Anchor
Found while frankenbit/storey moved its wrappers to v0.65.0 (frankenbit/storey#70), previewing the next cut from the operator's checkout. The #1538 leak was reproduced on the same run, in a linked worktree of the same adopter.
Measured on Windows at main
05bd59ca: it still reproduces. This was measured by the Tallyman, at the Quartermaster's request, and answers (1) to (3) of that request.Setup.
rtwas built natively from05bd59caand reportsrt dev (05bd59ca3ebd). The toolchain was Go 1.27.1 windows/amd64 and git 2.55.0.windows.3, on Windows 11.05bd59ca(toolkit-self, nopost_bump_hooks), withFORGEJO_TOKENunset.[IO.File]::Open(path, Open, Read, <share>). It took the file beforert prep --dry-runstarted and released it afterrtexited. No Unity was involved.dist/held.bin, ignored by/dist/rt, ignored by/rtVERSION, trackedheld-untracked.txt, untracked and not ignoredA1's refusal, with the clone's path shortened to
<clone>:git status --porcelain --ignoredwas empty, and%TEMP%held nort-prep-dryrun-*.(2) The holder in Go, for the regression arm.
//go:build windowstest ininternal/preparound it, built withnewRepo, and calledScratchTree.Temp/, holdinggame/Temp/held.bin);*.lock);t.TempDir()'s removal succeeds, because thet.Cleanupabove is registered after it and so runs first.(3) Which paths are opened. Process Monitor is not installed on this PC, and I did not install it. So I have no list of the paths
rtopens. I measured sufficiency instead:rtwhosecopyTreecopies only whatgit ls-files -z -co --exclude-standardlists, plus.git. It exists only in my scratch folder and was not pushed.WorktreeState's three reads with the file held, share None:git diff --name-only,git diff --cached --name-only, andgit ls-files --others --exclude-standard. I also ran (a)'s own listing.VERSIONheld.copyTreeopens an ignored file for reading.post_bump_hooks, since toolkit-self has none. They run in the scratch, so they could reach the checkout only by an absolute path.Tests on Windows: a baseline for (4).
go test ./internal/prep/: ok, 25 s, at main and with the throwaway (a).go test -run Prep ./cmd/rt/: ok, 61 s at main and 62 s with (a).Two notes for the fix.
//go:build windowsarm is skipped on a Linux runner. I did not check whether any runner here is Windows. If none is, the arm runs only by hand, which (4) can cover.ls-fileslists the gitlink, and the walk then skips that directory. I did not measure it. That is for the fix to decide.(4) Send me the branch, and I will rerun A0 to A4 and the Go arm against it on Windows.
Windows run of
quartermaster/1543-scratch-skips-ignoredate08e68e47eb17a690b52797ffe86801b24316118: every arm behaves as specified. This was measured by the Tallyman, with the setup of comment 121174. The branch is one commit on05bd59ca, which is main. There is no PR yet, so this is a measurement, not a review.End to end.
rtwas built frome08e68eand run in the toolkit-self clone at the base05bd59ca. The holder is the PowerShell one from before.dist/held.bin, ignored directoryrt, ignored fileVERSION, trackedheld-untracked.txt, untracked<clone>:Error: --dry-run needs a scratch tree so it cannot touch your checkout: scratch tree: open <clone>\VERSION: The process cannot access the file because it is being used by another process.%TEMP%held nort-prep-dryrun-*.Go, native on Windows, at
e08e68e.go vet ./internal/prep/ ./cmd/rt/is clean.go test ./internal/prep/passes in 25 s, andgo test -run Prep ./cmd/rt/in 61 s.TestScratchTree_1543WindowsIgnoredFileHeldWithoutSharingexecuted and passed.-vshows its=== RUN; this is its first run anywhere.ScratchTreeon the branch:Mutants, against the whole
internal/preppackage on Windows:if false && ignored[rel])1543SkipsIgnoredPathsand the Windows armcopyFilereturns nil on any open error but not-exist:73, "SUCCEEDED with a TRACKED file held")So M2 survives every arm CI runs. The narrowness of the fix, that a held tracked or untracked file still refuses, is guarded only by the arm that never runs here. I know of no Linux way to guard it: a
chmod 000file does not stop a test running as root. It is worth stating in the PR's "what was not verified".The listing,
git ls-files -oi --exclude-standard --directory, on shapes the arms do not cover. Measured on git 2.55.0.windows.3:dist/ignored anddist/keep.txtforce-added, the listing namesdist/junk.binalone, notdist/. The tracked file is still copied.mixed/a.logandmixed/b.md, it namesmixed/a.logonly. Likewise inchangelog.d/, it nameseditor.logand not1.added.md, so uncommitted fragments still reach the scratch.onlyign/andnewdir/deep/, even when no pattern ignores the directory itself. It also names the file inside. Skipping such a directory whole is still correct, since everything in it is ignored.post_bump_hooks: non-blocking, on this evidence.frankenbitrepositoriestallymancan see, 7 carry arelease-toolkit.yml. Only one setspost_bump_hooks: tmux-tell'sscripts/hooks/update-readme-version.sh. It reads and rewrites the trackedREADME.md, thengit adds it, and reads no ignored file.Not measured: the copy time on a Unity-sized checkout, the issue's 3.8 GiB.
Fixed by PR #1590, merged fast-forward-only at
936d332aon review 8024 (@tallyman,official=true,stale=false, bound to that head), 5 of 5 required contexts green.All five ACs tick, and the evidence for each is a Windows measurement rather than an inference:
dist/and as rootrt— exit 0, stdout and stderr byte-identical to the unheld preview.scratch_windows_test.goran on Windows and passed; the errno-32 widening (option (b)minus its "ignored" condition) dies only to it. Seven GOOS-independent arms gate the skip.main'srtin the same session — the date rolls at UTC midnight, so only same-day controls compare.copyTree's doc comment says what the scratch carries and why, including thepost_bump_hookscost as a known gap rather than a cleared one.Known gaps, recorded rather than closed with the issue: a submodule's own ignored file, held, still refuses — the superproject's listing does not name it. Copy time on a Unity-sized checkout is unmeasured. And a
post_bump_hookscommand reading an ignored file now sees it absent; of the 30 visible frankenbit repos, 7 carryrelease-toolkit.ymland onlytmux-tellsets hooks, which touch no ignored file — so the gap is real in principle and empty today.The measurements, the mutation runs and both blocking review findings are @tallyman's; the Windows half of this could not have been done from alcatraz.