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
| Flag | Type | Default | Meaning |
|---|---|---|---|
SOURCE (positional) | URL or path, required | — | The Git repo to clone; a local path is made absolute (source). |
DESTINATION (positional) | path | last component of the source | The workspace directory; created with all its parents if missing (source). |
--remote NAME | remote name | origin | Name of the new remote (source). |
--colocate | bool | false | Colocate the jj repo with the Git repo. Has no effect unless git.colocate is false (source). |
--no-colocate | bool | false | Keep the Git repo inside .jj instead; conflicts with --colocate (source). |
--depth N | non-zero integer | none (full history) | Create a shallow clone of that depth (source). |
-b, --branch (alias --bookmark) | name pattern, repeatable | all branches | Fetch only these branches; the first exact name that exists becomes the working-copy parent (source). |
-t, --tag | name pattern, repeatable | all tags | Fetch only these tags (source). |
--object-hash | sha1 or sha256 | git.object-hash | Hash algorithm of the new local Git repo; must match the remote's (source, ObjectHash). |
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
| Key | Default | Effect |
|---|---|---|
git.colocate | true | Whether 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-clone | true | Create a local bookmark tracking the remote's default branch (default, use). |
remotes.<name>.fetch-bookmarks, fetch-tags | unset (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 schema | Which 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
- Pre-checks.
--at-opis refused because there is no repo yet. The source goes throughabsolute_git_url, which letsgixparse it as a URL, an scp-like address or a local path and canonicalizes it against the current directory; that is why a laterjj git fetchworks even if the source was a relative path. The destination must not exist or must be an empty directory (source). - Options. Colocation is
!--no-colocatewhengit.colocateis true, otherwise--colocate. If either--branchor--tagis given, the other one defaults to "nothing" rather than "everything", so--branch mainfetches no tags (source). - Workspace.
init_workspaceuses the settings for the new path and callsWorkspace::init_colocated_gitorinit_internal_git(a bare Git repo in.jj/repo/store/git). Initialization commits the first operation, which checks out the root commit in workspacedefault(source). In a colocated workspace,.jj/.gitignoreis written with/*so Git ignores.jj(maybe_add_gitignore). - Remote.
git::add_remotewrites 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). - Fetch. Prints
Fetching into new repo in "…", then runs the sameGitFetchasjj git fetch, with--depthpassed through and synthetic predecessors turned off because there may be many new commits. It asks Git for the remote's default branch withgit remote show(get_default_branch,spawn_remote_show) (source). - Working branch and tracking. See §6. Then the import stats are printed and the transaction finishes (source).
- 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_factoriescreates.jj, the repo directory and its stores, then the working copy. If it fails, it removes.jjitself. - 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, andtrack_remote_bookmarkcreates the local bookmark; see the view page andjj bookmark track. - Working copy:
check_outcreates a new empty commit on the working branch and the working copy is updated to it, which is whereAdded N filescomes 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 description | What it does | Source |
|---|---|---|
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 repo | Imports 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
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
| Message | When | Source |
|---|---|---|
--at-op is not respected (exit 2) | --at-op was given. | clone.rs, test |
No destination specified and wasn't able to guess it | No destination and nothing usable after the last separator. | clone.rs |
Destination path exists and is not an empty directory | The 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:
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).