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
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/ | Owner | Contents and format |
|---|---|---|
repo | Workspace | A 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/type | StoreFactories | The backend name, plain text: git or Simple. |
repo/store/git_target | GitBackend | Path to the Git repository, relative to store/ where possible (§4). |
repo/store/extra/ | GitBackend | Stacked tables of jj-only commit metadata: change ID and predecessors (§4). |
repo/store/git/ | GitBackend | A bare Git repository, only when jj created it internally. |
repo/workspace_store/index | SimpleWorkspaceStore | Protobuf Workspaces: a name and path per workspace (§3). |
repo/op_store/operations/, views/ | SimpleOpStore | One protobuf Operation or View per file, named by its content hash (§5). |
repo/op_heads/heads/ | SimpleOpHeadsStore | One empty file per current operation, named by its ID (§6). |
repo/index/ | DefaultIndexStore | Binary index segments, plus a protobuf SegmentControl per indexed operation (§7). |
repo/submodule_store/ | DefaultSubmoduleStore | Only type. The store keeps its path and nothing else. |
working_copy/checkout | LocalWorkingCopy | Protobuf Checkout: operation ID and workspace name (§8). |
working_copy/tree_state | LocalWorkingCopy | Protobuf TreeState: tree IDs, per-file stat cache, sparse patterns (§8). |
.gitignore | CLI | /*, 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.
| Directory | Default implementation | type contents |
|---|---|---|
store | GitBackend / SimpleBackend | git / Simple |
workspace_store | SimpleWorkspaceStore | simple_workspace_store |
op_store | SimpleOpStore | simple_op_store |
op_heads | SimpleOpHeadsStore | simple_op_heads_store |
index | DefaultIndexStore | default |
submodule_store | DefaultSubmoduleStore | default |
working_copy (outside repo) | LocalWorkingCopy | local |
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:
- Internal (
jj git initwithout colocation): a bare repository atstore/git(init_internal). - Colocated: the workspace's own
.git(init_colocated). See jj git colocation. - External: an existing repository elsewhere, given with
--git-repo(init_external).
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 file | Held while |
|---|---|
repo/op_heads/heads/lock | Resolving zero or several op heads into one, so two processes don't both merge them (source). |
repo/store/extra/lock | Adding a table to the Git backend's metadata store. |
repo/workspace_store/index.lock | Editing the workspace list. |
working_copy/working_copy.lock | A 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 protobufSegmentControl(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: aCheckoutmessage with the operation the working copy was last updated at and the workspace's name (CheckoutState). An empty name is read asdefault, for working copies older than workspaces. -
tree_state: aTreeStatemessage 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 missingtree_statemeans "never saved";loadthen starts from the empty tree.
How these are used during snapshot and checkout is on The working copy.