bug(prep): rt prep --dry-run inside a linked worktree stages the whole preview into that worktree's index — the scratch copy shares its .git #1538
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
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
frankenbit/release-toolkit#1538
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?
Run inside a linked worktree,
rt prep --dry-runrewrites that worktree's git index. The preview's changes end up staged there: the composed CHANGELOG, the VERSION bump, the baked markers and the consumed fragments' deletions. The files on disk stay byte-identical.rtnotices, prints its own🔴 BUG (release-toolkit#708)line, and exits 0.Measured (reproduced 2026-10-03)
Setup.
8086700, withcore.autocrlf=falseand its push URL pointed at a path that does not exist.rtbuilt from that commit.git worktree add --detach <wt> HEAD.rt prep --dry-runrun at the root of the clone, then at the root of the worktree, with no forge token in the environment.golang:1.25git status --porcelain=v1 -ualllines before → afterWhat the worktree shows afterwards (first lines, Windows):
git hash-objecton disk equalsHEAD:forCHANGELOG.md,VERSION, a fragment andreusable-release.yml. The worktree's.git/worktrees/wt/indexchanged size (71231 → 70663 bytes). HEAD stayed at8086700.git diff --cached --statshows "24 files changed, 51 insertions(+), 31 deletions(-)". The stagedCHANGELOG.mdgains## [v0.65.0] - 2026-10-03. That is what a plaingit commitin that worktree would commit.rtdetects it. stderr carries[DRY-RUN] 🔴 BUG (release-toolkit#708): the scratch redirection LEAKED - <wt> changed during a dry run: …(cmd/rt/prep.go:176). The exit code stays 0: the check discloses and does not refuse.git reset -qbrought the status back to 0 lines.Mechanism
ScratchTreecopies the checkout,.gitincluded, into%TMP%/$TMPDIR(internal/prep/scratch.go:30-42,copyTree:48-89). ThenrunPreppointsrepoRootat the copy (cmd/rt/prep.go:152-159).In a linked worktree,
.gitis a regular file, not a directory:gitdir: <main>/.git/worktrees/<name>.copyTreecopies that file byte for byte (:81). So the scratch's.gitstill points at the operator's admin directory.Probed. A directory holding only a copy of the worktree's
.gitfile resolvesgit rev-parse --git-dirto<main>/.git/worktrees/wtand--git-common-dirto<main>/.git, with--show-toplevelas the copy.So every git write the dry run makes in the scratch lands in the operator's worktree index:
ScratchDiff'sgit add -A(scratch.go:124), called on every successful dry run (prep.go:422). It stages the whole preview, which matches the 28 lines above.prep.go:397on a stable cut.post_bump_hooksstage (prep.go:362-363, which runs in dry-run).The
prepkg.Addcalls atprep.go:681-724are incommitAndPush, which a dry run skips.The doc comment at
scratch.go:23-27reads ".git is copied rather than shared". In a linked worktree it is shared. The comment atprep.go:164reads "By construction it cannot have changed". In a linked worktree it can.Why the tests are green. Only
rt prepcallsScratchTree(git grep 'ScratchTree('). Its arms,TestPrep_dryRunLeavesTheCheckoutUntouched(cmd/rt/prep_test.go:332) andTestPrep_dryRunFailurePathLeavesTheCheckoutUntouched(:552), have two blind spots:git init(setupPrepRepo, :28), never with a linked worktree;Why it matters
git committhere would commit a release prep: a version bump, a composed changelog and deleted fragments. The person running it was told the dry run writes nothing..claude/worktrees/(git worktree list, 2026-10-03). #1531's own body tells reviewers to "Run demos in plain clones until that is looked at".Decision (for the implementer)
.gitis a gitfile. Copy orgit clone --localthe common dir, carry over the worktree's own HEAD and index, and replace the copied gitfile with a real.gitdirectory. Right if previews in worktrees must keep working, which is the common agent case. Costs: more code, and a copy of the common dir.--dry-runin a linked worktree, naming the reason and the plain-clone workaround. Right as a stop-gap: it is cheap and honest. Costs: previews in worktrees are lost.Acceptance criteria
rt prep --dry-runleavesgit status --porcelain=v1 -ualland the worktree's index identical before and after. Alternatively, it refuses before any write, naming the reason, under decision (b).ScratchTreeit goes red (AGENTS.md section 8, Axis B).8086700).ScratchTreedoc comment (scratch.go:11-28) and theprep.go:160-165comment say how a gitfile.gitis handled.Out of scope
.gitis also a gitfile. The same mechanism applies, but I did not measure it.gitdir:(worktree.useRelativePaths), where the copied file would point nowhere. Not measured.post_bump_hooksthat write outside their working directory. The flag help already says the scratch does not contain those.Anchor
Reported in PR #1531's "Not in this PR" list as "leaked writes into that worktree in one review run", while reviewing PR #1531 / issue #1529. Reproduced 2026-10-03 on a scratch clone at
8086700, on Windows and in Linux, with a plain-clone control. Origin of the scratch tree: #708.rt prep --dry-runrefuses when another program holds an ignored file open — the scratch copy reads every ignored file #1543