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
| Flag | Type | Default | Meaning |
|---|---|---|---|
NAMES (positional) | bookmark names, one or more | required | The 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 REVSET | revset | @ | The target, exactly one commit (source). |
-B, --allow-backwards | bool | off | Allow moving a bookmark to a commit that doesn't descend from its current target (source). |
2. Configuration
| Key | Default | Effect |
|---|---|---|
remotes.<name>.auto-track-bookmarks | unset | Bookmarks this command creates are tracked on that remote if their names match (docs). |
remotes.<name>.auto-track-created-bookmarks | unset | The 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
Every name is checked in one loop before the transaction starts, so one refused name means nothing is changed.
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
View.local_bookmarks | read, write | Each name's old target is read, then replaced with RefTarget::normal(target). A conflicted bookmark becomes a normal one. | view |
| Index (ancestry) | read | is_ancestor(old, target) for the fast-forward check (source). | index |
View.remote_views | read, maybe write | Tracked remote bookmarks decide "new" versus "resurrected" (has_tracked_remote_bookmarks). New names may be auto-tracked (track_remote_bookmark). | view |
View.head_ids | maybe write | The target is added as a head, so a hidden target becomes visible (set_local_bookmark_target). | view |
| Operation | write | One 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 textbookmark moveuses. - 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
| Message | When | Source |
|---|---|---|
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:
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.