jj / Views, bookmarks and tags

Views, bookmarks and tags

A view is the state of the repo at one operation: which commits are visible heads, where every bookmark and tag points locally and on each remote, what Git refs were last seen, and which commit each workspace has checked out. Every operation points at exactly one view.

On this page
  1. 1. The stored view: op_store::View
  2. 2. RefTarget: refs that can conflict
  3. How refs merge
  4. 3. Remote refs and tracking
  5. 4. The in-memory view: view::View
  6. 5. Merging views after concurrent operations

In short: commits never become "deleted" in jj. They're simply no longer reachable from the view's heads or refs. Moving a bookmark, abandoning a commit or switching workspaces all mean writing a new view. Refs are Merge<Option<CommitId>>, so a bookmark can be conflicted rather than silently overwritten.

1. The stored view: op_store::View

FieldTypeMeaning
head_idsHashSet<CommitId>The visible head commits. Everything visible is an ancestor of one. Never empty.
local_bookmarksBTreeMap<RefNameBuf, RefTarget>Local bookmarks and their (possibly conflicted) targets.
local_tagsBTreeMap<RefNameBuf, RefTarget>Local tags.
remote_viewsBTreeMap<RemoteNameBuf, RemoteView>Per remote: its bookmarks and tags as last seen, each with a tracking state (§3).
git_refsBTreeMap<GitRefNameBuf, RefTarget>Git refs as of the last import/export, by full name (e.g. refs/heads/main). jj compares against these to see what changed in Git.
git_headsBTreeMap<WorkspaceNameBuf, RefTarget>Each workspace's Git HEAD, for colocated repos.
wc_commit_idsBTreeMap<WorkspaceNameBuf, CommitId>The commit each workspace should have checked out (@). The working copy's own state records which commit it actually has.

op_store::View · View::make_root (the root view: just the root commit as head) · storage: the View schema

Views are content-addressed: the ViewId is a hash of the view, so two operations that end in the same state share one stored view. Its legacy fields and migrations (single-workspace wc_commit_id, bookmarks with embedded remote state, Git tags moved into the git remote) are covered on Every schema.

2. RefTarget: refs that can conflict

RefTarget is Merge<Option<CommitId>>, the same alternating add/remove structure used for conflicted trees (see Merge). Its shapes:

ShapeTermsMeaning
absent[None]The ref doesn't exist. Lookups return the static ABSENT_REF_TARGET instead of None.
normal[Some(c)]Points at one commit.
conflicted[Some(a), Some(base), Some(b)], …Two concurrent moves from base, one to a and one to b. Terms can be None, for example "deleted on one side, moved on the other".

How refs merge

When two operations both changed a ref, merge_ref_targets(left, base, right):

  1. Returns the trivial answer if one side didn't change it, or both sides made the same change.
  2. Otherwise it flattens the three targets into one Merge, simplifies it (cancelling equal add/remove pairs), and tries again.
  3. If that's still a conflict, find_pair_to_remove resolves fast-forwards using the index. When two adds are equal, or one is an ancestor of the other, it takes the older add, finds a remove that's an ancestor of it, and drops that remove/add pair. An absent remove counts as an ancestor of everything. It repeats until no pair qualifies. So "A moved forward to B" concurrently with "A moved forward to C", where C descends from B, resolves to C.
  4. Whatever remains is stored as a conflicted bookmark, which the user resolves by moving it.

3. Remote refs and tracking

A RemoteRef is a target: RefTarget plus a state: RemoteRefState:

StateMeaning
NewKnown from the remote but not merged into the local ref. tracked_target() returns absent.
TrackedMerged into the local ref of the same name. Later remote updates are merged too, and pushes compare against it.

RemoteView groups one remote's bookmarks and tags. merge_remote_refs merges target and state separately; if the states somehow conflict, the base state is kept (source). When pushing, classify_ref_push_action compares local and remote into a RefPushAction: Update, AlreadyMatches, LocalConflicted, RemoteConflicted or RemoteUntracked.

4. The in-memory view: view::View

The repo layer wraps the stored view in view::View, which adds one flag, head_normalized, and the editing rules:

MethodRule it enforces
add_head / remove_headEdit head_ids and mark them un-normalized (source).
replace_headsFast path that keeps the normalized flag. The new head must descend from the removed ones (source).
normalize_headsReduces head_ids to true heads using the index, drops the root commit if anything else is present, and pads an empty set with the root. Transaction::write asserts this ran before a view is persisted (source).
set_local_bookmark_targetAn absent target deletes the bookmark, and also deletes absent remote bookmarks that it tracked (source).
set_remote_bookmarkKeeps an absent remote ref only if it's tracked and the local bookmark still exists; otherwise removes it (source).
set_wc_commit / remove_workspace / rename_workspaceEdit wc_commit_ids and the matching git_heads entry together (source).
all_referenced_commit_idsEvery commit any field mentions, including removed terms of conflicts, which merging views needs (source).

5. Merging views after concurrent operations

When two operations happen concurrently, the next command merges them (see How a command runs). MutableRepo::merge first merges the other side's index in, then normalizes heads, then calls merge_view(base, other). That walks every field and applies only what other changed relative to base:

  • Working-copy commits don't use ancestry, since @ moves in arbitrary directions. If both sides moved it, this side wins; if either side removed the workspace, it's removed (merge_wc_commit).
  • Heads: with the default index, jj walks from the base heads to each side's heads to record which commits were rewritten or abandoned, then adds the other side's new heads.
  • Bookmarks, tags, Git refs, remote refs and Git heads each go through merge_ref_targets or merge_remote_refs (§2, §3).