jj / Every schema

Every schema

jj persists everything that isn't a Git object as Protocol Buffers: commits and trees in the simple backend, jj-only commit metadata in the Git backend, every operation and view, the working copy's state, index segment links, the workspace registry and secure-config metadata. This page covers all seven .proto files, every field, and the Rust code that converts each one.

On this page
  1. Conventions
  2. simple_store.proto
  3. TreeValue and TreeValue.File
  4. Tree and Tree.Entry
  5. Commit, Commit.Signature, Commit.Timestamp
  6. git_store.proto
  7. simple_op_store.proto
  8. Operation
  9. OperationMetadata, Timestamp, CommitPredecessors
  10. View
  11. Refs: RefTarget, RefConflict, RefTargetTerm, RefConflictLegacy
  12. Bookmarks, tags, remotes and Git refs
  13. local_working_copy.proto
  14. TreeState
  15. FileState, FileStateEntry, FileType, MaterializedConflictData
  16. SparsePatterns, WatchmanClock, Checkout
  17. default_index.proto
  18. simple_workspace_store.proto
  19. secure_config.proto

Conventions

  • All files are proto3 and compiled with prost into jj_lib::protos::<package>, except default_index and git_store, which the jj-default-index and jj-git-backend crates compile into their own private protos modules (source). In proto3, an unset scalar reads as its zero value, which is why so much of the conversion code checks for an empty bytes or string to 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's ContentHash, 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.
