jj-commit-cloud-poc / The client Backend

The client Backend

CommitCloudBackend is jj-lib's commit Backend trait implemented as gRPC calls. Every file, tree and commit read or write becomes one call to the server's BackendService, made on a fresh thread with a fresh runtime and a fresh connection.

On this page
  1. CommitCloudBackend
  2. Creating and loading
  3. Backend methods
  4. Conversions
  5. Commits
  6. Tree entries
  7. Sync/async bridging and the tonic channel
  8. Errors
  9. What this backend doesn't do

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

FieldTypeValue
server_urlStringFrom config.toml, or the init argument.
repo_idStringThe UUID the server returned from RegisterRepository, sent with every later request.
root_commit_idCommitId20 zero bytes (ROOT_COMMIT_ID_BYTES).
root_change_idChangeId16 zero bytes (ROOT_CHANGE_ID_BYTES).
empty_tree_idTreeIdGit's empty tree, 4b825dc6…4904 (EMPTY_TREE_ID_HEX).

struct · constants

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):

MethodBehaviourRPCSource
name"commit_cloud"—L209
commit_id_length, change_id_length20 and 16 bytes.—L213
root_commit_id, root_change_id, empty_tree_idThe fixed fields above.—L221
concurrency1. jj-lib treats this as a hint: a cloud backend "may want to set it to 100 or so" (doc).—L233
read_fileStreams 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_fileReads 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.WriteFileL281
read_symlink, write_symlinkUnsupported.—L308
read_copy, write_copy, get_related_copiesUnsupported("copies not supported").—L316
read_treeThe empty tree ID is answered locally. Otherwise it sends the tree ID and path, and converts the entries. NotFound becomes ObjectNotFound("tree").ReadTreeL328
write_treeAn empty tree returns the empty tree ID without a call. Otherwise it converts the entries (rejecting submodules) and sends them with the path.WriteTreeL373
read_commitThe 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.ReadCommitL398
write_commitSends the commit with an empty commit_id, and returns the server's ID with the commit as passed in. sign_with is ignored.WriteCommitL440
get_copy_recordsAn empty stream.—L462
gcDoes 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 TreeValueWriteRead
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.
GitSubmoduleUnsupported("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:

  1. a thread spawn and join,
  2. a tokio runtime construction,
  3. a new TCP connection and HTTP/2 handshake to the server,
  4. the RPC itself.

There is no shared channel, connection pool or timeout.

One read_commit call through CommitCloudBackend jj-lib calls read_commit. If the ID is the root commit ID, the backend returns make_root_commit without a call. Otherwise run_async spawns a thread with a current-thread tokio runtime. Inside it, BackendServiceClient::connect opens a new connection to the server, sends ReadCommitRequest with the repo ID and commit ID, and receives ReadCommitResponse. A NotFound status becomes BackendError::ObjectNotFound. The thread is joined, and commit_from_proto turns the message into a jj Commit, with secure_sig always None. jj-lib Store read_commit(id) root commit ID? yes → make_root_commit, no RPC run_async new thread + current_thread runtime connect + ReadCommit {repo_id, commit_id} NotFound → ObjectNotFound server BackendService ReadCommitResponse commit_from_proto secure_sig: None

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_commit returns 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. concurrency is 1.
  • gc does nothing and get_copy_records is always empty.