jj / How jj git remote works

How jj git remote works

jj git remote manages the remotes of the backing Git repository: add, list, remove, rename and set-url. Remotes live in the Git config, but jj also keeps remote bookmarks and tags per remote, so adding, removing and renaming also update jj's view, Git's remote-tracking refs and jj's own repo config.

On this page
  1. 1. Command-line surface
  2. 2. Configuration
  3. 3. End-to-end flow
  4. add
  5. remove
  6. rename
  7. set-url
  8. 4. Core architecture touchpoints
  9. 5. Transaction and operation
  10. 6. Algorithm and invariants
  11. Remote names
  12. Repo config
  13. 7. Errors and edge cases
  14. 8. Worked example

In short: add writes a [remote] section with the standard fetch refspec and creates an empty remote in the view. remove deletes the section, the branch sections that point at it, the remote's Git refs and its remote bookmarks and tags. rename does the same but moves everything to the new name, and rewrites trunk() and remotes.<old> settings in the repo config. set-url only edits the Git config and records no operation; list only reads it.

The group is RemoteCommand. Its doc line still says the Git repo is a bare repo inside .jj/, which is only true for non-colocated workspaces. Remote bookmarks and tags are described on the view page; fetching and pushing are jj git fetch and jj git push.

1. Command-line surface

SubcommandArgumentsNotes
addREMOTE URL [--push-url URL]Local paths are made absolute (source).
listnonePrints NAME URL, or NAME URL (push: PUSH_URL) when they differ, and <no URL> for a missing one (source).
removeREMOTE"Remove a Git remote and forget its bookmarks" (source).
renameOLD NEW(source)
set-urlREMOTE [URL] [--push URL] [--fetch URL]The positional URL is short for --fetch; the two conflict. A URL kind not given is left alone, so this can't remove a URL (source).

2. Configuration

jj reads remotes from the Git repo's config (.git/config, or .jj/repo/store/git/config when not colocated) and writes back only that file's own sections, so remotes defined in global Git config are seen but never rewritten there (save_git_config). A remote counts as existing only if it has a fetch or push URL; an empty [remote "x"] section is ignored (try_find_active_remote).

jj's own repo config holds per-remote settings (remotes.<name>.*) and possibly trunk(), which jj git clone and jj git init write. rename and remove keep those in step (source).

3. End-to-end flow

