Skip to content

版本管理 ​

快照将指定范围内的文件树保存为不可变版本。使用 commit 保存、log 查看历史、show 读取旧版文件、diff 对比文件在两个版本间的差异、restore 恢复已保存的内容。未提交或被排除的文件不在恢复范围内,ACL 和向量索引也不保存历史版本。

快照能力底层由内嵌在 Rust RAGFS 层的 gitoxide 驱动,按 account_id 维护一个逻辑 Git 仓库(每个账号一个仓库),使用快照 API 管理版本即可,无需直接修改底层仓库。

五个核心命令:

命令作用
commit把当前工作区状态保存成一个新快照
log从最新提交开始回溯历史
show查看某个提交的元数据,或读取该提交中某个文件的内容
diff以 unified diff 格式对比某个文件在两个快照中的内容
restore把目录(或整棵账号树)恢复到某个历史快照的状态

此外还提供账号级 .ovgitignore 排除规则的管理命令(get/set/delete),用于在 commit 时按规则排除匹配的文件。详见 ignore 管理。

核心概念 ​

  • 提交(commit):一个快照对应一个提交,由 40 位十六进制的 SHA-1 commit_oid 唯一标识。多数命令也接受 OID 的缩写前缀,或分支名(如 main)。
  • 分支(branch):默认分支为 main。除非显式传入,所有命令都作用在 main 上。
  • 正向恢复(forward-commit restore):restore 不会回退或改写历史。它会读取 source_commit 的内容,把差异写回工作区,并在当前 HEAD 之上生成一个新的提交。因此新提交的父提交是恢复操作发生前的 HEAD,而不是 source_commit。恢复保留此前的提交;选定的源文件树与 HEAD 一致时返回 noop,不生成新提交。该比较不检查工作区中未提交的改动。
  • 作用范围:commit 可以通过 paths 限定只快照部分 URI;restore 可以通过 project_dir 限定只恢复某个子目录,目录之外的文件保持不变。

ACL 权限 ​

快照使用操作发生时的当前 ACL,不保存、回滚或读取历史 ACL。未开启 ACL 的公共资源保持原有的全部可见行为;开启 ACL 后,权限要求如下:

操作权限要求
show(path=...) / diff / logread
commitwrite;目录会递归检查当前全部子节点,任一子节点无权则整次失败
restore 覆盖已有文件文件的 write
restore 新建文件父目录的 write
restore 删除文件文件的 write
.ovgitignore 读写删除ADMIN 或 ROOT

USER 和 ADMIN 调用 commit、log、restore 时必须显式传入 paths 或 project_dir;show 必须传入 path,不带 path 的全局提交元数据查询只保留给本地 ROOT 模式。用户可以操作自己有权访问的公共资源和自己的 viking://user/{user_id}/...,不能访问其他用户空间。目录操作会先完整鉴权,不会静默跳过无权子节点;restore 会先鉴权全部写入和删除项,再开始修改。

恢复后的既有节点保留当前 ACL。被恢复的新节点继承当前父目录 ACL,不会给执行 restore 的用户额外授予 manage。后台向量重建属于已授权操作的系统工作,不会再次受父目录 ACL 阻断。

API 实现介绍 ​

  • HTTP 路由:snapshot.py,前缀 /api/v1/snapshot。
  • 命名空间(SDK):client.py,暴露为 client.snapshot.*。
  • 底层语义实现:_snapshot.py 的 commit / restore / show / log / diff。
  • CLI 命令:main.rs 的 SnapshotCmd,子命令 snapshot.rs。

API 参考 ​

commit() ​

把当前工作区状态保存成一个新的快照。

局部提交保留范围外的上次快照内容。删除文件或目录后,仍需把该 URI 或其父目录传入 paths 才会记录删除。末尾 / 不声明类型。非 ROOT 提交对现存文件加 Exact、现存目录加 Tree;缺失路径在 filesystem 锁后端不加锁,在 cache 后端加 Tree。缺失目标的并发重建不保证被本次快照完整记录,见 提交范围与并发。

参数

参数类型必填默认值说明
messagestr是-提交说明
pathsList[str]否null限定本次快照的 viking:// URI 列表,条目可以是文件或目录;目录会按照快照的剪枝规则递归展开。USER/ADMIN 必须显式传入;null 只保留给本地 ROOT 模式的整棵账号树快照。传入空列表 [] 表示显式的空路径集(不会产生改动)。缺失路径会从新快照移除此前的同名文件及其子树;若此前也不存在则告警并无改动
branchstr否main要推进的分支
author_namestr否null覆盖默认的提交者名字(默认 viking-bot)
author_emailstr否null覆盖默认的提交者邮箱

