jj-vfs-poc

jj-vfs-poc architecture

A detailed, source-linked map of how jj-vfs-poc (the crate jjfsd) works inside: a read-only FUSE file system that shows a jj repository's commits and workspaces as directories, from the kernel's request down to jj-lib's store.

On this page
  1. The whole file system on one page
  2. The layers
  3. Kernel and fuser
  4. The FUSE adapter
  5. The virtual file layer
  6. The namespace
  7. jj-lib
  8. Module map
  9. Dependencies
  10. Status and limitations
  11. Pages

Upstream calls this a proof of concept, and promises no completeness, performance or API stability. These pages say plainly what it doesn't do yet. For jj's own internals (commits, trees, conflicts, the working copy), see the jj architecture reference. That reference describes its own pinned jj version, which can differ from the jj-lib 0.43.0 this crate builds against.

The whole file system on one page

Left: a request's path from a process down to the repository on disk. Right: what the mount contains. Every box links to the page that explains it (or to the pinned source while that page is being written). jj-lib boxes link to the jj reference or to jj-lib 0.43.0's source.

jj-vfs-poc architecture A process calls open or read on a path under the mountpoint. The kernel FUSE driver sends the request to the fuser library, which calls a method on JjFuse. JjFuse uses InodeMap to turn inode numbers into paths, then spawns the work onto the tokio runtime. Inside the runtime, PathMappedVfs implements VirtualFilesystem by asking the PathMapper, AllCommitsPathMapper, for a VirtualFile. The VirtualFile implementors are StaticDirectory, CommitsDirectory, WorkspacesDirectory and CommitTreeFile. They read from jj-lib's ReadonlyRepo, Store and MergedTree, which read the repository on disk. main.rs loads the repo once and starts the mount. Beside this, the mount's directory tree: commits with one directory per visible commit, and workspaces with one directory per workspace, each showing a commit's files. process: open / read / readdir kernel FUSE driver /dev/fuse fuser 0.18 Filesystem trait main.rs load repo, spawn_mount JjFuse 6 FUSE ops InodeMap inode ⇄ path tokio runtime: one task per request PathMappedVfs impl VirtualFilesystem AllCommitsPathMapper impl PathMapper: get_entry(path) Static Directory Commits Directory Workspaces Directory CommitTree File ReadonlyRepo jj-lib: view Store jj-lib: commits MergedTree jj-lib: trees repository on disk .jj/repo: store, op log, index Inside the mount / commits/ 000…000/ <commit id>/ dir/ file1.txt symlink workspaces/ default/ One directory per visible commit and per workspace, frozen at mount. Read-only. See the namespace page.

The layers

Kernel and fuser

main.rs takes a mountpoint and a workspace root, loads the repository at its current operation, and calls fuser::spawn_mount with the options RO and FSName("jjfs"). It then waits for SIGINT, SIGTERM or SIGHUP. The fuser crate talks to the kernel and calls methods on a Filesystem implementation. See starting and mounting.

The FUSE adapter

JjFuse implements six of those methods: init, lookup, getattr, read, readdir, readlink. Every one except init turns the inode into a path with InodeMap, spawns the work onto the tokio runtime, and replies from inside the task. See the FUSE adapter and inodes.

The virtual file layer

VirtualFilesystem is the path-based interface JjFuse calls: attributes, read, read a directory, and read a link. Its one implementation, PathMappedVfs, asks a PathMapper for a VirtualFile and calls one method on it. See the virtual file layer.

The namespace

AllCommitsPathMapper decides what exists: commits/<id>/… and workspaces/<name>/…, served by three small directory types and CommitTreeFile, which presents a commit's tree. See the mount's namespace and commits as directory trees.

jj-lib

Everything below that is jj-lib 0.43.0. The crate reads the repo's view (wc_commit_ids), evaluates the all() revset, loads commits from the Store, and reads trees, files and symlinks through MergedTree and the Store. It never writes, and never starts a transaction.

Module map

ModuleOwnsPage
fuseJjFuse, reply_async!, FileAttributes::to_fuse, errno mappingfuse
inode_mapInodeMapinodes
jj_errorJjErrorfuse
vfsVirtualFilesystem, PathMappedVfsvfs-layer
virtual_fileVirtualFile, FileType, FileAttributes, DirectoryEntry, DirectoryStreamvfs-layer
path_mapperPathMappervfs-layer
path_mapper_all_commitsAllCommitsPathMappernamespace
static_directoryStaticDirectorynamespace
commits_directoryCommitsDirectorynamespace
workspaces_directoryWorkspacesDirectorynamespace
commit_tree_fileCommitTreeFilecommit-trees
test_helpers (#[cfg(test)])setup_test_repo, user_settingstesting

These are the twelve pub mods in lib.rs. The binary, main.rs, uses the library as jjfsd::….

Dependencies

CrateVersionUsed for
jj-lib0.43.0Everything about the repository: workspace and repo loading, the view, revsets, the store, trees.
fuser0.18.0The FUSE protocol, the Filesystem trait, mounting.
tokio1.53.1The multi-threaded runtime FUSE requests run on, and signal handling. Features: rt-multi-thread, signal, macros, time.
futures0.3.33AsyncRead for file content, BoxStream for directory listings, and block_on in CommitsDirectory.
async-trait0.1.91The async methods on VirtualFilesystem, VirtualFile and PathMapper.
thiserror2.0.19JjError.
itertools0.15.0.unique() when merging a conflicted directory's listings.
ustr1.1.0Interned file names in InodeMap.
tracing, tracing-subscriber0.1.44, 0.3.23Instrumented spans on every operation, and an EnvFilter logger defaulting to INFO.
anyhow, libc1.0.104, 0.2.189Declared, but not referenced by any source file at this pin.
tempfile (dev)3Throwaway repositories and mountpoints in tests.
pollster (dev)0.4.0Declared. Only mentioned in a TODO comment in the integration test.

Source: Cargo.toml (edition 2024).

Status and limitations

The README says it plainly: this is a proof of concept, "beginning life as an intern project", with "no guarantee of completeness, performance, or API stability". At the pinned commit:

  • The mount is read-only, and implements only lookup, attributes, directory listing, reads and symlinks.
  • The repository is loaded once. The mount is a snapshot of the operation that was current at mount time.
  • Conflicted paths can't be read, and submodules show as unreadable files.
  • There are no timestamps, executable bits or real ownership: epoch times, 0644/0755, uid and gid 1000.
  • Performance shortcuts: sizes and reads at an offset read the file from the start, listings aren't paginated, and inodes are never freed.

Each is sourced on the namespace page's limitations table.

Pages

  • Starting and mounting

    What jjfsd does from main() to a live mount: arguments, tracing, mount options, the tokio runtime, loading the repo, and unmounting.

  • The FUSE adapter

    JjFuse: every FUSE operation it answers, how each maps onto the virtual file system, the async bridge, attributes and errno mapping.

  • Inodes

    How InodeMap hands out inode numbers, maps them to paths and back, and what it never forgets.

  • The virtual file layer

    The VirtualFilesystem, VirtualFile and PathMapper traits, PathMappedVfs, and how reads are served from async streams.

  • The mount's namespace

    What lives at each path of a mounted repository, how a path is resolved to a file, and which repo snapshot every lookup sees.

  • Commits as directory trees

    How a commit's tree becomes directories, files and symlinks, how conflicts and other value kinds are shown, and how content is streamed.

  • Tests and CI

    The test helpers, the unit tests per module, the mounted integration test, and the CI workflow.