jj / How jj bookmark set works

How jj bookmark set works

jj bookmark set points bookmarks at a commit by name. It creates the ones that don't exist and moves the ones that do. By default a move must go forward: the new target has to descend from the old one.

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. 7. Errors and edge cases
  8. 8. Worked example

In short: jj resolves the target (default @). For each name it decides whether the bookmark is new or moved, and refuses a backwards or sideways move unless --allow-backwards is given. It warns if the target is empty. In one transaction it points every named bookmark at the target. New bookmarks are auto-tracked on matching remotes, just like bookmark create. The operation is point bookmark <names> to commit <id>.

The command is cmd_bookmark_set. Its help text sums it up as "Create a new bookmark, or update an existing one by name" (source). The create-only form is jj bookmark create. To move bookmarks selected by where they point, rather than by name, see jj bookmark move. Bookmark storage is on the view page.

1. Command-line surface

FlagTypeDefaultMeaning
NAMES (positional)bookmark names, one or morerequiredThe bookmarks to create or update. Each is parsed as a revset symbol (source, parse_bookmark_name). Names are exact; there are no patterns.
-r, --revision, --to REVSETrevset@The target, exactly one commit (source).
-B, --allow-backwardsbooloffAllow moving a bookmark to a commit that doesn't descend from its current target (source).

BookmarkSetArgs

2. Configuration

KeyDefaultEffect
remotes.<name>.auto-track-bookmarksunsetBookmarks this command creates are tracked on that remote if their names match (docs).
remotes.<name>.auto-track-created-bookmarksunsetThe same, but only for bookmarks created with bookmark create/set. The two patterns are combined with "or" (source).

Moved and resurrected bookmarks are never auto-tracked (source). A resurrected bookmark keeps whatever tracking state it already had.

3. End-to-end flow

jj bookmark set: call flow cmd_bookmark_set loads the workspace, snapshotting the working copy, and resolves the single target. For each name, it is new if absent locally with no tracked remote bookmarks; otherwise it counts as moved if its target changes. Unless --allow-backwards is set, is_fast_forward must hold or the command fails. It warns if the target is empty. In a transaction it sets every named bookmark to the target and auto-tracks only the new ones on matching remotes. It prints Created and Moved counts, and tx.finish records "point bookmark … to commit …". Shared lifecycle: load workspace and repo, snapshot the working copy (CLI page) resolve_single_rev(-r, default @) absent and no tracked remotes → new else target differs → moved += 1 (already there → counted as neither) unless -B: is_fast_forward(old, target) old absent → ok any old add is an ancestor of target → ok else "Refusing to move bookmark backwards…" empty target → warning; start_transaction; build auto-track matchers per remote for each name: set_local_bookmark_target(name, normal(target)) — also makes the target a visible head if the name is new, for each remote whose pattern matches and that has a remote view: already on the remote? "Auto-tracking bookmark that exists on the remote: name@remote" track_remote_bookmark(name@remote) "Created N bookmarks pointing to …" (if N > 0); "Moved M bookmarks to …" (if M > 0) tx.finish(ui, "point bookmark <names> to commit <id>") writes the operation, exports to Git if colocated

Every name is checked in one loop before the transaction starts, so one refused name means nothing is changed.

4. Core architecture touchpoints

StructureRead / writeHowExplained on
View.local_bookmarksread, writeEach name's old target is read, then replaced with RefTarget::normal(target). A conflicted bookmark becomes a normal one.view
Index (ancestry)readis_ancestor(old, target) for the fast-forward check (source).index
View.remote_viewsread, maybe writeTracked remote bookmarks decide "new" versus "resurrected" (has_tracked_remote_bookmarks). New names may be auto-tracked (track_remote_bookmark).view
View.head_idsmaybe writeThe target is added as a head, so a hidden target becomes visible (set_local_bookmark_target).view
OperationwriteOne operation, described below.operations

5. Transaction and operation

  • Transaction: one, after every name passed the fast-forward check (source).
  • Operation description: point bookmark <name1, name2, …> to commit <full hex id> (source). This is the same text bookmark move uses.
  • Commits created or rewritten: none.
  • Working copy: snapshotted at the start, and otherwise untouched.
  • No-op names: a bookmark already at the target is still written, and still named in the operation description. It just isn't counted in the status line.

6. Algorithm and invariants

Classification (source): a name is new when there's no local bookmark and no tracked remote bookmark of that name. A deleted bookmark that still tracks a remote is being resurrected, and counts as moved, as the source comment says. Otherwise the name counts as moved unless it is already a normal bookmark pointing at the target.

The fast-forward rule (is_fast_forward): an absent old target always passes. A present one passes if any of its added commits is an ancestor of the new target. For a conflicted bookmark, that lets you resolve the conflict by pointing it at a descendant of one side. Strictly "all" sides would have to be ancestors, but jj deliberately allows "any". An old target equal to the new one passes, since a commit is its own ancestor.

Invariants: never rewrites a commit. Never moves a bookmark backwards or sideways without -B. Either every named bookmark is updated, or none is. Auto-tracking only touches bookmarks this run created.

7. Errors and edge cases

MessageWhenSource
Refusing to move bookmark backwards or sideways: <name>, hint Use --allow-backwards to allow it.No present side of the old target is an ancestor of the new target, and -B wasn't given.set.rs
Warning: Target revision is empty.The target has no changes and no description.set.rs
Warning: Auto-tracking bookmark that exists on the remote: <name>@<remote>A new bookmark is auto-tracked on a remote that already has it.set.rs
Failed to parse bookmark name: … (clap: invalid value)The name isn't a valid revset symbol.revset_util.rs
Revset `…` resolved to more than one revision (or none)-r isn't exactly one commit.revset_util.rs

8. Worked example

From upstream's test_bookmark_multiple_names. Bookmarks foo and bar sit on e8849ae1, and @ is its child 0e555a27. Then jj bookmark set -r@ bar baz moves bar forward and creates baz:

jj bookmark set: one move, one create Before: root, then qpvuntsm e8849ae1 with bookmarks bar and foo, then @ zsuskuln 0e555a27 with no bookmarks. After jj bookmark set -r@ bar baz: e8849ae1 keeps foo, and @ 0e555a27 has bar, which moved forward, and baz, which was created. before after set -r@ bar baz root e8849ae1 bar foo @ 0e555a27 no bookmarks root e8849ae1 foo @ 0e555a27 bar (moved) baz (created)

Upstream's snapshot, followed by a second run that names all three:

$ jj bookmark set -r@ bar baz
Warning: Target revision is empty.
Created 1 bookmarks pointing to zsuskuln 0e555a27 bar baz | (empty) (no description set)
Moved 1 bookmarks to zsuskuln 0e555a27 bar baz | (empty) (no description set)

$ jj bookmark set -r@ foo bar baz
Warning: Target revision is empty.
Moved 1 bookmarks to zsuskuln 0e555a27 bar baz foo | (empty) (no description set)

In the second run only foo changes. bar and baz were already there, so they aren't counted. Moving back is refused, as test_bookmark_move shows with jj bookmark set -r@- foo: Error: Refusing to move bookmark backwards or sideways: foo, which then succeeds with --allow-backwards.