Bookmarks & remotes
Bookmarks are jj’s named pointers — what git calls branches, minus the requirement to have one. You work on anonymous changes and attach a bookmark only when something outside the repo needs a stable name: a remote, a release, a reviewer.
Bookmarks
Section titled “Bookmarks”await jj.bookmark.create({ name: 'main', changeId }); // create-only (ALREADY_EXISTS otherwise)await jj.bookmark.set({ name: 'main', changeId }); // create-or-moveawait jj.bookmark.move({ name: 'main', to: id }); // repoint anywhereawait jj.bookmark.advance({ name: 'main', to: id }); // forward ONLY — throws // BOOKMARK_NOT_ADVANCEABLE on non-descendantsawait jj.bookmark.rename({ oldName, newName });await jj.bookmark.delete({ name });await jj.bookmark.list(); // [{ name, changeId, … }]In revsets: bookmark(main) is exact lookup, bookmarks(feat*) matches
patterns.
Remote tracking
Section titled “Remote tracking”await jj.bookmark.track({ name: 'main', remote: 'origin' });await jj.bookmark.untrack({ name: 'main' });await jj.bookmark.forget({ name: 'main' }); // drop local + tracking stateHonest caveat: track() records intent in bookmarks.json, but
git.fetch() does not yet auto-populate remote-tracking state from fetched
refs — that wiring (jj v0.44’s headline fetch behavior) is consciously
deferred; the repo CHANGELOG explains the architectural why. Track/untrack
lets you record and query the relationship by hand meanwhile.
await jj.tag.set({ name: 'v1.0.0', changeId }); // create-or-move (jj v0.35)await jj.tag.list(); // supports { pattern: 'v1*' }await jj.tag.track({ name: 'v1.0.0', remote: 'origin' }); // jj v0.44await jj.tag.untrack({ name: 'v1.0.0' });The v0.44 parity pass made tags remote-trackable exactly like bookmarks:
tag.list() grows a tracking: { remote, ref } field once tracked, and
.jj/tags.json uses a { tags, tracked } envelope (the old flat format is
still read). Revsets: tags([pattern]), remote_tags([pattern]).
The Git interop boundary
Section titled “The Git interop boundary”All remote work flows through isomorphic-git, which speaks Git’s smart-HTTP protocol. That means:
createJJ()needs bothgitandhttp(isomorphic-git/http/nodeor/web) — withouthttpyou getNETWORK_NOT_AVAILABLE.- Transport is HTTP(S) only. No
ssh://, nofile://— a local “remote” must be served over HTTP (the repo’s example 08 does exactly that withgit http-backendon loopback). - Browsers additionally need a
corsProxyfor most hosts.
Remote management, fetch, push
Section titled “Remote management, fetch, push”await jj.git.remote.add({ name: 'origin', url: 'https://…' });await jj.git.remote.list(); // also: remove, rename, setUrlawait jj.git.fetch({ remote: 'origin', refs: ['main'] }); // → { fetchedRefs: [{ name: 'refs/remotes/origin/main', oid }], updatedRefs }await jj.git.push({ remote: 'origin', refs: ['main'], force: false }); // → { pushedRefs, rejectedRefs }Shallow options on fetch/clone: depth, singleBranch, noTags,
relative. jj.remote.* aliases jj.git.* if you prefer that spelling.
Push what exists as a Git ref. Local bookmarks become refs/heads/*
via git.export(); run it before pushing a bookmark you just created.
git.import() goes the other way, importing Git refs as bookmarks — that’s
the sync story between the two worlds.
Read rejectedRefs. Per-ref push failures (non-fast-forward included)
land there rather than throwing. An “OK” push with an empty pushedRefs
array wasn’t OK.
Also note real jj refuses to push conflicted commits without
--allow-conflicts; this library currently has no such guard — it will
push what you tell it to.
clone()
Section titled “clone()”const { directory } = await jj.git.clone({ url, dir: 'checkout' });Clones (via isomorphic-git) into dir relative to the repo root and adds
.jj/ scaffolding. Do not run init() inside the result — it creates
a fresh root change and repoints refs/heads/main at it, orphaning the
fetched history. Drive clones at the git level (git.* /
isomorphic-git); do jj-native work in repos you initialized yourself.
Authentication
Section titled “Authentication”isomorphic-git’s callback auth passes straight through:
await jj.git.push({ remote: 'origin', refs: ['main'], onAuth: () => ({ username: 'token', password: process.env.GH_TOKEN }),});onAuth works on clone/fetch/push. Auth failures surface as
JJError code AUTH_FAILED; transport failures as NETWORK_ERROR. For
GitHub, a fine-grained PAT as the password (any username) is the usual
recipe. Never bake tokens into browser bundles — proxy through your own
backend instead.
Next: Configuration.