Python SDK (HTTP)

python
result = client.snapshot.commit(
    message="v1 initial import",
    paths=["viking://resources/my_md.md"],
)
print(result["commit_oid"])

TypeScript SDK

typescript
console.log(await client.gitCommit({ message: "Update docs", paths: ["resources/docs"] }));

HTTP API

POST /api/v1/snapshot/commit
bash
curl -X POST "http://localhost:1933/api/v1/snapshot/commit" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-key" \
  -d '{
    "message": "v1 initial import",
    "paths": ["viking://resources/my_md.md"]
  }'

CLI

bash
ov snapshot commit -m "v1 initial import" --paths viking://resources/my_md.md -o json

响应

新建快照时:

json
{
  "status": "ok",
  "result": {
    "result": "created",
    "commit_oid": "3f2a1b9c4d5e6f70819293a4b5c6d7e8f9a0b1c2",
    "changed": 3,
    "ignored": 1
  }
}

changed 为本次提交中新增/修改/删除的路径数;ignored 为本次被账号 .ovgitignore 规则排除的候选路径数(系统内置剪枝不计入)。当工作区相对上一次提交没有任何变化时返回 noop,commit_oid 为当前 HEAD(noop 同样返回 ignored,但不含 changed):

json
{
  "status": "ok",
  "result": {
    "result": "noop",
    "commit_oid": "3f2a1b9c4d5e6f70819293a4b5c6d7e8f9a0b1c2",
    "ignored": 0
  }
}

log() ​

从某个分支的 HEAD 开始,沿首个父提交(parents[0])逐层回溯历史,按时间从新到旧返回提交列表。

参数

参数类型必填默认值说明
branchstr否main要回溯的分支
limitint否20最多返回的提交数量。HTTP 接口限制范围为 1–500
pathsList[str]否null只返回修改了任一指定 viking:// URI 的提交;USER/ADMIN 必须显式传入。本地 ROOT 模式可省略以查询全局历史。最多接受 32 条路径,每条 account-relative 路径最多包含 64 个层级。HTTP 接口通过重复 paths 查询参数传入多个 URI

过滤发生在限制返回数量之前,因此 limit=10 和 paths=[X] 表示最多返回 10 条与 X 有关的提交,而不是先取最近 10 条提交再过滤。

为限制存储开销,过滤请求最多检查 1,000 条提交。如果尚未收集到请求数量的匹配结果,并且仍存在未检查的更早历史,接口将返回 INVALID_ARGUMENT 错误,而不是返回不完整的历史列表。非过滤请求不受该扫描预算限制,因为每检查一条提交都会推进返回数量限制。

Python SDK (HTTP)

python
history = client.snapshot.log(
    limit=10,
    paths=["viking://resources/a.md", "viking://resources/docs"],
)
for commit in history:
    print(commit["oid"], commit["message"])

TypeScript SDK

typescript
console.log(
  await client.gitLog("main", 20, [
    "viking://resources/a.md",
    "viking://resources/docs",
  ]),
);

HTTP API

GET /api/v1/snapshot/log?branch={branch}&limit={limit}&paths={uri1}&paths={uri2}
bash
curl --get "http://localhost:1933/api/v1/snapshot/log" \
  --data-urlencode "branch=main" \
  --data-urlencode "limit=10" \
  --data-urlencode "paths=viking://resources/a.md" \
  --data-urlencode "paths=viking://resources/docs" \
  -H "X-API-Key: your-key"

CLI

bash
ov snapshot log --limit 10 \
  --paths viking://resources/a.md,viking://resources/docs \
  -o json

响应

result 是一个提交元数据列表,每个元素与 show() 返回的提交元数据结构相同:

json
{
  "status": "ok",
  "result": [
    {
      "oid": "9a0b1c2d3e4f5061728394a5b6c7d8e9f0a1b2c3",
      "tree": "11223344556677889900aabbccddeeff00112233",
      "parents": ["3f2a1b9c4d5e6f70819293a4b5c6d7e8f9a0b1c2"],
      "author": {
        "name": "viking-bot",
        "email": "bot@openviking.local",
        "time_seconds": 1750300000,
        "tz_offset_seconds": 28800
      },
      "committer": {
        "name": "viking-bot",
        "email": "bot@openviking.local",
        "time_seconds": 1750300000,
        "tz_offset_seconds": 28800
      },
      "message": "v2 modify delete add"
    }
  ]
}

当分支还没有任何提交时,HTTP 接口返回 404 NOT_FOUND。


show() ​

查看某个提交的元数据;如果同时指定 path,则返回该提交中对应文件的内容。

