In short: the backend holds only the server URL, the repository ID and three fixed IDs (root commit,
root change, empty tree). init registers a new repository with the server and writes
config.toml. load reads it back. Each trait method converts jj's types to the
protobuf messages, runs the RPC through run_async, and converts the answer back. The root
commit and the empty tree are answered locally. Symlinks, copy tracking, Git submodules and stored
conflict IDs are unsupported. Commit signatures are sent but never read back.
The module is lib/src/cc_backend.rs, re-exported from
lib/src/lib.rs. The messages it builds are on
protobufs, and the IDs the server returns on
object-ids. jj's own view of a backend is on
/jj.
CommitCloudBackend
| Field | Type | Value |
|---|---|---|
server_url | String | From config.toml, or the init argument. |
repo_id | String | The UUID the server returned from RegisterRepository, sent with every later request. |
root_commit_id | CommitId | 20 zero bytes (ROOT_COMMIT_ID_BYTES). |
root_change_id | ChangeId | 16 zero bytes (ROOT_CHANGE_ID_BYTES). |
empty_tree_id | TreeId | Git's empty tree, 4b825dc6…4904 (EMPTY_TREE_ID_HEX). |
Creating and loading
init(store_path, server_url) connects, calls
RegisterRepository with name: None, and writes
{server_url, repo_id} as pretty TOML to <store>/config.toml. A TODO notes
that the optional repository name isn't passed yet. load(store_path)
only reads that file, and makes no network call until the first object is read or written.
CommitCloudConfig::load_from_store reads
store_path/config.toml if it exists. Otherwise it falls back to
store_path/../store/config.toml, so the op store and op heads store, whose paths are
.jj/repo/op_store and .jj/repo/op_heads, share the backend's file. A missing file
is an error.
The backend is registered under the name commit_cloud by
StoreFactoriesExt::add_commit_cloud, which also
registers the op store and op heads store under the same name. Load errors become
BackendLoadError.
Backend methods
Every method of jj-lib 0.43.0's Backend
trait is implemented (impl):
| Method | Behaviour | RPC | Source |
|---|---|---|---|
name | "commit_cloud" | — | L209 |
commit_id_length, change_id_length | 20 and 16 bytes. | — | L213 |
root_commit_id, root_change_id, empty_tree_id | The fixed fields above. | — | L221 |
concurrency | 1. jj-lib treats this as a hint: a cloud backend "may want to set it to 100 or so" (doc). | — | L233 |
read_file | Streams the file's chunks and concatenates them in memory, then returns a Cursor. The path is ignored. NotFound becomes ObjectNotFound("file"). | ReadFile (server stream) | L237 |
write_file | Reads the whole input into memory and sends it in one unary request. A TODO says to stream 64 KB chunks instead. Returns the server's file ID. | WriteFile | L281 |
read_symlink, write_symlink | Unsupported. | — | L308 |
read_copy, write_copy, get_related_copies | Unsupported("copies not supported"). | — | L316 |
read_tree | The empty tree ID is answered locally. Otherwise it sends the tree ID and path, and converts the entries. NotFound becomes ObjectNotFound("tree"). | ReadTree | L328 |
write_tree | An empty tree returns the empty tree ID without a call. Otherwise it converts the entries (rejecting submodules) and sends them with the path. | WriteTree | L373 |
read_commit | The root commit ID returns jj-lib's make_root_commit locally. Otherwise it fetches and converts the commit. A gRPC NotFound becomes ObjectNotFound("commit"). A response with no commit in it becomes BackendError::Other. | ReadCommit | L398 |
write_commit | Sends the commit with an empty commit_id, and returns the server's ID with the commit as passed in. sign_with is ignored. | WriteCommit | L440 |
get_copy_records | An empty stream. | — | L462 |
gc | Does nothing. | — | L471 |
Conversions
Commits
commit_to_proto copies every field: change ID,
parents, all root tree terms, description, author, committer, predecessors, conflict labels, and the
signature bytes if present. The commit_id is left empty for the server to fill in.
commit_from_proto reverses it, rebuilding the tree
Merge and the labels. It always sets secure_sig: None, so a signature sent on write
is never seen again. Missing signatures and timestamps default to empty or zero
(signature helpers).
Tree entries
jj TreeValue | Write | Read |
|---|---|---|
File { id, executable, copy_id } | All three sent. | copy_id kept only if exactly 20 bytes, otherwise replaced with 20 zero bytes. |
Symlink(id) | The ID is sent (though write_symlink itself is unsupported). | Rebuilt. |
Tree(id) | Sent. | Rebuilt. |
GitSubmodule | Unsupported("Git submodules are not supported"). | — |
(ConflictId on the wire) | — | Error: "tree entry should not have contained a ConflictId". |
tree_entry_to_proto · tree_entry_from_proto
jj-lib's placeholder copy ID is empty (CopyId::placeholder),
so a file written with it reads back with a 20-byte zero copy ID instead. The server never hashes
copy_id (see object-ids), so tree IDs
don't change.
Sync/async bridging and the tonic channel
run_async(f) spawns a new OS thread, builds a
current_thread tokio runtime with all drivers enabled, block_ons the future, and
joins the thread. A panic in the thread becomes an error string. Every backend method then calls
BackendServiceClient::connect(server_url) inside that future, so each read or write of a
single object costs:
- a thread spawn and join,
- a tokio runtime construction,
- a new TCP connection and HTTP/2 handshake to the server,
- the RPC itself.
There is no shared channel, connection pool or timeout.
Errors
Read paths map a gRPC NotFound to BackendError::ObjectNotFound with the object
type and hex ID. Any other error, whether a connection failure, another status, or a conversion error,
becomes BackendError::Other. Write paths make every failure BackendError::Other
(read mapping, write mapping).
Unsupported features return BackendError::Unsupported before any call is made.
What this backend doesn't do
- Symlinks can't be read or written, and copy tracking is off entirely.
- Commit signing is ignored on write, and signatures are dropped on read.
write_commitreturns the commit unchanged, though the jj-lib contract says it "should contain the data that was actually written" (doc). Since the server's ID covers fewer fields than the commit holds (object-ids), two different commits can come back with the same ID.- Whole files are held in memory on both read and write. There is no streaming upload.
- No connection reuse, no timeouts, no retries, no authentication.
concurrencyis 1. gcdoes nothing andget_copy_recordsis always empty.