jj / How jj status works

How jj status works

jj status (alias st) snapshots the working copy, then reports on it: what changed compared to the parents, which new files were left untracked, which commits @ sits on, and any conflicts in the working copy or in bookmarks.

On this page
  1. 1. Command-line surface
  2. 2. Configuration
  3. 3. End-to-end flow
  4. 4. Core architecture touchpoints
  5. 5. What it reads, and snapshotting
  6. 6. Algorithm and invariants
  7. Changes against the merged parents
  8. Collapsing untracked paths
  9. Conflicts and the resolving hint
  10. Conflicted bookmarks
  11. 7. Errors and edge cases
  12. 8. Example output

In short: jj loads the workspace and snapshots it, keeping the snapshot's statistics. If the workspace has a working-copy commit, it prints a summary diff of @ against its merged parents (with renames and copies), the untracked paths that snapshot found, collapsed to directories where it can, and one line each for @ and its parents. If @ is conflicted it lists the conflicted paths and may suggest where to start resolving. Finally it lists conflicted local and remote bookmarks. It writes nothing beyond what the snapshot itself records.

The command is cmd_status. The help text: "Show high-level repo status", covering "the working copy commit and its parents, and a summary of the changes in the working copy (compared to the merged parents)", conflicts in the working copy, and conflicted bookmarks (source). The st alias is a built-in default (source).

1. Command-line surface

ArgumentTypeDefaultMeaning
FILESETS (positional, repeatable)filesetall paths"Restrict the status display to these paths" (source, source). It filters the changes, the untracked paths and the conflicted paths; the @/@- lines and the bookmark warnings are always printed.

That is the whole surface: there is no -r and no format flag. The help text points to jj diff --summary -r <rev> for another revision (source); see jj diff.

Global flags such as --at-op and --ignore-working-copy are on the CLI page.

2. Configuration

KeyDefaultEffect
snapshot.auto-trackall()New files outside this fileset are left untracked and listed under "Untracked paths" (source, source, source).
snapshot.max-new-file-size1MiBNew files larger than this are left untracked, warned about on stderr, and listed under "Untracked paths". 0 means no limit (source, source).
templates.commit_summaryformat_commit_summary_with_refs(…)The template for the @ and @- lines (source, source, source).
hints.resolving-conflictstrueWhether to print the "To resolve the conflicts…" hint (source, source).
revset-aliases."immutable_heads()"trunk() | tags() | untracked_remote_bookmarks() | untracked_remote_tags()Conflicted ancestors that are immutable are left out of the hint (source, source).
colors."diff untracked"magentaColour of the ? lines (source, source).
ui.pager, ui.paginateless -FRXK, autoOutput goes through the pager (source, source).

3. End-to-end flow

jj status: call flow After the shared lifecycle, cmd_status loads the workspace with workspace_helper_with_stats, which snapshots it and returns the snapshot statistics, and prints the snapshot warnings. It loads the working-copy commit, turns the paths into a matcher and requests the pager. With no working-copy commit it prints "No working copy." Otherwise it collects the status and warns about explicit paths absent from @'s tree. It then prints either "The working copy has no changes." or the working copy changes and the untracked paths, then the @ and @- lines, then the conflict report or a hint that a parent's conflict was resolved. In both cases it ends by listing conflicted local and remote bookmarks. Shared lifecycle: parse args, load config (CLI page) workspace_helper_with_stats snapshot, keep SnapshotStats print_snapshot_stats (stderr) too-large files, non-UTF-8 names get_wc_commit; parse_file_patterns → matcher; request the pager no working-copy commit "No working copy." collect_working_copy_status; warn about unmatched paths only @'s tree is checked, not the parents' nothing changed "The working copy has no changes." changes summary diff vs merged parents untracked from the snapshot, dirs collapsed @ and @- lines templates.commit_summary conflict report paths, roots hint, or parent hint conflicted local bookmarks, then conflicted remote bookmarks read from the view; printed whether or not there is a working copy

4. Core architecture touchpoints