参数

参数类型必填默认值说明
target_refstr是-提交 OID(支持缩写前缀)、分支名或标签
pathstr否null某个文件的 viking:// URI;省略时返回提交元数据,但仅限本地 ROOT 模式

Python SDK (HTTP)

python
# 查看提交元数据(仅本地 ROOT 模式)
meta = client.snapshot.show("3f2a1b9c")
print(meta["message"], meta["parents"])

# 读取该提交中某个文件的内容
blob = client.snapshot.show("3f2a1b9c", path="viking://resources/my_project/guide.md")

TypeScript SDK

typescript
console.log(await client.gitShow("main", "viking://resources/docs/api.md"));

注意:带 path 读取文件内容时,Python 客户端返回 {"oid": str, "size": int, "bytes": bytes} 字典。

HTTP API

GET /api/v1/snapshot/show?target_ref={ref}[&path={uri}]
bash
# 提交元数据(返回 JSON,仅本地 ROOT 模式)
curl -X GET "http://localhost:1933/api/v1/snapshot/show?target_ref=3f2a1b9c" \
  -H "X-API-Key: your-key"

# 读取文件内容(返回二进制流)
curl -X GET "http://localhost:1933/api/v1/snapshot/show?target_ref=3f2a1b9c&path=viking://resources/my_project/guide.md" \
  -H "X-API-Key: your-key"

不带 path 时返回提交元数据 JSON;带 path 时返回原始字节流(Content-Type: application/octet-stream),并附带两个响应头:

  • X-Snapshot-Oid:blob 对象的 OID
  • X-Snapshot-Size:blob 字节数

CLI

bash
# 提交元数据(仅本地 ROOT 模式)
ov snapshot show 3f2a1b9c -o json

# 读取文件内容(默认输出到 stdout,可用 --out-file 写入本地文件)
ov snapshot show 3f2a1b9c --path viking://resources/my_project/guide.md --out-file ./guide.md

响应(提交元数据)

json
{
  "status": "ok",
  "result": {
    "oid": "3f2a1b9c4d5e6f70819293a4b5c6d7e8f9a0b1c2",
    "tree": "00112233445566778899aabbccddeeff00112233",
    "parents": [],
    "author": {
      "name": "viking-bot",
      "email": "bot@openviking.local",
      "time_seconds": 1750299000,
      "tz_offset_seconds": 28800
    },
    "committer": {
      "name": "viking-bot",
      "email": "bot@openviking.local",
      "time_seconds": 1750299000,
      "tz_offset_seconds": 28800
    },
    "message": "v1 initial import"
  }
}

diff() ​

对比一个 UTF-8 文件在两个快照引用中的内容,并返回 unified diff。to_ref 必填;省略 from_ref 时,旧版本按空文件处理,可用于展示文件的初始版本。

Python SDK (HTTP)

python
result = client.snapshot.diff(
    "viking://resources/my_project/guide.md",
    from_ref="3f2a1b9c",
    to_ref="9a0b1c2d",
)
print(result["diff_text"])

TypeScript SDK

typescript
const result = await client.gitDiff(
  "viking://resources/my_project/guide.md",
  "9a0b1c2d",
  "3f2a1b9c",
);
console.log(result.diff_text);

HTTP API

GET /api/v1/snapshot/diff?path={uri}&from={old_ref}&to={new_ref}
bash
curl --get "http://localhost:1933/api/v1/snapshot/diff" \
  --data-urlencode "path=viking://resources/my_project/guide.md" \
  --data-urlencode "from=3f2a1b9c" \
  --data-urlencode "to=9a0b1c2d" \
  -H "X-API-Key: your-key"

CLI

bash
ov snapshot diff viking://resources/my_project/guide.md \
  --from 3f2a1b9c \
  --to 9a0b1c2d

响应

json
{
  "status": "ok",
  "result": {
    "path": "viking://resources/my_project/guide.md",
    "from_commit": "3f2a1b9c...",
    "to_commit": "9a0b1c2d...",
    "change_type": "modified",
    "diff_text": "--- a/guide.md\n+++ b/guide.md\n@@ -1 +1 @@\n-old line\n+new line\n"
  }
}

change_type 为 added、deleted、modified 或 unchanged。参与对比的单侧文件上限为 10 MiB 和 100,000 行,生成的 diff 上限为 20 MiB;超限时返回 RESOURCE_EXHAUSTED,不会返回被截断的 diff。


restore() ​

把某个目录(或整棵账号树)恢复到 source_commit 时的状态。

