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
| Argument | Type | Default | Meaning |
|---|---|---|---|
WORKSPACES (positional, repeatable) | workspace names | required | Which 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
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 forforget. - 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>orremove workspaces <a>, <b>(source). - Outside the operation: the store entries, Git worktrees and directories.
jj undobrings the workspace back in the view, but its files are gone.
6. Algorithm and invariants
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
| Message | When | Source |
|---|---|---|
Cannot remove unreachable workspace '<name>' | No path recorded (for example, a workspace from before the store existed). | remove.rs |
Cannot access workspace '<name>' directory | The path can't be canonicalized, e.g. it was deleted. | remove.rs |
Cannot remove workspace '<name>' because it contains the repository | The main workspace. | remove.rs, test |
Cannot remove workspace '<name>' because it belongs to another repository | The 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_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 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).