StructureRead / writeHowExplained on
Working copyread (snapshot)The snapshot both updates @ and reports the new files it left untracked, with a reason for each (source).working copy
Commitsread@ and its parents, loaded by id (source, source).commits
Treesread@'s tree is diffed against the parents' merged tree; the untracked-path walk loads tracked subtrees.trees
Copy recordsreadCollected from each parent to @, filtered by the matcher, so renames and copies show as R and C (source, source).trees
ConflictsreadA commit is conflicted when its tree ids are not resolved (source); the conflicted paths come from conflicts_matching (source).conflicts
Revset enginereadOnly for the hint: conflicted, mutable ancestors of @, then their roots (source, source).revsets
ViewreadLocal and remote bookmarks whose target is not resolved (source).view
Operation logwritten only by the snapshotIf the files changed, the snapshot records a snapshot working copy operation; status starts no transaction of its own.operations

5. What it reads, and snapshotting

  • Snapshot: yes, and the statistics matter. status calls workspace_helper_with_stats rather than workspace_helper (source, source), because the "Untracked paths" section is built from SnapshotStats.untracked_paths. That list is not stored anywhere; it exists only for the snapshot just taken (source).
  • Without a snapshot: with --ignore-working-copy, or at an --at-op that is not the head operation, no snapshot runs and the statistics are empty (source, source), so no untracked paths are listed, and the changes shown are those recorded at the last snapshot.
  • Ignored files are not "untracked": a new file matching .gitignore is skipped silently and never appears (source, test).
  • Output: snapshot warnings and the unmatched-paths warning go to stderr. Everything else, including the conflict and bookmark warnings, goes to stdout through the pager. The changes are rendered into a buffer first, so the "Working copy changes:" header is printed only if the filtered diff is not empty (source).

6. Algorithm and invariants

Changes against the merged parents

collect_working_copy_status takes @, its parents, @'s tree and the parents' merged tree (source). For a merge commit the parent tree is the auto-merge of all parents, so a clean merge with no edits on top shows no changes (test). The changes are rendered with the ordinary diff renderer in Summary format, one M/A/D/R/C line per path (source).

Whether anything changed is decided on the whole tree, before the path filter: tracked changes exist when the two trees' ids differ (source), and untracked changes when the snapshot found any untracked path (source). So jj status file_1 prints "The working copy has no changes." only when nothing changed anywhere. If only other files changed it prints neither that line nor a header, just the @/@- lines (test, test).

Collapsing untracked paths

The untracked paths that match the filter are passed, in sorted order, to visit_collapsed_untracked_files together with @'s tree. For each path it walks the parent directories down a stack of tracked subtrees. At the first directory with no tracked subtree it prints that directory with a trailing separator and skips every later path under it; if every directory exists in the tracked tree it prints the file itself (source). The stack is reused between consecutive paths, so siblings do not reload the same subtrees (source).

How jj status collapses untracked paths Upstream's unit test. Eight untracked paths and a tracked tree holding top_level_file, dir2/d, dir3/partially_tracked/f and dir3/j produce six lines. dir/a and dir/b become dir/ because dir is not tracked at all; dir3/fully_untracked/g and h become dir3/fully_untracked/. dir2/c, dir3/partially_tracked/e and dir3/k stay as files because their directories are tracked. untracked paths (sorted) untracked_top_level_file dir/a dir/b dir2/c dir3/partially_tracked/e dir3/fully_untracked/g dir3/fully_untracked/h dir3/k tracked tree (@) top_level_file dir2/d dir3/partially_tracked/f dir3/j no dir/ subtree no dir3/fully_untracked/ subtree printed ? untracked_top_level_file ? dir/ ? dir2/c ? dir3/partially_tracked/e ? dir3/fully_untracked/ ? dir3/k per path: walk its parent directories down the tracked tree a directory with no tracked subtree → print "dir/", skip later paths under it every directory tracked → print the file

Paths are printed relative to the current directory, so from inside an untracked sub/ the collapsed entry is ./ (test). A file that was untracked with jj file untrack but still exists shows up as D in the changes and again under "Untracked paths" (test); see jj file untrack.

Conflicts and the resolving hint

