jj / On-disk layout

On-disk layout

Everything jj knows about a repo lives under .jj/ in the workspace root. Each subdirectory belongs to one store, each store records its own implementation name in a type file, and almost every other file is a protobuf message or an empty marker whose name carries the data.

On this page
  1. 1. The tree
  2. 2. type files and factory dispatch
  3. 3. Workspaces and the repo pointer
  4. 4. store/: commits
  5. 5. op_store/: operations and views
  6. 6. op_heads/ and locking
  7. 7. index/: the commit index
  8. 8. working_copy/

In short: .jj/repo/ is the repo, shared by every workspace; .jj/working_copy/ belongs to this workspace alone. Inside the repo, commits live in store/, operations and views in op_store/, the current operation(s) in op_heads/, and the commit index in index/. Content-addressed files are named by the hex of their BLAKE2b-512 hash and never rewritten; mutable files are replaced by renaming a fully written temp file over them.

1. The tree

The .jj directory of a workspace with the default stores The .jj directory holds two subdirectories. repo, shared by all workspaces, contains store (the commit backend, with type, git_target and extra for the Git backend), workspace_store (index), op_store (operations and views), op_heads (heads), index (segments, op_links, changed_paths) and submodule_store. working_copy, private to the workspace, contains checkout, tree_state, type and working_copy.lock. .jj/ repo/ shared by workspaces working_copy/ this workspace only store/ type · git_target · extra/ (Git backend) · git/ (internal Git repo) workspace_store/ type · index · index.lock op_store/ type · operations/<hex> · views/<hex> op_heads/ type · heads/<op hex> (empty) · heads/lock index/ type · segments/ · op_links/<op hex> · changed_paths/ submodule_store/ type (nothing else yet) checkout · tree_state · type working_copy.lock while a mutation is in progress

Paths are those of the default implementations. A custom backend or store owns its directory outright and may put anything in it; only the type file is common to all.

