jj / How jj git clone works

How jj git clone works

jj git clone creates a new jj workspace backed by a fresh Git repository, adds the source as a remote, fetches from it, and checks out a new working-copy commit on top of the remote's default branch. It runs as four separate operations, and removes what it created if any of the first three fail.

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. Choosing the working branch
  8. Clean-up on failure
  9. 7. Errors and edge cases
  10. 8. Worked example

In short: jj turns the source into an absolute URL and picks a destination directory, which must be missing or empty. It creates a colocated workspace (a .git next to .jj) unless colocation is turned off. Then it adds the remote origin (one operation) and fetches the requested branches and tags (another), asking Git for the remote's default branch. It tracks that branch locally, writes trunk() and any fetch patterns to the repo config, and finally checks out a new empty commit on top of the working branch (a fourth operation).

The command is cmd_git_clone. Most of the fetching is the same library code as jj git fetch; this page covers what differs. The workspace and repo it creates are described on the working copy and operations pages.

1. Command-line surface

FlagTypeDefaultMeaning
SOURCE (positional)URL or path, required—The Git repo to clone; a local path is made absolute (source).
DESTINATION (positional)pathlast component of the sourceThe workspace directory; created with all its parents if missing (source).
--remote NAMEremote nameoriginName of the new remote (source).
--colocateboolfalseColocate the jj repo with the Git repo. Has no effect unless git.colocate is false (source).
--no-colocateboolfalseKeep the Git repo inside .jj instead; conflicts with --colocate (source).
--depth Nnon-zero integernone (full history)Create a shallow clone of that depth (source).
-b, --branch (alias --bookmark)name pattern, repeatableall branchesFetch only these branches; the first exact name that exists becomes the working-copy parent (source).
-t, --tagname pattern, repeatableall tagsFetch only these tags (source).
--object-hashsha1 or sha256git.object-hashHash algorithm of the new local Git repo; must match the remote's (source, ObjectHash).

GitCloneArgs

Without a destination, the name is taken from the source: a trailing .git and then a trailing / are stripped, and whatever follows the last /, \ or : is used (clone_destination_for_source). So https://example.com/org/repo.git clones into repo.

2. Configuration

KeyDefaultEffect
git.colocatetrueWhether a clone is colocated; --colocate/--no-colocate override it (default, use).
git.object-hash"sha1"Hash algorithm when --object-hash isn't given (default, use).
git.track-default-bookmark-on-clonetrueCreate a local bookmark tracking the remote's default branch (default, use).
remotes.<name>.fetch-bookmarks, fetch-tagsunset (fetch all)Used when --branch/--tag aren't given, if set in user config (use, parsing). Written to the new repo config when they are given.
remotes.<name>.auto-track-bookmarks"~*" per the schemaWhich fetched branches become tracked local bookmarks, as in git fetch (schema, import options).

Settings are read for the new workspace's path, so conditional config scoped to the destination applies (source). After a successful clone jj may write a repo config file with revset-aliases."trunk()" and, if --branch or --tag were given, remotes.<name>.fetch-bookmarks/fetch-tags; the patterns are joined with |, and an empty list becomes ~* (matches nothing). If none of these apply, no repo config file is written (write_repo_presets, join_string_expressions).

3. End-to-end flow

