Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Workspaces

Two chapters ago, we worked on several branches at once without leaving our working copy, by editing commits directly. That covers most of what people use git branch switching for. Sometimes, though, we really do want two directories on disk at the same time.

For example, we may want a long build or test run in one tree while we keep editing in another. We may also want two versions of the code open side by side, or a scratch checkout for a tool that does a lot of work whenever files change.

git calls these worktrees. jj calls them workspaces.

Making one

$ jj workspace add ../feature-b
Created workspace in "../feature-b"
Working copy  (@) now at: omvuuyon b1c8e7a0 (empty) (no description set)
Parent commit (@-)      : posvwlwz 6c45c26f initial
Added 1 files, modified 0 files, removed 0 files

There’s now a second working directory at ../feature-b, backed by the same repository. It got its own fresh empty change, omvuuyon. This is the important part: each workspace has its own @.

$ jj workspace list
default: . vtmvqlrp 825c9b05 feature a
feature-b: ../feature-b omvuuyon b1c8e7a0 (empty) (no description set)

The workspace we started in is called default. Our new one took its name from the directory. We can use --name to pick a different name, and -r to start at a revision other than the default one.

The log shows both, marked with @ suffixed by the workspace name:

$ jj log
@  vtmvqlrp steve@steveklabnik.com 2024-03-23 11:04:16 default@ 825c9b05
│  feature a
│ ○  omvuuyon steve@steveklabnik.com 2024-03-23 11:04:16 feature-b@ b1c8e7a0
├─╯  (empty) (no description set)
○  posvwlwz steve@steveklabnik.com 2024-03-23 11:03:52 6c45c26f
│  initial
◆  zzzzzzzz root() 00000000

default@ is our working copy; feature-b@ is the other workspace’s. Two anonymous branches, two directories, one repository.

Now let’s cd ../feature-b. It behaves like any other jj repository, with the same history, bookmarks, and operation log. Work we do here is visible from the other workspace immediately, because there’s only one repository underneath.

$ jj st
Working copy changes:
A b.txt
Working copy  (@) : omvuuyon 66ff7d1e feature b
Parent commit (@-): posvwlwz 6c45c26f initial

What it isn’t

git worktree makes you assign a branch to each tree, and refuses to check out a branch that’s already checked out somewhere else. None of that applies here. A workspace just points its @ at a revision. There’s no branch to reserve and nothing to be exclusive about.

There is one rule to remember: we shouldn’t edit the same commit from two workspaces. jj doesn’t forbid it, but both directories would then hold files claiming to be the same commit, and whichever one snapshots last would win.

Stale working copies

Workspaces share history, so a rewrite in one can affect another workspace’s @. Let’s say we rebase or abandon a commit that another workspace is sitting on. That workspace will fall behind:

$ jj st
Error: The working copy is stale (not updated since operation 8f799e48797c).
Hint: Run `jj workspace update-stale` to update it.

Nothing has been damaged. The repository knows exactly where that commit went; the files on disk just haven’t caught up yet. We can update them as the hint suggests:

$ jj workspace update-stale
Working copy  (@) now at: omvuuyon 08f28542 renamed from elsewhere
Parent commit (@-)      : vtmvqlrp 825c9b05 feature a
Added 1 files, modified 0 files, removed 0 files
Updated working copy to fresh commit 08f28542e3a9

The change ID is unchanged — omvuuyon is the same change it always was — and any uncommitted edits in that directory are still there. It’s the same change, in its new position.

Not every rewrite makes a workspace stale. If we change a description from somewhere else, we haven’t touched any files, so the next command in the other workspace absorbs it without comment. Staleness is about the files on disk disagreeing with the commit they represent. If we’d rather update them automatically, we can set snapshot.auto-update-stale = true in our config.

Cleaning up

Deleting the directory isn’t enough, because the repository still has the workspace registered. Cleanup takes two steps, in either order:

$ jj workspace forget feature-b
$ rm -rf ../feature-b

forget unregisters the workspace and leaves the directory alone. The commits that workspace made are ordinary commits and stay in the repository; only the feature-b@ marker goes away.

One more command worth knowing, mostly for scripts:

$ jj workspace root
/home/steve/src/my-project

That’s the top of the current workspace, wherever in the tree you happen to be standing.

When to reach for one

Workspaces cost a full checkout on disk and a little bookkeeping. If we only need to move between pieces of work, editing commits in one directory is simpler. I reach for a workspace when I genuinely need two trees at once, such as something long-running in one while I keep working in the other.