Conventions
-
All files are
proto3and compiled withprostintojj_lib::protos::<package>, exceptdefault_indexandgit_store, which thejj-default-indexandjj-git-backendcrates compile into their own privateprotosmodules (source). In proto3, an unset scalar reads as its zero value, which is why so much of the conversion code checks for an emptybytesorstringto detect old formats. -
IDs are raw
bytes, not hex. Commit, tree, file, view and operation IDs are all stored this way. -
Content-addressed objects are named by a hash of the Rust value, not of the
proto bytes. The simple backend and the simple op store both compute the ID as
blake2b_hash(&value)over the Rust struct'sContentHash, then write the encoded proto to a file named after that ID (commit, view, operation). - Deprecated fields are kept so old repos still load. New code writes only the current fields; readers fall back to the deprecated ones when the current ones are empty.
| File | Stored at | Written by | Rust types |
|---|---|---|---|
simple_store.proto | .jj/repo/store/{commits,trees}/<hex id> (simple backend only) | SimpleBackend | backend::{Commit, Tree, TreeValue, Signature} |
git_store.proto | .jj/repo/store/extra/ stacked table, keyed by commit ID | GitBackend | jj-only parts of backend::Commit |
simple_op_store.proto | .jj/repo/op_store/{operations,views}/<hex id> | SimpleOpStore | op_store::{Operation, OperationMetadata, View, RemoteView, RemoteRef, RefTarget} |
local_working_copy.proto | .jj/working_copy/{tree_state,checkout} | LocalWorkingCopy / TreeState | TreeState, FileState, CheckoutState |
default_index.proto | .jj/repo/index/op_links/<op id hex> | DefaultIndexStore | commit and changed-path segment IDs |
simple_workspace_store.proto | .jj/repo/workspace_store/index | SimpleWorkspaceStore | workspace name → path |
secure_config.proto | metadata.binpb in a per-repo/workspace config directory | secure_config | ConfigMetadata |
simple_store.proto
The native object format of SimpleBackend, a backend that stores everything as
individual proto files. Git-backed repos never use these messages: commits and trees are Git
objects there. Full file.
syntax = "proto3";
package simple_store;
message TreeValue {
message File {
bytes id = 1;
bool executable = 2;
bytes copy_id = 3;
}
oneof value {
File file = 2;
bytes symlink_id = 3;
bytes tree_id = 4;
}
}
message Tree {
message Entry {
string name = 1;
TreeValue value = 2;
}
repeated Entry entries = 1;
}
message Commit {
repeated bytes parents = 1;
repeated bytes predecessors = 2;
// Alternating positive and negative terms
repeated bytes root_tree = 3;
// Labels for the terms of a conflict.
repeated string conflict_labels = 10;
bytes change_id = 4;
string description = 5;
message Timestamp {
int64 millis_since_epoch = 1;
int32 tz_offset = 2;
}
message Signature {
string name = 1;
string email = 2;
Timestamp timestamp = 3;
}
Signature author = 6;
Signature committer = 7;
optional bytes secure_sig = 9;
}
TreeValue and TreeValue.File
| Field | # | Type | Meaning | Rust |
|---|---|---|---|---|
file | 2 | File (oneof) | A regular file. | TreeValue::File |
file.id | 1 | bytes | Content ID of the file. | FileId |
file.executable | 2 | bool | Executable bit. | executable |
file.copy_id | 3 | bytes | Copy-tracking identity. | CopyId |
symlink_id | 3 | bytes (oneof) | A symlink's target object. | TreeValue::Symlink(SymlinkId) |
tree_id | 4 | bytes (oneof) | A subdirectory. | TreeValue::Tree(TreeId) |
There's no variant for Git submodules: writing TreeValue::GitSubmodule to the
simple backend panics with cannot store git submodules
(tree_value_to_proto,
tree_value_from_proto).
Tree and Tree.Entry
| Field | # | Type | Meaning |
|---|---|---|---|
entries | 1 | repeated Entry | Directory entries, sorted by name. The reader trusts the order and builds the tree with Tree::from_sorted_entries. |
Entry.name | 1 | string | One path component. |
Entry.value | 2 | TreeValue | What lives at that name. |
tree_to_proto / tree_from_proto
Commit, Commit.Signature, Commit.Timestamp
| Field | # | Type | Meaning | Rust (backend::Commit) |
|---|---|---|---|---|
parents | 1 | repeated bytes | Parent commit IDs. The simple backend refuses to write a commit with none. | parents: Vec<CommitId> |
predecessors | 2 | repeated bytes | Commits this one rewrote. Superseded by Operation.commit_predecessors, but still written. | predecessors |
root_tree | 3 | repeated bytes | Root tree IDs as alternating add/remove terms: one ID when resolved, 2n+1 when conflicted. | root_tree: Merge<TreeId> |
conflict_labels | 10 | repeated string | One label per term. Written only when the labels aren't resolved. | conflict_labels: Merge<String> |
change_id | 4 | bytes | Stable change ID. | ChangeId |
description | 5 | string | Commit message. | description |
author | 6 | Signature | Author. Missing fields default. | Signature |
committer | 7 | Signature | Committer. | Signature |
secure_sig | 9 | optional bytes | Cryptographic signature over the encoded proto without this field. | SecureSig { data, sig } |
Signature.name / email / timestamp | 1 / 2 / 3 | string / string / Timestamp | Identity and time. | Signature |
Timestamp.millis_since_epoch / tz_offset | 1 / 2 | int64 / int32 | Milliseconds since the Unix epoch, and the UTC offset in minutes. | Timestamp |
Signing works on the proto itself: write_commit encodes the commit, passes those
bytes to the signing function, and stores the result in secure_sig. When reading,
commit_from_proto takes secure_sig out first and re-encodes the
remainder, which reproduces exactly the bytes that were signed
(write,
convert,
signatures).
git_store.proto
The Git backend stores commits as real Git commits. That leaves out what Git can't
represent, and this one message carries it. The serialized bytes go into a stacked table under
.jj/repo/store/extra, keyed by the Git commit ID. Entries are added both when jj writes a commit and when it imports one from Git
(write,
import). Full file.
syntax = "proto3";
package git_store;
message Commit {
repeated bytes predecessors = 2;
bytes change_id = 4;
// Alternating positive and negative terms. Set only for conflicts.
// Resolved trees are stored in the git commit
repeated bytes root_tree = 1;
// TODO(#1624): delete when we assume that all commits use this format
bool uses_tree_conflict_format = 10;
bool is_open = 8 [deprecated = true];
bool is_pruned = 9 [deprecated = true];
}
| Field | # | Type | Status | Meaning |
|---|---|---|---|---|
root_tree | 1 | repeated bytes | read only | Conflicted root trees for commits written before conflicts moved into a commit header (about March 2024). New writes leave it empty. |
predecessors | 2 | repeated bytes | current | Rewritten-from commit IDs. |
change_id | 4 | bytes | current | The change ID. If it's empty, the change ID read from the Git commit is kept. |
is_open | 8 | bool | deprecated | Not read or written by current code. |
is_pruned | 9 | bool | deprecated | Not read or written by current code. |
uses_tree_conflict_format | 10 | bool | current | Always written as true. The legacy root_tree is honoured only when it's set. |
Everything else about a commit's jj view lives in the Git commit itself. A conflicted root
tree is written as a jj:trees header of space-separated hex tree IDs, which must
hold an odd number of at least three, so jj and Git can never disagree about a resolved commit's
contents. Conflict labels go in a jj:conflict-labels header, and the change ID can
go in a change-id header written as reverse hex. When a Git commit has no change
ID, jj derives a synthetic one from the commit ID
(jj:trees,
headers on write,
serialize_extras / deserialize_extras).
simple_op_store.proto
The operation log. Every operation and every view is one file under
.jj/repo/op_store/operations/ or views/, named by the hex of its
64-byte ID. The root operation and root view are never stored: both are all-zero IDs, and the
store synthesizes them on read (read/write).
Full file.
syntax = "proto3";
package simple_op_store;
message RefConflictLegacy {
repeated bytes removes = 1 [deprecated = true];
repeated bytes adds = 2 [deprecated = true];
}
// This is redundant with `RefTargetTerm`. It exists for historical reasons.
// TODO: Consider deprecating it.
message RefConflict {
message Term {
optional bytes value = 1;
}
repeated Term removes = 1;
repeated Term adds = 2;
}
// RefTarget that may be serialized in legacy form.
message RefTarget {
// New `RefConflict` type represents both `commit_id` and `conflict_legacy`.
oneof value {
bytes commit_id = 1 [deprecated = true];
RefConflictLegacy conflict_legacy = 2 [deprecated = true];
RefConflict conflict = 3;
}
}
// RefTarget term that should be serialized in native form, in which positive
// and negative terms alternate.
message RefTargetTerm {
optional bytes value = 1;
}
enum RemoteRefState {
New = 0;
Tracked = 1;
}
message RemoteBookmark {
string remote_name = 1;
RefTarget target = 2;
// Introduced in jj 0.11.
optional RemoteRefState state = 3;
}
message Bookmark {
string name = 1;
// Unset if the bookmark has been deleted locally.
RefTarget local_target = 2;
// Deprecated since jj 0.34.
repeated RemoteBookmark remote_bookmarks = 3 [deprecated = true];
}
message GitRef {
string name = 1;
// This field is just for historical reasons (before we had the RefTarget
// type). New GitRefs have (only) the target field.
// TODO: Delete support for the old format.
bytes commit_id = 2 [deprecated = true];
RefTarget target = 3;
}
message GitHead {
string name = 1;
RefTarget target = 2;
}
message RemoteRef {
string name = 1;
repeated RefTargetTerm target_terms = 2;
RemoteRefState state = 3;
}
message Tag {
string name = 1;
RefTarget target = 2;
}
message View {
repeated bytes head_ids = 1;
reserved 4;
bytes wc_commit_id = 2 [deprecated = true];
map<string, bytes> wc_commit_ids = 8;
// Local bookmarks and remote bookmarks in legacy form.
repeated Bookmark bookmarks = 5;
repeated Tag local_tags = 6;
// Introduced in jj 0.34.
repeated RemoteView remote_views = 11;
// Only a subset of the refs. For example, does not include refs/notes/.
repeated GitRef git_refs = 3;
// This field is just for historical reasons (before we had the RefTarget
// type). New Views have (only) the target field.
// TODO: Delete support for the old format.
bytes git_head_legacy = 7 [deprecated = true];
RefTarget git_head = 9 [deprecated = true];
// Whether "@git" tags have been migrated to remote_views.
bool has_git_refs_migrated_to_remote_tags = 12;
// Per-workspace Git HEAD targets, keyed by workspace name.
repeated GitHead git_heads = 13;
reserved 10;
}
message RemoteView {
string name = 1;
repeated RemoteRef bookmarks = 2;
repeated RemoteRef tags = 3;
}
message Operation {
bytes view_id = 1;
repeated bytes parents = 2;
OperationMetadata metadata = 3;
// Introduced in jj 0.30.
repeated CommitPredecessors commit_predecessors = 4;
// Whether or not `commit_predecessors` is recorded.
bool stores_commit_predecessors = 5;
}
// TODO: Share with store.proto? Do we even need the timezone here?
message Timestamp {
int64 millis_since_epoch = 1;
int32 tz_offset = 2;
}
message OperationMetadata {
Timestamp start_time = 1;
Timestamp end_time = 2;
string description = 3;
string hostname = 4;
string username = 5;
bool is_snapshot = 7;
optional string workspace_name = 8;
map<string, string> attributes = 6;
}
message CommitPredecessors {
bytes commit_id = 1;
repeated bytes predecessor_ids = 2;
}
Operation
| Field | # | Type | Meaning | Rust (op_store::Operation) |
|---|---|---|---|---|
view_id | 1 | bytes | The view this operation produced (must be 64 bytes). | view_id: ViewId |
parents | 2 | repeated bytes | Parent operations. More than one means concurrent operations were merged. An empty list (from before the root operation existed) is read as the root. | parents: Vec<OperationId> |
metadata | 3 | OperationMetadata | Who, when, what. | metadata |
commit_predecessors | 4 | repeated CommitPredecessors | New commit → the commits it was rewritten from (empty list = newly created). Introduced in jj 0.30. | commit_predecessors: Option<BTreeMap<CommitId, Vec<CommitId>>> |
stores_commit_predecessors | 5 | bool | Distinguishes "recorded and empty" from "not recorded": false reads as None. |
predecessor map and operation conversions
OperationMetadata, Timestamp, CommitPredecessors
| Field | # | Type | Meaning |
|---|---|---|---|
start_time / end_time | 1 / 2 | Timestamp | The transaction's start and end. Stored as TimestampRange. |
description | 3 | string | e.g. fixed 3 commits, shown by jj op log. |
hostname / username | 4 / 5 | string | Where and who. |
attributes | 6 | map<string, string> | Free-form key/value tags. Stored as BTreeMap. |
is_snapshot | 7 | bool | The operation only snapshotted the working copy. |
workspace_name | 8 | optional string | The workspace that ran it, if any. |
Timestamp.millis_since_epoch / tz_offset | 1 / 2 | int64 / int32 | Same encoding as commit timestamps. |
CommitPredecessors.commit_id / predecessor_ids | 1 / 2 | bytes / repeated bytes | One map entry. |
View
| Field | # | Type | Status | Meaning → Rust (op_store::View) |
|---|---|---|---|---|
head_ids | 1 | repeated bytes | current | Visible head commits → head_ids: HashSet<CommitId> |
wc_commit_id | 2 | bytes | deprecated | Single-workspace era. If set, read as the default workspace's commit. |
git_refs | 3 | repeated GitRef | current | Last-imported Git refs (not refs/notes/) → git_refs |
| 4 | 4 | reserved | Never reuse. | |
bookmarks | 5 | repeated Bookmark | current + legacy | Local bookmarks → local_bookmarks. Still written with remote bookmarks in legacy form for older readers. |
local_tags | 6 | repeated Tag | current | → local_tags |
git_head_legacy | 7 | bytes | deprecated | Pre-RefTarget Git HEAD. |
wc_commit_ids | 8 | map<string, bytes> | current | Workspace name → working-copy commit → wc_commit_ids |
git_head | 9 | RefTarget | deprecated | Still written for the default workspace (the source says remove in jj 0.51+). Read only when git_heads is empty. |
| 10 | 10 | reserved | Never reuse. | |
remote_views | 11 | repeated RemoteView | current (jj 0.34+) | Remote bookmarks and tags per remote → remote_views. If empty, the remote state comes from the legacy bookmarks field. |
has_git_refs_migrated_to_remote_tags | 12 | bool | current | Always written true. If false, refs/tags/* in git_refs are moved into the git remote's tags as tracked refs on read. |
git_heads | 13 | repeated GitHead | current | Per-workspace Git HEAD → git_heads |
view_to_proto · view_from_proto
Writing is deterministic: view_to_proto sorts head_ids
(source), and the generated
wc_commit_ids and OperationMetadata.attributes are BTreeMaps, so
map entries go out sorted by key (source).
The same view or operation always encodes to the same bytes; readers accept any order.
Refs: RefTarget, RefConflict, RefTargetTerm, RefConflictLegacy
A ref target in Rust is RefTarget = Merge<Option<CommitId>>: a normal ref,
an absent one, or a conflicted one. The proto has accumulated three encodings of it:
| Encoding | Fields | Status | Notes |
|---|---|---|---|
RefTarget.commit_id | oneof #1, bytes | deprecated | A plain, non-conflicted ID (jj ≤ 0.8). |
RefTarget.conflict_legacy → RefConflictLegacy | oneof #2; removes #1, adds #2 (repeated bytes) | deprecated | Can't represent absent terms. Converted with RefTarget::from_legacy_form. |
RefTarget.conflict → RefConflict | oneof #3; removes #1, adds #2 (repeated Term); Term.value #1 (optional bytes) | current | Always written, even for a normal ref, which is just one add and no removes. An absent term is a Term with no value. |
RefTargetTerm | value #1 (optional bytes) | current | A flat term list used by RemoteRef.target_terms: alternating add/remove, odd length, and an even count is a decode error. |
A missing RefTarget message reads as absent
(terms,
ref_target_to_proto,
ref_target_from_proto).
Bookmarks, tags, remotes and Git refs
| Message | Fields | Meaning |
|---|---|---|
Bookmark | name #1 string; local_target #2 RefTarget; remote_bookmarks #3 repeated RemoteBookmark (deprecated since 0.34) | A bookmark name with its local target. An unset target means the bookmark was deleted locally. Those entries are kept only to carry remote state, and aren't added to local_bookmarks. |
RemoteBookmark | remote_name #1; target #2 RefTarget; state #3 optional RemoteRefState (jj 0.11+) | Legacy remote bookmark. A missing state (jj < 0.11) reads as New. |
RemoteView | name #1; bookmarks #2, tags #3 (repeated RemoteRef) | Everything known about one remote → RemoteView. |
RemoteRef | name #1; target_terms #2 repeated RefTargetTerm; state #3 RemoteRefState | → RemoteRef { target, state } |
RemoteRefState (enum) | New = 0, Tracked = 1 | Whether the local ref tracks this remote ref. Unknown values are a decode error. |
Tag | name #1; target #2 RefTarget | A local tag. |
GitRef | name #1; commit_id #2 (deprecated); target #3 RefTarget | A full Git ref name (e.g. refs/heads/main). If target is unset, the legacy commit_id is used. |
GitHead | name #1; target #2 RefTarget | Keyed by workspace name. |
legacy bookmark views ·
remote views and refs ·
RemoteRefState
local_working_copy.proto
The state of one workspace's files on disk. TreeState is the whole
.jj/working_copy/tree_state file; Checkout is the
.jj/working_copy/checkout file. Full file.
syntax = "proto3";
package local_working_copy;
enum FileType {
Normal = 0;
Symlink = 1;
Executable = 2;
Conflict = 3 [deprecated = true];
GitSubmodule = 4;
}
message MaterializedConflictData {
// TODO: maybe we should store num_sides here as well
uint32 conflict_marker_len = 1;
}
message FileState {
int64 mtime_millis_since_epoch = 1;
uint64 size = 2;
FileType file_type = 3;
MaterializedConflictData materialized_conflict_data = 5;
reserved 4;
}
message FileStateEntry {
string path = 1;
FileState state = 2;
}
message SparsePatterns {
repeated string prefixes = 1;
}
message TreeState {
bytes legacy_tree_id = 1 [deprecated = true];
// Alternating positive and negative terms if there's a conflict, otherwise a
// single (positive) value
repeated bytes tree_ids = 5;
// Labels for the terms of a conflict.
repeated string conflict_labels = 7;
repeated FileStateEntry file_states = 2;
bool is_file_states_sorted = 6;
SparsePatterns sparse_patterns = 3;
WatchmanClock watchman_clock = 4;
}
message WatchmanClock {
oneof watchman_clock {
string string_clock = 1;
int64 unix_timestamp = 2;
}
}
message Checkout {
// The operation at which the working copy was updated.
bytes operation_id = 2;
// An identifier for this workspace. It is used for looking up the current
// working-copy commit in the repo view. Currently a human-readable name.
// TODO: Is it better to make this a UUID and a have map that to a name in
// config? That way users can rename a workspace.
string workspace_name = 3;
reserved 1;
}
TreeState
| Field | # | Type | Meaning |
|---|---|---|---|
legacy_tree_id | 1 | bytes (deprecated) | Used only when tree_ids is empty. |
file_states | 2 | repeated FileStateEntry | One entry per tracked file. This is what makes snapshots cheap: unchanged mtime and size mean the file isn't re-read. |
sparse_patterns | 3 | SparsePatterns | Which path prefixes are checked out. If missing (very old working copies), it's read as the whole repo. |
watchman_clock | 4 | WatchmanClock | Last fsmonitor clock, so the next snapshot can ask Watchman only for changes since then. |
tree_ids | 5 | repeated bytes | The checked-out tree as alternating terms (one when resolved) → MergedTree. |
is_file_states_sorted | 6 | bool | Always written true. If false, entries are sorted on load. |
conflict_labels | 7 | repeated string | Labels for a conflicted tree's terms. |
read · save · sparse patterns
FileState, FileStateEntry, FileType, MaterializedConflictData
| Field | # | Type | Meaning |
|---|---|---|---|
FileStateEntry.path / state | 1 / 2 | string / FileState | Repo-relative path (internal form) and its state. |
FileState.mtime_millis_since_epoch | 1 | int64 | Last-seen modification time. |
FileState.size | 2 | uint64 | Last-seen size. |
FileState.file_type | 3 | FileType | See below. |
| 4 | 4 | reserved | |
FileState.materialized_conflict_data | 5 | MaterializedConflictData | Set when the file on disk holds conflict markers. conflict_marker_len (#1, uint32) records the marker length used, so jj can parse them back. |
FileType | Value | Rust FileType |
|---|---|---|
Normal | 0 | Normal { exec_bit: false } |
Symlink | 1 | Symlink |
Executable | 2 | Normal { exec_bit: true }. It can appear on Windows when a repo is shared with a Unix jj, such as through WSL. |
Conflict | 3 (deprecated) | Read as Normal { exec_bit: false }, never written. |
GitSubmodule | 4 | GitSubmodule |
SparsePatterns, WatchmanClock, Checkout
| Field | # | Type | Meaning |
|---|---|---|---|
SparsePatterns.prefixes | 1 | repeated string | Checked-out path prefixes. |
WatchmanClock.string_clock | 1 (oneof) | string | Watchman's opaque clock. |
WatchmanClock.unix_timestamp | 2 (oneof) | int64 | Timestamp form. |
Checkout.operation_id | 2 | bytes | The operation at which the working copy was last updated. WorkingCopyFreshness::check_stale compares it with the repo's operation to decide whether the working copy is fresh, stale, or was updated by another process (source). |
Checkout.workspace_name | 3 | string | This workspace's name, used to look up its commit in View.wc_commit_ids. Empty reads as default. |
| 1 | 1 | reserved in Checkout | |
default_index.proto
The commit index is stored as immutable segment files. This single message links an operation to
the segments that make up the index as of that operation. It's written to
.jj/repo/index/op_links/<operation id hex>.
Full file.
syntax = "proto3";
package default_index;
// Control file to link operation to index segments.
message SegmentControl {
// Hash (or file name) of the commit index segment.
bytes commit_segment_id = 1;
// First commit stored in the changed-path index segments. Unset if
// changed-path index is disabled.
optional uint32 changed_path_start_commit_pos = 2;
// Hashes (or file names) of the changed-path index segments.
repeated bytes changed_path_segment_ids = 3;
}
| Field | # | Type | Meaning |
|---|---|---|---|
commit_segment_id | 1 | bytes | The head commit-index segment (in index/segments/). It names its parent segments itself. |
changed_path_start_commit_pos | 2 | optional uint32 | The global commit position where the changed-path index starts. Unset when the changed-path index is disabled. |
changed_path_segment_ids | 3 | repeated bytes | Changed-path segments (in index/changed_paths/). |
simple_workspace_store.proto
The repo-level registry of workspaces, kept in .jj/repo/workspace_store/index. Adding, forgetting and renaming a workspace each take a file lock around the read-modify-write. Full file.
syntax = "proto3";
package simple_workspace_store;
message Workspace {
string name = 1;
bytes path = 2;
}
message Workspaces {
repeated Workspace workspaces = 1;
}
| Field | # | Type | Meaning |
|---|---|---|---|
Workspace.name | 1 | string | Workspace name. Adding a name that already exists replaces the entry. |
Workspace.path | 2 | bytes | Workspace root, stored relative to the repo directory when possible (with forward slashes), as raw path bytes. Reading it back joins a relative path onto the repo directory and normalizes it, so callers always get an absolute path (get_workspace_path). |
Workspaces.workspaces | 1 | repeated Workspace | The whole file. |
secure_config.proto
jj keeps per-repo and per-workspace config in a config directory identified by a generated
config ID. Next to that config's config.toml it writes
metadata.binpb, which records which repo or workspace the config belongs to. jj
checks the stored path against the actual one when loading.
Full file.
syntax = "proto3";
package secure_config;
// Metadata about the configuration files for jj
message ConfigMetadata {
// The absolute path to the workspace's .jj or .jj/repo directory for
// workspaces and repos respectively.
// Access via file_util::path_to/from_bytes
optional bytes path = 1;
}
| Field | # | Type | Meaning |
|---|---|---|---|
ConfigMetadata.path | 1 | optional bytes | Absolute path of the workspace's .jj directory or the repo's .jj/repo directory, encoded with file_util::path_to_bytes. |