jj git clone: call flow cmd_git_clone rejects --at-op, makes the source URL absolute, picks the destination and checks it is missing or empty, then creates it. It resolves colocation, the branch and tag patterns and the object hash. Inside one guarded block it initializes the workspace (operation: add workspace default), adds the remote (operation: add git remote), and fetches and imports, tracking the default branch (operation: fetch from git remote into empty repo). If any of those fail, it deletes .jj, .git and the directory it created. Otherwise it writes trunk and fetch presets to the repo config and checks out a new commit on the working branch (operation: check out git remote's branch). reject --at-op; source = absolute_git_url(); destination must be missing or empty create_dir_all(destination) resolve colocate, --branch / --tag patterns, object hash; canonicalize the path guarded: any error here triggers clean-up init_workspace: init_colocated_git or init_internal_git, maybe write .jj/.gitignore op: add workspace 'default' configure_remote: git::add_remote, then reload the workspace and repo op: add git remote origin fetch_new_remote: GitFetch fetch + import_refs, get_default_branch pick the working branch, warn on missing names, track the default branch op: fetch from git remote into empty repo on error: remove .jj, .git and the directory if jj made it write_repo_presets: trunk(), fetch-bookmarks, fetch-tags "Setting the revset alias `trunk()` to ..." tx.check_out(working branch's commit) op: check out git remote's branch: NAME
  1. Pre-checks. --at-op is refused because there is no repo yet. The source goes through absolute_git_url, which lets gix parse it as a URL, an scp-like address or a local path and canonicalizes it against the current directory; that is why a later jj git fetch works even if the source was a relative path. The destination must not exist or must be an empty directory (source).
  2. Options. Colocation is !--no-colocate when git.colocate is true, otherwise --colocate. If either --branch or --tag is given, the other one defaults to "nothing" rather than "everything", so --branch main fetches no tags (source).
  3. Workspace. init_workspace uses the settings for the new path and calls Workspace::init_colocated_git or init_internal_git (a bare Git repo in .jj/repo/store/git). Initialization commits the first operation, which checks out the root commit in workspace default (source). In a colocated workspace, .jj/.gitignore is written with /* so Git ignores .jj (maybe_add_gitignore).
  4. Remote. git::add_remote writes the remote with fetch refspec +refs/heads/*:refs/remotes/<name>/* into the Git config (default_fetch_refspec) and registers the remote in the view. The workspace is then reloaded so the store's Git handle sees the new remote (source).
  5. Fetch. Prints Fetching into new repo in "…", then runs the same GitFetch as jj git fetch, with --depth passed through and synthetic predecessors turned off because there may be many new commits. It asks Git for the remote's default branch with git remote show (get_default_branch, spawn_remote_show) (source).
  6. Working branch and tracking. See §6. Then the import stats are printed and the transaction finishes (source).
  7. Presets and checkout. trunk() is set to <default>@<remote> only when the working branch is the default branch. If the working branch's remote bookmark is a single commit, a last transaction checks it out (source).

4. Core architecture touchpoints

  • Workspace and repo creation: Workspace::init_with_factories creates .jj, the repo directory and its stores, then the working copy. If it fails, it removes .jj itself.
  • Git backend: the commit store is a Git repo, colocated or internal; see the commits page.
  • View: the fetch imports <name>@<remote> remote bookmarks and tags, and track_remote_bookmark creates the local bookmark; see the view page and jj bookmark track.
  • Working copy: check_out creates a new empty commit on the working branch and the working copy is updated to it, which is where Added N files comes from.
  • Config: the repo-level config file is created only if there is something to write.

5. Transaction and operation

A successful clone writes four operations, in this order (test, where the newest entry, new empty commit, comes from a later jj new):

Operation descriptionWhat it doesSource
add workspace 'default'Checks out the root commit in the new workspace.workspace.rs
add git remote <name>Adds the remote to the Git config and the view.clone.rs
fetch from git remote into empty repoImports remote bookmarks and tags; may track the default branch.clone.rs
check out git remote's branch: <name>Creates the working-copy commit on the working branch.clone.rs

The last one is skipped when there is no working branch, for example when cloning an empty repo. In that case the fetch transaction has nothing to record and prints Nothing changed. (test). The repo config is written between the third and fourth operations and is not part of any operation, so jj undo doesn't remove it.

6. Algorithm and invariants

Choosing the working branch

How jj git clone picks the working branch For each exact name in the branch patterns that is not excluded by a negative pattern, a name with no remote bookmark after the fetch is recorded as missing, and the first one that exists becomes the working branch. If none exists, the remote's default branch is used, provided it was fetched. Missing names produce a warning. If the working branch is the default branch and git.track-default-bookmark-on-clone is true, the remote bookmark is tracked, creating a local bookmark. trunk is set only when the working branch is the default branch. for each exact name in --branch (not excluded by ~pattern): no NAME@remote after fetch → missing; first that exists → working branch none found → default branch only if DEFAULT@remote was fetched missing names → warning "No matching branches found on remote: ..." working = default and track-default-bookmark-on-clone → track_remote_bookmark creates the local bookmark, as Git would working = default → trunk() = DEFAULT@remote in repo config any working branch → check_out if its remote bookmark is one commit

Only exact names count: a glob like push-* limits what is fetched but never selects the working branch. When --branch names a branch other than the default, jj checks it out but neither tracks it automatically nor sets trunk() (source, source). Other fetched branches are tracked only if auto-track-bookmarks matches them, exactly as in jj git fetch.

Clean-up on failure

Everything from creating the workspace to the end of the fetch runs in one block. If it fails, jj removes .jj, .git too if colocated, and the destination directory if it didn't exist before. A failure to clean up is only a warning: Failed to clean up … (source). Errors in the later steps (writing the config, the checkout) leave the clone in place.

Invariants: clone never writes into a non-empty directory. A successful clone has exactly one remote and, if anything was fetched, a working-copy commit whose parent is the working branch's commit. trunk() is only pinned to the remote's default branch.

7. Errors and edge cases

MessageWhenSource
--at-op is not respected (exit 2)--at-op was given.clone.rs, test
No destination specified and wasn't able to guess itNo destination and nothing usable after the last separator.clone.rs
Destination path exists and is not an empty directoryThe destination is a file, a non-empty directory or an existing workspace.clone.rs, test
Failed to create <dir>The directory couldn't be created or canonicalized.clone.rs
Failed to parse name pattern: …A bad --branch/--tag pattern.revset_util.rs
Could not find repository at '…'The source isn't a Git repo; the clone is cleaned up.test
Warning: No matching branches found on remote: …An exact --branch name wasn't on the remote.clone.rs
Warning: Failed to clean up …Removing the partial clone failed.clone.rs
Warning: Failed to resolve `revset-aliases.trunk()` …Later, if the trunk bookmark is deleted or forgotten.test

Tags have no equivalent of the missing-branch warning: an upstream TODO notes it would need adding if tag patterns gain it (source).

8. Worked example

From upstream's test_git_clone_remote_default_bookmark. The source repo has main (its HEAD) and feature1, both at one commit. With remotes.origin.auto-track-bookmarks = '~*' (track nothing automatically), jj git clone source clone2 imports both remote bookmarks but tracks only the default branch main, sets trunk() to main@origin, and checks out a new commit on top:

The repo after jj git clone source clone2 The root commit, then commit ebeb70d8 with message "message", carrying the local bookmark main, the tracked remote bookmark main@origin and the untracked remote bookmark feature1@origin. The new empty working-copy commit 27e56779 is on top of it. trunk() resolves to main@origin. root ebeb70d8 message main (tracking main@origin), feature1@origin (untracked); trunk() 27e56779 @ (empty) new working-copy commit repo config written by clone revset-aliases."trunk()" = "main@origin"

Upstream's output (source):

Fetching into new repo in "$TEST_ENV/clone2"
bookmark: feature1@origin [new] untracked
bookmark: main@origin     [new] tracked
Setting the revset alias `trunk()` to `main@origin`.
Working copy  (@) now at: rzvqmyuk 27e56779 (empty) (no description set)
Parent commit (@-)      : qomsplrm ebeb70d8 feature1@origin main | message
Added 1 files, modified 0 files, removed 0 files

In the same test, moving the source's HEAD to feature1 makes the next clone track feature1 and set trunk() to feature1@origin. With git.track-default-bookmark-on-clone=false, both stay untracked, but trunk() is still set and the checkout still happens (source).