Documentation
Project history
Local Git-compatible project snapshots, structural diffs, reviewable branches, and optional GitHub mirroring.
Working copy and history
The application's SQLite records are the current project working copy. @cadcamfun/vcs stores Git-compatible blobs, trees, commits, and refs through an injected object backend; the sidecar persists that backend in SQLite. History operations work locally without a Git executable or a GitHub account.
A commit snapshots the project and its related design/manufacturing resources. Branches point to commits, while HEAD identifies the checked-out branch. Resource version tables coexist with this project-level history; a drawing version and a project commit have different scopes.
Commit and branch workflow
- Read
GET /api/projects/:id/vcs/statusto see the current branch, HEAD, and changed paths. - Create a branch with
POST /vcs/branches, then check it out withPOST /vcs/checkout. Branch paths here are relative to the project's API prefix. - Edit project resources and commit with
POST /vcs/commitand a non-empty message. - Inspect
GET /vcs/logandGET /vcs/diff. Log supports a revision, limit, and optional tracked path. - Open a project pull request, review its structural changes, and merge after resolving blocking reviews or conflicts.
Checkout, restore, and operations that move the working branch refuse a dirty working copy where required. A restore records a new commit representing the earlier content, optionally limited to selected paths. It preserves subsequent history rather than rewriting it.
Snapshot contents
project.json
drawings/<id>.json
components/<id>.json
toolpaths/<id>.json
materials/<id>.json
tools/<id>.json
machine-configs/<id>.json
costs/<id>.json
.cadcamfun/workflows/<name>.yml One JSON file per entity keeps unrelated changes separate. The snapshot includes library items referenced by toolpaths and production cost estimates so restored manufacturing data can retain its dependencies. Entity identifiers used as file names are restricted to safe letters, digits, underscores, and hyphens, with a maximum of 128 characters.
JSON keys are sorted recursively and files use stable pretty-printing with a final newline. Arrays retain their order. Extra text files can travel with a package-level snapshot; the public commit endpoint accepts extra files only under .cadcamfun/workflows/ with a safe YAML file name.
Objects use git's loose-object format byte for byte, so object IDs match those computed by git. Tree entries are either files (mode 100644) or directories (mode 40000). A commit signature records name, email, Unix-seconds time, and a +hhmm/-hhmm offset. Branches are stored under refs/heads/, and a project's default branch is main (packages/vcs/src/object.ts, repository.ts).
Diff and merge semantics
The repository computes a merge base, handles unchanged sides directly, and performs structural three-way merges for changed JSON. Independent object-property edits can merge without conflict. Concurrent incompatible edits to the same scalar or array are reported through JSON pointers; arrays are not merged element by element.
Entity diffs include path, resource kind, identifier and name when available, added/modified/deleted status, and property changes with before/after values. Non-JSON files use file-level conflict behavior. A repository merge with conflicts returns their paths and pointers without writing a merged commit.
Pull requests and permissions
Project pull requests live at /api/projects/:id/pulls. Opening one records a title, body, head, base, and author kind. Detail responses include the pull request, reviews, and changes. Reviews support comment, approve, and request-changes, with optional path and property pointer.
The latest non-comment verdict per reviewer controls a request-changes block. A merge is refused while any such verdict requests changes. The implementation does not require a positive approval when there is no blocking verdict. Merged pull-request diffs use the recorded merge commit so later branch movement does not change that review record.
Reading history, committing, branching, opening pull requests, and reviewing require project access. Working-copy moves and shared-branch maintenance require the owner or an organization admin/manager as enforced by the handler. Dirty or conflicting operations return 409; protected actions return 403; missing refs or resources return 404; other invalid operations return 400.
An AI actor cannot commit, restore, or merge into main. The assistant uses a proposal branch and opens a pull request for a person to review. Audit and notification records identify assistant-originated changes.
Optional GitHub mirror
GET and PUT /api/projects/:id/vcs/remote read or set an owner/repo GitHub mirror; null detaches it. Push and pull use POST /vcs/push and /vcs/pull, with the user's GitHub token in x-github-token for that call.
Push transfers reachable objects and updates the remote branch without overwriting unpulled remote commits. Pull fetches the remote branch and merges it locally, refusing a dirty working copy. Divergent incompatible JSON edits return conflicts rather than silently replacing local content.
The sidecar transport uses GitHub REST object/ref operations. The GitHub token is supplied per request and is not stored as project data. A local pull request is an application review record; it is not automatically a GitHub pull request.
Snapshots and restoration
snapshotToFiles converts a project snapshot to canonical file contents; filesToSnapshot reconstructs the domain lists. ProjectVcsRepo reads the current database resources and applies restored snapshots. Whole-project restoration can change multiple related tables, so review its diff and clear the working copy before switching history.
Transport and object APIs
The VCS package exports object encoding and reading, Repository, store adapters, canonical JSON and merge helpers, snapshot conversion, entity diffs, GitHub synchronization, and transport interfaces. Tests can inject an in-memory object backend or fake transport instead of accessing a user's repository.
Implementation entry points
packages/vcs/src/repository.ts owns commits, refs, traversal, restoration, and merge; json.ts owns structural merging; snapshot.ts defines tracked resources; diff.ts describes entity changes; github.ts and transport.ts implement mirroring. Sidecar project-vcs.ts adds actor policy and persistence, while http/vcs.ts supplies transport authorization and events.
History operations and automation
Workflow definitions are versioned with the project and run against committed files. A commit may trigger those workflows after history has been recorded; a failed workflow does not undo the commit. Keep local resource saves, project commits, workflow results, and remote synchronization distinct when diagnosing the state of a project.
Try the repository without application data
The object-store seam lets an integration exercise commits and branches entirely in memory. Run this inside a workspace that depends on @cadcamfun/vcs, such as apps/sidecar. It does not touch the desktop's project database.
import { Repository, memoryBackend, canonicalJson } from "@cadcamfun/vcs"
const repo = new Repository(memoryBackend())
const author = { name: "Workshop", email: "workshop@example.invalid", time: 0, tz: "+0000" }
repo.commit({ "project.json": canonicalJson({ name: "Blank", revision: 1 }) }, {
message: "Initial blank", author
})
repo.createBranch("proposal/wider-blank")
repo.checkout("proposal/wider-blank")
repo.commit({ "project.json": canonicalJson({ name: "Blank", revision: 2 }) }, {
message: "Record revised blank", author
})
console.log(repo.currentBranch()) // proposal/wider-blank
console.log(repo.log().length) // 2 Repository.commit receives the complete file tree for that commit, not a patch. The sidecar builds this tree from working database records and carried text files. A package-level integration must preserve files it intends to retain. The in-memory example does not apply sidecar ownership or AI actor policy; those are enforced by the project service and HTTP layer.
Source: packages/vcs/src/store.ts, repository.ts, and apps/sidecar/src/services/project-vcs.ts.
Review, restore, and synchronize deliberately
Record working-copy changes before switching branches. A new branch requires a resolvable source commit, so initialize an uncommitted project with its first commit before branching. Creating a branch only creates a ref; checkout is a separate operation that applies its snapshot to the working database.
For a pull request, compare the head against its merge base and review property-level differences. A later comment does not clear a previous request-changes verdict: the reviewer must submit a subsequent approve verdict. A merged PR can fast-forward or create a merge commit according to repository ancestry; inspect the recorded merge commit instead of assuming a fixed number of parents.
When merging into the checked-out base, the service requires a clean working copy and applies the new base snapshot. When another branch is checked out, merging the PR's base updates that branch's ref without replacing the current working database. A subsequent checkout loads that updated base.
Push and pull synchronize committed history. Unsaved UI content and uncommitted database state are not remote snapshots. A remote-ahead rejection is a request to fetch/merge first; it is not an instruction to overwrite the remote branch.
Source: packages/vcs/src/repository.ts, github.ts, and apps/sidecar/src/services/project-vcs.ts.
Recover from history failures
| Failure | Next action |
|---|---|
NothingToCommit | Compare database working files with HEAD. Saving the same resource values again does not create a different tree. |
| Invalid or existing branch | Choose a new safe ref name. Names cannot contain traversal, double slashes, ref-expression fragments, or a trailing slash/.lock suffix. |
| Uncommitted changes | Commit or deliberately restore/discard the intended resources before checkout, pull, or a protected working-copy operation. |
| Merge conflict with path/pointers | Inspect those properties in base/head histories, create a reviewed resolution, and retry. The failed repository merge did not create a partial merge commit. |
| A reviewer requested changes | Resolve the feedback and obtain an updated non-comment verdict from that reviewer. |
| AI actors cannot act on main | Use an AI proposal branch and a human-reviewed pull request. Changing a confirmation flag does not remove the actor restriction. |
Source: packages/vcs/src/repository.ts, apps/sidecar/src/services/project-vcs.ts, and http/vcs.ts. The API reference lists the actual request and response schemas.
Repository layout
A project is stored in the repository as one JSON file per entity. Editing a drawing therefore never touches a toolpath file, and the layout reads naturally on GitHub. snapshotToFiles writes this layout from a ProjectSnapshot, and filesToSnapshot reads it back.
| Snapshot list | Directory | Content |
|---|---|---|
project | project.json | Project metadata. |
drawings, components, toolpaths | same names | Design and manufacturing entities. |
materials, tools, machineConfigs | materials/, tools/, machine-configs/ | Library items the toolpaths reference, versioned with the project so a restored toolpath still finds them. |
costs | costs/ | Production cost estimates of the toolpaths. |
extra | any path | Text files stored verbatim: workflows, README, generated G-code. |
Commit, branch, diff, and merge in code
The Repository class works on any storage backend. The sidecar backs it with SQLite (tables GitObject and GitRef); memoryBackend() is used for tests and experiments. The following example is complete and its output is shown in comments.
import { Repository, memoryBackend, snapshotToFiles, describeDiff } from "@cadcamfun/vcs"
const repo = new Repository(memoryBackend())
const me = { name: "Ada", email: "ada@example.com", time: 1767225600, tz: "+0100" }
const files = (width: number, height: number) => snapshotToFiles({
project: { id: "p1", name: "Bracket" },
drawings: [{ id: "d1", name: "Plate", width, height }],
components: [], toolpaths: [],
})
const base = repo.commit(files(40, 25), { message: "base", author: me })
repo.currentBranch() // "main"
repo.createBranch("a"); repo.createBranch("b")
repo.checkout("a"); const a = repo.commit(files(50, 25), { message: "a: width 50", author: me })
repo.checkout("b"); repo.commit(files(40, 30), { message: "b: height 30", author: me })
describeDiff(repo, base, a)
// [{ path: "drawings/d1.json", kind: "drawings", id: "d1", name: "Plate",
// status: "modified", changes: [{ pointer: "/width", before: 40, after: 50 }] }]
repo.mergeInto("a", "b", me) // { kind: "merge", oid: "46e7…" }
// different properties of the same entity merge cleanly
repo.checkout("b"); repo.commit(files(60, 30), { message: "b: width 60", author: me })
repo.mergeInto("a", "b", me)
// { kind: "conflict", conflicts: [{ path: "drawings/d1.json", pointers: ["/width"] }] }
repo.createBranch("bad..name") // throws: Invalid branch name bad..name
repo.commit(sameFilesAsHead, { message: "x", author: me })
// throws: No changes since the last commit (unless allowEmpty: true) Merges are three-way at the level of JSON properties. When both sides changed different properties of one entity, the merge succeeds. When both changed the same property to different values, the result is a conflict that lists RFC 6901 pointers inside each file. Arrays of equal length are compared per index, so a moved vertex shows up as one change; arrays of different lengths are compared as a whole.
| Merge result | Meaning |
|---|---|
up-to-date | Nothing to merge; returns the current commit. |
fast-forward | The branch pointer moved forward; no merge commit was created. |
merge | A merge commit was created. |
conflict | No commit; conflicts lists paths and pointers to resolve. |
| Repository method | Purpose |
|---|---|
commit(files, { message, author, allowEmpty? }) | Write a full tree as a new commit on the current branch. |
log(rev = "HEAD", limit?) / fileLog(path, rev?, limit?) | History of the branch / of one file. |
createBranch(name, from = "HEAD"), deleteBranch, checkout | Branch management. Names must not contain .. or //, end in / or .lock, or contain @{. |
tag(name, rev = "HEAD"), tags() | Lightweight tags under refs/tags/. |
diff(from, to) | File-level changes: added, modified, deleted, with blob IDs. |
restorePaths(rev, paths, author, message?) | New commit restoring selected files from an older revision. |
revertTo(rev, author, message?) | New commit whose tree equals an older revision; history is not rewritten. |
merge(theirs, author) / mergeInto(into, theirs, author) | Three-way merge into the current branch / a named branch. |
isAncestor, mergeBase | Graph queries. |
Rules enforced by the application
The sidecar's project history service adds rules on top of the repository:
| Failure code | When |
|---|---|
blocked | An AI actor tries to commit or merge on main (the message says to use an ai/* branch and open a pull request), or a pull request is merged while a reviewer's latest verdict is request-changes. |
dirty | The working copy has uncommitted changes and the operation would overwrite them: "Uncommitted changes in N file(s); commit or discard them first". |
conflict | A merge produced conflicts; the failure carries the conflict list. |
nothing-to-commit | The tree is unchanged since the last commit. |
not-found | Unknown branch, commit, object, or pull request. |
remote | GitHub synchronisation failed. |
invalid | Other invalid input, for example reviewing or merging a pull request that is not open. |
Only the latest non-comment verdict of each reviewer counts. A reviewer who requested changes and later approved no longer blocks the merge. Commits, pull request events, and reviews are published as live events, so the desktop and the companion update without polling.
Byte-compatible with git
Object IDs are computed exactly as git computes them. Pushing to GitHub through the Git Data API keeps every commit ID identical on both sides, and a git clone of the mirror shows the same history as the application. GitHub accepts only fast-forward ref updates; pull and merge locally first when the remote has moved.
A real commit object
{
"oid": "44d2d03d23b27fbbc4b388e32e15dfb0f39fff87",
"parents": [],
"message": "First version\n",
"author": { "name": "Local user", "email": "local@cadcamfun.local", "time": 1792037121 }
} oidis a 40-character hex id of the commit object stored inGitObject.- The first commit has no parents; a merge commit has two.
- The message is stored with a trailing newline, as Git does.
- Without an
x-session-token, the author is the installation's local profile.timeis in Unix seconds.
| Route | Body | Required |
|---|---|---|
POST …/vcs/commit | message, authorKind, files | message |
POST …/vcs/branches | name, from | name |
POST …/vcs/checkout | branch | branch |
POST …/vcs/restore | rev, paths | rev |
POST …/pulls | title, body, head, base, authorKind | title, head |