jj / How jj workspace update-stale works

How jj workspace update-stale works

A workspace's files are stale when another workspace (or a concurrent command) changed its working-copy commit, so the files on disk no longer match it. jj workspace update-stale saves any edits made to the stale files, then checks out the commit the repo now says the workspace should have. If the operation the files came from has been lost, it makes a recovery commit instead.

On this page
  1. 1. Command-line surface
  2. 2. Configuration
  3. 3. End-to-end flow
  4. 4. Core architecture touchpoints
  5. 5. Transaction and operation
  6. 6. Algorithm and invariants
  7. Freshness
  8. Recovery commit
  9. 7. Errors and edge cases
  10. 8. Worked example

In short: jj loads the repo at the operation the working copy last recorded, snapshots the files there (so edits aren't lost), and reloads at the head operation. If the working copy really is stale, it resets Git's HEAD in a colocated workspace, checks out the desired working-copy commit, and prints Updated working copy to fresh commit …. If not, it prints Attempted recovery, but the working copy is not stale. If the working copy's operation can't be read, it creates a RECOVERY COMMIT on the current working-copy commit and checks that out.

The command is cmd_workspace_update_stale, a thin wrapper around recover_stale_working_copy. Workspaces become stale because several share one repo; see jj workspace add and the working copy page.

1. Command-line surface

No arguments (WorkspaceUpdateStaleArgs). The command must be able to update the working copy, so --ignore-working-copy is refused (source, test).

2. Configuration

KeyDefaultEffect
snapshot.auto-update-stalefalseWhen true, any command that finds the working copy stale runs this same recovery itself instead of failing (default, use).

3. End-to-end flow

jj workspace update-stale: call flow The command loads the operation recorded in the working copy. If it can't be found, jj creates and checks out a recovery commit. Otherwise it loads the repo at that operation, snapshots the files, and reloads at the head operation. It then checks freshness against the desired working-copy commit. Fresh or updated: it says the working copy is not stale. Stale or sibling: it resets Git HEAD if colocated, checks out the desired commit, and prints Updated working copy to fresh commit. Finally it snapshots again to import Git refs. load the working copy's recorded operation not found "Failed to read working copy's current operation; …" → recovery commit (§6) found: load the repo there; snapshot the files edits to the stale files go into a commit on that old op reload at the head op (merging with the snapshot op) desired = the view's working-copy commit WorkingCopyFreshness::check_stale(locked wc, desired, repo) Fresh / Updated "Attempted recovery, but the working copy is not stale." Stale / Sibling colocated: reset git head check out the desired commit "Updated working copy to fresh …" snapshot again (imports Git refs and HEAD if colocated) print untracked-file stats from both snapshots

4. Core architecture touchpoints

  • Working copy: each working copy records the operation it was last updated to. Comparing that with the repo's head operation is how staleness is detected (WorkingCopyFreshness). The check-out itself verifies the files still match the stale commit, so a concurrent change fails with Concurrent working copy operation. Try again. (update_stale_working_copy).
  • Operations: the snapshot is written on the old operation, so it may diverge from the head; loading the repo again merges the two. A source comment notes that the merged repo's working-copy commit stays the one it already had (source). See operations.
  • Git: in a colocated workspace, Git's HEAD is moved to the new working-copy parent first, as its own reset git head operation (source).

5. Transaction and operation

  • Snapshot: snapshot working copy, on the stale operation, only if the files had changed.
  • reset git head, colocated workspaces only, if HEAD moved.
  • The check-out records no operation: the repo already says where the working copy should be; only the working copy catches up (source). When nothing is stale, nothing at all is recorded (test).
  • Recovery: recovery commit, with the workspace's name in its metadata (source).

6. Algorithm and invariants

Freshness

How WorkingCopyFreshness classifies a working copy If the working copy's operation is the repo's operation, it is Fresh. Otherwise jj finds their closest common ancestor operation. If that is the repo's operation, the working copy is ahead: Updated. If it is the working copy's operation, the working copy is behind: Stale, unless the files already match the commit, which is Fresh. Otherwise they are siblings: SiblingOperation. wc op == repo op → Fresh; otherwise find their closest common ancestor A A == repo op wc is ahead → Updated A == wc op wc is behind → Stale neither → SiblingOperation files already match the commit's tree → Fresh

An ordinary command treats Stale as an error (The working copy is stale …, with a hint to run this command) and SiblingOperation as an internal error suggesting jj op integrate (source). This command handles both by checking out the desired commit. A source TODO notes that only one common ancestor is tested (source).

Recovery commit

If the working copy's operation is gone (for example, abandoned and garbage-collected), jj can't know which commit the files were based on. It makes a new commit on top of the workspace's current working-copy commit with a description starting RECOVERY COMMIT FROM `jj workspace update-stale`, explaining that its diff may include changes from several commits. It makes that commit the working copy's recorded state (recover), then snapshots, so the files on disk end up in it (source, create_and_check_out_recovery_commit).

Invariants: edits in the stale files are snapshotted before anything is checked out, so they aren't lost; untracked files are reported, not deleted.

7. Errors and edge cases

MessageWhenSource
Attempted recovery, but the working copy is not stale.Nothing to do.cli_util.rs, test
This command must be able to update the working copy., hint Don't use --ignore-working-copy.That flag was given.cli_util.rs
Concurrent working copy operation. Try again.The files changed again during recovery.cli_util.rs
Failed to read working copy's current operation; attempting recovery. … then Created and checked out recovery commit …The recorded operation is missing.cli_util.rs, test

8. Worked example

From upstream's test_workspaces_updated_by_other. Workspaces default and secondary both sit on commit 9a462e35. In default, a change to file is squashed into that parent, which rewrites it to b853f7c8 and rebases secondary@ onto it. The secondary directory's files now lag behind:

A secondary workspace made stale by jj squash in default Before: default@ 393250c5 and secondary@ 54703666 on 9a462e35. After the squash in default: default@ 3a9b690d and secondary@ 90f3d42e on b853f7c8, but the secondary directory's files still match the old commit, so the working copy is stale until jj workspace update-stale checks out 90f3d42e. before after jj squash in default root 9a462e35 393250c5 default@ 54703666 secondary@ root b853f7c8 rewritten parent 3a9b690d default@ 90f3d42e secondary@; files still old

Upstream's output in the secondary directory (source):

$ jj st
Error: The working copy is stale (not updated since operation 8e2504e2592d).
Hint: Run `jj workspace update-stale` to update it.
See https://docs.jj-vcs.dev/latest/working-copy/#stale-working-copy for more information.

$ jj workspace update-stale
Working copy  (@) now at: pmmvwywv 90f3d42e (empty) (no description set)
Parent commit (@-)      : qpvuntsm b853f7c8 (no description set)
Added 0 files, modified 1 files, removed 0 files
Updated working copy to fresh commit 90f3d42e0bff

The secondary files were clean, so the snapshot recorded nothing and no divergent commit was created (source).