In short: on every invocation, before any command runs, jj canonicalizes the current directory and walks
up from it to the nearest directory that has a .jj subdirectory. That directory is the
workspace root, and a loader for it is kept, or the error from building one. With -R there
is no search: the given path must itself be a workspace root. jj root asks for that loader,
turns its root path into bytes and prints them with a newline. It loads no repository, operation or
commit, does not snapshot the working copy and records no operation.
The command is cmd_root. The help
text: "Show the current workspace root directory (shortcut for jj workspace root)"
(source). It is registered as Root(root::RootArgs)
(source) and dispatched like every other command
(source). The interesting part is the workspace lookup in
CliRunner, which every command shares.
1. Command-line surface
RootArgs {} is an empty struct, so jj root takes no arguments or flags of its
own (source). Only two global flags change what it does:
| Flag | Effect on jj root |
|---|---|
-R, --repository PATH | Use the workspace at PATH instead of searching from the current directory. PATH is joined to the current directory and canonicalized (left as given if that fails), then must contain .jj itself: there is no search upward, so -R some/subdir fails. A bad -R errors before the command starts (source). |
--ignore-working-copy, --at-op | Accepted, but have no effect: jj root neither snapshots nor loads an operation. |
The global flags are on the CLI page.
2. Configuration
No setting changes the output. jj still loads the user config, and the repo and workspace config of the
workspace it found, because it needs them to resolve aliases and the default command before it knows
which command is running (source). cmd_root reads
none of it, and writes straight to stdout without requesting the pager
(source).
3. End-to-end flow
4. Core architecture touchpoints
| Piece | Role here |
|---|---|
WorkspaceLoaderFactory | Builds a loader for a candidate root. The CLI holds one as workspace_loader_factory; the default is DefaultWorkspaceLoaderFactory. |
WorkspaceLoader | Knows a workspace's root and repo path, and can load the full Workspace. jj root only ever calls workspace_root(); it never calls load. |
maybe_workspace_loader | The Result the CLI stores for the command, read through workspace_loader(), which clones the error if there is one. |
file_util::path_to_bytes | Turns the path into the bytes written to stdout. |
| Repo, op log, view, working copy | Not touched. Compare snapshotting and the on-disk workspace layout. |
5. What it reads, and snapshotting
Only file-system metadata: whether each ancestor of the current directory has a .jj
directory, and in the workspace found, whether .jj/repo is a file, which is then read and
followed (source). The shared lifecycle also reads the repo and
workspace config files (source), but nothing else under
.jj/repo is opened: no store, operation or index. It never calls workspace_helper, so the
working copy is not snapshotted and no operation is written, with or without
--ignore-working-copy.
6. Algorithm and invariants
Finding the root
find_workspace_dir is
cwd.ancestors().find(|path| path.join(".jj").is_dir()).unwrap_or(cwd): the current
directory first, then each parent up to the file-system root. The first match wins, so in a workspace
nested inside another the inner one is found. If nothing matches, the current directory itself is
handed to the loader, which then fails with NoWorkspaceHere
(source).
A canonical path
The current directory is canonicalized before the search "for consistency with
Workspace::workspace_root()" (source). So
the printed path has symlinks resolved: entering a workspace through a symlinked directory prints the
real location. The search also follows the real parents, not the parents of the path you typed.
When the error appears
Without -R, failing to find a workspace is not an error at start-up, since commands such
as jj git init run outside one: the error is stored with the user path "."
(source) and raised only when a command asks for the loader,
which jj root does first (source). With
-R, the error is raised before any command runs, with the path as typed
(source).
Secondary workspaces
A workspace made by jj workspace add has a
.jj/repo file holding the path of the main repo directory, resolved against its own
.jj.
The loader canonicalizes that path and requires it to be a directory
(source). jj root still prints the secondary
workspace's own root, never the main one; the pointer only has to resolve.
Versus jj workspace root
Without arguments jj workspace root runs the same code: workspace_loader(), then
the same three output lines (source). Its --name option
(source) is different: it loads the repository without snapshotting,
checks the name is in the view's working-copy commits, and asks the workspace store for that workspace's
recorded path, warning if it cannot be accessed (source).
7. Errors and edge cases
| Situation | Message |
|---|---|
No .jj in the current directory or any ancestor | There is no jj repo in "."; with -R, the path as typed. If that directory has a .git directory, the hint "It looks like this is a git repo. You can create a jj repo backed by it by running this: jj git init" (source). |
.jj/repo points at something that is not a directory | "The repository directory at … is missing. Was it moved?" (source). |
.jj/repo cannot be read, or its target does not exist | "Cannot access path" with the I/O error as the cause, since canonicalizing a missing path fails before the directory check (source, source, source). |
.jj/repo is not UTF-8 (Windows only) | "Repo path could not be decoded" (source). |
| The current directory was deleted, or cannot be read | "Could not determine current directory", hinting "Did you update to a commit where the directory doesn't exist or can't be accessed?" (source). |
| The root path is not UTF-8 (Windows only) | "Invalid UTF-8 sequence". On Unix a path is already bytes and the conversion cannot fail (source, source). |
8. Example output
Run from a subdirectory, the output is the workspace root followed by a newline; upstream checks this for both the simple and the git backends (source). The path below is illustrative:
$ cd ~/src/myrepo/lib/src
$ jj root
/home/alice/src/myrepo
Outside any workspace it fails with exit status 1 (source):
$ cd /
$ jj root
Error: There is no jj repo in "."