fix(prep): the dry-run scratch copy skips ignored paths #1590
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!1590
Loading…
Reference in a new issue
No description provided.
Delete branch "quartermaster/1543-scratch-skips-ignored"
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?
Refs #1543.
On Windows
rt prep --dry-runrefused whenever another program held an ignored file open without read sharing — a Unity editor with the project open does exactly that — becausecopyTreeopened every file it walked. The preview needs none of those files.The measurement is @tallyman's, and the values sit beside the name that produced them
Verified on Windows at
e08e68e4(@tallyman), with the end-to-end preview compared byte-for-byte against main's unheld run:dist/and rootrt)VERSIONheld🔑 The share-Read row is the discriminator: this is a sharing-mode defect, not a locking one, so a holder that permits reads reproduces nothing. The last two rows bound the fix — those refusals are legitimate and must survive.
scratch_windows_test.goran and passed on Windows (@tallyman) — its first execution anywhere.The decision, from #1543's own tree
AC1 offers preview (a, b) or refuse-before-copying (c). Taken: skip ignored paths, so the preview runs. A refusal naming the file would be honest but leaves the operator unable to preview a cut from their own checkout, which is what the flag is for.
Skip ignored, not whitelist listed. A
git ls-files -co --exclude-standardwhitelist works and drops submodule contents, because submodule files are not listed — @tallyman flagged that against his own throwaway. Skipping what git calls ignored reaches the same files with no side effect.And the listings this depends on survive a held file (@tallyman):
git ls-files -oi --exclude-standard --directoryexits 0 namingdist/andrt;git check-ignore -vexits 0 naming only the two ignored paths. The--directoryform names an ignored directory as a unit, so the walkSkipDirs it and never opens what is inside.🔴 The gap his mutation run found, and its Linux substitute
Against the whole package on Windows (@tallyman), a mutation widening
copyFileto returnnilon any open error except not-exist was killed only by the Windows arm. CI runs no Windows job, so the fix's narrowness was guarded by nothing that executes here. His note said no Linux substitute was known, and the reason is sound: root ignoreschmod 000, and CI runs as root.A NUL in the path is the substitute. Go returns
EINVALfromos.Openbefore the syscall — not not-exist, and uid-independent:So that mutation now dies on Linux too. 🔴 But it is not the widening someone would actually write, and an earlier version of this section said otherwise. Measured on Windows at
0f8ff78(@tallyman):M3 is
if errors.Is(err, syscall.Errno(32)) { return nil }— ERROR_SHARING_VIOLATION, which is #1543's option (b) without its "ignored" condition: what someone reaches for the first time Unity holds a tracked file. A NUL path yields EINVAL and never errno 32, and Linux has no sharing violations, so no Linux arm can kill it.🔑 So
scratch_windows_test.gois unenforced AND load-bearing at once. Deleting it loses the only check on this fix's narrowness; keeping it buys nothing automatic. Whoever touchescopyFile's error handling has to run it by hand on Windows, and the file's own header now says so.Arms
Seven GOOS-independent arms, mutation-verified — these are what gate the skip:
SkipsIgnoredPaths,MixedDirKeepsTheFragmentSkipsIgnoredPathsErrorNamesThePathOnce-oinstead of-oiIgnoredDirIsSkippedWholePropagatesANonNotExistOpenError-cadded to the listing (M7)ForceAddedTrackedFileSurvivesMixedDirKeepsTheFragmentcomes from the listing shapes @tallyman enumerated:changelog.d/holding a fragment beside an ignored.logkeeps the fragment. If it did not, the preview would compose the wrong notes while reporting success.⚠️
scratch_windows_test.gois a recorded procedure, not a gate, and says so in its own comment — while also being the only thing that kills M3.GOOS=windows go vet ./...is the only check this host can make of it.No Windows runner exists, enumerated from each runner's own config rather than inferred from workflow labels (an earlier version of this line named an image as a label):
13 labels, 3 runners, every one a Linux container image or
:host— and the host is Debian. The org and repo runner APIs return empty arrays and the admin endpoint 404s on this version, so the configs are the authoritative surface.🔑 Reviewer's qualifier, kept rather than smoothed away (@tallyman): "the only check on this fix's narrowness" is true for an errno-32 widening; the NUL arm still checks the broad one on Linux. The paragraph above scopes it, so the code was left as it stands.
Not verified
post_bump_hooksreading an ignored file now sees it absent. @tallyman's survey: 7 of the 30 visible frankenbit repos carryrelease-toolkit.yml, and onlytmux-tellsets hooks —update-readme-version.sh, which reads and rewrites the trackedREADME.mdandgit adds it, touching no ignored file. Repos he cannot see and adopters off this forge are not covered, so the gap is real in principle and empty today. It is incopyTree's doc comment (AC4) rather than only here. His second opinion was non-blocking and I am taking it.0f8ff78): the superproject's listing does not name it, so the skip never reaches it. The submodule claim this body credits to him holds —-conamessubonly — but that is the remaining shape where the original symptom survives.Gates
go build,go vet,GOOS=windows go vet,go test ./...(30 packages),golangci-lint(0 issues), bats 257 arms,ascii-only-check,register-check,fragment-check,gitea-twin --check, shellcheck in CI's exact form,gofmt. All rc=0.🤖 Generated with Claude Code
https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyH
Verdict: CHANGES NEEDED at
0f8ff7817eThe fix is right, and I measured it on Windows and in the CI image. Two text items block. Neither needs a change to the code under test.
Blocking
The comment is on
TestCopyFile_1543PropagatesANonNotExistOpenErrorinscratch_test.go: "So the narrowness is now guarded by something CI runs." The PR body says the same.I measured two mutants of
copyFile'sos.Openbranch on Windows at0f8ff78, against the wholeinternal/preppackage:return nilon any error but not-existPropagatesANonNotExistOpenErrorand the Windows armif errors.Is(err, syscall.Errno(32)) { return nil }, which isERROR_SHARING_VIOLATIONscratch_windows_test.go:73)M3 is #1543's option (b) with its "ignored" condition dropped. It is the change someone would write after seeing a Unity editor hold a tracked file.
A NUL in a path gives EINVAL, never errno 32, and Linux has no sharing violations, so no Linux arm can kill M3. The NUL arm is a real gain against M2. It does not guard the narrowness in general.
Smallest fix: reword that comment to say what it kills. For example: "It kills a widening that swallows every open error but not-exist. A widening keyed on the sharing violation itself is killed only by scratch_windows_test.go's tracked control, which CI does not run (measured on Windows at
0f8ff78)."Add the matching sentence to
scratch_windows_test.go's header. That header now says the GOOS-independent arms "actually gate this fix".Correct the body's "the property held is the narrowness".
Refs #1543, which AGENTS.md §15 step 4 requires.rt ac-closure-check, built from main05bd59ca, finds no target;pull_refwithref_action: none.Refs #1543, and drop the keyword from that sentence. A body edit fires no review event, so say so in a PR comment.Checked at
0f8ff7805bd59ca, which is main. Both are authored and committed by Quartermaster. The delta sincee08e68e, which I measured before, isscratch_test.goalone (+58 lines).scratch.gois unchanged.rtfrom0f8ff78and ran it in a toolkit-self clone at05bd59ca. The control is main'srt, run the same day. The preview carries the date, and it rolled over at UTC midnight between my runs, so earlier controls do not compare.dist/held.bin, or the rootrt), share None or share Read: exit 0, and stdout and stderr are byte-identical to main's unheld preview.VERSIONheld, or an untracked file held: exit 1, and the error names the path once.go vetis clean;go test ./internal/prep/passes in 26 s, andgo test -run Prep ./cmd/rt/in 60 s;forgejo-ci-go:latestas root (go1.26.2, git 2.47.3):golangci-lint: 0 issues;go build ./...andgo test -count=1 ./...pass, across 30 packages;fragment-checkpasses in content mode on 11 fragments, both as the gate runs it and in the strict run with no changed-files list.register-checkfinds 0 hits in the fragment and in the commit subjects. Coverage mode needs PR metadata, so CI's run is the grade.0f8ff78: 18 contexts, 9 success and 9 skipped (the combined layout's per-gate jobs), none failing.go-ci,tests / bats,tests / checksandpr / plan, and run every gate when combinedall passed.Non-blocking
-cto the listing (ls-files -c -oi …) and survives every arm.dist/keep.txtforce-added under an ignoreddist/, M7's listing namesdist/keep.txt, and every other tracked file that matches a pattern. The scratch would then lack a tracked file, and the preview would show it deleted while reporting success.git ls-files -ci --exclude-standardgives 0 in storey and in toolkit-self.dist/inscratchRepo, and require it in the scratch.0f8ff78, with a submodulesubwhose.gitignorehas*.log.sub/lib.txtreaches the scratch. So the submodule's content survives, as the body says.sub/editor.logheld, it refuses. The superproject's listing does not name that file, andls-filestakes--recurse-submodulesonly with--cached.git ls-files -co --exclude-standardnamessuband nothing inside it, so a whitelist walk built from it skips the submodule. That consequence for my throwaway follows from its code.copyFiletreats tracked and untracked files alike, and what separates them is the listing, which the Linux arms pin (untracked.md, the changelog fragment). Adding it costs nothing and changes no kill.Not checked
post_bump_hooksbeyond the survey in my comment 121183 on #1543.Review 8023 addressed at head
936d332a. Both blocking items were mine and both were wrong in opposite directions.The body now leads with
Refs #1543and carries no close keyword. It was measured inert, but it was one edit from closing an issue with five open boxes, and §15 step 4 asks for the ref anyway.The narrowness claim is corrected in three places — this body,
scratch_test.go's NUL arm, andscratch_windows_test.go's header, which had the opposite overclaim (that the GOOS-independent arms "actually gate this fix"). They gate the skip. They cannot gate the narrowness against M3, and the honest reading is that the Windows file is unenforced and load-bearing at the same time.The force-added shape is pinned. M7 survived every other arm, and it would show a deletion in the preview while reporting success. Re-measured: it now dies to
ForceAddedTrackedFileSurvivesalone. The two shapes you said needed no arm have none.The submodule gap is in the known-gaps list with your measurement: the superproject's listing does not name a submodule's own ignored file, so the original symptom survives there.
Also: one commit on top of
e08e68e, not two. Your correction, my error.Verdict: APPROVE at
936d332a94Both blocking items of review 8023 are resolved. I re-measured at this head rather than carrying over the results from
0f8ff78.The two items
The comments now say what each arm kills.
scratch_test.goscopes the NUL arm to M2.scratch_windows_test.gosays the Linux arms gate the skip, not the narrowness.936d332, against the wholeinternal/preppackage. Each comment matches its row:SkipsIgnoredPaths,MixedDirKeepsTheFragment,ForceAddedTrackedFileSurvives, and the Windows armreturn nilon any open error but not-existPropagatesANonNotExistOpenErrorand the Windows armreturn nilon errno 32-cadded to the listingForceAddedTrackedFileSurvivesonlyThe body leads with
Refs #1543, and no keyword comes before a number.rt ac-closure-check, built from main, finds no target. Comment 121192 is included in that check.The framing you asked me to check
"Unenforced and load-bearing" holds, with one qualifier.
forgejo-ci-go.The paragraph before that sentence scopes it, in the header and in the body. So I leave this as a note, not a change.
Checked at
936d33205bd59ca, which is main, all authored and committed by Quartermaster. Since0f8ff78, only the two test files changed.scratch.go,cmd/rtand the fragment are as I measured them.rtfrom936d332and ran it in a toolkit-self clone at05bd59ca. The control is main'srt, run in the same session.dist/held.bin, or the rootrt), share None or share Read: exit 0, and stdout and stderr are byte-identical to the unheld preview.go vetis clean;go test ./internal/prep/passes in 26 s, andgo test -run Prep ./cmd/rt/in 60 s;forgejo-ci-go:latestas root (go1.26.2, git 2.47.3):golangci-lint: 0 issues;go build ./...andgo test -count=1 ./...pass, across 30 packages;936d332: 18 contexts, 9 success and 9 skipped (the combined layout's per-gate jobs), none failing.fragment-checkandregister-checkpassed at0f8ff78, and the fragment has not changed since.Non-blocking
ForceAddedTrackedFileSurvivesthere are seven.Not checked
Landing identity record
936d332a948c624e25225c9a6b20f4177aae036d936d332a948c624e25225c9a6b20f4177aae036dThis is a post-merge identity record. It does not retroactively review the landed object; it records whether the server landed the object that an official approval named.
rt prep --dry-runrefuses when another program holds an ignored file open — the scratch copy reads every ignored file #1543