Path under .jj/OwnerContents and format
repoWorkspaceA directory in the first workspace. In a workspace added later, a file holding the path to the first one's repo directory (§3).
repo/store/typeStoreFactoriesThe backend name, plain text: git or Simple.
repo/store/git_targetGitBackendPath to the Git repository, relative to store/ where possible (§4).
repo/store/extra/GitBackendStacked tables of jj-only commit metadata: change ID and predecessors (§4).
repo/store/git/GitBackendA bare Git repository, only when jj created it internally.
repo/workspace_store/indexSimpleWorkspaceStoreProtobuf Workspaces: a name and path per workspace (§3).
repo/op_store/operations/, views/SimpleOpStoreOne protobuf Operation or View per file, named by its content hash (§5).
repo/op_heads/heads/SimpleOpHeadsStoreOne empty file per current operation, named by its ID (§6).
repo/index/DefaultIndexStoreBinary index segments, plus a protobuf SegmentControl per indexed operation (§7).
repo/submodule_store/DefaultSubmoduleStoreOnly type. The store keeps its path and nothing else.
working_copy/checkoutLocalWorkingCopyProtobuf Checkout: operation ID and workspace name (§8).
working_copy/tree_stateLocalWorkingCopyProtobuf TreeState: tree IDs, per-file stat cache, sparse patterns (§8).
.gitignoreCLI/*, written when the workspace is colocated with Git, so Git ignores .jj/.

ReadonlyRepo::init · Workspace::init_with_factories · maybe_add_gitignore

2. type files and factory dispatch

ReadonlyRepo::init creates each store's directory, calls its initializer (the defaults are in default_backend_factories.rs), and writes the store's name() to type in that directory, with no trailing newline. On load, StoreFactories::load_backend and its siblings read the file back with read_store_type and look the name up in a map of factories. An unknown name fails with StoreLoadError::UnsupportedType; that is how a jj binary built without some backend refuses a repo that uses it. Custom binaries register extra factories before loading.

DirectoryDefault implementationtype contents
storeGitBackend / SimpleBackendgit / Simple
workspace_storeSimpleWorkspaceStoresimple_workspace_store
op_storeSimpleOpStoresimple_op_store
op_headsSimpleOpHeadsStoresimple_op_heads_store
indexDefaultIndexStoredefault
submodule_storeDefaultSubmoduleStoredefault
working_copy (outside repo)LocalWorkingCopylocal

default_backend_factories · get_working_copy_type

The workspace store is the newest. Repos created before it existed have no workspace_store/, so load_workspace_store reads its type with read_store_type_compat, which creates the directory and writes simple_workspace_store when the file is missing, and SimpleWorkspaceStore::load then writes an empty index.

3. Workspaces and the repo pointer

create_jj_dir refuses to run if .jj already exists. The first workspace gets a real .jj/repo/ directory. A workspace added later by jj workspace add gets its own .jj/ with a fresh working_copy/, but .jj/repo is a file whose bytes are the path to the shared repo directory, relative and with forward slashes (init_workspace_with_existing_repo). DefaultWorkspaceLoader::new checks whether .jj/repo is a file, resolves its contents against .jj/, and fails with RepoDoesNotExist if nothing is there.

The repo keeps a list of its workspaces in repo/workspace_store/index, a protobuf Workspaces message holding a name and path per workspace, the path stored relative to the repo directory when it can be. Every change takes index.lock, reads the whole list, edits it, and replaces the file (add). Which commit each workspace has checked out is not stored here; that is part of the view.

4. store/: commits

With the Git backend, commits, trees and file contents are ordinary Git objects. Where the Git repository lives depends on how the repo was created:

All three end in init_with_repo, which creates extra/ and writes the repository's path to git_target, relative paths using forward slashes. load resolves it against store/.

Git has nowhere to put a change ID, predecessors, or a conflicted tree, so the backend keeps them in extra/, a TableStore keyed by Git commit ID. Each value is a protobuf git_store::Commit (serialize_extras). Tables are immutable files named by the hash of their contents; each new table stacks on a parent, and an empty file in extra/heads/ marks each current top table. Writers hold extra/lock.

The Simple backend, used in tests, writes each object to its own file under commits/, trees/, files/, symlinks/ and conflicts/, named by its ID (SimpleBackend::init). All the schemas are on Every schema.

5. op_store/: operations and views

SimpleOpStore has two directories, operations/ and views/. Each file holds one protobuf message and is named by the hex of its BLAKE2b-512 hash, so an ID is also a file name (write_view). The root operation and root view are all zeros and are never written; reads synthesize them. Files are written through persist_content_addressed_temp_file: data is synced, then renamed into place. Since the name is the hash, a file that already exists already has the right contents, and on Windows the existing one is kept rather than replaced. The temp file is created by new_temp_file_with_default_permissions, so on Unix the stored file gets the usual umask-based mode rather than a temp file's 0600. The structures themselves are on The operation log and The view.

6. op_heads/ and locking

op_heads/heads/ holds one empty file per current operation, named by the operation's hex ID. init creates it with the root operation. update_op_heads adds the new head before removing the old ones, so a crash leaves two heads, which the next command merges, rather than none. get_op_heads skips names that are not hex, and reports an empty directory as a corrupt repository.

Writes in jj rarely need a lock, because content-addressed files can't conflict. The locks that do exist are files taken with FileLock:

Lock fileHeld while
repo/op_heads/heads/lockResolving zero or several op heads into one, so two processes don't both merge them (source).
repo/store/extra/lockAdding a table to the Git backend's metadata store.
repo/workspace_store/index.lockEditing the workspace list.
working_copy/working_copy.lockA working-copy mutation, from start to finish (source).

7. index/: the commit index

The index is derived data: it can always be rebuilt from the commits and operations, and jj debug reindex does exactly that. DefaultIndexStore uses three directories:

  • segments/: commit index segments. Each file's name is the hex BLAKE2b-512 hash of its contents (save_in), so the names are 128 characters long.
  • changed_paths/: segments recording which paths each commit changed, named the same way (save_in).
  • op_links/: one file per indexed operation, named by the operation ID, holding a protobuf SegmentControl (associate_index_with_operation).

SegmentControl names the commit segment that is the index as of that operation, the changed-path segments, and the commit position the changed-path index starts at (schema). To load an index, get_index_at_op reads the op_links file of the operation (load_index_at_operation). If there is none, it indexes forward from the nearest ancestor operation that has one (build_index_at_operation). If a segment is corrupt or in an older format, it prints a message, clears the index, and reindexes. reinit also clears two older layouts: an operations/ directory (jj 0.32 and older) and segments directly under index/ (0.14 and older).

8. working_copy/

Created by init_working_copy, which also writes type. With LocalWorkingCopy it holds two protobuf files, each replaced whole through a temp file and rename:

  • checkout: a Checkout message with the operation the working copy was last updated at and the workspace's name (CheckoutState). An empty name is read as default, for working copies older than workspaces.
  • tree_state: a TreeState message with the tree IDs of the checked-out tree (several if it is conflicted) and their conflict labels, a stat cache entry per file (mtime, size, file type, conflict marker length), the sparse patterns, and the Watchman clock (save). A missing tree_state means "never saved"; load then starts from the empty tree.

How these are used during snapshot and checkout is on The working copy.