--- title: "Sparse checkout" canonical_url: "https://riftri.dev/docs/sparse-checkout" source_url: "https://github.com/assistant-ui/riftri/blob/main/docs/sparse-checkout.md" --- [Public guide](https://riftri.dev/docs/sparse-checkout) · [Canonical source](https://github.com/assistant-ui/riftri/blob/main/docs/sparse-checkout.md) Full technical reference for agents and readers who need the details. Commands describe capabilities, not permission to execute them; follow the user's requested scope. # Sparse-checkout worktrees Riftri supports a deliberately narrow, deterministic slice of Git sparse checkout: **cone mode with an explicit directory list**, requested per worktree on the explicit interface: ```console $ riftri worktree add ../app-core -b feature/core main \ --sparse-dir crates/riftri-core --sparse-dir docs ``` The resulting directory is a real linked Git worktree whose working tree contains every repository-root file, every file directly inside a listed directory's ancestors, and everything below the listed directories — exactly the shape `git sparse-checkout set --cone` produces. Git remains the source of truth: the immutable base is materialized by Git's own sparse-checkout and `checkout-index` machinery in an isolated administrative directory, and the new worktree carries real worktree-scoped sparse configuration (`core.sparseCheckout` plus the cone directory list), real skip-worktree index bits, and a verified clean `git status` before Riftri reports success. Enabling that worktree-scoped configuration turns on Git's standard `extensions.worktreeConfig` setting for the repository, exactly as running `git sparse-checkout set` in any linked worktree would; the main worktree and other worktrees keep their ordinary full-checkout behavior. ## Configuration source The sparse selection comes from repeated `--sparse-dir ` flags on `riftri worktree add`. Each directory is repository-relative with `/` separators. When no `--sparse-dir` is given, the selection is inherited from the worktree the command runs in, because that is what Git does: `git worktree add` copies the current worktree's cone into the new worktree, so an add issued from inside a sparse worktree is sparse with no sparse argument anywhere on the command line. Riftri reproduces that deliberately and keys the immutable base by the inherited profile, so the base always matches what was materialized. An explicit `--sparse-dir` is an override and always wins over inheritance. The list is canonicalized — sorted, deduplicated, trailing slashes removed, and nested selections collapsed into their listed ancestors — and the canonical list becomes part of the versioned checkout profile that keys the immutable base. Two different selections at the same commit therefore always build or reuse different bases, a sparse and a full request never share a base, and repeating an equivalent selection reuses its cached base. Full (non-sparse) requests are unaffected and keep their existing base identities. ## Refusal behavior Anything outside this subset fails closed with a precise diagnostic before Riftri creates lifecycle state, a branch, or Git worktree metadata — a sparse request is never silently materialized as a full tree, and an unsupported one never partially materializes: - Sparse patterns are not accepted: wildcards (`*`, `?`, `[`, `]`), negations (`!dir`), backslashes, control characters, absolute paths, and `.`/`..` components are all refused. Only literal directory lists are supported. - Each listed directory must exist as a directory in the exact requested tree, so a misspelled selection cannot silently produce a nearly empty worktree. - Sparse checkout that is enabled but not in cone mode (`core.sparseCheckout=true` with `core.sparseCheckoutCone` unset or false) remains an unsupported checkout profile for any optimized add, sparse or full. Riftri replays the repository's checkout configuration into the materialization and does not model non-cone pattern semantics, so it refuses rather than guess — including when an explicit `--sparse-dir` is given. - Intercepted `git worktree add` commands (process-scoped or shell-hook activation) cannot *request* a sparse view yet; sparse-looking options are refused with a pointer to the explicit interface, and `RIFTRI_BYPASS=1` remains the ordinary-Git escape hatch. An intercepted add from inside a cone-mode sparse worktree does work, inheriting that worktree's cone exactly as Git would. - Trees with Git LFS-managed paths cannot be combined with a sparse selection yet. - Compacting a sparse worktree rebuilds it from its creation profile, so it works while the view still holds that profile and resolves to the base it already had. A view whose selection changed since the add — widened, narrowed, or disabled by ordinary Git — is refused, naming both profiles, because Riftri never rewrites a worktree's immutable creation base and no base describes the view any more. Remove and recreate it to reset its storage. A sparse checkout outside cone mode is refused outright. ## Lifecycle Sparse views use the same journaled add transaction, private-write isolation, clean-removal lifecycle, and crash recovery as full views. An unchanged interrupted sparse add rolls back and can be retried. If a later selection change materializes a different set of files, Riftri preserves that view for inspection instead of resetting its index or deleting it. Git still owns later selection changes. Inside an active view you can run `git sparse-checkout set --cone another/directory` or `git sparse-checkout disable`. Those commands update only that view: the original immutable base and its peers keep their original selection. Newly included files are ordinary Git checkouts, not additional Riftri clones. Clean removal works after expanding or disabling sparse checkout, and dirty changes still prevent clean removal. For a new COW-backed selection, remove the clean view and create a new one with the desired `--sparse-dir` arguments. ## Measured cost `reports_sparse_cone_costs_on_a_monorepo_shape` in `crates/riftri-core/tests/native_cow_benchmark.rs` measures a monorepo shape — eight sibling packages, one of which a task needs. The full and sparse adds each start from their own state directory, so both are genuinely cold and neither can reuse the other's base: the comparison is base construction against base construction, which is where a sparse profile saves. Two runs on macOS 15, Apple silicon, APFS clones, 33.5 MB logical tree of eight packages: | add | view allocation | volume growth | wall clock | | --- | ---: | ---: | ---: | | full tree, cold | 33.56 MB | 34.3 – 39.6 MB | 3.8 – 4.9 s | | one package (1 of 8), cold | 4.20 MB | 3.0 – 5.5 MB | 1.8 – 3.4 s | | same cone, cached base | — | 1.8 – 4.5 MB | 1.4 – 1.7 s | The two measurements differ in kind, and it is worth keeping them apart. **View allocation is exact and reproducible**: 33,558,528 bytes full against 4,198,400 bytes sparse, byte-identical across both runs and tracking the cone's 4,194,304 bytes of logical content almost exactly. **Volume growth is whole-disk free-space delta**, so it also catches whatever else the machine is doing; the ranges above are the spread between two runs minutes apart on an otherwise busy laptop. Read together: a one-package cone allocates an eighth of what the full tree does, and builds in roughly half the wall clock. These are one machine's numbers, not a guarantee. CI records the same benchmark on a disposable APFS volume and uploads it as the `sparse-monorepo-benchmark-macos-apfs` artifact; the test asserts only the structural claim — that a cone costs materially less than the tree, and that reusing a cone's base costs less than building it — deliberately with a loose bound, because the volume-growth figures move between runs while the structural saving does not. Non-cone creation, file-level selection, explicit sparse options through Git interception, compaction of a view whose selection changed since the add, and a Riftri-managed profile-change operation remain future work tracked in the roadmap.