In short: .jj/working_copy/ holds two small files. checkout records which
operation and workspace the files on disk belong to. tree_state records the checked-out
tree plus, for every tracked file, its type, mtime and size. Snapshotting walks the directory in
parallel, re-reads only files whose recorded state no longer matches, and writes a new tree. The
repo layer then rewrites @ to point at it.
1. The interface
Working copies are pluggable. The CLI picks a WorkingCopyFactory
by name; local is the default.
| Trait | Methods |
|---|---|
WorkingCopy (unlocked, read-only) |
workspace_name, operation_id, tree, sparse_patterns, and start_mutation(), which takes the lock and returns a LockedWorkingCopy. |
LockedWorkingCopy |
old_operation_id and old_tree (as of locking); snapshot(options);
check_out(commit); reset(commit) and recover(commit), which
repoint the state without touching files; set_sparse_patterns;
rename_workspace; and finish(operation_id), which writes the state
with the new operation and releases the lock.
|
SnapshotOptions and stats
| Field | Meaning | From config |
|---|---|---|
base_ignores | Ignore patterns to start from (user and repo excludes). Each directory's .gitignore is layered on during the walk. | Git exclude files |
start_tracking_matcher | New, untracked files that match start being tracked. | snapshot.auto-track, default all() |
force_tracking_matcher | Track these even if they're ignored or too large (used by jj file track). | |
max_new_file_size | Larger new files stay untracked. Already-tracked files are always snapshotted. | snapshot.max-new-file-size, default 1MiB |
progress | Callback for the progress bar. |
A snapshot returns SnapshotStats: untracked paths, each with an UntrackedReason
(FileTooLarge { size, max_size } or FileNotAutoTracked), plus paths skipped
because their names aren't valid UTF-8. A checkout returns CheckoutStats: updated, added,
removed, and skipped files, where skipped means an untracked file was already in the way
(source,
defaults).
2. The stored state
| File | Contents | Rust |
|---|---|---|
.jj/working_copy/checkout | The operation the files were last updated at, and this workspace's name | CheckoutState |
.jj/working_copy/tree_state | The checked-out tree (as Merge<TreeId> plus conflict labels), one FileState per tracked path, sorted, the sparse patterns, and the last Watchman clock | TreeState |
.jj/working_copy/working_copy.lock | Held while a LockedWorkingCopy exists | start_mutation |
Each FileState holds a file_type
(Normal { exec_bit }, Symlink or GitSubmodule), mtime,
size, and materialized_conflict_data, which is set when the file on disk
holds conflict markers and records their length. The exact encoding is on
Every schema.
start_mutation re-reads both files after taking the lock, so a locked working copy
always sees the latest state.
3. Snapshotting
- Builds the matcher: the sparse patterns, intersected with the union of the fsmonitor's changed paths and the force-tracking matcher. Without an fsmonitor, the fsmonitor part matches everything. If the matcher can't match anything, the snapshot returns immediately.
-
Walks the directory as a queue of per-directory jobs in a rayon scope, layering each
directory's
.gitignore. Paths that were tracked but are no longer on disk are reported as deleted (emit_deleted_files). -
For each present file,
get_updated_tree_valuedecides whether it's clean. It's clean when it was tracked, its type, mtime and size match the storedFileState, and its mtime is older than thetree_statefile's own mtime. That last check avoids a race: a file modified in the same instant the state was saved can't be trusted. Clean files are skipped entirely. -
Otherwise the file is written to the store, keeping the existing
copy_idand, where the filesystem can't express it, the executable bit. If the path was conflicted in the tree, the contents are parsed for conflict markers withconflicts::update_from_content, using the recorded marker length or the minimum of 7. A fully resolved file becomes a normal file; partly edited markers update the individual conflict sides (write_path_to_store). -
Results come back over channels and are folded in: new tree values go into a
MergedTreeBuilderseeded with the old tree, deleted paths are removed, changed file states are merged into the sorted map, and the builder writes the new tree. - The Watchman clock is saved, but only if no untracked paths were found, so untracked files get rescanned next time.
The repo layer then compares the new tree with @'s tree and, if they differ, records a
snapshot working copy operation. See
How a command runs.
4. Checking out
check_out(commit) diffs the old tree against the new one within the sparse patterns and
applies each change on disk (TreeState::update).
Files, symlinks and executable bits are written directly. A conflicted path is
materialized: its sides are rendered into one file with conflict markers in the configured
ui.conflict-marker-style, and the marker length is recorded in its FileState
so the next snapshot can parse it back (write_conflict).
New files are created with create_new, so an untracked file already at that path is never
overwritten. It's counted as skipped instead. Paths that resolve into .git or
.jj are rejected.
After checkout, the state is saved with the new tree and fresh file states, and
finish(op_id) writes the operation into checkout.
5. Stale working copies
Each workspace's working copy records the operation it was updated at. The repo's view records the
commit it should have (wc_commit_ids). WorkingCopyFreshness::check_stale
compares the two using the closest common ancestor of the working copy's operation and the repo's:
| Common ancestor is… | Result |
|---|---|
| (operations equal) | Fresh |
| the repo's operation | Updated: another process moved the working copy forward. Reload the repo there. |
| the working copy's operation | Fresh if the checked-out tree already equals @'s tree, otherwise WorkingCopyStale. |
| neither | SiblingOperation |
A stale working copy is repaired by jj workspace update-stale, or automatically with
snapshot.auto-update-stale. It checks out the commit the repo expects.
6. Settings and file monitors
| Setting | Effect |
|---|---|
ui.conflict-marker-style | How conflicts are materialized on checkout and parsed on snapshot. |
working-copy.eol-conversion | Optional CRLF ↔ LF conversion on snapshot and checkout. |
working-copy.exec-bit-change | respect, ignore or auto (default): whether executable-bit changes on disk count. auto checks whether the filesystem supports executable bits (source). |
fsmonitor.backend | none or watchman. With Watchman, only paths reported changed since the stored clock are examined. |
TreeStateSettings · FsmonitorSettings
Sparse patterns are currently plain path prefixes. Paths outside them are neither snapshotted nor
checked out. jj sparse set changes them through
LockedWorkingCopy::set_sparse_patterns, which checks out or removes the affected files.