What each jj git remote subcommand changes A grid of the five subcommands against the four places they can change. add writes the Git config and adds an empty remote to the view. list reads the Git config only. remove deletes the Git config sections, the remote's Git refs, its view entries and git_refs, and its repo config settings. rename moves all four. set-url writes the Git config only and records no operation. Git config Git refs jj view (op) jj repo config add new [remote] section url, pushurl, fetch — empty remote view — list read only — — (no operation) — remove drop [remote] and its [branch] sections delete refs/remotes/R/* and remote-tag refs drop the remote's refs and its git_refs drop remotes.R; reset trunk() if on R rename move section, new refspec, branch remote rename refs/remotes/ OLD/* to NEW/* move remote view and git_refs move remotes.OLD; rewrite trunk() set-url replace url and/or pushurl — — (no operation) —

add

The URLs go through absolute_git_url, then git::add_remote validates the name, refuses an existing remote, writes url, pushurl and fetch = +refs/heads/*:refs/remotes/<name>/*, and calls ensure_remote so the view has the remote even before any fetch. jj then warns if another remote already uses exactly the same fetch or push URL, without printing the URL in case it holds credentials (warn_if_remote_url_matches).

remove

git::remove_remote removes the [remote] section and every [branch] section whose remote or pushRemote is this remote, deletes refs/remotes/<name>/*, jj's fetch refs refs/jj/remote-bookmarks/<name>/* and refs/jj/remote-tags/<name>/* in Git (remove_remote_git_refs), then removes the remote from the view with all its remote bookmarks and tags, and its entries in git_refs. Local bookmarks are kept. The CLI then cleans the repo config (source).

rename

git::rename_remote validates the new name, requires the old remote to exist and the new one not to, and requires the standard configuration: exactly one fetch refspec, the default one, and no push refspecs. It writes the section under the new name with a new refspec, points matching [branch] sections at the new name, removes the old section, renames the Git refs in all three namespaces (source; reflog message renamed remote OLD to NEW), and moves the remote view and git_refs.

set-url

git::set_remote_urls does nothing if no URL is given, otherwise replaces the given URLs and saves the config. It looks the remote up without applying Git's URL rewrite rules, so it edits what is written in the config.

4. Core architecture touchpoints

  • Git config: edited with gix and saved back to the repo's own config file. A save_remote helper adds an empty section first to work around a gitoxide bug (source).
  • Git refs: remote-tracking refs are deleted or renamed in one ref transaction, with each edit checking the old value (add_ref, remove_ref).
  • View: ensure_remote, remove_remote, rename_remote act on the per-remote map of bookmarks and tags; see the view page.
  • Repo config: trunk() and remotes.<name> tables (source).

5. Transaction and operation

SubcommandOperationSource
addadd git remote <name>add.rs
removeremove git remote <name>, only if the view changedremove.rs
renamerename git remote <old> to <new>, only if the view changedrename.rs
list, set-urlnonelist.rs, set_url.rs

Only the view changes are part of the operation. The Git config, the Git refs and the repo config are edited directly, so jj undo after remove or rename brings back the remote bookmarks in jj but not the remote in Git. For a remote named git, the view isn't touched (its name is reserved for NAME@git), so no operation is recorded and no Nothing changed. is printed (source).

6. Algorithm and invariants

Remote names

validate_remote_name applies Git's own rules, then rejects git, reserved for the backing repo's refs (main@git), and any name with a slash, because jj splits refs/remotes/REMOTE/NAME at the first slash (parse_git_ref). add, rename's new name and set-url are validated; remove and rename's old name aren't, so a remote named git that came from Git can still be removed or renamed away, but not back (test).

Repo config

On rename, the [remotes.OLD] table becomes [remotes.NEW], and a trunk() alias of the form NAME@OLD becomes NAME@NEW, printing Updating the revset alias `trunk()` to `…`. On remove, the table is deleted and such a trunk() is deleted too, printing Resetting the revset alias `trunk()` to default value. Only a trunk() that parses as a single remote symbol is touched (source).

Invariants: after remove, no Git ref, view entry or git_refs entry for the remote remains. After rename, the remote's fetch refspec, Git refs, view entry and repo-config settings all use the new name. Nonstandard remotes are refused rather than half-converted.

7. Errors and edge cases

MessageWhenSource
No git remote named '<name>'remove, rename or set-url on an unknown remote.git.rs, test
Git remote named '<name>' already existsadd or rename to an existing name.test
Git remote named 'git' is reserved for local Git repositoryAdding, renaming to or setting URLs on git.git.rs, test
Git remotes with slashes are incompatible with jj: <name>A name containing /.git.rs
Git remote named '<name>' has nonstandard configurationrename with a custom refspec or push refspec; remove/rename with unknown keys in the remote or branch sections.git.rs
Unexpected Git error when managing remotesFor example, an unparsable URL in the config (also breaks list).test
Warning: Remote <other> already uses the same URL., hint to remove the new oneadd with a URL another remote has.add.rs

8. Worked example

From upstream's test_git_remote_rename_updates_trunk. A repo cloned with jj git clone --branch=main remote local has the remote origin, the remote bookmark main@origin, and a repo config with trunk() set to main@origin and fetch-bookmarks under [remotes.origin]. Then jj git remote rename origin upstream moves all of it:

jj git remote rename origin upstream Before: Git config has remote origin with fetch refspec to refs/remotes/origin, Git has refs/remotes/origin/main, jj's view has main@origin, and the repo config has remotes.origin and trunk() equal to main@origin. After: every one of these uses upstream instead: remote upstream, refs/remotes/upstream/main, main@upstream, remotes.upstream and trunk() equal to main@upstream. before after [remote "origin"] fetch → refs/remotes/origin/* [remote "upstream"] fetch → refs/remotes/upstream/* Git ref refs/remotes/origin/main Git ref refs/remotes/upstream/main view: main@origin (tracked by main) view: main@upstream (tracked by main) [remotes.origin] fetch-bookmarks = "main" trunk() = "main@origin" [remotes.upstream] fetch-bookmarks = "main" trunk() = "main@upstream"

Upstream's output, and trunk() still resolving afterwards (source):

$ jj git remote rename origin upstream
Updating the revset alias `trunk()` to `main@upstream`.
$ jj log -r 'trunk()' -T description
◆  init
│
~

The fetch-bookmarks entry comes from cloning with --branch; see jj git clone. That the bookmark was tracked follows from git.track-default-bookmark-on-clone; the test itself only checks trunk().