Skip to content

多版本管理(快照)

OpenViking 在 VikingFS 之上提供了一套基于 Git 的多版本管理能力,称为快照(Snapshot)。它把某个账号(account)下的整棵资源树保存成一系列不可变的提交(commit),让你能够回溯历史、对比版本,并把工作区恢复到任意一个历史状态。

快照能力底层由内嵌在 Rust RAGFS 层的 gitoxide 驱动,按 account_id 维护一个逻辑 Git 仓库(每个账号一个仓库),对调用方完全透明——你无需关心 .ovgit 目录、对象库或引用细节。

四个核心命令:

命令作用
commit把当前工作区状态保存成一个新快照
log从最新提交开始回溯历史
show查看某个提交的元数据,或读取该提交中某个文件的内容
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 始终单调向前推进,历史永远不会丢失。
  • 作用范围commit 可以通过 paths 限定只快照部分 URI;restore 可以通过 project_dir 限定只恢复某个子目录,目录之外的文件保持不变。

API 实现介绍

API 参考

commit()

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

参数

参数类型必填默认值说明
messagestr-提交说明
pathsList[str]null限定本次快照的 viking:// URI 列表,条目可以是文件或目录;目录会按照快照的剪枝规则递归展开。null 表示对整棵账号树做快照。传入空列表 [] 表示显式的空路径集(不会产生改动)。如果某个路径在 VFS 和前一次快照中都不存在,会输出一条 warn,并按"对该名称下任何子树执行删除"处理
branchstrmain要推进的分支
author_namestrnull覆盖默认的提交者名字(默认 viking-bot
author_emailstrnull覆盖默认的提交者邮箱

Python SDK (Embedded / 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 规则排除的候选路径数(系统内置剪枝不计入)。当工作区相对上一次提交没有任何变化时返回 noopcommit_oid 为当前 HEAD(noop 同样返回 ignored,但不含 changed):

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

log()

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

参数

参数类型必填默认值说明
branchstrmain要回溯的分支
limitint20最多返回的提交数量。HTTP 接口限制范围为 1–500

Python SDK (Embedded / HTTP)

python
history = client.snapshot.log(limit=10)
for commit in history:
    print(commit["oid"], commit["message"])

TypeScript SDK

typescript
console.log(await client.gitLog("main", 20));

HTTP API

GET /api/v1/snapshot/log?branch={branch}&limit={limit}
bash
curl -X GET "http://localhost:1933/api/v1/snapshot/log?branch=main&limit=10" \
  -H "X-API-Key: your-key"

CLI

bash
ov snapshot log --limit 10 -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(支持缩写前缀)、分支名或标签
pathstrnull某个文件的 viking:// URI;省略时返回提交元数据

Python SDK (Embedded / HTTP)

python
# 查看提交元数据
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 读取文件内容时,Embedded(本地)客户端直接返回原始 bytesHTTP 客户端返回 {"oid": str, "size": int, "bytes": bytes} 字典。

HTTP API

GET /api/v1/snapshot/show?target_ref={ref}[&path={uri}]
bash
# 提交元数据(返回 JSON)
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
# 提交元数据
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"
  }
}

restore()

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

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

参数

参数类型必填默认值说明
source_commitstr-要恢复到的来源:提交 OID(支持缩写前缀)、分支名或标签
project_dirstrnull要恢复的子目录 viking:// URI;省略时恢复整棵账号树
branchstrmain要推进的分支
dry_runboolfalse仅计算并返回差异,不做任何写入
messagestrnull新提交的说明;省略时自动生成
author_namestrnull覆盖默认的提交者名字
author_emailstrnull覆盖默认的提交者邮箱

Python SDK (Embedded / HTTP)

python
result = client.snapshot.restore(
    project_dir="viking://resources/my_project",
    source_commit="3f2a1b9c",
    message="restore to v1",
)
print(result["result"], result["new_commit_oid"])

# 先预演,确认要改动哪些文件
plan = client.snapshot.restore(
    project_dir="viking://resources/my_project",
    source_commit="3f2a1b9c",
    dry_run=True,
)
print(plan["diff"])

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 -m "restore to v1" -o json

# 预演
ov snapshot restore 3f2a1b9c viking://resources/my_project --dry-run -o json

响应(applied)

成功写入并生成新提交时,resultapplied。注意 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(删除,缺失即成功、幂等)。三者都只需请求上下文中的账号,无路径参数。

get_gitignore()

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

Python SDK (Embedded / 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 (Embedded / 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 (Embedded / 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
import openviking as ov

client = ov.OpenViking()
client.initialize()

root = "viking://resources/my_project"

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

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

# 3. 查看历史
for c in client.snapshot.log(limit=10):
    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 状态码错误码
分支/提交不存在,或 showpath 在该提交中不存在404NOT_FOUND
恢复期间分支被并发提交改写(CAS 冲突)409CONFLICT
.ovgitignore 过大、非 UTF-8,或包含不支持的 ! 取反/反斜杠转义语法(commit 时校验)400INVALID_ARGUMENT
请求体包含未知字段(请求模型为 extra="forbid"400INVALID_ARGUMENT

相关文档

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

Released under the Apache-2.0 License.