jj / How jj git root works

How jj git root works

jj git root prints the path of the Git repository that stores a jj repo's commits. The command itself is a dozen lines; the path it prints was decided when the repo was created, and is re-resolved every time the repo is loaded.

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. Where git_target points
  8. A canonical path, mostly
  9. Secondary workspaces
  10. What it is for
  11. 7. Errors and edge cases
  12. 8. Example output

In short: jj loads the workspace and repo and snapshots the working copy, like most commands. Loading the repo loads the commit backend, and the Git backend reads .jj/repo/store/git_target, joins it to the store directory and opens the Git repo there. jj git root asks the store for that backend, fails if the repo uses another backend, and prints the backend's Git directory with a newline: .jj/repo/store/git for a plain jj git init, the .git directory for a colocated or external repo.

The command is cmd_git_root. The help text: "Show the underlying Git directory of a repository using the Git backend" (source). It is the Root(GitRootArgs) variant of GitCommand (source), dispatched by cmd_git; the whole git group only exists when jj is built with the git feature (source). Compare jj root, which prints the workspace root instead.

1. Command-line surface

GitRootArgs {} is an empty struct: no arguments or flags of its own (source). The global flags that matter:

FlagEffect on jj git root
-R, --repository PATHUse the workspace at PATH, as for every command (see jj root).
--ignore-working-copySkips the snapshot and the colocated Git import. The printed path is the same.
--at-opLoads the repo at that operation, which also skips the snapshot (source). The Git path does not depend on the operation, so the output is the same.

The global flags are on the CLI page.

2. Configuration

No setting changes the output. The settings the Git backend is loaded with, a GitBackendSettings the caller builds from config (source: the Git executable, whether to write change-id headers, and the user name and email it passes to gix), do not affect the path, and cmd_git_root writes straight to stdout without the pager (source). As with every snapshotting command, snapshot.auto-update-stale decides what happens if the working copy is stale (source).

3. End-to-end flow

jj git root: call flow The shared lifecycle finds the workspace. Loading it loads the repo's store; the commit backend type comes from store/type, and the Git backend reads store/git_target, joins it to the store path, canonicalizes it except for a final .git component and opens the Git repo. workspace_helper then snapshots the working copy, importing Git HEAD and refs if the workspace is colocated. cmd_git_root calls get_git_backend, which fails with "The repo is not backed by a Git repo" for any other backend, then prints git_repo_path and a newline. Shared lifecycle: find the workspace root (see jj root) load_backend: read .jj/repo/store/type, pick the factory "git" → GitBackend::load; "simple" → SimpleBackend GitBackend::load: store_path.join(git_target), canonicalized gix opens the repo at that path as is; base_repo keeps it workspace_helper: load at the head op, snapshot colocated: import Git HEAD before, Git refs after (skipped by --ignore-working-copy) get_git_backend(store): downcast the backend not a GitBackend → "The repo is not backed by a Git repo" path_to_bytes(git_repo_path()) → stdout, then "\n" no pager, no transaction of its own

4. Core architecture touchpoints

PieceRole here
RepoLoader::init_from_file_systemBuilds the Store around the backend that load_backend picks from store/type. The Git factory is registered only with the git feature (source).
GitBackendHolds base_repo, the gix repository opened at load time. git_repo_path() is base_repo.path(): "the .git directory or the repository itself if it's bare".
get_git_backendstore.backend_impl(), a downcast of the boxed backend; None becomes UnexpectedGitBackendError, which the CLI turns into a user error (source).
file_util::path_to_bytesTurns the path into the bytes written to stdout.
Working copy, Git importSnapshotted before the command runs; see snapshotting and jj git import.

5. What it reads, and snapshotting

Unlike jj root, jj git root calls workspace_helper, so it loads the repo at the head operation and, unless --ignore-working-copy or --at-op is given, takes the Git import/export lock and snapshots the working copy (source). In a colocated workspace the snapshot also imports Git HEAD first and Git refs afterwards (source). Either can record an operation, even though the command only prints a path. Beyond that it reads store/type and store/git_target, and gix opens the Git repo; no commit, tree or ref is read for the output itself.

6. Algorithm and invariants

Where git_target points

Every Git-backed repo writes store/git_target once, at init (source). What it holds depends on how the repo was made:

Created bygit_targetPrinted
jj git init (not colocated)git, a bare repo inside the store (source)…/.jj/repo/store/git
jj git init --colocateThe workspace root relative to the store, plus .git: ../../../.git (source, source)…/.git
jj git init --git-repo PATHRelative to the store if the Git repo is inside the workspace, else the absolute path (source)That repo's .git

jj git colocation moves the repo and rewrites this file, so the answer follows it.

A canonical path, mostly

On load the target is joined to the store path and passed through canonicalize_git_repo_path: symlinks are resolved, except that a final .git component is kept and only its parent is canonicalized, so a colocated repo is not reopened as a bare one. gix is told to open that path as is (source). The printed path is therefore absolute even when git_target is relative.

Secondary workspaces

A workspace made by jj workspace add shares the repo, and so the store, of the workspace it came from. jj git root in either prints the same Git directory.

What it is for

Tools that look for .git cannot find the store of a non-colocated repo. Upstream's GitHub guide sets GIT_DIR=$(jj git root) so the GitHub CLI works there (source).

7. Errors and edge cases

SituationMessage
The repo uses another backend, such as the simple backend"The repo is not backed by a Git repo", exit status 1, after loading and snapshotting (source, test).
git_target is missing or its target cannot be openedLoading fails before the command runs: "The repository appears broken or inaccessible" (source, source).
jj built without the git featureNo git command, and a Git-backed repo has no backend factory: "This version of the jj binary doesn't support this type of repo" (source).
Not in a workspaceAs for jj root.
The path is not UTF-8 (Windows only)A user error from path_to_bytes; on Unix it cannot fail (source).

8. Example output

Upstream's tests cover each layout (source); the paths below are illustrative:

$ jj git init repo && cd repo
$ jj git root
/home/alice/repo/.jj/repo/store/git

$ jj git init --colocate other && cd other
$ jj git root
/home/alice/other/.git