In short: jj resolves -r (default @). For each name, it refuses if the tag
already exists and --allow-move isn't given, and counts new and moved tags. It warns if the
target is empty. In one transaction every name is set to point at the target, jj prints
Created N tags pointing to … and/or Moved N tags to …, and records
set tag <names> to commit <id>. If @ became immutable, the
transaction's finish adds a new empty working-copy commit on top.
The command is cmd_tag_set, also available as
jj tag s (source). Unlike
jj bookmark set, there is no fast-forward
rule: an existing tag can be moved anywhere, but only on request. Tags are stored in the view next to
bookmarks; see the view page.
1. Command-line surface
| Flag | Type | Default | Meaning |
|---|---|---|---|
NAMES (positional) | tag names, required | — | Exact names, parsed as revset symbols, so names like foo@ need quoting (source, parse_tag_name). |
-r, --revision, --to REVSET | revset | @ | The target, exactly one commit (source). |
--allow-move | bool | false | Allow moving tags that already exist (source). |
2. Configuration
The default immutable_heads() includes tags(), so every tagged commit and its
ancestors become immutable (source). That is
what produces the new working-copy commit when @ is tagged. In a colocated workspace, the tag
is exported to Git when the transaction finishes.
3. End-to-end flow
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
View.local_tags | read, write | Each name is set to RefTarget::normal(target). Unlike the bookmark setter, set_local_tag_target doesn't add the target to the view's heads (source, view). | view |
| Immutable set | read | The new tag makes its commit immutable through tags() in immutable_heads(). | revsets |
| Working-copy commit | maybe created | A new empty commit on top of @ if @ was tagged (source). | commits |
| Operation | write | One operation. | operations |
5. Transaction and operation
- Transaction: one, after every name passed the check (source).
- Operation description:
set tag <name1, name2, …> to commit <id>, listing every name given, including ones that didn't change (source). - Commits: none rewritten; possibly one new working-copy commit.
jj undorestores the previous tags and working copy.
6. Algorithm and invariants
Every name is checked before anything changes, so one refused name stops the whole command
(source). A deleted tag counts as absent here. A source TODO notes that if tracked remote tags gain push
support, deleted tags should count as present until pushed, as bookmark set already does. The counts only feed
the status lines: a tag already at the target is still "set", but counted as neither new nor moved.
Invariants: after the command every named tag points exactly at the target, as a normal
(non-conflicted) ref. Existing tags never move without --allow-move. No commit is rewritten.
7. Errors and edge cases
| Message | When | Source |
|---|---|---|
Refusing to move tag: <name>, hint Use --allow-move to update existing tags. | A named tag exists. | set.rs |
Warning: Target revision is empty. | The target has no changes and no description. | set.rs |
Warning: The working-copy commit became immutable; a new commit has been created on top of it. | @ was tagged. | cli_util.rs |
clap: invalid value 'foo@' for '<NAMES>...': Failed to parse tag name: Syntax error | A name that isn't a valid symbol; quote it ('foo@'). An empty name fails the same way. | revset_util.rs, test |
Warning: Failed to export some tags: foo@git: Ref cannot point to the root commit in Git | A tag on the root commit, when exported to Git. | test |
8. Worked example
From upstream's test_tag_set_delete. After jj commit -mcommit1,
jj tag set -r@- foo bar creates two tags. Then jj tag set foo baz fails, and
jj tag set --allow-move foo baz moves foo and creates baz at @:
Upstream's output (source):
Warning: Target revision is empty.
Created 1 tags pointing to rlvkpnrz bbc74930 (empty) (no description set)
Moved 1 tags to rlvkpnrz bbc74930 (empty) (no description set)
Warning: The working-copy commit became immutable; a new commit has been created on top of it.
Working copy (@) now at: yqosqzyt 13cbd515 (empty) (no description set)
Parent commit (@-) : rlvkpnrz bbc74930 (empty) (no description set)