这是正向恢复:它会计算 source_commit 与当前 HEAD 之间的差异并写回工作区,然后在当前 HEAD 之上生成一个新的提交。新提交的父提交是恢复前的 HEAD(而非 source_commit),历史不会被改写。project_dir 之外的文件保持不变。

参数

参数类型必填默认值说明
source_commitstr是-要恢复到的来源:提交 OID(支持缩写前缀)、分支名或标签
project_dirstr否null要恢复的子目录 viking:// URI;USER/ADMIN 必须显式传入,省略时恢复整棵账号树仅用于本地 ROOT 模式
branchstr否main要推进的分支
dry_runbool否false仅计算并返回差异,不做任何写入
messagestr否null新提交的说明;省略时自动生成
author_namestr否null覆盖默认的提交者名字
author_emailstr否null覆盖默认的提交者邮箱

Python SDK (HTTP)

python
# 先预演,确认要改动哪些文件
plan = client.snapshot.restore(
    project_dir="viking://resources/my_project",
    source_commit="3f2a1b9c",
    dry_run=True,
)
print(plan["diff"])
python
# 核对计划后,再执行恢复
result = client.snapshot.restore(
    project_dir="viking://resources/my_project",
    source_commit="3f2a1b9c",
    message="restore to v1",
)
print(result["result"])
if result["result"] == "applied":
    print(result["new_commit_oid"])

TypeScript SDK

typescript
console.log(await client.gitRestore({
  projectDir: "viking://resources/docs",
  sourceCommit: "3f2a1b9c",
}));

HTTP API

POST /api/v1/snapshot/restore
bash
curl -X POST "http://localhost:1933/api/v1/snapshot/restore" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-key" \
  -d '{
    "project_dir": "viking://resources/my_project",
    "source_commit": "3f2a1b9c",
    "message": "restore to v1"
  }'

CLI

bash
# 位置参数依次为 <source_commit> <project_dir>
# 先预演
ov snapshot restore 3f2a1b9c viking://resources/my_project --dry-run -o json

# 核对计划后,再执行恢复
ov snapshot restore 3f2a1b9c viking://resources/my_project -m "restore to v1" -o json

响应(applied)

成功写入并生成新提交时,result 为 applied。注意 parent_commit 等于恢复前的旧 HEAD,印证了正向恢复语义:

json
{
  "status": "ok",
  "result": {
    "result": "applied",
    "new_commit_oid": "c3d4e5f60718293a4b5c6d7e8f9a0b1c2d3e4f50",
    "source_commit": "3f2a1b9c4d5e6f70819293a4b5c6d7e8f9a0b1c2",
    "parent_commit": "9a0b1c2d3e4f5061728394a5b6c7d8e9f0a1b2c3",
    "written": 1,
    "deleted": 1,
    "unchanged": 1,
    "written_paths": ["resources/my_project/guide.md"],
    "deleted_paths": ["resources/my_project/changelog.md"],
    "task_id": "snapshot_restore_reindex-..."
  }
}

当恢复产生向量副作用(写入/删除文件)时,响应会附带一个 task_id,可通过 GET /api/v1/tasks/{task_id} 轮询后台向量重建进度。

响应(noop)

来源与当前状态字节级一致、无需改动时返回 noop,不生成新提交:

json
{
  "status": "ok",
  "result": {
    "result": "noop",
    "head": "9a0b1c2d3e4f5061728394a5b6c7d8e9f0a1b2c3",
    "source": "3f2a1b9c4d5e6f70819293a4b5c6d7e8f9a0b1c2"
  }
}

响应(dry_run)

dry_run=true 时只返回计划差异,不做任何写入。差异中的路径均相对于 project_dir:

json
{
  "status": "ok",
  "result": {
    "result": "dry_run",
    "head": "9a0b1c2d3e4f5061728394a5b6c7d8e9f0a1b2c3",
    "source": "3f2a1b9c4d5e6f70819293a4b5c6d7e8f9a0b1c2",
    "diff": {
      "to_write": [{"path": "guide.md", "oid": "..."}],
      "to_delete": ["changelog.md"],
      "unchanged": ["notes/todo.md"]
    }
  }
}

ignore 管理 ​

账号根目录下的 .ovgitignore 是账号级排除规则文件。在 commit 时,匹配规则的文件被排除出快照;规则文件本身不会被 .ovgitignore 规则忽略(即使规则匹配 .ovgitignore 也不会被排除),且不进入向量索引。规则只影响 commit,不影响 restore/show/log。

语法为常见 glob 子集:空行被忽略、# 开头为注释、行首尾空白被裁剪;不支持 ! 取反与反斜杠转义;文件大小上限 64 KiB(写入时即校验)。匹配路径为账号相对 Git 树路径(/ 分隔)。

