jj-commit-cloud-poc / The custom jj binary

The custom jj binary

The cli crate builds a binary named jj. It is stock jj-cli 0.43.0 with two additions: the commit-cloud store factories, and one subcommand, jj cc init, which registers a repository on a server and creates a local workspace that points at it.

On this page
  1. main.rs
  2. jj cc init
  3. Step by step
  4. What crosses the wire
  5. Stock commands on commit-cloud stores
  6. What the binary doesn't do

In short: main.rs is 47 lines. It hands CliRunner a set of store factories named commit_cloud and a cc subcommand. jj cc init runs jj-lib's ordinary workspace initialisation with the commit-cloud stores swapped in. After that, every other command is jj-cli's own: it finds commit_cloud in the repo's type files and reads and writes through gRPC without knowing it.

main.rs

main builds the runner in three calls:

CallWhat it addsSource
CliRunner::init()Stock jj-cli, including its default store factories (local, Git, simple).cli_util.rs
.add_store_factories(create_store_factories())A StoreFactories::empty() with only the commit-cloud backend, op store and op heads store, all named commit_cloud. jj-cli merges these into its defaults, so this binary still opens ordinary jj repositories.L23–27, repo.rs, merge
.add_subcommand(run_custom_command)The cc command group, whose only member is init. The handler ignores the Ui and CommandHelper jj-cli passes it.L9–21, L29–39

The index store, submodule store and working copy are jj-lib's defaults. Only commits, trees, files, operations, views and op heads live on the server.

jj cc init

Arguments (CcInitArgs):

ArgumentDefaultEffect
--server <URL>requiredThe gRPC endpoint, for example http://localhost:8080. Stored in config.toml.
--createoffIgnored: let _ = args.create; (L34). A TODO asks for a server-side guard (L22). Every init registers a new repository, flag or not.
[DESTINATION].The workspace root. It must exist and must not already contain .jj.

Step by step

  1. Build UserSettings from StackedConfig::with_defaults() and a Signer from them (L37–42). This is jj's built-in default config only. The user's config files aren't read by this command, so the first operation and working-copy commit don't carry the user's name or settings.
  2. Define three initialiser closures (L44–67). The backend one calls CommitCloudBackend::init. The op store and op heads store ones call their load, because they have nothing to create.
  3. Call jj-lib's Workspace::init_with_factories with those closures and the default index, submodule and working-copy factories (L69–83). From here jj-lib drives:
    1. Create .jj/ (create_jj_dir). If it already exists, init fails with DestinationExists.
    2. ReadonlyRepo::init creates .jj/repo/store/ and runs the backend closure. It calls RegisterRepository, receives a new UUID, and writes store/config.toml with server_url and repo_id. jj-lib then writes store/type = commit_cloud.
    3. It creates op_store/ and op_heads/, runs their closures and writes their type files. Both find the config through the ../store/config.toml fallback (util.rs), which works only because the backend closure has already run. The op heads store ignores the root operation ID it is given, and nothing is written for it on the server. An empty repository already reports the root as its head.
    4. It creates the local index/ and submodule_store/, then loads the root operation and its view from the server.
    5. init_working_copy starts a transaction and checks out a new empty commit on top of the root. Committing the transaction writes the commit, the view and the operation "add workspace 'default'", then publishes it.
    6. The local working-copy state is written under .jj/working_copy/, and the workspace is recorded.
  4. Print "Initialized remote Commit Cloud repository" (L85).

Any error is turned into a user_error. If anything inside init_with_factories fails, jj-lib deletes the new .jj/ (L338–341). The repository it may already have registered on the server stays registered, and nothing ever unregisters it.

What crosses the wire

The sequence below shows what jj cc init sends, in order. Every RPC opens its own connection (see client-backend). It leaves out the backend's commit reads.

The writes jj cc init makes, locally and over gRPC Three lifelines: the local .jj directory, the jj process, and jj-cc-server. jj creates .jj/repo/store, calls RegisterRepository and gets a repo ID back, then writes store/config.toml and store/type. It creates op_store, op_heads, index and submodule_store with their type files. It reads the root operation and root view from the server, then writes the working-copy commit with WriteCommit, the view with WriteView and the operation with WriteOperation. Finally it calls UpdateOpHeads with old heads [root] and the new operation, and writes the local working-copy state. local .jj/ jj cc init jj-cc-server mkdir .jj/repo/store RegisterRepository repo_id (UUID v4) store/config.toml, store/type op_store/, op_heads/, index/, submodule_store/ + type files ReadOperation, ReadView (root) WriteCommit (working copy) WriteView WriteOperation UpdateOpHeads([root] → new op) working_copy/ state

Commit, view and operation IDs are computed or accepted as described in object-ids. The op-heads lock taken before UpdateOpHeads is a no-op (L89–91).

Stock commands on commit-cloud stores

Once the workspace exists, jj describe, jj log, jj op log and the rest are jj-cli's code, unchanged (see /jj/cli). Loading the workspace reads each type file under .jj/repo/ and looks the name up in the merged factories. For commit_cloud, each factory's load reads config.toml and makes no network call (repo.rs). From then on, every Backend, OpStore and OpHeadsStore call jj-lib makes becomes an RPC. The working copy and index stay local.

A command that changes the repository ends the same way init does: a transaction writes its new objects, then the view (write_view), then the operation (write_operation), then publishes with update_op_heads. So WriteView, WriteOperation and UpdateOpHeads close every write, in that order. A snapshot of a changed file adds WriteFile and WriteTree before the commit. The upstream tests exercise this with describe, log and op log (see testing). Commands that need Git or other backends' features aren't covered by anything upstream.

What the binary doesn't do

  • --create does nothing, and there is no way to attach a second workspace to an existing remote repository: cc init always registers a new one.
  • cc init uses jj's default config, not the user's.
  • A failed init can leave an orphan registration on the server.
  • Concurrent writers from two workspaces lose operations silently (client-op-store).
  • The binary is named jj, so installing it shadows a stock jj on the PATH.