In short: BackendService carries the content-addressed objects jj-lib's
Backend trait deals in (commits, trees, files, and on paper symlinks), and
OpStoreService carries the operation log (operations, views and op heads). Every request
names a repository by the UUID the server handed out at registration. On every write, the
server computes the object's ID and sends it back. The messages are a trimmed copy of jj's
own storage protos: they drop Git refs and tags, and they add the repo ID.
Both files are proto3, in packages commit_cloud.backend and
commit_cloud.op_store. For the protos jj itself uses on disk, see
/jj/protobufs. That page describes jj's own pinned
version, which is newer than the jj-lib 0.43.0 used here.
How the Rust code is generated
The cc-common crate compiles both files at build time.
common/build.rs calls
tonic_build::configure().compile_protos(&["proto/backend.proto", "proto/op_store.proto"], &["proto"]),
which generates prost message structs plus a tonic client and server for each service.
common/src/lib.rs includes them as
cc_common::backend and cc_common::op_store. That gives types such as
cc_common::backend::Commit, the client
backend_service_client::BackendServiceClient, and the server trait
backend_service_server::BackendService. The same file defines the shared constants
both sides rely on (source):
| Item | Type/Signature | Value |
|---|---|---|
COMMIT_ID_LENGTH | usize | 20 |
CHANGE_ID_LENGTH | usize | 16 |
OPERATION_ID_LENGTH, VIEW_ID_LENGTH | usize | 16 |
ROOT_COMMIT_ID_BYTES, ROOT_CHANGE_ID_BYTES, ROOT_OPERATION_ID_BYTES, ROOT_VIEW_ID_BYTES | byte arrays | all zeros, of the lengths above |
EMPTY_TREE_ID_HEX / EMPTY_TREE_ID_BYTES | &str / [u8; 20] | 4b825dc642cb6eb9a060e54bf8d69288fbee4904, Git's empty-tree hash |
The services
BackendService
backend.proto, lines 6–27. All RPCs are unary except ReadFile, which streams its response.
| RPC | Request | Response | Client caller | Server handler |
|---|---|---|---|---|
RegisterRepository | RegisterRepositoryRequest | RegisterRepositoryResponse | CommitCloudBackend::init | register_repository |
ReadCommit | ReadCommitRequest | ReadCommitResponse | read_commit | read_commit |
WriteCommit | WriteCommitRequest | WriteCommitResponse | write_commit | write_commit |
ReadTree | ReadTreeRequest | ReadTreeResponse | read_tree | read_tree |
WriteTree | WriteTreeRequest | WriteTreeResponse | write_tree | write_tree |
ReadFile | ReadFileRequest | stream ReadFileResponse | read_file | read_file |
WriteFile | WriteFileRequest | WriteFileResponse | write_file | write_file |
ReadSymlink | ReadSymlinkRequest | ReadSymlinkResponse | never called: read_symlink returns Unsupported (source) | UNIMPLEMENTED |
WriteSymlink | WriteSymlinkRequest | WriteSymlinkResponse | never called: write_symlink returns Unsupported (source) | UNIMPLEMENTED |
OpStoreService
op_store.proto, lines 7–22. All six RPCs are unary.
Every handler except RegisterRepository first checks the repo ID is registered and
returns NOT_FOUND ("repository should have been registered before …") if it isn't (ensure_repo_registered_error).
On the client, every call opens a fresh tonic connection to the configured server URL. The
client Backend
and server
pages cover the calls themselves.
backend.proto messages
Fields are listed in proto order. "jj-lib type" is the jj-lib 0.43.0 type the client converts the field to and from. "—" means the field never becomes a jj-lib value.
RegisterRepositoryRequest
backend.proto, lines 31–33 · converted by client init, server
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
name | optional string | 1 | Optional display name. The client always sends None, and the server ignores the field. | — | L32 |
RegisterRepositoryResponse
backend.proto, lines 35–37 · converted by server
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
repo_id | string | 1 | A random UUID v4 string the server generates. The client saves it in the store's config.toml and sends it on every later request. | CommitCloudConfig.repo_id | L36 |
Timestamp
backend.proto, lines 41–44 · converted by signature_to_proto / signature_from_proto
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
millis_since_epoch | int64 | 1 | Milliseconds since the Unix epoch. | MillisSinceEpoch | L42 |
tz_offset | int32 | 2 | Time-zone offset in minutes. | Timestamp.tz_offset | L43 |
Signature
backend.proto, lines 46–50 · converted by signature_to_proto / signature_from_proto
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
name | string | 1 | Author or committer name. | Signature.name | L47 |
email | string | 2 | Email address. | Signature.email | L48 |
timestamp | Timestamp | 3 | When it was signed. A missing timestamp decodes as zero (unwrap_or_default). | Timestamp | L49 |
Commit
backend.proto, lines 53–64 · converted by commit_to_proto / commit_from_proto
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
commit_id | bytes | 1 | Empty when the client writes, because commit_to_proto sets vec![]. The server fills it in with the ID it computes. If a request carries a non-empty ID, the server stores the commit under that ID without checking it. | CommitId (the key, not a field) | L54 |
change_id | bytes | 2 | The change ID, 16 bytes. | Commit.change_id | L55 |
parent_commit_ids | repeated bytes | 3 | Parent commit IDs, in order. | Commit.parents | L56 |
root_tree_id | repeated bytes | 4 | One tree ID per term of the root tree. A resolved tree has exactly one. | Commit.root_tree (Merge<TreeId>) | L57 |
description | string | 5 | The commit message. | Commit.description | L58 |
author | Signature | 6 | Author signature. | Commit.author | L59 |
committer | Signature | 7 | Committer signature. | Commit.committer | L60 |
predecessors | repeated bytes | 8 | Commits this one rewrote (deprecated in jj-lib in favour of the operation's commit_predecessors). | Commit.predecessors | L61 |
conflict_labels | repeated string | 9 | Labels for each term of a conflicted tree. Converted with ConflictLabels::from_vec. | Commit.conflict_labels | L62 |
secure_sig | optional bytes | 10 | Signature bytes, when the commit is signed. On read the client always sets secure_sig: None, so signatures don't survive a round trip, and write_commit ignores its signing function. | SecureSig | L63 |
ReadCommitRequest
backend.proto, lines 66–69 · converted by client, server
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
repo_id | string | 1 | The repository UUID. | — | L67 |
commit_id | bytes | 2 | The 20-byte commit ID. The client answers the root commit locally and never asks for it. | CommitId | L68 |
ReadCommitResponse
backend.proto, lines 71–73 · converted by server
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
commit | Commit | 1 | The stored commit. A missing commit is a NOT_FOUND status, which the client maps to BackendError::ObjectNotFound. | Commit | L72 |
WriteCommitRequest
backend.proto, lines 75–78 · converted by client, server
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
repo_id | string | 1 | The repository UUID. | — | L76 |
commit | Commit | 2 | The commit to store, with commit_id empty. | Commit | L77 |
WriteCommitResponse
backend.proto, lines 80–82 · converted by server
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
commit_id | bytes | 1 | The ID the server computed with compute_git_commit_hash. The client uses it as the commit's ID. | CommitId | L81 |
File
backend.proto, lines 86–90 · converted by tree_entry_to_proto / tree_entry_from_proto
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
id | bytes | 1 | Content ID of the file's blob. | FileId | L87 |
executable | bool | 2 | Executable bit. | bool | L88 |
copy_id | bytes | 3 | Copy-tracking ID. On read it is kept only if it is exactly 20 bytes, and otherwise replaced with 20 zero bytes (the ROOT_COMMIT_ID_BYTES constant). | CopyId | L89 |
TreeValue
backend.proto, lines 92–99 · converted by tree_entry_to_proto / tree_entry_from_proto
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
file | File (oneof | 1 | A regular file. | TreeValue::File | L94 |
symlink_id | bytes (oneof | 2 | A symlink. It can be stored in a tree, but the symlink content RPCs are not implemented. | TreeValue::Symlink | L95 |
tree_id | bytes (oneof | 3 | A subdirectory. | TreeValue::Tree | L96 |
conflict_id | bytes (oneof | 4 | No counterpart in jj-lib 0.43's TreeValue. The client never writes it and rejects it on read ("tree entry should not have contained a ConflictId"). Git submodules have no field here, and the client rejects them on write with BackendError::Unsupported. | — | L97 |
TreeEntry
backend.proto, lines 101–104 · converted by tree_entry_to_proto / tree_entry_from_proto
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
name | string | 1 | One path component. | RepoPathComponentBuf | L102 |
value | TreeValue | 2 | What the name points at. | TreeValue | L103 |
ReadTreeRequest
backend.proto, lines 106–110 · converted by client, server
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
repo_id | string | 1 | The repository UUID. | — | L107 |
tree_id | bytes | 2 | The tree's ID. The server answers the well-known empty-tree ID itself, without storage. | TreeId | L108 |
path | string | 3 | The directory's repo path. The client sends it, and the server ignores it. | RepoPath | L109 |
ReadTreeResponse
backend.proto, lines 112–115 · converted by server
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
tree_id | bytes | 1 | Echo of the requested ID. | TreeId | L113 |
entries | repeated TreeEntry | 2 | The tree's entries. | Tree | L114 |
WriteTreeRequest
backend.proto, lines 117–121 · converted by client, server
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
repo_id | string | 1 | The repository UUID. | — | L118 |
path | string | 2 | The directory's repo path. Sent, and ignored by the server. | RepoPath | L119 |
entries | repeated TreeEntry | 3 | The tree's entries, in the order jj-lib gives them. | Tree | L120 |
WriteTreeResponse
backend.proto, lines 123–125 · converted by server
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
tree_id | bytes | 1 | The ID the server computed with compute_git_tree_hash. | TreeId | L124 |
ReadFileRequest
backend.proto, lines 129–132 · converted by client, server
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
repo_id | string | 1 | The repository UUID. | — | L130 |
file_id | bytes | 2 | The blob's ID. | FileId | L131 |
ReadFileResponse
backend.proto, lines 134–136 · converted by server
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
chunk | bytes | 1 | File content. The RPC is server-streaming, but the server sends the whole file as one chunk. | file bytes | L135 |
WriteFileRequest
backend.proto, lines 138–141 · converted by client, server
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
repo_id | string | 1 | The repository UUID. | — | L139 |
content | bytes | 2 | The whole file in one message. Both the .proto and the server carry a TODO to make this streaming so files over 4MB don't hit gRPC's message size limit. | file bytes | L140 |
WriteFileResponse
backend.proto, lines 143–145 · converted by server
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
file_id | bytes | 1 | The ID the server computed with compute_git_blob_hash. | FileId | L144 |
ReadSymlinkRequest
backend.proto, lines 149–152 · converted by server (unimplemented)
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
repo_id | string | 1 | The repository UUID. | — | L150 |
symlink_id | bytes | 2 | The symlink's ID. | SymlinkId | L151 |
ReadSymlinkResponse
backend.proto, lines 154–156 · converted by server (unimplemented)
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
target | string | 1 | The link target. | String | L155 |
WriteSymlinkRequest
backend.proto, lines 158–161 · converted by server (unimplemented)
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
repo_id | string | 1 | The repository UUID. | — | L159 |
target | string | 2 | The link target. | String | L160 |
WriteSymlinkResponse
backend.proto, lines 163–165 · converted by server (unimplemented)
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
symlink_id | bytes | 1 | The new symlink's ID. | SymlinkId | L164 |
op_store.proto messages
The proto comments flag one subtlety that drives the server's hashing: map fields
(attributes, wc_commit_ids, local_bookmarks,
remote_bookmarks) serialise in no particular order, so "The server MUST perform key-sorted
(or some other deterministic) hashing scheme to generate view_id"
(source). The
object IDs
page shows how it does that.
OperationMetadata
op_store.proto, lines 88–97 · converted by operation_to_proto / operation_from_proto
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
start_time_millis | int64 | 1 | Start time. Only the milliseconds are sent: the time-zone offset is dropped and reads back as 0. | TimestampRange.start | L89 |
end_time_millis | int64 | 2 | End time, likewise without its offset. | TimestampRange.end | L90 |
description | string | 3 | What the operation did, for example describe commit …. | OperationMetadata.description | L91 |
is_snapshot | bool | 4 | Whether this operation only snapshotted the working copy. | OperationMetadata.is_snapshot | L92 |
workspace_name | optional string | 5 | The workspace that ran it, if any. | Option<WorkspaceNameBuf> | L93 |
hostname | string | 6 | Host that ran it. | OperationMetadata.hostname | L94 |
username | string | 7 | User that ran it. | OperationMetadata.username | L95 |
attributes | map<string, string> | 8 | Free-form key/value tags. A proto map, so its wire order isn't deterministic. The proto comment says the server must hash it in a deterministic order. | BTreeMap<String, String> | L96 |
CommitPredecessors
op_store.proto, lines 99–102 · converted by operation_to_proto
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
commit_id | bytes | 1 | A commit created by the operation. | CommitId | L100 |
predecessor_ids | repeated bytes | 2 | The commits it replaced. | Vec<CommitId> | L101 |
Operation
op_store.proto, lines 104–110 · converted by operation_to_proto / operation_from_proto
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
view_id | bytes | 1 | The view this operation produced. | ViewId | L105 |
parents | repeated bytes | 2 | Parent operations. | Vec<OperationId> | L106 |
metadata | OperationMetadata | 3 | See OperationMetadata. A missing one decodes as all defaults. | OperationMetadata | L107 |
commit_predecessors | repeated CommitPredecessors | 4 | One entry per rewritten commit. | Option<BTreeMap<CommitId, Vec<CommitId>>> | L108 |
commit_predecessors_set | bool | 5 | Whether the map is present at all. A repeated field can't tell None from an empty map, so this flag does. | Option::is_some | L109 |
ReadOperationRequest
op_store.proto, lines 26–29 · converted by client, server
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
repo_id | string | 1 | The repository UUID. | — | L27 |
operation_id | bytes | 2 | The 16-byte operation ID. The server synthesises the root operation if it isn't stored. | OperationId | L28 |
ReadOperationResponse
op_store.proto, lines 31–33 · converted by server
WriteOperationRequest
op_store.proto, lines 35–38 · converted by client, server
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
repo_id | string | 1 | The repository UUID. | — | L36 |
operation | Operation | 2 | The operation to store. | Operation | L37 |
WriteOperationResponse
op_store.proto, lines 40–42 · converted by server
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
operation_id | bytes | 1 | The ID the server computed with hash_operation. | OperationId | L41 |
RefTargetTerm
op_store.proto, lines 114–116 · converted by ref_target_to_proto / ref_target_from_proto
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
commit_id | bytes | 1 | One commit in a ref target's add or remove list. | CommitId | L115 |
RefTarget
op_store.proto, lines 118–121 · converted by ref_target_to_proto / ref_target_from_proto, rebuilt with RefTarget::from_legacy_form
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
removes | repeated RefTargetTerm | 1 | The removed terms of a possibly conflicted ref. | RefTarget::removed_ids | L119 |
adds | repeated RefTargetTerm | 2 | The added terms. A plain bookmark has one add and no removes. | RefTarget::added_ids | L120 |
RemoteRef
op_store.proto, lines 123–126 · converted by view_to_proto, view_from_proto
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
target | RefTarget | 1 | Where the bookmark points on the remote. Missing reads as absent. | RefTarget | L124 |
is_tracked | bool | 2 | Tracked or not. | RemoteRefState::Tracked / New | L125 |
View
op_store.proto, lines 132–137 · converted by view_to_proto / view_from_proto
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
head_ids | repeated bytes | 1 | Visible head commits. | HashSet<CommitId> | L133 |
wc_commit_ids | map<string, bytes> | 2 | Working-copy commit per workspace name. | BTreeMap<WorkspaceNameBuf, CommitId> | L134 |
local_bookmarks | map<string, RefTarget> | 3 | Local bookmarks by name. | BTreeMap<RefNameBuf, RefTarget> | L135 |
remote_bookmarks | map<string, RemoteRef> | 4 | Remote bookmarks, flattened into one map keyed "bookmark@remote". On read the key is split at the last @, so a remote name containing @ would not round-trip. | remote_views[remote].bookmarks[bookmark] | L136 |
ReadViewRequest
op_store.proto, lines 46–49 · converted by client, server
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
repo_id | string | 1 | The repository UUID. | — | L47 |
view_id | bytes | 2 | The 16-byte view ID. The server synthesises the root view if it isn't stored. | ViewId | L48 |
ReadViewResponse
op_store.proto, lines 51–54 · converted by server
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
view_id | bytes | 1 | Echo of the requested ID. | ViewId | L52 |
view | View | 2 | The stored view. | View | L53 |
WriteViewRequest
op_store.proto, lines 56–59 · converted by client, server
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
repo_id | string | 1 | The repository UUID. | — | L57 |
view | View | 2 | The view to store. | View | L58 |
WriteViewResponse
op_store.proto, lines 61–63 · converted by server
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
view_id | bytes | 1 | The ID the server computed with hash_view. | ViewId | L62 |
GetOpHeadsRequest
op_store.proto, lines 67–69 · converted by client, server
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
repo_id | string | 1 | The repository UUID. | — | L68 |
GetOpHeadsResponse
op_store.proto, lines 71–73 · converted by server
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
op_head_ids | repeated bytes | 1 | Current operation heads. A repository with none yet reports the root operation's ID. | Vec<OperationId> | L72 |
UpdateOpHeadsRequest
op_store.proto, lines 75–79 · converted by client, server
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
repo_id | string | 1 | The repository UUID. | — | L76 |
old_op_head_ids | repeated bytes | 2 | Heads the new operation replaces. | &[OperationId] | L77 |
new_op_head_id | bytes | 3 | The new head. | OperationId | L78 |
UpdateOpHeadsResponse
op_store.proto, lines 81–83 · converted by server
| Field | Type | Proto # | Meaning | jj-lib type | Source |
|---|---|---|---|---|---|
current_op_head_ids | repeated bytes | 1 | The heads after the update, as the store reports them. | Vec<OperationId> | L82 |
What the wire doesn't carry
Converting jj-lib's types to these messages loses some information. Each item below is visible in the conversion code:
- Tags, Git refs and Git HEAD.
Viewhas no fields for them.view_from_protofillslocal_tagsandgit_refswith empty maps andgit_headwith an absent target, andview_to_protonever reads a remote's tags (source). The proto comment says the Git fields were omitted on purpose. - Operation time zones. Only the millisecond timestamps are sent. Both offsets read back as 0 (source).
- Commit signatures. Written if present, but always read back as
None(source). - Copy IDs that aren't 20 bytes long are replaced with zeros on read (source). Copy history itself isn't supported (
read_copy,write_copyandget_related_copiesreturnUnsupported). - Symlink contents and Git submodules. Symlinks can appear in trees, but their targets can't be stored or read. Submodules are rejected outright.