Documentation/Working with Scribe
Branches & merging6 min read

Branches and conflict resolution

On this page

Scribe supports named branches with stable IDs, create/list/rename/switch, three-way merge, and one persisted conflict workflow for merge and stale local edits. main remains the protected default name. Branch edits use ordinary file edit and submit commands. Branch deletion, history rewriting, and automatic binary merging are excluded: accepted history remains retained.

Serving requires repository schema 3 and TCP v2. The repository stores branch heads, merge-parent links, durable operation outcomes, snapshot staging and an index for historical path collisions. Ordinary commits use commit format 1; merge commits use format 2 with a second parent. New stores use schema 3. Older clients are rejected at handshake. Older stores require explicit offline conversion before serving; startup never migrates or falls back.

A branch starts at a pinned source revision. Creation copies only metadata, in bounded transactions, and shares all asset objects. An unpublished snapshot is invisible until one durable activation transaction records its name, head, and operation outcome. Operation IDs and exact intents persist before sending. Names cannot be confused by case or Unicode normalization. Renaming preserves IDs, workspace bindings, ancestry, and locks. Locks remain repository-wide.

Change numbers remain a single monotonically increasing repository sequence; each branch has its own head and each commit has its own parent(s). A merge pins both heads and records the source parent even when no file bytes change. Moving the target while a merge is pending requires reconciliation again. Ancestry prevents repeated merges from reapplying already integrated changes.

Switching requires no opened files, pending submissions, resolutions, or held locks; untracked and unexpected files retain the existing recovery protection. Switch intent is persisted before changing server registration or local state, and sync resumes to the pinned destination after interruption.

Text merging uses bounded three-way line comparison. Non-overlapping edits can merge automatically. Binary, add/add, delete/edit, mode, and path-shape conflicts require a user decision. Persisted base/ours/theirs references survive restarts. resolve lists unresolved paths and accepts ours, theirs, or manually edited contents. Unresolved paths prevent submission. Resolving a stale file updates its expected server base without silently discarding local work. Merge supports inspection, completion by submit, and abort with recovery of intervening edits.

Commands

sh
# Stop the server before upgrading an existing older data root.
scribe-server migrate --data-dir /path/to/data --branches --dry-run
scribe-server migrate --data-dir /path/to/data --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 retry                               # 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                                    # list/resume 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.

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.

For ordinary stale submissions, run scribe resolve --update, resolve any conflicts, and submit again. The same command updates a completed merge plan if another client advanced the target before submission. 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.

Scope and costs

  • Merge requires a full-view workspace with no opened files or unfinished sync. 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.
  • 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.
  • Branch creation copies metadata in bounded pages and transfers no asset content. Its cost grows with file count; 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.
  • File renames use add/delete semantics. Rename/edit combinations may need manual resolution; there is no heuristic rename detection.
  • Path-case, normalization, and file/directory conflicts require a compatible whole-tree choice. Conflicting choices are not applied or submitted.
  • Multiple best merge bases require an explicit --base <change> after review. 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.
  • main cannot be renamed. There is no branch deletion, pruning, rebase, or history rewrite. There is no separate push step: submit publishes immediately.
  • Optimized merge requires repository schema 3 and workspace schema 2. Upgrade existing schema-1/2 repositories explicitly with migrate --branches. Offline conversion is a maintenance operation, not a compatibility serving mode. Older clients are rejected; there is no destructive downgrade.

Measured metadata cost of merge planning

Planning reads the candidate change page, batches of path states, and historical path-relation probes instead of three complete trees. Release-mode run on 2026-09-06, Apple M5 Max, macOS/arm64, Rust 1.96.0. Each repository holds accepted metadata for every path with 20 edited files spread across the project; median and p95 over 100 warm-cache samples.

Tracked filesChanged filesNew metadata p50New metadata p95Previous three-tree metadata reads
100,000200.068 ms0.075 ms45.696 ms
1,000,000200.076 ms0.128 ms584.922 ms

These are index measurements on a fixture-seeded database, not complete merge timings and not an end-to-end million-file workspace.

Reproduce from the repository root:

sh
cargo test --release --locked -p scribe-metadata qualify_merge_index_scale -- --ignored --nocapture

Whole-merge time still includes hashing affected files, downloading missing chunks, writing files, and durable flushes; those scale with changed asset volume, not tracked-file count. Branch creation copies metadata in bounded pages and remains proportional to file count. Linux and Windows have not been measured.

Source docs/branches.mdSnapshot 93d02b17