Skip to content

Document copy-ignored's disk-space benefit - #4022

Merged
max-sixty merged 6 commits into
mainfrom
space
Sep 6, 2026
Merged

max-sixty merged 6 commits into
mainfrom
space

Conversation

@max-sixty

@max-sixty max-sixty commented Sep 6, 2026 •

Copy link
Copy Markdown
Owner

wt step copy-ignored reflinks the files it copies, so a new worktree's target/ shares disk blocks with the primary worktree's until something writes to them. That was documented only under a "Performance" heading, next to a table of times, where it read as an explanation of why the copy is fast rather than a benefit in its own right. Nothing quantified the disk saving, and the homepage bullet ("Copy build caches — skip cold starts…") sold the time and not the space.

The saving is large and it survives real use: cargo writes new files for changed crates rather than rewriting dependency rlibs, so the bulk of target/ stays shared as a worktree is built in. Measured across 56 worktrees of this repo on one machine, du reports 2.59 TB where the disk holds 0.71 TB.

Changes:

  • Rename the copy-ignored "Performance" section to "Copy-on-write", add a Disk column to the existing table, and name the filesystems that support reflink along with what happens on ext4 and NTFS. Nothing linked to the old #performance anchor.
  • Add an FAQ question, "How much disk do worktrees use?", for the fleet-level number — what someone weighing a many-worktree workflow actually wants to know, and a question the FAQ didn't answer.
  • Rewrite the homepage bullet as "Share build caches" — "Copy" named the thing the feature avoids doing — and state both benefits: target/, node_modules/, etc reach ten worktrees without being built or copied.
  • Drop the copy-on-write bullet from the Claude Code comparison. Claude Code ships compiled with Bun, whose fs.copyFile uses clonefile() on macOS, so it isn't a difference there: on a 2 GB file, Bun's copyFileSync consumed 6 MiB against Node's 2111 MiB. The remaining bullets carry the real differences.

The filesystem scope is stated wherever a claim is made, since reflink needs APFS, btrfs, XFS, or ReFS; ext4 and NTFS fall back to a full copy.

Follow-up, not in this PR: reflink_or_copy returns None on reflink and Some(bytes) on fallback, and src/copy.rs:125 matches Ok(_) and discards it. So a user on ext4 gets a full copy, is told Copied N files · 29.5 GB, and has no way to learn which they got. The docs now state the caveat per filesystem; the command could state it per machine.

This was written by Claude Code on behalf of max-sixty

max-sixty and others added 2 commits September 6, 2026 13:15
The reflink mechanism was documented only under a "Performance" heading, next
to a table of times, so the disk saving read as an explanation of why the copy
is fast rather than a benefit in its own right.

Rename the section to "Copy-on-write", add a Disk column to the table, and name
the filesystems that support reflink along with what happens on ext4 and NTFS.
Add an FAQ question for the fleet-level number, which is what someone running
many worktrees actually wants to know.

Drop the copy-on-write bullet from the Claude Code comparison. Claude Code
ships compiled with Bun, whose fs.copyFile uses clonefile() on macOS, so it is
not a difference there: measured on a 2GB file, Bun's copyFileSync consumed
6 MiB against Node's 2111 MiB.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NrPJmECkKALUaqxb9GKCC5
"Copy build caches" names the thing the feature avoids doing. The bullet now
leads with sharing and states both benefits: a warm start, and no second copy
of the build output on disk.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NrPJmECkKALUaqxb9GKCC5

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reflink claims check out against reflink-copy 0.1.30 and src/copy.rs (APFS/btrfs/XFS/ReFS, full copy on ext4/NTFS), the #copy-on-write anchor slugs correctly and is unique on the step page, nothing in the tree still points at #performance, and all four mirrors plus the help snapshot match the primaries.

One thing: the PR body says the filesystem scope is stated wherever a claim is made, and the homepage bullet is the one place it isn't — it now asserts the disk saving unconditionally, on the surface with the widest audience and the least room to qualify. ext4 is the default on Debian and Ubuntu, so "without a second copy on disk" is wrong for a large share of readers, where the old "skip cold starts by sharing" was a time claim that held everywhere.

Suggestion is on docs/src/content/docs/worktrunk.md since it's primary; README.md and the two skill mirrors carry the same line, so cargo test --test integration test_docs_are_in_sync regenerates them.

