Worktrees that start warm
git worktree gives you a clean checkout and a cold cache. lane clones the ignored files by reference, so a new branch builds in seconds.
I run a lot of branches in parallel now. Some are mine, most are agents'. Each one wants its own checkout, so git worktree add became the most-run command on my machine.
It has one problem. A new worktree has none of the files Git ignores: no target/, no node_modules/, no virtualenv, no .env. Every new branch starts with a full install and a cold build, even though a warm copy of all of it is sitting one directory over.
#Copy without copying
Modern filesystems can clone a file by reference. The new file shares the old file's blocks until one of them is written to. Only then does the filesystem allocate new storage, and only for the blocks that changed.
On macOS this is clonefile(2) on APFS:
use std::ffi::CString;
use std::os::raw::{c_char, c_int};
extern "C" {
fn clonefile(src: *const c_char, dst: *const c_char, flags: u32) -> c_int;
}
pub fn reflink(src: &Path, dst: &Path) -> io::Result<()> {
let src = CString::new(src.as_os_str().as_bytes())?;
let dst = CString::new(dst.as_os_str().as_bytes())?;
match unsafe { clonefile(src.as_ptr(), dst.as_ptr(), 0) } {
0 => Ok(()),
_ => Err(io::Error::last_os_error()),
}
}
On Linux it is the FICLONE ioctl, which works on btrfs and on XFS with reflink enabled:
pub fn reflink(src: &File, dst: &File) -> io::Result<()> {
match unsafe { libc::ioctl(dst.as_raw_fd(), libc::FICLONE, src.as_raw_fd()) } {
-1 => Err(io::Error::last_os_error()),
_ => Ok(()),
}
}
clonefile will clone a whole directory tree in one call. That matters: a node_modules with 40,000 files is one syscall instead of 40,000.
#lane new
lane new creates a branch and a worktree under .lane/trees/, then walks the ignored entries in your main checkout and clones each one into the new tree:
$ lane new fix-login
$ cargo build # finishes in the time it takes to link
If the filesystem cannot reflink, lane makes a normal worktree and skips the ignored files. It never falls back to a byte-for-byte copy of your caches. A slow, silent 6 GB copy is worse than a cold build you can see.
lane new --dirty also carries your tracked edits and untracked files, which is how I hand a half-finished idea to a second branch.
.lane/trees/ is excluded through .git/info/exclude, so worktrees are never committed and you do not need to touch .gitignore.
#Memory that survives the branch
Branches are short. What you learned on them is not. The second half of lane is notes anchored to code:
$ lane note add src/auth.rs -a 'fn verify' \
'must stay constant-time; early return leaks token length'
Notes are Markdown files under .lane/memory/, committed with the repo. Each one is anchored to a declaration, a Markdown heading, a component block, or a whole file, and stores a fingerprint of that span. Comments and whitespace are normalized out.
lane check compares the fingerprints against the current code:
| Result | Meaning |
|---|---|
fresh |
the anchored span is unchanged |
content-changed |
its implementation changed |
contract-changed |
its declaration changed |
anchor-missing |
the symbol no longer resolves |
unverifiable |
lane has no grammar for that anchor |
Each drifted note gets exactly one of three answers: confirm (still true), replace (needs new words), or retire (no longer applies). Retired notes move to .lane/attic/, not the trash.
New notes always get their own file. Two branches can annotate the same function without touching the same bytes, and if they disagree, Git shows you the conflict.
#Lane never calls a model
This is a deliberate line. lane stores what an agent, or a person, decided must stay true. It checks whether the code under that note has moved. It does not summarize, guess, or write notes for you. Everything it does is a file operation or a hash.
$ curl -fsSL https://lane.lukeed.com | sh
$ eval "$(lane shellenv)"
$ lane init
The source is on GitHub.