jj / How jj workspace remove works

How jj workspace remove works

jj workspace remove does what jj workspace forget does and then deletes the workspace's directory. Before deleting anything it snapshots the workspace into a new commit, so changes that were never committed survive. It refuses the workspace that holds the repository, and a workspace with files the snapshot would leave untracked.

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. 7. Errors and edge cases
  8. 8. Worked example

In short: jj requires a writable working copy, skips unknown names with a warning, and for each workspace looks up its directory in the workspace store. It refuses if the directory contains the repo, or now belongs to another repo. It snapshots each workspace at the operation its working copy last recorded, so a stale workspace works too, and fails if any file would stay untracked. Changes go into a new commit on top of the working-copy commit. One transaction (remove workspace <name>) adds those commits and removes the workspaces from the view. Then it deletes their store entries, unlinks Git worktrees, and finally deletes each directory.

The command is cmd_workspace_remove. To stop tracking a workspace without touching the disk, use jj workspace forget, which every error here suggests.

1. Command-line surface

ArgumentTypeDefaultMeaning
WORKSPACES (positional, repeatable)workspace namesrequiredWhich workspaces to remove; unlike forget, there is no default (source).

--ignore-working-copy, --at-op and --no-integrate-operation are refused with This command must be able to update the working copy. plus the forget hint (source, check_working_copy_writable, test).

2. Configuration

None specific. Snapshotting uses the removed workspace's snapshot.auto-track and snapshot.max-new-file-size; a new file outside the first or over the second makes the command fail rather than be lost (source). The Git worktree step uses git.executable-path.

3. End-to-end flow

jj workspace remove: call flow cmd_workspace_remove checks the working copy is writable and filters unknown names. For each workspace it reads the path from the workspace store, canonicalizes it, refuses if it contains the repo or belongs to another repo. It snapshots each workspace at its own last operation, failing on untracked files and writing any changes as a new hidden commit. One transaction adds those commits as heads and removes the workspaces, then it forgets them in the workspace store, unlinks Git worktrees, and deletes each directory. writable working copy required; unknown names → warning; none left → "Nothing changed." for each: path from the workspace store, canonicalized refuse: no path, unreadable, contains the repo, or another repo's workspace all checks happen before anything changes snapshot_workspace for each, at the operation its working copy last recorded untracked files → error; changes → new hidden commit on its working-copy commit one transaction: add_head for each snapshot commit, remove_workspace for each op: "remove workspace NAME" / "remove workspaces A, B" workspace_store.forget(names) (forget keeps them; remove doesn't) unlink_git_worktree for each directory remove_dir_all → "Removed workspace directory …" (failure: warning)

4. Core architecture touchpoints

  • Workspace store: the source of each workspace's directory, and the entries are removed afterwards (forget, get_workspace_path).
  • Working copy: each workspace is loaded from its own directory, with its own environment, and snapshotted against the repo at the operation its working copy last recorded. A stale workspace can therefore be removed; its snapshot builds on the old working-copy commit (snapshot_workspace, test). See the working copy page.
  • View: remove_workspace, which may abandon an empty working-copy commit, exactly as for forget.
  • Git: worktree unlinking, as for forget (unlink_git_worktree).

5. Transaction and operation

  • Snapshots: no operation of their own. A workspace with changes gets a new commit, a child of its working-copy commit described SNAPSHOT OF REMOVED WORKSPACE `<name>`, written hidden from a transaction that is never finished. The working-copy commit isn't rewritten, so nothing is rebased and immutability isn't checked (source).
  • Removal: one transaction adds each snapshot commit as a head and removes the workspaces: remove workspace <name> or remove workspaces <a>, <b> (source).
  • Outside the operation: the store entries, Git worktrees and directories. jj undo brings the workspace back in the view, but its files are gone.

6. Algorithm and invariants

