In short: a command never edits the repo in place. It loads an immutable ReadonlyRepo at
the head operation, records the working copy's current state as a commit, makes its changes in a
Transaction, and finishes by writing a new view and operation and moving the op heads
to it. Undo, concurrency handling and --at-op all build on that.
1. The lifecycle at a glance
2. Startup, config and dispatch
main is one line:
CliRunner::init().version(…).run(). CliRunner::init
assembles everything a custom jj binary can override: the clap app, default config layers,
config migrations, store factories (backends), working-copy factories, the workspace-loader
factory, revset and template extensions, and the dispatch function, which defaults to
commands::run_command.
run_internal then:
- Canonicalizes the current directory and finds the nearest workspace, if any, so that repo and workspace config can be read before parsing arguments.
- Loads config layers in order: system, user, repo, then workspace. Then it resolves and migrates the config, because aliases and the default command depend on config.
- Expands aliases, parses the "early" arguments (
--config,--config-file,--color, …) and adds any config they supply, then runs the full clap parse. - If
-R/--repositorywas given, it reloads repo and workspace config from that path instead. - Builds
UserSettingsand aCommandHelperwrapping everything above, then calls the dispatch function, wrapped in any dispatch hooks.
run_command is one big
match on the parsed Command enum, calling
<module>::cmd_<name>(ui, command_helper, args). Commands with subcommands, such as
git, bookmark and operation, dispatch again inside their module.
git and gerrit exist only with the git feature, and bench only with bench.
3. Loading the repo
Almost every command starts with command.workspace_helper(ui),
which calls workspace_helper_with_stats:
load_workspacefinds.jj/and constructs theWorkspacewith the configured store and working-copy factories.-
If the working copy is writable, it takes the Git import/export lock first, so the
repo it loads matches the op head that Git refs will be synced against. Writable means no
--at-op(or--at-op=@) and no--ignore-working-copy(source). -
resolve_operationpicks the operation to load. With--at-opit resolves that operation. Otherwise it reads the op heads, and if there's more than one (concurrent commands), it printsConcurrent modification detected, resolving automatically.and merges them into a new operation describedreconcile divergent operations. workspace.load_at(&op)builds theReadonlyRepo: that operation, its view, and its index (source).- If the working copy may be snapshotted, it snapshots it (§4) and reports any untracked files.
Read-only commands that must not touch the working copy use
workspace_helper_no_snapshot instead.
4. Snapshotting the working copy
In jj, the files on disk are a commit, @. Before a command runs,
snapshot_impl records them:
- In a repo colocated with Git, it first imports Git's
HEAD, in casegitmoved it. -
snapshot_working_copylocks the working copy and checks it isn't stale (below). It then asks the working copy for a new tree. The local working copy skips re-reading any file whose type, mtime and size still match its storedFileState(FileState::is_clean). -
If the tree changed, it opens a transaction marked
is_snapshotand rewrites the working-copy commit with the new tree. If@has become immutable, it instead creates a new commit on top and warns:The working-copy commit is immutable; a new commit has been created on top of it.It then updateswc_commit_ids, rebases descendants, and commits the transaction as an operation describedsnapshot working copy. - When colocated, it imports Git refs last, which may itself rebase the working-copy commit.
Stale working copies
The working copy records the operation it was last updated at (Checkout.operation_id).
handle_stale_working_copy compares it with the
loaded repo, using WorkingCopyFreshness::check_stale:
| Freshness | Meaning | What happens |
|---|---|---|
Fresh | Same operation as the repo. | Continue. |
Updated(op) | Another process updated the working copy after the repo was loaded. | Reload the repo at that operation and continue. |
WorkingCopyStale | The working copy is behind the repo, and its checked-out tree differs from @'s. A working copy that's behind but already matches counts as Fresh. | Error: The working copy is stale (not updated since operation …), with a hint to run jj workspace update-stale. If snapshot.auto-update-stale is set, jj recovers automatically instead. |
SiblingOperation | The repo and working copy are on sibling operations. | Internal error with a hint to run jj op integrate <op>. |
| operation missing | The working copy's operation can't be read. | Error: Could not read working copy's operation. |
5. Making changes in a transaction
A mutating command calls start_transaction.
It wraps jj-lib's Transaction (a MutableRepo over the loaded
ReadonlyRepo) and tags it via start_repo_transaction:
the workspace name, and an args attribute holding the full command line,
shell-quoted (source). That's how jj op log can show
exactly which command made each operation. The command then works through
tx.repo_mut(): it writes commits, rewrites them, and moves bookmarks or the working-copy
commit.
6. Finishing: rebase, export, write, publish
WorkspaceCommandTransaction::finish(ui, description):
- If nothing changed, it prints
Nothing changed.and records no operation at all. -
rebase_mutable_descendantsrebases every descendant of a rewritten commit, skipping commits that were already immutable, and reportsRebased N descendant commits. -
It takes the Git import/export lock, then
finish_transactionruns. If the new@is immutable, it creates a fresh commit on top. When colocated, it resets Git'sHEADand index and exports jj's refs to Git (jj_lib::git::export_refs). -
maybe_commit_transactioncallsTransaction::write, which writes the view, then an operation pointing at it (with the parent operation, metadata, and thecommit_predecessorsmap of what was rewritten from what), then the index for that operation. Nothing is visible yet. -
publishtakes the op-heads lock and replaces the parent op heads with the new operation. From this moment, other jj processes see the change. With--no-integrate-operation, this step is skipped and the operation ID is printed instead (leave_unpublished). -
The working copy on disk is updated to the new
@, which printsWorking copy (@) now at: …and warns about conflicts. Finally, repo changes are reported, along with a warning ifuser.nameoruser.emailisn't configured.
Because the operation is published before the working copy is updated, a crash in between
leaves the working copy merely stale, never lost. The next command detects it (§4), and
jj workspace update-stale repairs it.
7. Global flags that change the lifecycle
| Flag | Effect on the lifecycle |
|---|---|
-R, --repository <PATH> | Load this workspace instead of searching up from the current directory. Its repo and workspace config are loaded too. |
--ignore-working-copy | Skip the snapshot at the start, and don't update the working copy at the end. |
--at-operation, --at-op <OP> | Load the repo at that operation instead of the op heads. This implies --ignore-working-copy unless the value is @. Mutating commands still work, as if they'd run concurrently from that point. |
--no-integrate-operation | Write the operation but don't publish it. Print its ID instead, and don't update the working copy. Side effects outside the repo, such as a git push, still happen. |
--ignore-immutable | Disable the immutability check on rewrites, except for the root commit. |
--debug | Turn on debug logging. |
--color, --quiet, --no-pager, --config, --config-file | "Early" arguments, parsed before the rest because they affect config and output. |
8. Errors and exit codes
Commands return Result<(), CommandError>. A CommandError has a kind, the
error, and optional hints. handle_command_result
(clap errors)
maps the kind to an exit code:
| Kind | Printed as | Exit code |
|---|---|---|
| success | 0 | |
User | Error: … | 1 |
Config | Config error: … plus a pointer to the config docs | 1 |
Cli | clap's rendered message, or Error: … for other CLI errors. --help and --version also go through this path and print to stdout. | 2 (0 for --help/--version) |
BrokenPipe | nothing; the output was closed (e.g. piped to head) | 3 |
Internal | Internal error: … | 255 |