FileStored atWritten byRust types
simple_store.proto.jj/repo/store/{commits,trees}/<hex id> (simple backend only)SimpleBackendbackend::{Commit, Tree, TreeValue, Signature}
git_store.proto.jj/repo/store/extra/ stacked table, keyed by commit IDGitBackendjj-only parts of backend::Commit
simple_op_store.proto.jj/repo/op_store/{operations,views}/<hex id>SimpleOpStoreop_store::{Operation, OperationMetadata, View, RemoteView, RemoteRef, RefTarget}
local_working_copy.proto.jj/working_copy/{tree_state,checkout}LocalWorkingCopy / TreeStateTreeState, FileState, CheckoutState
default_index.proto.jj/repo/index/op_links/<op id hex>DefaultIndexStorecommit and changed-path segment IDs
simple_workspace_store.proto.jj/repo/workspace_store/indexSimpleWorkspaceStoreworkspace name → path
secure_config.protometadata.binpb in a per-repo/workspace config directorysecure_configConfigMetadata

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#TypeMeaningRust
file2File (oneof)A regular file.TreeValue::File
file.id1bytesContent ID of the file.FileId
file.executable2boolExecutable bit.executable
file.copy_id3bytesCopy-tracking identity.CopyId
symlink_id3bytes (oneof)A symlink's target object.TreeValue::Symlink(SymlinkId)
tree_id4bytes (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#TypeMeaning
entries1repeated EntryDirectory entries, sorted by name. The reader trusts the order and builds the tree with Tree::from_sorted_entries.
Entry.name1stringOne path component.
Entry.value2TreeValueWhat lives at that name.

tree_to_proto / tree_from_proto

Commit, Commit.Signature, Commit.Timestamp

Field#TypeMeaningRust (backend::Commit)
parents1repeated bytesParent commit IDs. The simple backend refuses to write a commit with none.parents: Vec<CommitId>
predecessors2repeated bytesCommits this one rewrote. Superseded by Operation.commit_predecessors, but still written.predecessors
root_tree3repeated bytesRoot tree IDs as alternating add/remove terms: one ID when resolved, 2n+1 when conflicted.root_tree: Merge<TreeId>
conflict_labels10repeated stringOne label per term. Written only when the labels aren't resolved.conflict_labels: Merge<String>
change_id4bytesStable change ID.ChangeId
description5stringCommit message.description
author6SignatureAuthor. Missing fields default.Signature
committer7SignatureCommitter.Signature
secure_sig9optional bytesCryptographic signature over the encoded proto without this field.SecureSig { data, sig }
Signature.name / email / timestamp1 / 2 / 3string / string / TimestampIdentity and time.Signature
Timestamp.millis_since_epoch / tz_offset1 / 2int64 / int32Milliseconds 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#TypeStatusMeaning
root_tree1repeated bytesread onlyConflicted root trees for commits written before conflicts moved into a commit header (about March 2024). New writes leave it empty.
predecessors2repeated bytescurrentRewritten-from commit IDs.
change_id4bytescurrentThe change ID. If it's empty, the change ID read from the Git commit is kept.
is_open8booldeprecatedNot read or written by current code.
is_pruned9booldeprecatedNot read or written by current code.
uses_tree_conflict_format10boolcurrentAlways 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#TypeMeaningRust (op_store::Operation)
view_id1bytesThe view this operation produced (must be 64 bytes).view_id: ViewId
parents2repeated bytesParent 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>
metadata3OperationMetadataWho, when, what.metadata
commit_predecessors4repeated CommitPredecessorsNew 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_predecessors5boolDistinguishes "recorded and empty" from "not recorded": false reads as None.

predecessor map and operation conversions

OperationMetadata, Timestamp, CommitPredecessors

Field#TypeMeaning
start_time / end_time1 / 2TimestampThe transaction's start and end. Stored as TimestampRange.
description3stringe.g. fixed 3 commits, shown by jj op log.
hostname / username4 / 5stringWhere and who.
attributes6map<string, string>Free-form key/value tags. Stored as BTreeMap.
is_snapshot7boolThe operation only snapshotted the working copy.
workspace_name8optional stringThe workspace that ran it, if any.
Timestamp.millis_since_epoch / tz_offset1 / 2int64 / int32Same encoding as commit timestamps.
CommitPredecessors.commit_id / predecessor_ids1 / 2bytes / repeated bytesOne map entry.

metadata conversions

View

Field#TypeStatusMeaning → Rust (op_store::View)
head_ids1repeated bytescurrentVisible head commits → head_ids: HashSet<CommitId>
wc_commit_id2bytesdeprecatedSingle-workspace era. If set, read as the default workspace's commit.
git_refs3repeated GitRefcurrentLast-imported Git refs (not refs/notes/) → git_refs
44reservedNever reuse.
bookmarks5repeated Bookmarkcurrent + legacyLocal bookmarks → local_bookmarks. Still written with remote bookmarks in legacy form for older readers.
local_tags6repeated Tagcurrent→ local_tags
git_head_legacy7bytesdeprecatedPre-RefTarget Git HEAD.
wc_commit_ids8map<string, bytes>currentWorkspace name → working-copy commit → wc_commit_ids
git_head9RefTargetdeprecatedStill written for the default workspace (the source says remove in jj 0.51+). Read only when git_heads is empty.
1010reservedNever reuse.
remote_views11repeated RemoteViewcurrent (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_tags12boolcurrentAlways written true. If false, refs/tags/* in git_refs are moved into the git remote's tags as tracked refs on read.
git_heads13repeated GitHeadcurrentPer-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:

EncodingFieldsStatusNotes
RefTarget.commit_idoneof #1, bytesdeprecatedA plain, non-conflicted ID (jj ≤ 0.8).
RefTarget.conflict_legacy → RefConflictLegacyoneof #2; removes #1, adds #2 (repeated bytes)deprecatedCan't represent absent terms. Converted with RefTarget::from_legacy_form.
RefTarget.conflict → RefConflictoneof #3; removes #1, adds #2 (repeated Term); Term.value #1 (optional bytes)currentAlways written, even for a normal ref, which is just one add and no removes. An absent term is a Term with no value.
RefTargetTermvalue #1 (optional bytes)currentA 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

MessageFieldsMeaning
Bookmarkname #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.
RemoteBookmarkremote_name #1; target #2 RefTarget; state #3 optional RemoteRefState (jj 0.11+)Legacy remote bookmark. A missing state (jj < 0.11) reads as New.
RemoteViewname #1; bookmarks #2, tags #3 (repeated RemoteRef)Everything known about one remote → RemoteView.
RemoteRefname #1; target_terms #2 repeated RefTargetTerm; state #3 RemoteRefState→ RemoteRef { target, state }
RemoteRefState (enum)New = 0, Tracked = 1Whether the local ref tracks this remote ref. Unknown values are a decode error.
Tagname #1; target #2 RefTargetA local tag.
GitRefname #1; commit_id #2 (deprecated); target #3 RefTargetA full Git ref name (e.g. refs/heads/main). If target is unset, the legacy commit_id is used.
GitHeadname #1; target #2 RefTargetKeyed 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#TypeMeaning
legacy_tree_id1bytes (deprecated)Used only when tree_ids is empty.
file_states2repeated FileStateEntryOne entry per tracked file. This is what makes snapshots cheap: unchanged mtime and size mean the file isn't re-read.
sparse_patterns3SparsePatternsWhich path prefixes are checked out. If missing (very old working copies), it's read as the whole repo.
watchman_clock4WatchmanClockLast fsmonitor clock, so the next snapshot can ask Watchman only for changes since then.
tree_ids5repeated bytesThe checked-out tree as alternating terms (one when resolved) → MergedTree.
is_file_states_sorted6boolAlways written true. If false, entries are sorted on load.
conflict_labels7repeated stringLabels for a conflicted tree's terms.

read · save · sparse patterns

FileState, FileStateEntry, FileType, MaterializedConflictData

Field#TypeMeaning
FileStateEntry.path / state1 / 2string / FileStateRepo-relative path (internal form) and its state.
FileState.mtime_millis_since_epoch1int64Last-seen modification time.
FileState.size2uint64Last-seen size.
FileState.file_type3FileTypeSee below.
44reserved
FileState.materialized_conflict_data5MaterializedConflictDataSet when the file on disk holds conflict markers. conflict_marker_len (#1, uint32) records the marker length used, so jj can parse them back.
FileTypeValueRust FileType
Normal0Normal { exec_bit: false }
Symlink1Symlink
Executable2Normal { exec_bit: true }. It can appear on Windows when a repo is shared with a Unix jj, such as through WSL.
Conflict3 (deprecated)Read as Normal { exec_bit: false }, never written.
GitSubmodule4GitSubmodule

file state conversions

SparsePatterns, WatchmanClock, Checkout

Field#TypeMeaning
SparsePatterns.prefixes1repeated stringChecked-out path prefixes.
WatchmanClock.string_clock1 (oneof)stringWatchman's opaque clock.
WatchmanClock.unix_timestamp2 (oneof)int64Timestamp form.
Checkout.operation_id2bytesThe 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_name3stringThis workspace's name, used to look up its commit in View.wc_commit_ids. Empty reads as default.
11reserved in Checkout

CheckoutState load/save

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#TypeMeaning
commit_segment_id1bytesThe head commit-index segment (in index/segments/). It names its parent segments itself.
changed_path_start_commit_pos2optional uint32The global commit position where the changed-path index starts. Unset when the changed-path index is disabled.
changed_path_segment_ids3repeated bytesChanged-path segments (in index/changed_paths/).

load · write

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#TypeMeaning
Workspace.name1stringWorkspace name. Adding a name that already exists replaces the entry.
Workspace.path2bytesWorkspace 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.workspaces1repeated WorkspaceThe whole file.

read/write · add

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#TypeMeaning
ConfigMetadata.path1optional bytesAbsolute path of the workspace's .jj directory or the repo's .jj/repo directory, encoded with file_util::path_to_bytes.

metadata read/write · path validation