git-lazy-mount is a Linux-only FUSE filesystem that lazily mounts a Git
repository without a full clone. One command —
git lazy-mount https://github.com/example/huge-repo ~/huge-repo
— partial-clones the repo, mounts a transparent virtual working tree, validates
it, and returns. Afterward stock Git, editors, and builds work directly, with
no wrapper, alias, environment activation, or git lazy-mount workflow verb.
Files materialize (hydrate) on read or edit.
This is the authoritative specification. It is written to match the shipped code;
where it summarizes a subsystem, the linked area document under docs/ owns
the implementation detail. macOS (FSKit) and Windows (ProjFS) backends are out of
scope; notes on them live under future-platforms/.
The design is deliberately clean: there is no custom stage, no custom
branch/commit state, no commit-adoption bridge, and no headless-first
architecture. The real .git/index is the only stage; stock Git owns refs, HEAD,
commits, merges, and conflict stages.
The executable is named git-lazy-mount, so Git exposes it as git lazy-mount.
1. Product contract
The primary command replaces the initial git clone for one working copy:
git lazy-mount https://github.com/example/huge-repo ~/huge-repo
It must not return successfully until:
- the partial Git repository exists;
- the virtual filesystem is mounted;
- the projected root is readable;
- the synthetic
.gitentry resolves correctly; - stock Git recognizes the repository;
- the FSMonitor integration is live;
- a basic Git health check succeeds.
After the command returns, the following must use the user's ordinary Git executable with no wrapper, shell alias, environment activation, or workflow verb:
cd ~/huge-repo
ls; cat README.md; $EDITOR src/main.rs
git status; git diff; git add -p; git commit -m "…"; git commit --amend
git log; git branch; git switch feature; git checkout other
git merge topic; git rebase main; git stash
git fetch; git pull; git push
cargo build; make; rg 'pattern'
Running an unrelated command such as git clone … from inside the mount must
behave like ordinary Git.
1.1 The git lazy-mount verb surface
git lazy-mount itself stays out of the daily workflow. The shipped subcommands
are limited to lifecycle and diagnostics:
git lazy-mount <url> <path> # mount (primary form)
git lazy-mount unmount <path> # release the kernel mount
git lazy-mount doctor <path> # report mountpoint / mounted / show-toplevel (--json)
There is also one hidden, internal subcommand, __serve, spawned by the mount
flow to hold the kernel mount after the parent command returns (see
§5.5). It is not a user-facing command.
Providing alternative workflow commands such as git lazy-mount add | commit | branch | switch | push | git -- would mean transparency had failed; they do not
exist.
2. Definition of "stock Git works"
The compatibility target is an unmodified, upstream Git executable discovered from
the user's PATH.
Allowed integration points:
a standard .git gitfile
normal Git configuration, hooks, refs, and reflogs
core.fsmonitor
the real Git index
normal partial-clone configuration
normal alternates
Not allowed as the product design:
shadowing or replacing the git executable
shell aliases for built-in Git commands
a required command wrapper
LD_PRELOAD interception
process-specific filesystem lies
a disposable Git repository per command
importing or "adopting" commits after Git exits
a second staging database
a second authoritative branch database
a second implementation of Git commit semantics
The filesystem exposes the same namespace and contents to Git, editors, builds, and ordinary applications. Results never depend on the caller's process name.
3. Two separate success dimensions
Every Git command is classified along two independent axes.
Compatibility: correct / partially correct / unsupported. Correct means
exit status, stdout/stderr, HEAD, refs, reflogs, index, pseudorefs, working-tree
contents, conflicts, hooks, and resulting commits match a normal checkout.
Laziness and performance: fully lazy / bounded hydration / potentially eager. A command is not "fully supported at scale" merely because it produces the
right result after fetching every changed blob. For example, unmodified Git may
materialize every path changed by a branch switch — measure that behavior rather
than hiding it. The per-command matrix lives in
compatibility.md.
4. Lessons as invariants
Each item below is enforced by the architecture and covered by tests. They are the core design invariants the implementation upholds.
Do not report a mount before mounting. The mount is reported ready only after
the kernel mount is live and Git health checks pass. Readiness is observed by
polling for the synthetic .git to appear, then running git rev-parse --show-toplevel / --is-inside-work-tree. Mount identity is a monotonic
MountGeneration counter (crates/core/src/ids.rs) used to detect stale kernel
and view entries; there is no named mount state machine and no mount registry.
Do not maintain two stages. The single stage is the real $GIT_DIR/index.
git add, git add -p, git reset, git restore --staged, merges, and conflict
stages operate on that index directly through stock Git.
Do not maintain two ref models. Git owns HEAD, refs/heads/*,
refs/remotes/*, refs/tags/*, ORIG_HEAD/FETCH_HEAD/MERGE_HEAD/…,
sequencer and rebase state, and reflogs. Plain git commit, git amend, git rebase, and git push update normal Git state directly.
Do not use skip-worktree as a universal trick. The correctness baseline works
without marking every index entry skip-worktree. The mount builds a full real
index from HEAD (git read-tree HEAD) and relies on FSMonitor, not on
sparse/skip-worktree semantics, to keep git status cheap. (The interop bridge in
crates/git-store/src/interop.rs does mark every entry skip-worktree, but that
is a throwaway operational index for running stock Git against the shared store —
not the mount hot path.)
Directory listing must not hydrate file contents. readdir returns names,
inode IDs, and d_type only — never exact sizes or blob reads. It merges baseline
tree and overlay children at O(direct children). readdirplus is not enabled.
Do not buffer entire files per callback. All content paths are
file-descriptor / streaming based. Reads are serviced by pread into a cache
file; writes by pwrite into an overlay FD. Memory use is not proportional to
blob size (reading a 64 MiB blob grows RSS by roughly one request-sized buffer,
far below the blob size; the test pins it under 24 MiB for a 64 MiB blob).
Real file handles; never fh = 0. Each successful open allocates a unique
handle (next_fh is an AtomicU64 starting at 1). Reads and writes are serviced
strictly by handle, so open-unlink and rename-while-open work without a path.
Bounded pools, not thread-per-callback. FUSE callbacks run on two fixed-size worker pools (16 object-I/O threads + 4 metadata threads), never one OS thread per request.
Empty directories are real workspace state. An untracked empty directory is a
durable overlay Dir entry, not just an inode-table row; it survives lookup,
readdir, unmount, and remount. Git omits empty directories from commits.
The change journal must be durable. A process-local Mutex<Vec<…>> is not a
sufficient FSMonitor backing store. Changes are appended to a durable on-disk log
that is replayed on open, and any discontinuity yields a full-invalidation
response.
Baseline + overlay model the working tree. The virtual working tree is a read-only baseline tree plus a writable overlay (see §5.2).
Linux only; no premature platform scaffolding. A crate that merely compiles on another OS is not platform support. The shipped system is Linux / FUSE only.
5. Architecture
5.1 Two sources of truth
| Owner | State |
|---|---|
| Git (the native admin gitdir) | HEAD, branches, refs, reflogs, remote-tracking refs, the real .git/index (the only stage), conflict stages 1/2/3, commit/amend, merge/rebase/cherry-pick/stash/bisect/sequencer state, tags, push/fetch config |
| The projection (custom state) | only the virtual working-tree bytes: baseline + overlay + tombstones + the synthetic .git; the FUSE projection; the durable change journal; the content cache; inode/handle tables |
The projection's parsed views of Git state are disposable caches, rebuilt from the
real gitdir. We never mirror Git state into a second authoritative model, never
import commits after Git exits, and never keep a second stage or branch database.
See git-state-model.md.
5.2 Working-tree model: baseline + overlay
working tree(path) =
1. synthetic entry (root .git gitfile) reserved, protected
2. overlay file / dir / symlink local writes
3. overlay tombstone deletions (incl. tombstoned ancestor)
4. overlay clean-rename base-ref rename/subtree mapping
5. baseline Git tree entry (HEAD tree) lazy, unmaterialized
6. absent
Initial state: baseline = checked-out commit tree, overlay = empty. The
baseline answers "what would this unmaterialized path contain in the logical
working tree" — not what is staged, what HEAD is, or what branch is checked out;
those come from Git. This is why a baseline is necessary: index-only operations
(git reset --mixed, git restore --staged, git rm --cached) change the index
without changing working-tree bytes, and the baseline/overlay split preserves the
working tree while the real index moves independently.
The baseline tree is fixed at projection open from the HEAD tree and is immutable for that projection's life; there is no baseline-advancement machinery. Branch-changing commands stay correct because stock Git writes every changed path through the FUSE write path into the overlay (see worktree-model.md and the eagerness note in §7).
5.3 Crates
The workspace is exactly nine crates (Cargo package names in parentheses):
| Crate | Package | Role |
|---|---|---|
core |
glm-core |
Backend-agnostic vocabulary: ObjectId (format-tagged raw bytes, never assumed SHA-1), GitMode, RepoPath (raw non-NUL bytes), TreeObject, MountGeneration, the per-path state axes Source/SemanticStatus/Residency/Durability, FetchPolicy, and Error/ErrorCode with stable string codes + Linux errno mapping. |
git-store |
glm-git-store |
Git-CLI object access: long-lived cat-file --batch-command sessions, object_size, smudge_blob, fd hardening (CLOEXEC, GIT_NO_LAZY_FETCH, GIT_OPTIONAL_LOCKS), byte-exact tree parsing, and the interop.rs operational-index bridge. |
git-repo |
glm-git-repo |
AdminRepo: the transparent clone, build_index (git read-tree HEAD), and the FSMonitor seed. |
worktree |
glm-worktree |
The Projection (baseline + overlay + content cache + single-flight), the Overlay (JSON sidecars), and the ChangeJournal. |
fs-common |
glm-fs-common |
InodeTable: stable inode identity with generations; ROOT_INO = 1 is the only pre-allocated inode. |
fuse |
glm-fuse |
TransparentFs: the fuser::Filesystem implementation, real handle table, and the two bounded worker pools. |
cli |
glm-cli |
The git-lazy-mount binary and the git-lazy-mount-fsmonitor hook binary. |
sgrep |
glm-sgrep |
A separate standalone remote-grep CLI that reads the overlay/journal directly so search fetches nothing. Not part of the mount. |
testkit |
glm-testkit |
Shared test helpers. |
There is no daemon, ipc, namespace, overlay, object-provider,
filtered-cache, fsmonitor, or platform crate.
5.4 On-disk layout
$XDG_DATA_HOME/git-lazy-mount/ (else ~/.local/share/git-lazy-mount/)
workspaces/<16-hex-hash-of-mountpoint>/
git/ real native partial-clone admin gitdir (NOT inside FUSE)
glm-fsmonitor/changes.log durable NUL-separated change journal
cache/ content-addressed cache files (materialized working-tree bytes)
overlay/
meta/ one atomic JSON sidecar per overlay entry (sha256(path).json)
content/ native files holding writable content bytes
anchor/ temporary clone anchor, discarded after init
The mounted worktree (~/huge-repo/) projects one synthetic read-only regular
file .git whose bytes are gitdir: /abs/.../workspaces/<id>/git. The admin
gitdir is configured with core.worktree = <mountpoint>, so stock Git resolves
the repo through the gitfile and operates on the mounted worktree using its normal
index, refs, locks, and hooks. The admin gitdir lives on a native filesystem,
never inside FUSE, so Git's index.lock, packed-refs, ref/config locks,
reflogs, sequencer/merge/rebase state, and atomic renames all work normally.
The synthetic .git is protected: any create/replace/delete/rename/chmod/write of
it, or any attempt to create a path beneath it, fails safely. Protection is
enforced in Projection::child_path (returns ErrorCode::Authentication), not by
a reserved inode. A repo tree entry literally named .git never shadows the
synthetic one.
Storage durability (atomic sidecars + fsync, content retention via Linux fd
survival) is owned by durability-security.md; the
overlay/baseline/tombstone model is owned by
worktree-model.md.
5.5 Command surface
The mount flow does not run a per-user daemon and uses no IPC socket. Instead
the parent command spawns a single detached, hidden __serve child that holds
the kernel mount:
cmd_mountdoes the clone/index/seed work, then spawnsgit-lazy-mount __serve --gitdir … --mountpoint … --cache … --overlay …with stdio nulled. The child is reparented to init and is not waited on; the parent polls for the synthetic.git, runs health checks, prints success, and exits.cmd_serve(the__servebody) opens theAdminRepo, opens theChangeJournal, builds theProjectionwith that journal, and callsglm_fuse::mount, which blocks until the mount is released.cmd_unmountrunsfusermount3 -u(falling back tofusermount -u, thenumount); the serving child'smount()returns and it exits.cmd_doctorreports mountpoint, mounted-or-not, andshow-toplevel(optionally as JSON).
The FSMonitor hook (git-lazy-mount-fsmonitor) and sgrep read the durable
journal file directly — there is no socket and no protocol version to negotiate.
5.6 Startup sequence
cmd_mount runs as a straight-line sequence:
- Preflight. Create the mountpoint if needed and require it to be empty; derive the deterministic per-mountpoint workspace paths.
- Clone.
AdminRepo::clonerunsgit clone --no-checkout --separate-git-dir=<gitdir>with the default--filter=tree:0, setscore.worktree=<mountpoint>, adds--no-single-branchwhen--depthis set, and runs withGIT_TERMINAL_PROMPT=0. A full-object clone (filter rejected with--allow-full-object-clone) still implies no checkout. - Build the index.
build_indexrunsgit read-tree HEAD, faulting the HEAD tree hierarchy and fetching zero blobs. This is O(tracked paths) of metadata work and is reported honestly, never marketed as O(1). - Configure FSMonitor. Set
core.fsmonitorto thegit-lazy-mount-fsmonitorbinary beside the executable, andcore.fsmonitorHookVersion=2. (Nothing else is configured — nountrackedCache, nohooksPath, noindex.version.) - Seed first status. Open the (empty) change journal, then
seed_fsmonitor_validso the firstgit statusfaults zero blobs (see §6.4). - Serve. Spawn the detached
__servechild. - Validate. Poll
mountpoint/.gitfor readiness (up to 1000 × 10 ms), then run the health checks and print success.
The startup ordering and its deadlock-avoidance constraints are owned by
deadlock-startup-recovery.md.
5.7 The tree:0 default and its rationale
The default partial-clone filter is tree:0: every commit is fetched (so
history, merge-base, git log, and branch switching all work), but no trees or
blobs are. build_index then faults only the HEAD tree hierarchy; blobs hydrate
on read. This is both correct and cheap.
Two alternatives are deliberately not the default:
--depth 1(shallow) grafts the commits, which breaksgit merge/git rebaseand hides history. Not a default.blob:nonewith full history would download every tree from all of history — slow and large on big repos. Not a default.
blob:none and --depth remain valid explicit overrides:
git lazy-mount <url> <path> --branch main
git lazy-mount <url> <path> --depth 1
git lazy-mount <url> <path> --filter blob:none
git lazy-mount <url> <path> --allow-full-object-clone
6. Subsystems
Each subsystem is summarized here at spec altitude; its area document owns the depth.
6.1 Worktree model
Area doc: worktree-model.md.
The Projection layers the durable overlay over the fixed baseline HEAD tree.
resolve() follows the order in §5.2.
readdir() merges baseline and overlay children at O(direct children) with no
blob fetch. Overlay entries are File / Symlink / Dir / Tombstone /
BaseRef{oid,mode}; a clean file or subtree rename is metadata-only (overlay
re-key plus baseline base-refs at the destination, source tombstoned) and fetches
no blob contents. RENAME_NOREPLACE is honored; RENAME_EXCHANGE is rejected.
Repository paths are raw RepoPath byte sequences (never lossily converted to
UTF-8), so paths with invalid UTF-8, newlines, leading dashes, quotes, and control
characters round-trip correctly.
6.2 FUSE semantics
Area doc: fuse-semantics.md.
TransparentFs implements these fuser operations: init (which negotiates
FUSE_ATOMIC_O_TRUNC), lookup, forget, getattr, setattr, readlink,
open, create, read, write, flush, fsync, release, mkdir, unlink,
rmdir, rename, symlink, opendir, readdir, releasedir, access, and
statfs. Not implemented (these fall through to the fuser default, ENOSYS):
link, mknod, all xattr ops, fallocate, copy_file_range, lseek, file
locking (getlk/setlk/flock), destroy, and batch_forget.
Handles are real (Handle::Read / Handle::Write, fh from 1, never 0); reads
and writes are serviced strictly by fh via pread/pwrite into an FD, with no
whole-file Vec<u8> buffering. First writable O_TRUNC seeds an empty overlay
file with no baseline fetch; a partial overwrite copies up once and then writes in
place. Open-unlink and rename-while-open keep working through the FD because
service does not depend on a path. Inode identity (InodeTable) is stable across
repeated lookups, with a per-inode generation; ROOT_INO = 1 is the only
pre-allocated inode. Two fixed bounded pools run callbacks: 16 object-I/O threads
and 4 metadata threads (so ls stays responsive while reads hydrate). There are
no separate decompress/filter/network pools and no backpressure/cancellation
machinery.
6.3 Object fetching
Area doc: object-fetching.md.
materialize_path streams a baseline blob through cat-file into a
content-addressed cache file and serves range reads from its FD via a
ContentHandle (pread, bounded RSS). Concurrent faults of the same object are
coalesced by a per-ObjectId single-flight map (inflight: Mutex<HashMap<ObjectId, Arc<Mutex<()>>>>), so a hundred concurrent reads of one
missing blob cause one retrieval. Smudge/working-tree content is produced by git cat-file --filters --path --attr-source. Fetch eligibility is gated by
FetchPolicy (CacheOnly / MustNotFetch / AllowNetwork / Prefetch); a
missing object under an offline policy maps to a bounded filesystem error
(OfflineMissingObject → EIO, NotFound → ENOENT).
getattr is the one metadata op that may fault: a Git tree entry carries no exact
working-tree size, so ls -l / stat of an unmaterialized file faults its blob
once. This is fundamental to lazy-blob fetching and is separate from git status,
which faults zero blobs (next section).
6.4 FSMonitor v2 + change journal
Area doc: fsmonitor.md.
Git's FSMonitor v2 hook receives (version, previous_token) and returns a new
token, a NUL, then the relative paths changed since that token. Responses are
inclusive: false positives are acceptable, false negatives are not. The token
wire form is glm1:workspace:epoch:seq:generation; epoch and generation are
fixed at 1 and 0.
The ChangeJournal (<gitdir>/glm-fsmonitor/changes.log) is a durable
NUL-separated append log replayed into memory on open. record() writes and
sync_data()s before the FUSE reply, so an acknowledged mutation is always
visible to the next query. query() returns a full invalidation (/) for any
token it cannot place: an empty token while seq > 0, an unparseable token, a
workspace/epoch/generation mismatch, or a seq beyond the current sequence.
The zero-blob first-status finding (canonical here). The first clean git status faults zero blobs, the same as every later one. A freshly read-tree'd
index carries no FSMonitor extension, so without intervention Git stats (and so
faults) every entry on the first status before writing the extension. The fix,
applied at mount right after read-tree, pre-seeds the FSMonitor index extension
(seed_fsmonitor_valid): every entry is marked CE_FSMONITOR_VALID carrying the
journal's seq-0 token, so Git's refresh_cache_ent early-returns before any
lstat and the hook answers "nothing changed" at the seq-0 token. Two carve-outs
keep it correct: (a) checkout conversion attributes
(filter/ident/working-tree-encoding/CRLF eol) are detected by reading the
tracked .gitattributes blobs directly; if any path declares such an attribute
the entire seed is skipped, so Git's first status checks every path normally and
never hides a diff — bounded by a 20-second attribute-read timeout; (b) the seeded token
must match the hook's identity, else Git falls back safely to the eager scan.
Verified zero-fault on an 81k-file real mount.
6.5 Git state model
Area doc: git-state-model.md.
The transparent design — git clone --separate-git-dir + core.worktree + a
synthetic .git gitfile served by the projection — gives stock Git an ordinary
repository whose working tree happens to be virtual. Git owns all repository state
(§5.1); the projection owns only working-tree bytes.
Index-only updates leave baseline and overlay untouched; working-tree updates
flow through FUSE into the overlay exactly as ordinary filesystem operations
would. We never infer a working-tree update from a changed index.
6.6 Index strategy
Area doc: index-strategy.md.
The mount uses a full real index built by git read-tree HEAD (faulting HEAD
trees, fetching zero blobs) — the maximum-compatibility correctness baseline.
git-store's interop.rs synthesizes a separate throwaway operational index
(every entry skip-worktree) only to let stock Git run against the shared store off
the mount hot path; it is exercised by the store integration tests, not the FUSE
path. Scalability of larger-repo index strategies (sparse / dynamic skip-worktree
/ a minimal provider extension) is discussed there; the shipped choice is the
full index.
6.7 Durability and security
Area doc: durability-security.md.
Overlay durability is per-entry: one atomic JSON sidecar per entry
(id_for(path) = sha256(path)+".json") under meta/, content bytes in native
files under content/, each published temp-file → fsync → rename with a
parent-directory fsync so an acknowledged create/rename survives a crash. There is
no SQLite, no namespace database, and no content-id nonce; the in-memory overlay
index is a disposable cache rebuilt from the sidecars on open. Authentication uses
the user's normal credential helper during the initial mount only; FUSE callbacks
are non-interactive (GIT_TERMINAL_PROMPT=0) and gate fetches through
FetchPolicy. The threat model treats repository data as untrusted (path
traversal, symlink races, decompression bombs, credential redaction); raw paths
are escaped safely for display and JSON.
6.8 Deadlock, startup, and recovery
Area doc: deadlock-startup-recovery.md.
Git processes run inside the mount and can trigger FUSE callbacks; callbacks need Git objects. The invariants that prevent deadlock:
FUSE callbacks never invoke Git porcelain or a worktree-scanning command
FUSE callbacks never wait on the index lock held by the requesting Git process
object readers target the native gitdir directly (long-lived cat-file --batch-command)
GIT_NO_LAZY_FETCH=1 on inspection subprocesses that must not recursively fetch
all mount/session file descriptors are CLOEXEC and not inherited by children
Bounded worker pools keep the fuser dispatch loop free to answer a FLUSH
during a fork/exec.
7. Required plain-Git compatibility surface
Do not claim transparent Git compatibility until these commands pass mounted
end-to-end tests through a real /dev/fuse mount without a wrapper. The full
per-command correctness-and-laziness matrix is in
compatibility.md.
discovery/inspection : rev-parse --show-toplevel, status [--porcelain=v2],
diff [--cached], log, show, ls-files, cat-file,
branch, tag, remote -v
staging/commit : add [path|-A|-u|-p], reset path, restore --staged,
commit [-a|--amend|--fixup|-S], rm [--cached], mv
branch/worktree : branch, switch [-c], checkout [-- path], restore,
reset [--soft|--mixed|--hard]
history : merge [--abort], rebase [--continue|--abort],
cherry-pick [--continue|--abort], revert, stash [pop]
remote : fetch [--prune], pull [--rebase], push
[--force-with-lease|--tags]
working-tree utils : clean [-n|-fd], grep, blame, bisect, mergetool, difftool
maintenance : fsck, gc, maintenance run, repack, prune
Plain git push is required; there is no bespoke push command and no second lease
model. Git's refs, remote-tracking refs, reflogs, and push safety are
authoritative.
Eagerness is measured, not hidden. Branch-changing commands
(switch/checkout/reset --hard/merge/rebase) are correct but potentially
eager: stock Git writes every changed path through the FUSE write path. Measured,
a branch switch over an M-of-N delta touches O(M) blobs (the delta), not O(N) (the
repo). A release may ship stock-Git-compatible while labeling branch transitions
"potentially eager"; it must not claim lazy branch switching until demonstrated.
8. Hydration budgets
These are automated assertions, not aspirations.
| Operation | Budget |
|---|---|
Mount (tree:0) |
fetch zero working-file blobs merely to project the tree; the full-index build does O(tracked paths) metadata work and reports it honestly |
ls <dir> |
zero child blobs, zero smudge filters, O(direct children) namespace work |
ls -l <dir> |
may fault each blob once for exact size (fundamental); reported distinctly |
Clean git status --porcelain=v2 |
zero blobs, zero smudge filters, no full per-file stat scan — for the first and all subsequent clean statuses (via the seed in §6.4) |
cat path |
at most the one required blob plus its attribute/filter metadata |
| 100 concurrent reads of one missing file | one underlying object retrieval (single-flight) |
open(O_WRONLY|O_TRUNC) |
does not fetch the old blob |
| Repeated 4 KiB writes to a 1 GiB file | no full-file read/rewrite per callback; no allocation proportional to file size |
| Clean rename of an unmaterialized file/subtree | zero blob contents |
git log / branch / tag / status |
do not hydrate working-tree blobs to inspect metadata |
9. Limitations
By-design and deferred behaviors are registered in
limitations.md. The load-bearing ones:
getattrsize hydration is fundamental to lazy blobs. The exact size of an unmaterialized file requires its blob, sols -l/statfaults it once. Not closeable without a server-side size manifest.- Smudge-side
.gitattributes/ LFS serve the raw baseline blob. A smudge-filtered file (eol=crlf,ident, an LFS pointer) reads as its stored bytes, not the smudged bytes. Commits stay byte-correct because the clean filter is the inverse, and Git's content comparison stays clean. - Branch transitions are potentially eager (§7).
- The change journal has no compaction and a fixed epoch/generation (1/0).
- LFS end-to-end and nested lazy submodules are deferred (some submodule tests
are
#[ignore]'d).
10. Project status
Linux-only, real-/dev/fuse-CI tested (CI runs on ubuntu-latest only). The
transparent mount drives the full stock-Git surface: stock Git, editors, and
builds operate directly on the virtual working tree with no wrapper.
Not supported yet: end-to-end LFS, full nested submodules, and a shared object
cache across workspaces. Other platforms (macOS/Windows) are out of scope; see
future-platforms/.
Build: cargo build --release -p glm-cli --features fuse produces
git-lazy-mount; the git-lazy-mount-fsmonitor hook is built alongside and must
sit next to it. Requires libfuse3 and system Git (≥ 2.36).
11. Implementation discipline
The priority order, highest first:
stock Git correctness
user-data durability
filesystem correctness
transparent UX
measured laziness
large-repository performance
shared-cache optimization
additional platforms
The system must never claim to be transparent while any of these hold: a registry
says "mounted" without a kernel mount; plain Git cannot discover the repository; a
gitdir is generated per command; commits must be imported after Git exits; the
custom stage differs from .git/index; status only works through a wrapper; push
only works through a bespoke command; ls hydrates every file in a directory;
read allocates the complete blob; each write rewrites the whole file; open handles
are path lookups in disguise; open-unlink fails; empty untracked directories
vanish; one FUSE callback spawns one OS thread; FSMonitor state disappears silently
on restart; or Git paths are converted lossily to UTF-8.