提供三个方法:get_gitignore(读取,缺失返回空串)、set_gitignore(写入)、delete_gitignore(删除,缺失即成功、幂等)。三者都要求 ADMIN 或 ROOT 权限,只需请求上下文中的账号,无路径参数。

get_gitignore() ​

读取账号 .ovgitignore 内容;文件不存在时返回空字符串。

Python SDK (HTTP)

python
content = client.snapshot.get_gitignore()

TypeScript SDK

typescript
console.log(await client.gitGetIgnore());

HTTP API

GET /api/v1/snapshot/ignore
bash
curl -X GET "http://localhost:1933/api/v1/snapshot/ignore" \
  -H "X-API-Key: your-key"

CLI

bash
ov snapshot ignore-get -o json

响应

json
{
  "status": "ok",
  "result": "*.log\n"
}

不带 -o json 时,CLI 直接把原始内容打到 stdout(可重定向到文件)。

set_gitignore() ​

写入账号 .ovgitignore 内容(覆盖)。写入前校验大小上限(64 KiB);语法(取反、转义等)在 commit 时由 Rust 层校验。

参数

参数类型必填默认值说明
contentstr是-.ovgitignore 文件内容(UTF-8)

Python SDK (HTTP)

python
client.snapshot.set_gitignore(content="*.log\n")

TypeScript SDK

typescript
await client.gitSetIgnore("*.tmp\n.cache/\n");

HTTP API

PUT /api/v1/snapshot/ignore
bash
curl -X PUT "http://localhost:1933/api/v1/snapshot/ignore" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-key" \
  -d '{"content": "*.log\n"}'

CLI

bash
# 用 --content 直接传内容,或用 --file 从文件读取
ov snapshot ignore-set --content "*.log" -o json
ov snapshot ignore-set --file ./my-rules -o json

响应

json
{
  "status": "ok",
  "result": null
}

delete_gitignore() ​

删除账号 .ovgitignore。文件不存在也视为成功(幂等)。

Python SDK (HTTP)

python
client.snapshot.delete_gitignore()

TypeScript SDK

typescript
await client.gitDeleteIgnore();

HTTP API

DELETE /api/v1/snapshot/ignore
bash
curl -X DELETE "http://localhost:1933/api/v1/snapshot/ignore" \
  -H "X-API-Key: your-key"

CLI

bash
ov snapshot ignore-delete -o json

响应

json
{
  "status": "ok",
  "result": null
}

典型流程 ​

下面演示一个"提交 → 修改 → 恢复"的完整流程(Python SDK):

python
from openviking_sdk import SyncHTTPClient

client = SyncHTTPClient(url="http://localhost:1933", api_key="your-key")
client.initialize()

root = "viking://resources/my_project"

# 1. 写入初始内容并提交 v1
client.write(
    uri=f"{root}/guide.md",
    content="# Guide\n\nv1 content\n",
    mode="create",
)
v1 = client.snapshot.commit(message="v1 initial import", paths=[root])

# 2. 修改后再提交 v2
client.write(
    uri=f"{root}/guide.md",
    content="# Guide\n\nv2 content\n",
    mode="replace",
)
v2 = client.snapshot.commit(message="v2 update", paths=[root])

# 3. 查看历史
for c in client.snapshot.log(limit=10, paths=[root]):
    print(c["oid"][:8], c["message"])

# 4. 把工作区恢复到 v1(会在 v2 之上生成一个新提交)
client.snapshot.restore(project_dir=root, source_commit=v1["commit_oid"], message="restore to v1")

client.close()

更多端到端示例参见仓库中的 examples/snapshot/ 目录,涵盖 SDK、HTTP、CLI 三种调用方式。

错误处理 ​

场景HTTP 状态码错误码
分支/提交不存在,或 show 的 path 在该提交中不存在404NOT_FOUND
未传入必要的操作范围,或当前身份缺少对应 ACL 权限403PERMISSION_DENIED
恢复期间分支被并发提交改写(CAS 冲突)409CONFLICT
.ovgitignore 过大、非 UTF-8,或包含不支持的 ! 取反/反斜杠转义语法(commit 时校验)400INVALID_ARGUMENT
请求体包含未知字段(请求模型为 extra="forbid")400INVALID_ARGUMENT

相关文档 ​

  • 文件系统:快照建立在文件系统资源之上
  • 后台任务:通过 GET /api/v1/tasks/{task_id} 跟踪 restore 触发的后台向量重建
  • API 概览:完整端点总览

Open source under the AGPL-3.0 License. Font licenses