jj / How jj workspace add works

How jj workspace add works

jj workspace add creates another working copy of the same repo in a new directory. The new workspace gets its own .jj that points back at the existing repo, its own sparse patterns, and its own working-copy commit, by default on the same parents as the current one.

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 picks a name (the destination's basename unless --name), refuses a name already in use or a non-empty directory, and creates the directory. In a colocated repo it may first add a Git worktree there. It writes .jj/repo as a pointer to the existing repo, records the workspace (operation add workspace '<name>', on the root commit), copies the sparse patterns, and then creates the real working-copy commit on the chosen parents (operation create initial working-copy commit in workspace <name>).

The command is cmd_workspace_add. Its counterparts are jj workspace forget and jj workspace remove. How a workspace's working copy is tracked is on the working copy page.

1. Command-line surface

FlagTypeDefaultMeaning
DESTINATION (positional)path—Where to create the workspace (source).
--name NAMEworkspace namebasename of the destination(source)
-r, --revisionrevsets, repeatableparents of the current @Parents of the new working-copy commit, as if by jj new r1 r2 … (source).
-m, --messagestring, repeatableemptyDescription of the new working-copy commit (source).
--colocate / --no-colocateboolsee §2Create (or not) a Git worktree for the workspace (source).
--sparse-patternscopy, full, emptycopyCopy the current workspace's patterns, include everything, or include nothing (source).

WorkspaceAddArgs

2. Configuration

Without --colocate or --no-colocate, a Git worktree is created only when the current workspace is colocated and git.colocate is true (its default) (source). -m descriptions get the usual configured trailers added (source). The new workspace doesn't get settings of its own: jj uses the current workspace's settings to create it (source).

3. End-to-end flow

jj workspace add: call flow cmd_workspace_add picks and checks the name, creates the destination directory or requires it to be empty, and maybe creates a Git worktree. init_workspace_with_existing_repo writes .jj/repo pointing at the repo, commits add workspace with a checkout of the root commit, and records the path in the workspace store. The command writes .jj/.gitignore if colocated, applies the sparse patterns, then in a transaction creates the working-copy commit on the chosen parents, edits it, and finishes as create initial working-copy commit in workspace. name = --name or basename; refuse empty or existing name create the directory, or require it to be empty colocated? git::create_worktree → "Created Git worktree for the new workspace." Workspace::init_workspace_with_existing_repo .jj/repo = path to the repo; check out root → op "add workspace 'NAME'" workspace_store.add(name, path) "Created workspace in …"; warn if DESTINATION has no separator; .jj/.gitignore set sparse patterns (copy / full / empty) in the new working copy transaction: parents = -r, or the current @'s parents (root if none) tree = merge of parent trees; new commit with -m description and trailers tx.edit(new commit) → op "create initial working-copy commit in workspace NAME" working copy updated: "Added N files, …"

4. Core architecture touchpoints

  • Workspace layout: the new .jj has a repo file holding a path (relative if possible) to the existing .jj/repo directory, and its own working_copy directory (source). That file is how jj tells a secondary workspace from the main one.
  • Workspace store: .jj/repo/workspace_store/index maps names to paths relative to the repo, written under a lock (add). Other commands use it to find a workspace's directory, for example jj workspace root --name and jj workspace remove.
  • View: each workspace has an entry in the view's wc_commit_ids, which is what makes it exist in the repo; see the view page.
  • Git: with colocation, git worktree add --orphan runs first, so the new directory also works with plain git (create_worktree); .jj/.gitignore keeps Git from seeing jj's files (maybe_add_gitignore).

5. Transaction and operation

  • Two operations: add workspace '<name>', which checks out a new commit on the root (init_working_copy), then create initial working-copy commit in workspace <name>, which replaces it with the real one (source). The first, now-empty commit is abandoned by edit, so it doesn't show up (see the two operations in a test's op log).
  • Attribution: both operations are recorded as performed in the workspace the command was run in, not the new one (source, source). So jj undo there undoes them, instead of refusing an operation from another workspace (source).
  • Sparse patterns are written to the new working copy's state without an operation (source).

6. Algorithm and invariants

Where the new workspace's working-copy commit goes Without -r, the new working-copy commit is a sibling of the current workspace's working-copy commit: it has the same parents. With -r, its parents are the resolved revisions, like jj new. Without a current working-copy commit, it goes on the root commit. no -r: share the parents of the current @ -r A -r B: like jj new A B root P default@ NAME@ new root A B NAME@ new, merge of A and B

The new commit's tree is the merge of its parents' trees, so it starts with no changes of its own (source). A description is set only if -m is given; trailers are added only then, because a commit without a description is treated as discardable once it's no longer a working copy, and a trailer-only description would break that (source).

Invariants: names are unique and non-empty; jj never writes into a non-empty directory; the current workspace is unchanged.

7. Errors and edge cases

MessageWhenSource
This command must be able to update the working copy.Run with --ignore-working-copy or --no-integrate-operation, checked before anything else; the hint names the flag to drop. --at-op is allowed.add.rs, check
New workspace name cannot be empty--name ""; nothing is created.add.rs, test
Destination path is not valid UTF-8The basename can't be a name.add.rs
Workspace named '<name>' already existsThe name is in the view; nothing is created.add.rs, test
Destination path exists and is not an empty directoryIncluding re-adding a forgotten workspace in place, which a source TODO says should work.add.rs, test
Warning: Workspace created inside current directory. …DESTINATION has no path separator, so it may have been meant as a name.add.rs, test

8. Worked example

From upstream's test_workspaces_add_second_and_third_workspace. After jj commit -m initial in workspace default, jj workspace add --name second ../secondary creates a sibling of default@ on initial and checks out its one file:

Created workspace in "../secondary"
Working copy  (@) now at: rzvqmyuk bcc858e1 (empty) (no description set)
Parent commit (@-)      : qpvuntsm 7b22a8cb initial
Added 1 files, modified 0 files, removed 0 files

Both workspaces then show in the log and in jj workspace list (source):

@  504e3d8c1bcd default@
│ ○  bcc858e1d93f second@
├─╯
○  7b22a8cbe888 "initial"
◆  000000000000

default: . rlvkpnrz 504e3d8c (empty) (no description set)
second: ../secondary rzvqmyuk bcc858e1 (empty) (no description set)