Safety checks before jj workspace remove deletes a directory Each named workspace must have a recorded path, the path must be accessible, it must not contain the repository (so the main workspace can't be removed), and loading a workspace there must give the same repository. Failing any check aborts the whole command before anything is changed. path recorded? "unreachable workspace" accessible? "Cannot access …" not the repo's? "contains the repository" same repo? "another repository" all workspaces pass → snapshot, remove, delete any failure → error with the forget hint (except the main-workspace case); nothing changed

The "contains the repository" check is why the main workspace (whose .jj/repo is the real repo directory) can't be removed; the source deliberately doesn't suggest forget there either (source). The "another repository" check protects a directory that was replaced by an unrelated repo after the workspace was added (test).

Untracked files stop the command; ignored files are lost. If the snapshot leaves a new file untracked (outside snapshot.auto-track or too large) or skips a path that isn't valid UTF-8, the command fails after the snapshot warnings and the directory stays (source). Ignored files aren't reported, so they are deleted with the directory (source). Snapshots run one workspace at a time before the transaction, so if a later workspace fails, nothing is removed.

Invariants: the repo is never deleted; every removed workspace's changes are in a commit first; the view, store and disk agree that the workspace is gone, unless a directory deletion fails, which is only a warning.

7. Errors and edge cases

MessageWhenSource
Cannot remove unreachable workspace '<name>'No path recorded (for example, a workspace from before the store existed).remove.rs
Cannot access workspace '<name>' directoryThe path can't be canonicalized, e.g. it was deleted.remove.rs
Cannot remove workspace '<name>' because it contains the repositoryThe main workspace.remove.rs, test
Cannot remove workspace '<name>' because it belongs to another repositoryThe directory now holds a different repo.remove.rs, test
Cannot remove workspace '<name>' with unsnapshotted changes, after Warning: Refused to snapshot some files:The snapshot left a file untracked or skipped a path that isn't valid UTF-8.remove.rs, test
Warning: The current workspace '<name>' no longer exists after this operation. …Removing the workspace you're in; its directory is still deleted.test
Warning: Failed to remove workspace directory "…".Deleting failed; the workspace is still gone from the repo.remove.rs

8. Worked example

From upstream's test_workspaces_remove_snapshots_before_removal. A workspace secondary is added after initial, then a 14-byte file is written there without running any jj command in it. With --config=snapshot.max-new-file-size=10, jj workspace remove secondary refuses the file, fails with Cannot remove workspace 'secondary' with unsnapshotted changes, and leaves the directory. Without that option:

Snapshot commit created for workspace 'secondary': ff0805347b96
Removed workspace directory "$TEST_ENV/secondary".
The secondary workspace's change survives its removal Before: secondary@ is an empty working-copy commit on initial, and the file unsnapshotted.txt exists only on disk in the secondary directory. After: the old working-copy commit 058f604d stays, empty and without a marker, and a new commit ff080534 on top of it adds unsnapshotted.txt. The directory is deleted. before after jj workspace remove secondary initial default@ secondary@ (empty) + file on disk only initial default@ 058f604d (empty) ff080534 A unsnapshotted.txt

The file is now in a commit (source):

$ jj log --summary
@  rlvkpnrz test.user@example.com 2001-02-03 08:05:08 504e3d8c
│  (empty) (no description set)
│ ○  mzvwutvl test.user@example.com 2001-02-03 08:05:11 ff080534
│ │  SNAPSHOT OF REMOVED WORKSPACE `secondary`
│ │  A unsnapshotted.txt
│ ○  pmmvwywv test.user@example.com 2001-02-03 08:05:09 058f604d
├─╯  (empty) (no description set)
○  qpvuntsm test.user@example.com 2001-02-03 08:05:08 7b22a8cb
│  initial
│  A file
◆  zzzzzzzz root() 00000000

If the workspace was stale (secondary@ rewritten from the main workspace first), the snapshot still builds on the old working-copy commit, which then shows as divergent with the rewritten one (test).