If @ has a conflict, status warns and prints each conflicted path that matches the filter with its number of sides and anything unusual among them (a deletion, an executable, a symlink, a directory or a submodule), after simplifying the conflict (source, source). It then finds the ancestors of @'s parents that have conflicts, minus the immutable set (source), and passes them to report_repo_conflicts. That prints the hint only if the list is not empty and hints.resolving-conflicts is on; it names the roots of those commits as jj new <change id> lines, so with a chain of conflicted commits it points at the first one (source, test). If @ is the only conflicted commit, as when a merge conflicts in @ itself, no hint is printed.

If @ is not conflicted but a parent is, it prints "Hint: Conflict in parent commit has been resolved in working copy." once (source). jj resolve is the tool the hint suggests.

Conflicted bookmarks

Last, for any workspace state, it reads the view: local bookmarks whose target is not resolved, and remote bookmarks (name@remote) whose target is not resolved (source). Each group gets a warning, one indented name per line, and a hint: jj bookmark set for local ones, fetching again for remote ones (source). A conflicted bookmark arises from concurrent operations, as in the test; see how the view merges refs.

Invariants: status never rewrites a commit or records an operation of its own; any operation comes from the snapshot. The path filter never hides the @/@- lines or the bookmark warnings, and never changes whether "no changes" is printed.

7. Errors and edge cases

MessageWhenSource
No working copy.The workspace has no working-copy commit, for example after jj workspace forget. Bookmark conflicts are still reported.status.rs, test
Warning: No matching entries for paths: <paths>An explicit path is absent from @'s tree. Only @'s tree is checked, so a path deleted in @, or an untracked file that is listed under "Untracked paths", still triggers it.status.rs, cli_util.rs, test
Warning: Refused to snapshot some files: with sizes, and a hint to raise snapshot.max-new-file-sizeA new file is larger than the limit. The hint's example command is jj --config snapshot.max-new-file-size=<n> status.cli_util.rs, cli_util.rs
Warning: Skipped some paths because they are not valid UTF-8:A file name cannot be represented as a repo path.cli_util.rs
Warning: There are unresolved conflicts at these paths:@ is conflicted. Printed even if the filter matches no conflicted path.status.rs
Warning: These bookmarks have conflicts: / These remote bookmarks have conflicts:A local or remote bookmark has a conflicted target.status.rs
A working-copy errorThe snapshot fails, or the working copy is stale and snapshot.auto-update-stale is off (the default).cli_util.rs, working copy

8. Example output

From upstream's test_status_copies: @ modifies copy-source, adds copy-target as a copy of it, and renames rename-source. Copy records turn the add and the delete into C and R:

$ jj status
Working copy changes:
M copy-source
C {copy-source => copy-target}
R {rename-source => rename-target}
Working copy  (@) : rlvkpnrz c2fce842 (no description set)
Parent commit (@-): qpvuntsm ebf799bc (no description set)

With snapshot.auto-track = "none()", from test_status_untracked_files, after two files were tracked explicitly and committed. sub/ now has a tracked file, so its untracked file is listed by name:

$ jj status
Untracked paths:
? always-untracked-file
? sub/always-untracked
Working copy  (@) : mzvwutvl daa133b8 (empty) (no description set)
Parent commit (@-): qpvuntsm b8c1286d (no description set)

A conflict two commits back, from upstream's conflict hints test. The hint names mzvwutvl, the merge where the conflict started, not the parent:

$ jj status
The working copy has no changes.
Working copy  (@) : yqosqzyt 06b8a9dd (conflict) (empty) boom-cont-2
Parent commit (@-): royxmykx fc966143 (conflict) (empty) boom-cont
Warning: There are unresolved conflicts at these paths:
conflicted1.txt    2-sided conflict
conflicted2.txt    2-sided conflict
Hint: To resolve the conflicts, start by creating a commit on top of
the first conflicted commit:
  jj new mzvwutvl
Then use `jj resolve`, or edit the conflict markers in the file directly.
Once the conflicts are resolved, you can inspect the result with `jj diff`.
Then run `jj squash` to move the resolution into the conflicted commit.