jj / How jj bookmark advance works

How jj bookmark advance works

jj bookmark advance moves the bookmarks closest to a target revision forward onto it. By default that's the nearest bookmarked ancestors of @, moved to @, and both "closest" and the target come from configurable revsets.

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 (--to, else revsets.bookmark-advance-to, default @). With bookmark names, it picks the local bookmarks matching them. Without, it evaluates revsets.bookmark-advance-from (default heads(::to & bookmarks())) with to bound to the target, and picks the bookmarks pointing into it. Bookmarks already at the target are dropped; if none remain, No bookmarks to update. Every move must be a fast-forward, and there's no override. In one transaction the bookmarks are pointed at the target, recorded as point bookmark <names> to commit <id>.

The command is cmd_bookmark_advance. Its help text calls jj bookmark advance --to x the equivalent of jj bookmark move --from 'heads(::x & bookmarks())' --to x (source); compare jj bookmark move, which has no configured defaults but allows --allow-backwards. Bookmark storage is on the view page.

1. Command-line surface

FlagTypeDefaultMeaning
NAMES (positional)name patternsnoneAdvance bookmarks with these names; globs by default. When given, revsets.bookmark-advance-from is ignored (source, parse_union_name_patterns).
-t, --to REVSETrevsetrevsets.bookmark-advance-toThe target, exactly one commit (source).

BookmarkAdvanceArgs

2. Configuration

KeyDefaultEffect
revsets.bookmark-advance-to@The target when --to isn't given. If it doesn't resolve to one commit, the error gets a hint naming this key (default, source).
revsets.bookmark-advance-fromheads(::to & bookmarks())Which commits' bookmarks to advance when no names are given. It can refer to the target as to (default, source).

The default from-revset means "the nearest bookmarked commits at or below the target", so in a stack only the top bookmark moves, not every bookmark beneath it.

3. End-to-end flow

jj bookmark advance: call flow cmd_bookmark_advance loads the workspace and resolves the target from --to or revsets.bookmark-advance-to, hinting at the config key on failure. With names it matches local bookmarks by pattern and warns about missing exact names. Without names it parses revsets.bookmark-advance-from with the local variable to bound to the target's commit ID, evaluates it, and keeps bookmarks with any present target in it. Bookmarks already at the target are dropped. If none remain it prints No bookmarks to update. Otherwise any non-fast-forward fails, an empty target warns, and a transaction points every match at the target and records the operation. load workspace; target = resolve_single_rev(--to or revsets.bookmark-advance-to) NAMES given local_bookmarks_matching(patterns) warn on exact names with no local bookmark no NAMES parse revsets.bookmark-advance-from, with to = target's commit ID; evaluate; keep bookmarks with any present target in it drop bookmarks already at the target; none left → "No bookmarks to update." any match not a fast-forward → "Refusing to advance bookmark backwards or sideways: …" empty target → "Warning: Target revision is empty." start_transaction; set_local_bookmark_target(name, normal(target)) for each "Advanced N bookmarks to …"; several matched without names → hint tx.finish(ui, "point bookmark <names> to commit <id>")

4. Core architecture touchpoints

StructureRead / writeHowExplained on
View.local_bookmarksread, writeMatched by name or by target, then set to RefTarget::normal(target). A conflicted bookmark becomes a normal one.view
Revset enginereadThe from-revset is parsed with a local variable to holding the target's commit ID, evaluated once, and used as a membership test (source).revsets
Index (ancestry)readis_ancestor for the fast-forward check (is_fast_forward).index
View.head_idsmaybe writeThe target is added as a head (set_local_bookmark_target).view
OperationwriteOne operation if anything moved, none otherwise.operations

5. Transaction and operation

  • Transaction: one, after selection and the checks (source). With nothing to update, no operation is recorded.
  • Operation description: point bookmark <name1, name2, …> to commit <full hex id>, the same text as bookmark move and set (source).
  • Commits created or rewritten: none. The working copy is only snapshotted.

6. Algorithm and invariants

Without names, the from-revset is parsed with a revset parse context whose local variables include to, set to a string node holding the target's full commit ID, so it behaves like a symbol naming that commit (source). Parse warnings are printed under In revsets.bookmark-advance-from. Every local bookmark is then checked: it is selected if any of its present targets is in the evaluated set (source).

With the default heads(::to & bookmarks()), that set is the bookmarked commits among the target's ancestors (including itself) that have no bookmarked descendant in that set. If the target already carries a bookmark, the set is just the target, and every selected bookmark is then dropped as "already there", giving No bookmarks to update. (source).

The fast-forward test is is_fast_forward: some present side of the old target must be an ancestor of the new one. Unlike bookmark move, there is no flag to skip it (source).

Invariants: never creates or deletes bookmarks, never moves one backwards or sideways, never rewrites commits. Either every selected bookmark moves or none does.

7. Errors and edge cases

MessageWhenSource
No bookmarks to update.Nothing selected, or everything is already at the target.advance.rs
Refusing to advance bookmark backwards or sideways: <name>A selected bookmark isn't an ancestor of the target. Nothing moves.advance.rs
Revset `…` didn't resolve to any revisions, with hint `revsets.bookmark-advance-to` controls the default target. …The target doesn't resolve to one commit; the hint appears only when --to wasn't given.advance.rs
Warning: Target revision is empty.The target has no changes and no description.advance.rs
Hint: Specify bookmark by name to update just one of the bookmarks.Several moved and no names were given.advance.rs
Warning: No matching bookmarks for names: …An exact name has no local bookmark.mod.rs
Revset parse errorsrevsets.bookmark-advance-from doesn't parse.advance.rs

8. Worked example

From upstream's test_bookmark_advance_default. A stack a (bookmark A), b (bookmark B), c, d, and @ = e. Plain jj bookmark advance:

jj bookmark advance with the default revsets Before: a with bookmark A, b with bookmark B, then c, d, and e at @. The from revset heads(::@ and bookmarks()) is just b, because a is an ancestor of b. So only B moves, to e. A stays on a. before after jj bookmark advance a A b B ← heads(::to & bookmarks()) c d (empty) e @ a A b c d (empty) e @ B

Upstream's snapshot (source):

Advanced 1 bookmarks to vruxwmqv 7753a73e B | e

Other cases in the same test: jj bookmark advance A moves A straight to e, since names bypass the from-revset; with --config=revsets.bookmark-advance-from=A|B both move and the "Specify bookmark by name" hint appears; run with @ on b, it prints No bookmarks to update.; and jj bookmark advance B --to A fails with Refusing to advance bookmark backwards or sideways: B (source).