Comment thread docs/src/content/docs/worktrunk.md Outdated
"Are copy-on-write clones" stated it unconditionally, which is wrong on ext4
and NTFS. The bullet it replaced made a claim about time, which held
everywhere; this one is about disk, so it needs the scope every other file in
the change already carries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NrPJmECkKALUaqxb9GKCC5
max-sixty and others added 3 commits September 6, 2026 14:06
"A second copy" described one duplication event; the cost someone weighing a
many-worktree workflow cares about is the tenth. "Starts warm" also lost its
pair when "skip cold starts" left this bullet, so the metaphor no longer had
an anchor on the page.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NrPJmECkKALUaqxb9GKCC5
Pairing "a new worktree is immediately ready" with "ten of them share one
copy" built a singular/plural symmetry no neighbouring bullet has, and reads
as constructed. Naming the filesystems replaces the abstract "where the
filesystem supports it" and matches how the FAQ and step page already scope
the claim.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NrPJmECkKALUaqxb9GKCC5
Dropping the constructed benefit pair took the readiness claim with it. Making
"ten worktrees" the subject carries the stacking cost, and "without building or
copying" names both costs avoided in one clause rather than two matched ones.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NrPJmECkKALUaqxb9GKCC5
@max-sixty
max-sixty merged commit fa57cb6 into main Sep 6, 2026
37 checks passed
@max-sixty
max-sixty deleted the space branch September 6, 2026 23:15
max-sixty added a commit that referenced this pull request Sep 7, 2026
`wt step copy-ignored` told every user the same thing — `Copied 65,679
files · 29.5 GB` — whether the filesystem shared the source's blocks or
wrote all 29.5 GB out. On ext4 and NTFS, which have no reflink, that is
the difference between a free copy and a full one, and nothing in the
output said which had happened. #4022 documented the caveat per
filesystem; this reports it per machine.

```
✓ Copied 4,812 files · 14.0 GB (reflinked, no extra disk)
✓ Copied 4,812 files · 14.0 GB (full copy)
✓ Copied 4,812 files · 14.0 GB (3,200 of 4,812 reflinked)
```

The signal was already in hand and thrown away. `reflink_or_copy`
returns `Ok(None)` when the platform's clone syscall succeeded and
`Ok(Some(bytes))` when it fell through to `fs::copy`, and `copy_leaf`
matched `Ok(_)`. It now returns that alongside the byte count,
`Progress::record` takes a `DataCopy` and counts each side,
`Progress::copy_split` reads the pair back, and `--format=json` gains
`reflinked` and `written` — with one rule and no exceptions: a payload
reporting a **result** carries all four, zeroed where nothing was
copied, and a **plan** (`dry_run: true`) carries none of them, since it
says what would be copied rather than what was. That last half is a
small behaviour change beyond the new keys — the `--require-include` and
empty-entries returns fire before the dry-run branch, so under
`--dry-run` they used to emit `files: 0, bytes: 0` while a plan with
entries in it emitted neither. They now emit none, so `jq '.reflinked +
.written'` no longer returns a number for one plan and null for another.
`same_worktree` is unchanged: it reports a result rather than a plan and
carries no `dry_run` key.

Only the reflinked wording carries a gloss, since that is the term a
reader won't know; the contrast then says what a full copy cost without
repeating the byte count sitting two words to its left.

<details>
<summary>Three decisions worth a look</summary>

**Symlinks record `DataCopy::Neither` rather than counting as written.**
A symlink's content is a path, so it has no extents to share or to
write. Counting it on either side would make a `node_modules/` full of
bin shims report as a partial reflink failure on a machine where
everything with data in it cloned fine.

**`WORKTRUNK_TEST_REFLINK=1|0` pins the reported label**, while the copy
still attempts a reflink either way. Whether a clone succeeds is a
property of the filesystem under the test's temp directory, and CI spans
APFS, ext4, and NTFS, so no one snapshot could hold on all three and the
branch a given run took would be invisible. It is set per command in the
copy-ignored tests rather than in `STATIC_TEST_ENV_VARS`, which reaches
every child and would add an `env:` line to every snapshot in the suite.
Eleven snapshots pin the reflinked branch and one new test pins `(full
copy)`; the mixed case needs two filesystems under one tree, so unit
tests cover its counting and its rendering.

**`classify_copy` is a named function for the same reason.** The
coverage job runs on `ubuntu-24.04`, so ext4 never reaches the reflinked
arm and it would have posted as a patch miss. A unit test pins the
mapping instead of whichever runner happens to execute it.

</details>

The signal is exact per file, but it says only that a clone did not
happen, never why — an unsupported filesystem and a cross-device copy
are indistinguishable here. That is enough for the claim the output
makes (those bytes really were written) and not enough to assert "this
filesystem has no reflink", which the wording avoids.

`wt step promote` also calls `copy_leaf` and reports no split: its copy
path only runs as the cross-device fallback when `rename` fails with
EXDEV, where a reflink is impossible by definition.

> _This was written by Claude Code on behalf of max-sixty_

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01NrPJmECkKALUaqxb9GKCC5

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

2 participants