Snapshots (Multi-Version Management)
Snapshots save immutable versions of a selected file tree. Use commit to save a version, log to browse history, show to read an older file, diff to compare a file between versions, and restore to recover saved content. Uncommitted or excluded files are outside the recovery scope; ACLs and vector indexes are not versioned.
Snapshots are powered by gitoxide embedded in the Rust RAGFS layer, maintaining one logical Git repository per account_id. Use the snapshot APIs to manage versions; direct edits to the underlying repository are not part of this workflow.
The five core commands:
| Command | Purpose |
|---|---|
commit | Save the current workspace state as a new snapshot |
log | Walk commit history starting from the newest |
show | View a commit's metadata, or read a file's content from that commit |
diff | Compare one file between two snapshots as a unified diff |
restore | Restore a directory (or the whole account tree) to a past snapshot |
In addition, account-level .ovgitignore exclusion rules can be managed (get/set/delete) to exclude matching files from commit. See Ignore management.
Core Concepts
- Commit: A snapshot is a commit, uniquely identified by a 40-hex SHA-1
commit_oid. Most commands also accept an abbreviated OID prefix or a branch name (e.g.main). - Branch: The default branch is
main. Unless you pass one explicitly, every command operates onmain. - Forward-commit restore:
restoredoes not rewind or rewrite history. It reads the content atsource_commit, writes the diff back into the workspace, and creates a new commit on top of the current HEAD. The new commit's parent is therefore the HEAD that existed before the restore — notsource_commit. The restore preserves prior commits; when the selected source tree matches HEAD, it returnsnoopwithout creating a commit. This comparison does not inspect uncommitted workspace changes. - Scope:
commitcan be limited to specific URIs viapaths;restorecan be limited to a subtree viaproject_dir, leaving files outside it untouched.
ACL permissions
Snapshots use the current ACL at operation time. ACLs are not stored in snapshots, read from historical versions, or rolled back. Public resources without ACLs keep their legacy visibility; once ACL is enabled, these permissions apply:
| Operation | Required permission |
|---|---|
show(path=...) / diff / log | read |
commit | write; directories recursively check every current descendant and fail as a whole if any node is denied |
restore overwriting a file | write on the file |
restore creating a file | write on the parent directory |
restore deleting a file | write on the file |
Read/write/delete .ovgitignore | ADMIN or ROOT |
USER and ADMIN callers must provide paths for commit and log, project_dir for restore, and path for show; account-wide commit metadata lookup without path is reserved for local ROOT mode. A user can operate on accessible shared resources and their own viking://user/{user_id}/... space, but not another user's space. Directory operations preflight the complete scope instead of silently skipping denied descendants. restore authorizes every planned write and deletion before it mutates the workspace.
Existing nodes keep their current ACL after restore. Newly restored nodes inherit the current parent ACL; the user who runs restore is not granted manage. The background vector rebuild is system work for an already-authorized operation, so parent ACLs do not block it again.
Implementation
- HTTP routes: snapshot.py, prefix
/api/v1/snapshot. - SDK namespace: client.py, exposed as
client.snapshot.*. - Underlying semantics:
commit/restore/show/log/diffin _snapshot.py. - CLI: the
SnapshotCmdin main.rs, subcommands in snapshot.rs.
API Reference
commit()
Save the current workspace state as a new snapshot.
Partial commits preserve the previous snapshot outside the requested scope. After deleting a file or directory, include that URI or its parent in paths to record the deletion. A trailing / does not declare its type. Non-ROOT commits use Exact for existing files and Tree for existing directories; missing targets are unlocked with the filesystem lock provider and use Tree with the cache provider. Concurrent recreation of a missing target is not guaranteed to be fully captured; see commit scope and concurrency.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| message | str | Yes | - | Commit message |
| paths | List[str] | No | null | viking:// URIs to scope the snapshot to; entries may be files or directories. Directories are expanded recursively with the snapshot pruning rules applied. USER/ADMIN callers must provide this field; null account-wide snapshots are reserved for local ROOT mode. An empty list [] is forwarded as an explicit empty path set (no-op). A path that exists in neither the VFS nor the previous snapshot logs a warning and is treated as a no-op deletion |
| branch | str | No | main | Branch to advance |
| author_name | str | No | null | Override the default author name (default viking-bot) |
| author_email | str | No | null | Override the default author email |
Python SDK (HTTP)
result = client.snapshot.commit(
message="v1 initial import",
paths=["viking://resources/my_md.md"],
)
print(result["commit_oid"])TypeScript SDK
console.log(await client.gitCommit({ message: "Update docs", paths: ["resources/docs"] }));HTTP API
POST /api/v1/snapshot/commitcurl -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
ov snapshot commit -m "v1 initial import" --paths viking://resources/my_md.md -o jsonResponse
When a new snapshot is created:
{
"status": "ok",
"result": {
"result": "created",
"commit_oid": "3f2a1b9c4d5e6f70819293a4b5c6d7e8f9a0b1c2",
"changed": 3,
"ignored": 1
}
}changed is the number of paths added/updated/removed in this commit; ignored is the number of candidate paths skipped by the account .ovgitignore rules (built-in system pruning is not counted). When the workspace is unchanged relative to the last commit, the result is noop and commit_oid is the current HEAD (noop also returns ignored but has no changed):
{
"status": "ok",
"result": {
"result": "noop",
"commit_oid": "3f2a1b9c4d5e6f70819293a4b5c6d7e8f9a0b1c2",
"ignored": 0
}
}log()
Starting from a branch's HEAD, walk history along the first parent (parents[0]) and return commits newest-first.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| branch | str | No | main | Branch to walk |
| limit | int | No | 20 | Max commits to return. The HTTP endpoint accepts values from 1 to 500 |
| paths | List[str] | No | null | Return only commits that changed any specified viking:// URI; USER/ADMIN callers must provide it, while local ROOT mode may omit it for account-wide history. At most 32 paths are accepted, and each account-relative path may contain at most 64 components. Pass multiple URIs to HTTP as repeated paths query parameters |
Filtering happens before the result limit is applied, so limit=10 with paths=[X] returns up to 10 commits related to X rather than filtering only the 10 newest commits.
To bound storage work, a filtered request inspects at most 1,000 commits. If the requested number of matches has not been collected and older uninspected history remains, the request returns an INVALID_ARGUMENT error instead of a partial history list. Unfiltered history is not subject to this scan budget because every inspected commit advances the result limit.
Python SDK (HTTP)
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
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}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
ov snapshot log --limit 10 \
--paths viking://resources/a.md,viking://resources/docs \
-o jsonResponse
result is a list of commit metadata, each element having the same shape as the metadata returned by show():
{
"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"
}
]
}When the branch has no commits yet, the HTTP endpoint returns
404 NOT_FOUND.
show()
View a commit's metadata; if path is given, return that file's content from the commit instead.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| target_ref | str | Yes | - | Commit OID (abbreviated prefix allowed), branch name, or tag |
| path | str | No | null | viking:// URI of a single file; omitting it returns commit metadata only in local ROOT mode |
Python SDK (HTTP)
# View commit metadata (local ROOT mode only)
meta = client.snapshot.show("3f2a1b9c")
print(meta["message"], meta["parents"])
# Read a file's content from the commit
blob = client.snapshot.show("3f2a1b9c", path="viking://resources/my_project/guide.md")TypeScript SDK
console.log(await client.gitShow("main", "viking://resources/docs/api.md"));Note: when reading a file (
pathgiven), the Python client returns a{"oid": str, "size": int, "bytes": bytes}dict.
HTTP API
GET /api/v1/snapshot/show?target_ref={ref}[&path={uri}]# Commit metadata (returns JSON; local ROOT mode only)
curl -X GET "http://localhost:1933/api/v1/snapshot/show?target_ref=3f2a1b9c" \
-H "X-API-Key: your-key"
# File content (returns a binary stream)
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"Without path, the response is commit metadata JSON. With path, the response is a raw byte stream (Content-Type: application/octet-stream) plus two headers:
X-Snapshot-Oid: the blob object's OIDX-Snapshot-Size: the blob size in bytes
CLI
# Commit metadata (local ROOT mode only)
ov snapshot show 3f2a1b9c -o json
# Read file content (defaults to stdout; use --out-file to write to a local file)
ov snapshot show 3f2a1b9c --path viking://resources/my_project/guide.md --out-file ./guide.mdResponse (commit metadata)
{
"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()
Compare one UTF-8 file between two snapshot refs and return a unified diff. to_ref is required. When from_ref is omitted, the older side is treated as an empty file, which is useful for displaying the initial version.
Python SDK (HTTP)
result = client.snapshot.diff(
"viking://resources/my_project/guide.md",
from_ref="3f2a1b9c",
to_ref="9a0b1c2d",
)
print(result["diff_text"])TypeScript SDK
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}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
ov snapshot diff viking://resources/my_project/guide.md \
--from 3f2a1b9c \
--to 9a0b1c2dResponse
{
"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 is added, deleted, modified, or unchanged. Each side is limited to 10 MiB and 100,000 lines, and the generated diff is limited to 20 MiB; larger requests return RESOURCE_EXHAUSTED rather than a truncated diff.
restore()
Restore a directory (or the whole account tree) to its state at source_commit.
This is a forward-commit restore: it computes the diff between source_commit and the current HEAD, writes it back into the workspace, and creates a new commit on top of the current HEAD. The new commit's parent is the pre-restore HEAD (not source_commit), so history is never rewritten. Files outside project_dir are left untouched.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| source_commit | str | Yes | - | What to restore from: commit OID (abbreviated prefix allowed), branch name, or tag |
| project_dir | str | No | null | viking:// URI of the subtree to restore. USER/ADMIN callers must provide it; omitting it for a whole-account restore is reserved for local ROOT mode |
| branch | str | No | main | Branch to advance |
| dry_run | bool | No | false | Compute and return the diff only; write nothing |
| message | str | No | null | Message for the new commit; auto-generated when omitted |
| author_name | str | No | null | Override the default author name |
| author_email | str | No | null | Override the default author email |
Python SDK (HTTP)
# Preview which files would change first
plan = client.snapshot.restore(
project_dir="viking://resources/my_project",
source_commit="3f2a1b9c",
dry_run=True,
)
print(plan["diff"])# Apply after reviewing the plan
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
console.log(await client.gitRestore({
projectDir: "viking://resources/docs",
sourceCommit: "3f2a1b9c",
}));HTTP API
POST /api/v1/snapshot/restorecurl -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
# Positional args are <source_commit> then <project_dir>
# Preview first
ov snapshot restore 3f2a1b9c viking://resources/my_project --dry-run -o json
# Apply after reviewing the plan
ov snapshot restore 3f2a1b9c viking://resources/my_project -m "restore to v1" -o jsonResponse (applied)
On a successful write that produces a new commit, result is applied. Note parent_commit equals the old (pre-restore) HEAD, confirming the forward-commit semantics:
{
"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-..."
}
}When the restore has vector side effects (files written/deleted), the response carries a task_id you can poll via GET /api/v1/tasks/{task_id} to track the background vector rebuild.
Response (noop)
When the source is byte-identical to the current state, the result is noop and no new commit is created:
{
"status": "ok",
"result": {
"result": "noop",
"head": "9a0b1c2d3e4f5061728394a5b6c7d8e9f0a1b2c3",
"source": "3f2a1b9c4d5e6f70819293a4b5c6d7e8f9a0b1c2"
}
}Response (dry_run)
With dry_run=true, only the planned diff is returned and nothing is written. Diff paths are relative to project_dir:
{
"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 management
The .ovgitignore file at the account root is an account-level exclusion file. At commit time, files matching the rules are excluded from the snapshot; the rules file itself is never ignored by .ovgitignore rules (a rule matching .ovgitignore does not exclude it) and never enters vector indexing. Rules affect only commit, not restore/show/log.
The syntax is a common glob subset: blank lines are ignored, #-prefixed lines are comments, leading/trailing whitespace is trimmed; ! negation and backslash escaping are unsupported; the file is capped at 64 KiB (validated on write). Matching uses account-relative Git tree paths (/-separated).
Three methods are provided: get_gitignore (read, empty string when absent), set_gitignore (write), and delete_gitignore (delete, missing is success and idempotent). All three require ADMIN or ROOT permission, use the account from the request context, and take no path argument.
get_gitignore()
Reads the account .ovgitignore content; returns an empty string when the file is absent.
Python SDK (HTTP)
content = client.snapshot.get_gitignore()TypeScript SDK
console.log(await client.gitGetIgnore());HTTP API
GET /api/v1/snapshot/ignorecurl -X GET "http://localhost:1933/api/v1/snapshot/ignore" \
-H "X-API-Key: your-key"CLI
ov snapshot ignore-get -o jsonResponse
{
"status": "ok",
"result": "*.log\n"
}Without
-o json, the CLI prints the raw content to stdout (so it can be redirected to a file).
set_gitignore()
Writes the account .ovgitignore content (overwrites). The size limit (64 KiB) is validated up front; syntax (negation, escaping) is validated at commit time by the Rust layer.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| content | str | Yes | - | The .ovgitignore content (UTF-8) |
Python SDK (HTTP)
client.snapshot.set_gitignore(content="*.log\n")TypeScript SDK
await client.gitSetIgnore("*.tmp\n.cache/\n");HTTP API
PUT /api/v1/snapshot/ignorecurl -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
# Pass content inline with --content, or read from a file with --file
ov snapshot ignore-set --content "*.log" -o json
ov snapshot ignore-set --file ./my-rules -o jsonResponse
{
"status": "ok",
"result": null
}delete_gitignore()
Deletes the account .ovgitignore. Missing is success (idempotent).
Python SDK (HTTP)
client.snapshot.delete_gitignore()TypeScript SDK
await client.gitDeleteIgnore();HTTP API
DELETE /api/v1/snapshot/ignorecurl -X DELETE "http://localhost:1933/api/v1/snapshot/ignore" \
-H "X-API-Key: your-key"CLI
ov snapshot ignore-delete -o jsonResponse
{
"status": "ok",
"result": null
}A Typical Flow
A complete "commit → modify → restore" flow (Python SDK):
from openviking_sdk import SyncHTTPClient
client = SyncHTTPClient(url="http://localhost:1933", api_key="your-key")
client.initialize()
root = "viking://resources/my_project"
# 1. Write initial content and commit 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. Modify and commit 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. Walk history
for c in client.snapshot.log(limit=10, paths=[root]):
print(c["oid"][:8], c["message"])
# 4. Restore the workspace to v1 (creates a new commit on top of v2)
client.snapshot.restore(project_dir=root, source_commit=v1["commit_oid"], message="restore to v1")
client.close()For more end-to-end examples, see the examples/snapshot/ directory in the repository, covering the SDK, HTTP, and CLI surfaces.
Error Handling
| Scenario | HTTP Status | Error Code |
|---|---|---|
Branch/commit not found, or show's path does not exist in that commit | 404 | NOT_FOUND |
| A required operation scope is omitted, or the caller lacks the corresponding ACL permission | 403 | PERMISSION_DENIED |
| Branch concurrently advanced during restore (CAS conflict) | 409 | CONFLICT |
.ovgitignore too large, non-UTF-8, or containing unsupported ! negation/backslash escaping (validated at commit time) | 400 | INVALID_ARGUMENT |
Request body contains an unknown field (request model is extra="forbid") | 400 | INVALID_ARGUMENT |
Related Documentation
- File System: snapshots build on filesystem resources
- Background Tasks: track the background vector rebuild triggered by restore via
GET /api/v1/tasks/{task_id} - API Overview: full endpoint reference