A branch is a named line of development within a repository. Use ordinary
add, edit, delete, move, and submit commands to work on it, then merge its
changes into another branch. main is the protected default branch: it cannot
be renamed. Branch deletion and history rewriting are not supported.
Creating a branch shares immutable metadata snapshots and existing asset objects.
Renaming a branch preserves its identity, history, and workspace bindings.
Locks apply to a path across the entire repository, including other branches.
The accepted locks.rules in .scribe/config.toml on main govern every
branch; changing a feature branch's rules proposes a policy change but does not
activate it. Workspace profiles use the selected branch's config instead. See
mandatory lock rules.
Change numbers come from one repository-wide sequence, while each branch has
its own head revision.
A merge compares the source and target with a common ancestor. Non-overlapping
text edits can merge automatically; binary files and overlapping edits need a
choice or manual editing. scribe resolve records those decisions, and
scribe submit publishes the merge. Unresolved conflicts survive restarts and
prevent submission. Merge ancestry records the source revision even if the
merge changes no file bytes, so later merges can identify shared history.
Locks are decided before a merge or cherry-pick writes anything. Planning lists
the current holders of the paths the result would change. A path another
workspace holds refuses the plan, naming up to five holders, and leaves every
working file untouched; wait for the holder to release it, or release it
explicitly, then run scribe merge --continue. For paths the active policy
requires a lock for, the integration acquires the lock for this workspace
before applying, recording the claim first, so scribe merge --abort releases
exactly those acquisitions. Keeping the target's version writes nothing, needs
no lock, and never blocks. A successful merge submission releases its submitted locks by default. Use
scribe submit --keep-locks to retain them; scribe lock release releases them explicitly.
Commands
Run these commands inside a workspace. For an older repository, follow the offline migration procedure before using branches.
scribe branch list
scribe branch create environment --from main # snapshot current source head
scribe branch create experiment --from main --revision 42
scribe branch rename experiment lighting # identity/history stay the same
scribe branch --continue # recover a lost branch-operation reply
scribe switch environment # resume with switch or sync if interrupted
# Work on a branch with the usual add/edit/delete/submit commands.
scribe edit Source/lighting.cpp
scribe submit -m "Update lighting"
scribe switch main
scribe merge environment # pin source and prepare local results
scribe status
scribe resolve # inspect current conflict resolution
scribe resolve --export /tmp/lighting-conflict # base/, ours/, theirs/ for external tools
scribe resolve --theirs Assets/level.umap
# Or edit the working file using an editor, then acknowledge that result:
scribe resolve --working Source/lighting.cpp
scribe submit -m "Merge environment"ours is the target workspace's version; theirs is the source branch (or
latest server version when resolving stale work). Missing sides represent
file deletion. --ours, --theirs, and --working take paths or --all.
--all chooses that side for every path in the plan, including automatic
merges; use individual paths to retain other automatic results.
Keeping the target's version for a path the source changed writes no file, but
it is still a decision the submission has to carry. scribe status shows those
paths opened for keep; scribe revert on one only retires the record. A keep
submits no bytes and changes no tree, so a merge that only declines the source
still lists those paths in its change set.
scribe merge --continue resumes without changing the pinned source. A merge
still needs submit to become shared history. scribe merge --abort (also
resolve --abort) restores the target states of the affected paths and retains
displaced local files in recovery. Extra opened paths outside the plan stay open.
After a repository restore, branch --continue still resolves recorded outcomes and
snapshot intents retained in the backup. If the server refuses an unknown old
intent because its incarnation changed, the client archives it in
.scribe/_state/recovery/branch-<operation>.pending. Sync and explicitly select the
branch operation again; a reused revision number never silently selects new
history.
If a restore invalidates an active resolution, resolve --abort archives its
marker and resolution records in .scribe/_state/recovery/resolution-<workflow-id>.pending
and clears the active workflow without downloading old targets. Working files,
opened edits and baselines remain intact. Locks the merge acquired are not
released along that path, because the restored repository's lock table is not
this workflow's to interpret; review scribe lock list after syncing. Run
scribe sync, then scribe resolve --update and select the files to keep
before submitting again.
For ordinary stale submissions, run scribe resolve --update, resolve any
conflicts, and submit again. Finish or abort pending moves before updating their
source revisions. The same command updates a completed, ungrouped merge plan
in a full-view workspace if another client advanced the target before submission.
Unrelated target commits do not invalidate an integration: the server validates
source paths, submitted paths, move origins, and their collision dependencies
against the current head, then accepts with that head as its parent. This also
preserves partial-view plans, rename groups, and manual choices. If those
dependencies actually changed, partial-view integrations and merges with rename
or collision groups must be aborted and restarted against the new target; preserve and reapply recovered manual edits as needed. Resolve existing
conflicts first. If all stale local edits are discarded in favor of the server,
the workflow finishes without creating an empty ordinary submission.
Switching branches
A branch switch first updates the workspace registration, then changes the local branch and downloads its files. If registration is refused before the local transition, the client cancels the pending switch and keeps the original branch, files, and sync watermark intact. This also releases an older stuck switch when resumed. A transport failure retains pending intent because the registration may have succeeded; a refusal after local materialization starts retains the recovery state as well. Resume those switches after restoring connectivity or access.
Repository access does not grant ownership of another account's working copy. Sign in as the workspace owner, or create a separate workspace under your account. Workspaces registered without an authenticated owner cannot be claimed by signing in later. The macOS permission notice offers sign-in and workspace creation in the app; creating a new working copy leaves the existing local files intact.
Verified merge coverage
A merge submission pins the merge base it was planned against alongside the
target head and the source revision. Before taking the publication gate the
server recomputes the merge base from its own history and refuses a base branch
that disagrees with it (base_mismatch). It then walks every path the source
changed between that base and the pinned source revision, skipping paths the
source did not really change and paths the target already holds identically,
and requires each remaining path to appear in the change set — as an add, edit,
delete, or keep. A path whose live target spelling was renamed is covered by the
submitted new spelling, which the server maps back through recorded file
lineage.
A submission that omits part of the source delta is rejected with
merge_incomplete, naming up to five uncovered paths, and records no history:
without this check a planning defect would mark the source merged while its
changes stayed invisible from the target, and the next merge would report
already_merged. The verification uses the same budget as the client planner
(100,000 paths / 32 MiB of path names) and reports limit_exceeded beyond it.
Cherry-picks carry no base; their base is the picked commit's parent, which the
server derives itself.
Requirements and limits
- Merge supports full and partial views, with no opened files or unfinished sync. See partial-view integration for excluded content and conflicts. It verifies affected tracked files, preserving unrelated local edits. A paged prerequisite delta updates the workspace to the pinned target first; files already at that repository state are not reopened or replaced.
- Merge and cherry-pick planning reads the active lock policy once and lists
the repository's locks in bounded pages, stopping at the last planned path.
Stale opened work updated with
resolve --updatekeeps the locks itseditacquired and takes none: only an integration writes paths this workspace never opened. - Planning reads divergent first-parent change indexes and queries only the candidate paths at base/target/source. Historical folded-path lookups detect case and file/directory collisions without downloading a complete tree. Resolve uses the same targeted queries. Already-merged sources return before any tracked-file verification or tree lookup.
- Planning is bounded to 100,000 candidate paths, 32 MiB of path names and 128 MiB of encoded resolution rows. Larger plans fail before merge writes; merge an earlier source revision first. Pages contain at most 1,024 changed paths or examine 128 commits. Work grows with divergent history and affected paths, including conflicting descendants; it is not constant time. Each connected rename/collision group is limited to 1,024 paths and 32 MiB of encoded resolution records so its choice fits one bounded transaction.
- Branch creation stores the source snapshot roots and transfers no asset content. It performs no per-file copying, including on nested forks. Branch name and ancestry validation still have their own costs; switching transfers missing chunks.
- Text merging is conservative: valid UTF-8, no NUL bytes, at most 4 MiB per input and 8 million line-comparison cells. Larger or ambiguous edits require an explicit choice or an external tool. No binary merge is attempted.
- Per-file logs follow fork origins and merge parents, retaining each source
revision's branch attribution and deduplicating shared ancestors. Use
scribe log <path> --first-parentto omit merge-source ancestry. A source revision is historical context, not proof that conflict resolution chose its bytes. History reads use one metadata snapshot, exact-path ranges and the sparse repository-wide merge-parent index; they skip unrelated ordinary commits. Work is capped at 100,000 branch, path-row and merge-row visits per request; exceeding the budget returnslimit_exceeded, never partial history.scribe log <path> --all-branchesadditionally follows indexed file lineage into unmerged branches at a pinned repository snapshot. Filename reuse starts a separate file incarnation. Itsin_ancestryfield describes commit ancestry, not whether a merge selected that revision's bytes. See cross-branch history for pagination. scribe moverecords an add/delete pair with its exact predecessor revision. File history follows those records, including moves on a merged source branch. Merge planning follows this lineage through chained and folder moves. A rename on one side and edit on the other merges at the renamed destination; matching renames use their agreed destination. Competing names, rename/delete, and occupied destinations form explicit resolution groups. Selecting a member selects the whole group. See rename-aware merging for content choices, layout choices, and--working. There is no heuristic rename detection for moves made outside Scribe.- Path-case, normalization, and file/directory conflicts require a compatible layout for the complete collision group. Conflicting choices are not applied or submitted.
- Multiple best merge bases require an explicit
--base <change>after review. Multiple base-file ancestors for one file are also refused; abort and select an appropriate earlier explicit base before replanning. Origin reads use the lineage index, with a 100,000-visit / 32 MiB budget per query batch. Source revisions must be on the source's first-parent history and at or after its creation. Historical tree queries before a branch existed are refused. maincannot be renamed. There is no branch deletion, pruning, rebase, or history rewrite. There is no separate push step: submit publishes immediately. A creation that never activated is not history: an operator can release its reserved name offline withscribe-server staging-branches --abandon.- Merge requires repository schema 3 with snapshot profile 3 and workspace schema 2. Pre-snapshot development repositories are incompatible with this build; do not force their profile markers. Older clients are rejected.
Measuring merge planning
Merge planning queries changed paths and their historical states. Its work grows with divergent history and affected paths. Branch creation shares the source snapshot without enumerating its files. Snapshot regression tests also count visited pages and inserted rows on generated metadata; large asset repositories are unnecessary for checking those growth properties.
Developers can run the opt-in metadata benchmark from the repository root:
cargo test --release --locked -p scribe-metadata qualify_merge_index_scale -- --ignored --nocaptureThis measures index queries on generated repository metadata. Complete merge time also includes hashing affected files, downloading missing chunks, writing local files, and durable flushes. Use a representative workspace to measure those costs.
Developer reference: branch publication
Serving requires repository schema 3 and TCP v2, or authenticated protocol v4.
Ordinary commits use format 1; merge commits use format 2 with a second parent.
The repository stores branch heads, merge parents, operation outcomes, pending
snapshots, historical path-collision indexes, exact move predecessors, and the
bidirectional file_lineage index. Submit accepts history and its derived
indexes in the same durable metadata transaction.
Branch reservation pins the source revision's exact-path and Unicode-collision radix roots in one durable transaction. It creates no per-file history anchors and no snapshot pages. The branch remains invisible until a second durable transaction records its head and operation outcome. Subsequent submits replace only pages along changed paths; unchanged subtrees remain shared. Branch operation IDs and exact intents persist before transmission. Switch intent likewise persists before changing server registration or local state; interrupted sync resumes to the pinned destination. See the format specification for records and durability and recovery for ordering.
Staging lifecycle
A branch record is written in the staging state as soon as its name is
reserved, and stays invisible: it has no head, no serving query returns it, and
only a retry of the same operation id can activate it. The reservation is what
makes a name unique, so it also survives the client that made it. A workspace
that still holds the intent finishes it with scribe branch --continue; if that
client is gone — its .scribe/_state/ lost, its workspace discarded — nothing can
complete or replace the reservation, and create keeps reporting that the name
is already reserved.
Releasing such a name is offline maintenance, not a branch deletion: a staging
branch was never history. With the server stopped,
scribe-server staging-branches lists the unfinished snapshots (name, branch
id, pending operation id, staged rows, pinned change) and --abandon discards
one, deleting its single staged snapshot-root row and removing the
reservation last in bounded durable transactions. An activated branch is refused. See
operations.