jj / How jj root works

How jj root works

jj root prints the root directory of the workspace you are in. It is one of the smallest commands in jj: it never opens the repository, and all the work it needs was already done while the command line was being parsed.

On this page
  1. 1. Command-line surface
  2. 2. Configuration
  3. 3. End-to-end flow
  4. 4. Core architecture touchpoints
  5. 5. What it reads, and snapshotting
  6. 6. Algorithm and invariants
  7. Finding the root
  8. A canonical path
  9. When the error appears
  10. Secondary workspaces
  11. Versus jj workspace root
  12. 7. Errors and edge cases
  13. 8. Example output

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:

FlagEffect on jj root
-R, --repository PATHUse 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-opAccepted, 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

jj root: call flow During the shared lifecycle jj canonicalizes the current directory. Without -R it walks up to the nearest ancestor containing a .jj directory; with -R it takes the given path, joined to the current directory and canonicalized, with no search. DefaultWorkspaceLoader::new checks that .jj is a directory and follows a .jj/repo pointer file if there is one. The loader, or the error, is stored. cmd_root asks for it with workspace_loader, which returns the stored error if there was one, then converts the workspace root to bytes and writes them and a newline to stdout. Shared lifecycle: parse args, load config (CLI page) cwd = current_dir, canonicalized symlinks resolved, to match Workspace::workspace_root() no -R: find_workspace_dir(cwd) nearest ancestor with a .jj/ directory -R PATH: cwd.join(PATH), canonicalized no search; an error here stops jj at once DefaultWorkspaceLoader::new(root) .jj must be a directory; a .jj/repo file points at another workspace's repo cmd_root → command.workspace_loader()? the stored loader, or the stored load error path_to_bytes(workspace_root) → stdout, then "\n" no repo load, no snapshot, no pager, no operation

4. Core architecture touchpoints

PieceRole here
WorkspaceLoaderFactoryBuilds a loader for a candidate root. The CLI holds one as workspace_loader_factory; the default is DefaultWorkspaceLoaderFactory.
WorkspaceLoaderKnows 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_loaderThe Result the CLI stores for the command, read through workspace_loader(), which clones the error if there is one.
file_util::path_to_bytesTurns the path into the bytes written to stdout.
Repo, op log, view, working copyNot 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

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

SituationMessage
No .jj in the current directory or any ancestorThere 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 "."