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
| Field | Type | Meaning |
|---|---|---|
head_ids | HashSet<CommitId> | The visible head commits. Everything visible is an ancestor of one. Never empty. |
local_bookmarks | BTreeMap<RefNameBuf, RefTarget> | Local bookmarks and their (possibly conflicted) targets. |
local_tags | BTreeMap<RefNameBuf, RefTarget> | Local tags. |
remote_views | BTreeMap<RemoteNameBuf, RemoteView> | Per remote: its bookmarks and tags as last seen, each with a tracking state (§3). |
git_refs | BTreeMap<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_heads | BTreeMap<WorkspaceNameBuf, RefTarget> | Each workspace's Git HEAD, for colocated repos. |
wc_commit_ids | BTreeMap<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:
| Shape | Terms | Meaning |
|---|---|---|
| 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):
- Returns the trivial answer if one side didn't change it, or both sides made the same change.
- Otherwise it flattens the three targets into one
Merge, simplifies it (cancelling equal add/remove pairs), and tries again. -
If that's still a conflict,
find_pair_to_removeresolves 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. - 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:
| State | Meaning |
|---|---|
New | Known from the remote but not merged into the local ref. tracked_target() returns absent. |
Tracked | Merged 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:
| Method | Rule it enforces |
|---|---|
add_head / remove_head | Edit head_ids and mark them un-normalized (source). |
replace_heads | Fast path that keeps the normalized flag. The new head must descend from the removed ones (source). |
normalize_heads | Reduces 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_target | An absent target deletes the bookmark, and also deletes absent remote bookmarks that it tracked (source). |
set_remote_bookmark | Keeps 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_workspace | Edit wc_commit_ids and the matching git_heads entry together (source). |
all_referenced_commit_ids | Every 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_targetsormerge_remote_refs(§2, §3).