文档
项目历史
本地 Git 兼容项目快照、结构差异、可审查分支和可选的 GitHub 镜像。
从英文自动翻译。 查看英文原文
工作副本和历史记录
应用程序的 SQLite 记录是当前项目的工作副本。 @cadcamfun/vcs 通过注入的对象后端存储与 Git 兼容的 blob、树、提交和引用; sidecar 将该后端保留在 SQLite 中。历史操作可以在本地进行,无需 Git 可执行文件或 GitHub 帐户。
提交快照项目及其相关的设计/制造资源。分支指向提交,而 HEAD 标识签出分支。资源版本表与该项目级历史记录共存;绘图版本和项目提交具有不同的范围。
提交和分支工作流程
- 读
GET /api/projects/:id/vcs/status查看当前分支、HEAD 和更改的路径。 - 创建一个分支
POST /vcs/branches,然后检查一下POST /vcs/checkout。这里的分支路径是相对于项目的API前缀的。 - 编辑项目资源并提交
POST /vcs/commit和一个非空消息。 - 检查
GET /vcs/log和GET /vcs/diff。日志支持修订、限制和可选的跟踪路径。 - 打开项目拉取请求,审查其结构更改,并在解决阻塞审查或冲突后进行合并。
检出、恢复和移动工作分支的操作会在需要时拒绝脏工作副本。恢复记录代表早期内容的新提交,可以选择限制为选定的路径。它保留了后来的历史,而不是重写它。
快照内容
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 每个实体一个 JSON 文件将不相关的更改分开。快照包括刀具路径引用的库项目和生产成本估算,因此恢复的制造数据可以保留其依赖性。用作文件名的实体标识符仅限于安全字母、数字、下划线和连字符,最多为 128 个字符。
JSON 键以递归方式排序,文件使用稳定的漂亮打印和最后的换行符。数组保留其顺序。额外的文本文件可以与包级快照一起传输;公共提交端点仅接受额外的文件 .cadcamfun/workflows/ 使用安全的 YAML 文件名。
对象使用 git 的松散对象格式逐字节,因此对象 ID 与由 git。树条目要么是文件(模式 100644)或目录(模式 40000)。提交签名记录姓名、电子邮件、Unix 秒时间和 +hhmm/-hhmm 抵消。分支存储在 refs/heads/,项目的默认分支是 main (packages/vcs/src/object.ts, repository.ts).
差异和合并语义
该存储库计算合并基础,直接处理未更改的边,并对更改的 JSON 执行结构三向合并。独立的对象属性编辑可以合并而不会发生冲突。通过 JSON 指针报告对同一标量或数组的并发不兼容编辑;数组不会逐个元素合并。
实体差异包括路径、资源种类、标识符和名称(如果可用)、添加/修改/删除状态以及之前/之后值的属性更改。非 JSON 文件使用文件级冲突行为。具有冲突的存储库合并会返回它们的路径和指针,而无需编写合并的提交。
拉取请求和权限
项目拉取请求位于 /api/projects/:id/pulls。打开一个记录标题、正文、头部、基础和作者类型。详细响应包括拉取请求、评论和更改。评论支持 comment, approve, 和 request-changes,带有可选路径和属性指针。
每个审阅者的最新非评论裁决控制请求更改块。当任何此类判决请求更改时,合并将被拒绝。当没有阻止判决时,实施不需要积极批准。合并的拉取请求差异使用记录的合并提交,因此以后的分支移动不会更改该审查记录。
读取历史记录、提交、分支、打开拉取请求和审查需要项目访问权限。工作副本移动和共享分支维护需要所有者或组织管理员/经理(由处理程序强制执行)。脏操作或冲突操作返回 409;受保护的操作返回 403;缺少引用或资源返回 404;其他无效操作返回400。
AI 参与者无法提交、恢复或合并到 main。助理使用提案分支并打开拉取请求供人员审查。审核和通知记录可识别助理发起的变更。
可选的 GitHub 镜像
GET 和 PUT /api/projects/:id/vcs/remote 读取或设置 owner/repo GitHub 镜像; null 将其分离。推拉使用 POST /vcs/push 和 /vcs/pull,用户的 GitHub 令牌位于 x-github-token 为了那个电话。
推送传输可到达的对象并更新远程分支,而不会覆盖未拉取的远程提交。 Pull 获取远程分支并将其合并到本地,拒绝脏工作副本。不同的不兼容 JSON 编辑会返回冲突,而不是默默地替换本地内容。
sidecar 传输使用 GitHub REST 对象/引用操作。 GitHub 令牌是根据请求提供的,不会存储为项目数据。本地拉取请求是应用程序审核记录;它不会自动成为 GitHub 拉取请求。
快照和恢复
snapshotToFiles 将项目快照转换为规范文件内容; filesToSnapshot 重建域列表。 ProjectVcsRepo 读取当前数据库资源并应用恢复的快照。整个项目恢复可以更改多个相关表,因此在切换历史记录之前检查其差异并清除工作副本。
传输和对象 API
VCS包导出对象编码和读取, Repository、存储适配器、规范 JSON 和合并助手、快照转换、实体差异、GitHub 同步和传输接口。测试可以注入内存中对象后端或伪造传输,而不是访问用户的存储库。
实施切入点
packages/vcs/src/repository.ts 拥有提交、引用、遍历、恢复和合并; json.ts 拥有结构合并; snapshot.ts 定义跟踪资源; diff.ts 描述实体变化; github.ts 和 transport.ts 实施镜像。边车 project-vcs.ts 添加参与者策略和持久性,同时 http/vcs.ts 提供运输授权和事件。
历史操作和自动化
工作流定义随项目进行版本控制,并针对提交的文件运行。记录历史记录后,提交可能会触发这些工作流程;失败的工作流程不会撤消提交。在诊断项目状态时,保持本地资源保存、项目提交、工作流程结果和远程同步不同。
尝试没有应用程序数据的存储库
对象存储接缝允许集成练习完全在内存中提交和分支。在依赖于的工作区中运行它 @cadcamfun/vcs, 例如 apps/sidecar。它不会触及桌面的项目数据库。
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 接收该提交的完整文件树,而不是补丁。 sidecar 根据工作数据库记录和携带的文本文件构建这棵树。包级集成必须保留它想要保留的文件。内存中的示例不应用 sidecar 所有权或 AI 参与者策略;这些是由项目服务和 HTTP 层强制执行的。
来源: packages/vcs/src/store.ts, repository.ts, 和 apps/sidecar/src/services/project-vcs.ts.
刻意查看、恢复和同步
在切换分支之前记录工作副本更改。新分支需要可解析的源提交,因此在分支之前使用其第一次提交来初始化未提交的项目。创建分支只是创建一个引用;签出是一项单独的操作,它将其快照应用于工作数据库。
对于拉取请求,将头部与其合并基础进行比较并检查属性级别的差异。后来的评论并没有清除之前的请求更改判决:审阅者必须提交后续的批准判决。合并的 PR 可以根据存储库祖先快进或创建合并提交;检查记录的合并提交,而不是假设固定数量的父级。
合并到签出的基础时,服务需要一个干净的工作副本并应用新的基础快照。当签出另一个分支时,合并 PR 的基础会更新该分支的引用,而不会替换当前工作数据库。随后的结账会加载更新后的基础。
推和拉同步提交的历史记录。未保存的 UI 内容和未提交的数据库状态不是远程快照。远程提前拒绝是首先获取/合并的请求;它不是覆盖远程分支的指令。
来源: packages/vcs/src/repository.ts, github.ts, 和 apps/sidecar/src/services/project-vcs.ts.
从历史故障中恢复
| 失败 | 下一步行动 |
|---|---|
NothingToCommit | 将数据库工作文件与 HEAD 进行比较。再次保存相同的资源值不会创建不同的树。 |
| 无效或现有分支 | 选择一个新的安全参考名称。名称不能包含遍历、双斜杠、引用表达式片段或尾部斜杠/.lock 后缀。 |
| 未提交的更改 | 在签出、拉取或受保护的工作副本操作之前提交或故意恢复/丢弃预期资源。 |
| 合并与路径/指针的冲突 | 检查基础/头部历史记录中的这些属性,创建经过审查的解决方案,然后重试。失败的存储库合并未创建部分合并提交。 |
| 审稿人要求更改 | 解决反馈并从该审阅者那里获取更新的无评论判决。 |
| AI 演员无法对主要角色采取行动 | 使用人工智能提案分支和人工审查的拉取请求。更改确认标志不会删除参与者限制。 |
来源: packages/vcs/src/repository.ts, apps/sidecar/src/services/project-vcs.ts, 和 http/vcs.ts。这 API参考 列出实际的请求和响应模式。
存储库布局
项目作为每个实体一个 JSON 文件存储在存储库中。因此,编辑工程图永远不会触及刀具路径文件,并且布局在 GitHub 上自然读取。 snapshotToFiles 从a写入这个布局 ProjectSnapshot, 和 filesToSnapshot 读回来。
| 快照列表 | 目录 | 内容 |
|---|---|---|
project | project.json | 项目元数据。 |
drawings, components, toolpaths | 相同的名字 | 设计、制造实体。 |
materials, tools, machineConfigs | materials/, tools/, machine-configs/ | 刀具路径引用的库项目,与项目一起进行版本控制,因此恢复的刀具路径仍然可以找到它们。 |
costs | costs/ | 刀具路径的生产成本估算。 |
extra | 任何路径 | 逐字存储的文本文件:工作流程、自述文件、生成的 G 代码。 |
代码中的提交、分支、差异和合并
这 Repository 类适用于任何存储后端。 sidecar 用 SQLite 支持它(表 GitObject 和 GitRef); memoryBackend() 用于测试和实验。以下示例已完成,其输出显示在注释中。
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) 合并在 JSON 属性级别上是三向的。当双方改变一个实体的不同属性时,合并成功。当两者将相同的属性更改为不同的值时,结果是 conflict 列出每个文件内的 RFC 6901 指针。相等长度的数组按索引进行比较,因此移动的顶点显示为一个更改;不同长度的数组作为一个整体进行比较。
| 合并结果 | 意义 |
|---|---|
up-to-date | 没有什么可以合并的;返回当前提交。 |
fast-forward | 分支指针向前移动;没有创建合并提交。 |
merge | 创建了合并提交。 |
conflict | 没有提交; conflicts 列出要解析的路径和指针。 |
| 存储库方法 | 目的 |
|---|---|
commit(files, { message, author, allowEmpty? }) | 在当前分支上写入完整的树作为新的提交。 |
log(rev = "HEAD", limit?) / fileLog(path, rev?, limit?) | 一个文件的分支/的历史记录。 |
createBranch(name, from = "HEAD"), deleteBranch, checkout | 分公司管理。名称不得包含 .. 或 //, 结束于 / 或 .lock,或包含 @{. |
tag(name, rev = "HEAD"), tags() | 轻量级标签下 refs/tags/. |
diff(from, to) | 文件级更改:使用 Blob ID 添加、修改、删除。 |
restorePaths(rev, paths, author, message?) | 新提交从旧版本恢复所选文件。 |
revertTo(rev, author, message?) | 新提交的树等于旧版本;历史不会被改写。 |
merge(theirs, author) / mergeInto(into, theirs, author) | 三路合并到当前分支/命名分支。 |
isAncestor, mergeBase | 图形查询。 |
应用程序执行的规则
sidecar 的项目历史服务在存储库顶部添加规则:
| 故障代码 | 什么时候 |
|---|---|
blocked | AI 参与者尝试提交或合并 main (消息说要使用 ai/* 分支并打开拉取请求),或者在审阅者的最新结论是合并拉取请求时 request-changes. |
dirty | 工作副本有未提交的更改,操作将覆盖它们:“N 个文件中未提交的更改;首先提交或丢弃它们”。 |
conflict | 合并产生了冲突;失败带有冲突列表。 |
nothing-to-commit | 自上次提交以来,树没有变化。 |
not-found | 未知的分支、提交、对象或拉取请求。 |
remote | GitHub 同步失败。 |
invalid | 其他无效输入,例如审查或合并未打开的拉取请求。 |
只有每个审稿人最新的非评论裁决才有效。请求更改并随后获得批准的审阅者不再阻止合并。提交、拉取请求事件和评论都会作为实时事件发布,因此桌面和配套版本无需轮询即可更新。
与 git 字节兼容
对象 ID 的计算方式与 git 的计算方式完全相同。通过 Git Data API 推送到 GitHub 可以使双方的每个提交 ID 保持一致,并且 git clone 镜像显示与应用程序相同的历史记录。 GitHub 仅接受快进参考更新;当远程移动时,首先在本地拉取并合并。
真正的提交对象
{
"oid": "44d2d03d23b27fbbc4b388e32e15dfb0f39fff87",
"parents": [],
"message": "First version\n",
"author": { "name": "Local user", "email": "local@cadcamfun.local", "time": 1792037121 }
} oid是存储在中的提交对象的 40 字符十六进制 IDGitObject.- 第一次提交没有父母;合并提交有两个。
- 与 Git 一样,消息存储时带有尾随换行符。
- 没有
x-session-token,作者是安装的本地配置文件。time以 Unix 秒为单位。
| 路线 | 身体 | 必需的 |
|---|---|---|
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 |