chore: import upstream snapshot with attribution
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
@@ -0,0 +1,152 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from mirage.server.version.errors import NoSuchBranchError
|
||||
from mirage.server.version.state_tree import (CACHE_PREFIX, META_PATH,
|
||||
blob_to_meta, meta_to_blob,
|
||||
to_state, tree_inputs_from_state)
|
||||
from mirage.server.version.store import VersionStore
|
||||
from mirage.types import DriftPolicy, StateKey
|
||||
from mirage.workspace.snapshot import (apply_state_dict, install_fingerprints,
|
||||
to_state_dict)
|
||||
|
||||
|
||||
async def snapshot_tree(store: VersionStore, ws) -> bytes:
|
||||
return await snapshot_tree_from_state(store, await to_state_dict(ws))
|
||||
|
||||
|
||||
async def snapshot_tree_from_state(store: VersionStore, state: dict) -> bytes:
|
||||
entries, meta = tree_inputs_from_state(state)
|
||||
tree_entries: dict[str, bytes] = {}
|
||||
for path, data in entries.items():
|
||||
tree_entries[path] = await store.write_blob(data)
|
||||
tree_entries[META_PATH] = await store.write_blob(meta_to_blob(meta))
|
||||
return await store.write_tree(tree_entries)
|
||||
|
||||
|
||||
async def commit(store: VersionStore,
|
||||
ws,
|
||||
branch: str = "main",
|
||||
message: str = "") -> bytes:
|
||||
return await commit_state(store, await to_state_dict(ws), branch, message)
|
||||
|
||||
|
||||
async def commit_state(store: VersionStore,
|
||||
state: dict,
|
||||
branch: str = "main",
|
||||
message: str = "") -> bytes:
|
||||
tree = await snapshot_tree_from_state(store, state)
|
||||
branches = await store.branches()
|
||||
parents: list[bytes] = []
|
||||
if branch in branches:
|
||||
parents = [await store.head(branch)]
|
||||
elif branches:
|
||||
raise NoSuchBranchError(branch)
|
||||
return await store.commit(tree, parents, branch, message)
|
||||
|
||||
|
||||
async def branch(store: VersionStore,
|
||||
name: str,
|
||||
from_branch: str = "main") -> None:
|
||||
head = await store.head(from_branch)
|
||||
await store.set_branch(name, head)
|
||||
|
||||
|
||||
async def read_version(store: VersionStore,
|
||||
version: bytes) -> tuple[dict[str, bytes], dict]:
|
||||
tree = (await store.read_commit(version)).tree
|
||||
contents = await store.read_tree(tree)
|
||||
meta_oid = contents.pop(META_PATH, None)
|
||||
meta = (blob_to_meta(await store.read_blob(meta_oid))
|
||||
if meta_oid is not None else {
|
||||
"mounts": []
|
||||
})
|
||||
entries: dict[str, bytes] = {}
|
||||
for path, oid in contents.items():
|
||||
entries[path] = await store.read_blob(oid)
|
||||
return entries, meta
|
||||
|
||||
|
||||
async def resolve_ref(store: VersionStore, ref) -> bytes:
|
||||
if isinstance(ref, str):
|
||||
if ref in await store.branches():
|
||||
return await store.head(ref)
|
||||
return ref.encode()
|
||||
return ref
|
||||
|
||||
|
||||
async def checkout(store: VersionStore,
|
||||
ws,
|
||||
ref,
|
||||
drift_policy: DriftPolicy = DriftPolicy.STRICT) -> None:
|
||||
version = await resolve_ref(store, ref)
|
||||
entries, meta = await read_version(store, version)
|
||||
state = to_state(entries, meta)
|
||||
await ws._cache.clear()
|
||||
await apply_state_dict(ws, state)
|
||||
install_fingerprints(ws,
|
||||
state.get(StateKey.FINGERPRINTS) or [], drift_policy)
|
||||
|
||||
|
||||
def _strip_meta(changes: dict[str, list[str]]) -> dict[str, list[str]]:
|
||||
return {
|
||||
kind: [
|
||||
p for p in paths
|
||||
if p != META_PATH and not p.startswith(CACHE_PREFIX)
|
||||
]
|
||||
for kind, paths in changes.items()
|
||||
}
|
||||
|
||||
|
||||
async def version_log(store: VersionStore, branch: str) -> list[dict]:
|
||||
out: list[dict] = []
|
||||
for oid in await store.log(branch):
|
||||
commit_obj = await store.read_commit(oid)
|
||||
out.append({
|
||||
"id": oid.decode(),
|
||||
"message": commit_obj.message.decode(),
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
async def version_diff(store: VersionStore, version_a: bytes,
|
||||
version_b: bytes) -> dict[str, list[str]]:
|
||||
tree_a = (await store.read_commit(version_a)).tree
|
||||
tree_b = (await store.read_commit(version_b)).tree
|
||||
return _strip_meta(await store.diff(tree_a, tree_b))
|
||||
|
||||
|
||||
async def diff_live_vs_ref(store: VersionStore, state: dict,
|
||||
ref) -> dict[str, list[str]]:
|
||||
live_tree = await snapshot_tree_from_state(store, state)
|
||||
version = await resolve_ref(store, ref)
|
||||
ref_tree = (await store.read_commit(version)).tree
|
||||
return _strip_meta(await store.diff(ref_tree, live_tree))
|
||||
|
||||
|
||||
async def status(store: VersionStore,
|
||||
ws,
|
||||
branch: str = "main") -> dict[str, list[str]]:
|
||||
return await status_state(store, await to_state_dict(ws), branch)
|
||||
|
||||
|
||||
async def status_state(store: VersionStore,
|
||||
state: dict,
|
||||
branch: str = "main") -> dict[str, list[str]]:
|
||||
live_tree = await snapshot_tree_from_state(store, state)
|
||||
if branch in await store.branches():
|
||||
head_tree = (await store.read_commit(await store.head(branch))).tree
|
||||
else:
|
||||
head_tree = await store.write_tree({})
|
||||
return _strip_meta(await store.diff(head_tree, live_tree))
|
||||
@@ -0,0 +1,40 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Protocol
|
||||
|
||||
from dulwich.repo import Repo
|
||||
|
||||
from mirage.server.paths import resolve_within_root, validate_path_segment
|
||||
|
||||
|
||||
class VersionBackend(Protocol):
|
||||
|
||||
def open_repo(self, workspace_id: str) -> Repo:
|
||||
...
|
||||
|
||||
|
||||
class LocalBackend:
|
||||
|
||||
def __init__(self, root: str | Path) -> None:
|
||||
self._root = Path(root)
|
||||
|
||||
def open_repo(self, workspace_id: str) -> Repo:
|
||||
path = resolve_within_root(self._root,
|
||||
validate_path_segment(workspace_id))
|
||||
if (path / "objects").is_dir():
|
||||
return Repo(str(path))
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return Repo.init_bare(str(path))
|
||||
@@ -0,0 +1,30 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
|
||||
class HeadMovedError(Exception):
|
||||
|
||||
def __init__(self, branch: str) -> None:
|
||||
self.branch = branch
|
||||
super().__init__(
|
||||
f"branch {branch!r} moved since this commit was prepared; "
|
||||
"refusing to overwrite (re-read the head and retry)")
|
||||
|
||||
|
||||
class NoSuchBranchError(Exception):
|
||||
|
||||
def __init__(self, branch: str) -> None:
|
||||
self.branch = branch
|
||||
super().__init__(f"no branch {branch!r}; create it first with "
|
||||
"`mirage workspace branch`")
|
||||
@@ -0,0 +1,172 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import json
|
||||
|
||||
from mirage.types import CacheKey, MountKey, ResourceStateKey, StateKey
|
||||
from mirage.workspace.snapshot.state import to_state_dict
|
||||
from mirage.workspace.snapshot.tar_io import _json_default
|
||||
from mirage.workspace.snapshot.utils import FORMAT_VERSION
|
||||
|
||||
META_PATH = ".mirage-meta.json"
|
||||
CACHE_PREFIX = ".mirage-cache/"
|
||||
|
||||
|
||||
def _is_reserved(tree_path: str) -> bool:
|
||||
return tree_path == META_PATH or tree_path.startswith(CACHE_PREFIX)
|
||||
|
||||
|
||||
def _tree_path(prefix: str, rel: str) -> str:
|
||||
p = prefix.strip("/")
|
||||
r = rel.lstrip("/")
|
||||
return f"{p}/{r}" if p else r
|
||||
|
||||
|
||||
def _rel_path(prefix: str, tree_path: str) -> str:
|
||||
p = prefix.strip("/")
|
||||
rest = tree_path[len(p) + 1:] if p else tree_path
|
||||
return "/" + rest
|
||||
|
||||
|
||||
def _belongs(tree_prefix: str, tree_path: str) -> bool:
|
||||
if not tree_prefix:
|
||||
return True
|
||||
return tree_path == tree_prefix or tree_path.startswith(tree_prefix + "/")
|
||||
|
||||
|
||||
def meta_to_blob(meta: dict) -> bytes:
|
||||
return json.dumps(meta, default=_json_default).encode("utf-8")
|
||||
|
||||
|
||||
def blob_to_meta(data: bytes) -> dict:
|
||||
return json.loads(data.decode("utf-8"))
|
||||
|
||||
|
||||
async def to_tree_inputs(ws) -> tuple[dict[str, bytes], dict]:
|
||||
return tree_inputs_from_state(await to_state_dict(ws))
|
||||
|
||||
|
||||
def tree_inputs_from_state(state: dict) -> tuple[dict[str, bytes], dict]:
|
||||
entries: dict[str, bytes] = {}
|
||||
mounts_meta: list[dict] = []
|
||||
for mount in state[StateKey.MOUNTS]:
|
||||
prefix = mount[MountKey.PREFIX]
|
||||
resource_state = dict(mount[MountKey.RESOURCE_STATE])
|
||||
files = resource_state.pop(ResourceStateKey.FILES, {})
|
||||
for rel, data in files.items():
|
||||
entries[_tree_path(prefix, rel)] = data
|
||||
mounts_meta.append({
|
||||
MountKey.INDEX:
|
||||
mount[MountKey.INDEX],
|
||||
MountKey.PREFIX:
|
||||
prefix,
|
||||
MountKey.MODE:
|
||||
mount[MountKey.MODE],
|
||||
MountKey.CONSISTENCY:
|
||||
mount[MountKey.CONSISTENCY],
|
||||
MountKey.RESOURCE_CLASS:
|
||||
mount[MountKey.RESOURCE_CLASS],
|
||||
MountKey.RESOURCE_STATE:
|
||||
resource_state,
|
||||
})
|
||||
cache = state[StateKey.CACHE]
|
||||
config = {
|
||||
StateKey.MIRAGE_VERSION: state[StateKey.MIRAGE_VERSION],
|
||||
StateKey.DEFAULT_SESSION_ID: state[StateKey.DEFAULT_SESSION_ID],
|
||||
StateKey.DEFAULT_AGENT_ID: state[StateKey.DEFAULT_AGENT_ID],
|
||||
StateKey.CURRENT_AGENT_ID: state[StateKey.CURRENT_AGENT_ID],
|
||||
CacheKey.LIMIT: cache[CacheKey.LIMIT],
|
||||
CacheKey.MAX_DRAIN_BYTES: cache[CacheKey.MAX_DRAIN_BYTES],
|
||||
}
|
||||
cache_meta: list[dict] = []
|
||||
for i, entry in enumerate(cache[CacheKey.ENTRIES]):
|
||||
ref = f"{CACHE_PREFIX}{i}"
|
||||
entries[ref] = entry[CacheKey.DATA]
|
||||
cache_meta.append({
|
||||
CacheKey.KEY: entry[CacheKey.KEY],
|
||||
CacheKey.FINGERPRINT: entry.get(CacheKey.FINGERPRINT),
|
||||
CacheKey.TTL: entry.get(CacheKey.TTL),
|
||||
CacheKey.CACHED_AT: entry.get(CacheKey.CACHED_AT),
|
||||
CacheKey.SIZE: entry.get(CacheKey.SIZE),
|
||||
"ref": ref,
|
||||
})
|
||||
meta = {
|
||||
"mounts": mounts_meta,
|
||||
"config": config,
|
||||
"cache": cache_meta,
|
||||
"fingerprints": state.get(StateKey.FINGERPRINTS) or [],
|
||||
"sessions": state.get(StateKey.SESSIONS) or [],
|
||||
}
|
||||
return entries, meta
|
||||
|
||||
|
||||
def to_state(entries: dict[str, bytes], meta: dict) -> dict:
|
||||
mounts: list[dict] = []
|
||||
for mount in meta["mounts"]:
|
||||
prefix = mount[MountKey.PREFIX]
|
||||
tree_prefix = prefix.strip("/")
|
||||
resource_state = dict(mount[MountKey.RESOURCE_STATE])
|
||||
files: dict[str, bytes] = {}
|
||||
for tree_path, data in entries.items():
|
||||
if _is_reserved(tree_path):
|
||||
continue
|
||||
if _belongs(tree_prefix, tree_path):
|
||||
files[_rel_path(prefix, tree_path)] = data
|
||||
resource_state[ResourceStateKey.FILES] = files
|
||||
mounts.append({
|
||||
MountKey.INDEX: mount[MountKey.INDEX],
|
||||
MountKey.PREFIX: prefix,
|
||||
MountKey.MODE: mount[MountKey.MODE],
|
||||
MountKey.CONSISTENCY: mount[MountKey.CONSISTENCY],
|
||||
MountKey.RESOURCE_CLASS: mount[MountKey.RESOURCE_CLASS],
|
||||
MountKey.RESOURCE_STATE: resource_state,
|
||||
})
|
||||
config = meta.get("config", {})
|
||||
cache_entries: list[dict] = []
|
||||
for c in meta.get("cache", []):
|
||||
cache_entries.append({
|
||||
CacheKey.KEY: c[CacheKey.KEY],
|
||||
CacheKey.DATA: entries[c["ref"]],
|
||||
CacheKey.FINGERPRINT: c.get(CacheKey.FINGERPRINT),
|
||||
CacheKey.TTL: c.get(CacheKey.TTL),
|
||||
CacheKey.CACHED_AT: c.get(CacheKey.CACHED_AT),
|
||||
CacheKey.SIZE: c.get(CacheKey.SIZE),
|
||||
})
|
||||
return {
|
||||
StateKey.VERSION:
|
||||
FORMAT_VERSION,
|
||||
StateKey.MIRAGE_VERSION:
|
||||
config.get(StateKey.MIRAGE_VERSION, "unknown"),
|
||||
StateKey.MOUNTS:
|
||||
mounts,
|
||||
StateKey.SESSIONS:
|
||||
meta.get("sessions", []),
|
||||
StateKey.DEFAULT_SESSION_ID:
|
||||
config.get(StateKey.DEFAULT_SESSION_ID, "default"),
|
||||
StateKey.DEFAULT_AGENT_ID:
|
||||
config.get(StateKey.DEFAULT_AGENT_ID, "default"),
|
||||
StateKey.CURRENT_AGENT_ID:
|
||||
config.get(StateKey.CURRENT_AGENT_ID, "default"),
|
||||
StateKey.CACHE: {
|
||||
CacheKey.LIMIT: config.get(CacheKey.LIMIT, "512MB"),
|
||||
CacheKey.MAX_DRAIN_BYTES: config.get(CacheKey.MAX_DRAIN_BYTES),
|
||||
CacheKey.ENTRIES: cache_entries,
|
||||
},
|
||||
StateKey.HISTORY:
|
||||
None,
|
||||
StateKey.JOBS: [],
|
||||
StateKey.FINGERPRINTS:
|
||||
meta.get("fingerprints", []),
|
||||
StateKey.LIVE_ONLY_MOUNTS: [],
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import asyncio
|
||||
import stat
|
||||
import time
|
||||
|
||||
from dulwich.diff_tree import (CHANGE_ADD, CHANGE_DELETE, CHANGE_MODIFY,
|
||||
tree_changes)
|
||||
from dulwich.objects import Blob, Commit, Tree
|
||||
from dulwich.repo import Repo
|
||||
|
||||
from mirage.server.version.backend import VersionBackend
|
||||
from mirage.server.version.errors import HeadMovedError
|
||||
|
||||
FILE_MODE = 0o100644
|
||||
DIR_MODE = 0o40000
|
||||
AUTHOR = b"mirage <mirage@local>"
|
||||
|
||||
|
||||
def _add_blob(repo: Repo, data: bytes) -> bytes:
|
||||
blob = Blob.from_string(data)
|
||||
repo.object_store.add_object(blob)
|
||||
return blob.id
|
||||
|
||||
|
||||
def _read_blob(repo: Repo, oid: bytes) -> bytes:
|
||||
return repo.object_store[oid].as_raw_string()
|
||||
|
||||
|
||||
def _build_tree(repo: Repo, entries: dict[str, bytes]) -> bytes:
|
||||
tree = Tree()
|
||||
subdirs: dict[str, dict[str, bytes]] = {}
|
||||
for path, oid in entries.items():
|
||||
if "/" in path:
|
||||
head, rest = path.split("/", 1)
|
||||
subdirs.setdefault(head, {})[rest] = oid
|
||||
else:
|
||||
tree.add(path.encode(), FILE_MODE, oid)
|
||||
for name, sub in subdirs.items():
|
||||
tree.add(name.encode(), DIR_MODE, _build_tree(repo, sub))
|
||||
repo.object_store.add_object(tree)
|
||||
return tree.id
|
||||
|
||||
|
||||
def _read_tree(repo: Repo, oid: bytes, prefix: str = "") -> dict[str, bytes]:
|
||||
out: dict[str, bytes] = {}
|
||||
for name, mode, sha in repo.object_store[oid].items():
|
||||
rel = name.decode()
|
||||
full = f"{prefix}/{rel}" if prefix else rel
|
||||
if stat.S_ISDIR(mode):
|
||||
out.update(_read_tree(repo, sha, full))
|
||||
else:
|
||||
out[full] = sha
|
||||
return out
|
||||
|
||||
|
||||
def _commit(repo: Repo, tree_oid: bytes, parents: list[bytes], branch: str,
|
||||
message: str) -> bytes:
|
||||
commit = Commit()
|
||||
commit.tree = tree_oid
|
||||
commit.parents = list(parents)
|
||||
commit.author = commit.committer = AUTHOR
|
||||
now = int(time.time())
|
||||
commit.author_time = commit.commit_time = now
|
||||
commit.author_timezone = commit.commit_timezone = 0
|
||||
commit.encoding = b"UTF-8"
|
||||
commit.message = message.encode()
|
||||
repo.object_store.add_object(commit)
|
||||
ref = b"refs/heads/" + branch.encode()
|
||||
expected_old = parents[0] if parents else None
|
||||
if expected_old is None:
|
||||
ok = repo.refs.add_if_new(ref, commit.id)
|
||||
else:
|
||||
ok = repo.refs.set_if_equals(ref, expected_old, commit.id)
|
||||
if not ok:
|
||||
raise HeadMovedError(branch)
|
||||
repo.refs.set_symbolic_ref(b"HEAD", ref)
|
||||
return commit.id
|
||||
|
||||
|
||||
def _head(repo: Repo, branch: str) -> bytes:
|
||||
return repo.refs[b"refs/heads/" + branch.encode()]
|
||||
|
||||
|
||||
def _set_branch(repo: Repo, name: str, oid: bytes) -> None:
|
||||
repo.refs[b"refs/heads/" + name.encode()] = oid
|
||||
|
||||
|
||||
def _read_commit(repo: Repo, oid: bytes) -> Commit:
|
||||
return repo.object_store[oid]
|
||||
|
||||
|
||||
def _branches(repo: Repo) -> list[str]:
|
||||
prefix = b"refs/heads/"
|
||||
names = [
|
||||
name[len(prefix):].decode() for name in repo.get_refs()
|
||||
if name.startswith(prefix)
|
||||
]
|
||||
return sorted(names)
|
||||
|
||||
|
||||
def _log(repo: Repo, branch: str) -> list[bytes]:
|
||||
head = repo.refs[b"refs/heads/" + branch.encode()]
|
||||
return [entry.commit.id for entry in repo.get_walker(include=[head])]
|
||||
|
||||
|
||||
def _diff(repo: Repo, tree_a: bytes, tree_b: bytes) -> dict[str, list[str]]:
|
||||
added: list[str] = []
|
||||
modified: list[str] = []
|
||||
deleted: list[str] = []
|
||||
for change in tree_changes(repo.object_store, tree_a, tree_b):
|
||||
if change.type == CHANGE_ADD:
|
||||
added.append(change.new.path.decode())
|
||||
elif change.type == CHANGE_DELETE:
|
||||
deleted.append(change.old.path.decode())
|
||||
elif change.type == CHANGE_MODIFY:
|
||||
modified.append(change.new.path.decode())
|
||||
return {
|
||||
"added": sorted(added),
|
||||
"modified": sorted(modified),
|
||||
"deleted": sorted(deleted),
|
||||
}
|
||||
|
||||
|
||||
class VersionStore:
|
||||
|
||||
def __init__(self, repo: Repo) -> None:
|
||||
self._repo = repo
|
||||
|
||||
@classmethod
|
||||
async def open(cls, backend: VersionBackend,
|
||||
workspace_id: str) -> "VersionStore":
|
||||
repo = await asyncio.to_thread(backend.open_repo, workspace_id)
|
||||
return cls(repo)
|
||||
|
||||
async def write_blob(self, data: bytes) -> bytes:
|
||||
return await asyncio.to_thread(_add_blob, self._repo, data)
|
||||
|
||||
async def read_blob(self, oid: bytes) -> bytes:
|
||||
return await asyncio.to_thread(_read_blob, self._repo, oid)
|
||||
|
||||
async def write_tree(self, entries: dict[str, bytes]) -> bytes:
|
||||
return await asyncio.to_thread(_build_tree, self._repo, entries)
|
||||
|
||||
async def read_tree(self, oid: bytes) -> dict[str, bytes]:
|
||||
return await asyncio.to_thread(_read_tree, self._repo, oid)
|
||||
|
||||
async def commit(self, tree_oid: bytes, parents: list[bytes], branch: str,
|
||||
message: str) -> bytes:
|
||||
return await asyncio.to_thread(_commit, self._repo, tree_oid, parents,
|
||||
branch, message)
|
||||
|
||||
async def head(self, branch: str) -> bytes:
|
||||
return await asyncio.to_thread(_head, self._repo, branch)
|
||||
|
||||
async def set_branch(self, name: str, oid: bytes) -> None:
|
||||
await asyncio.to_thread(_set_branch, self._repo, name, oid)
|
||||
|
||||
async def read_commit(self, oid: bytes) -> Commit:
|
||||
return await asyncio.to_thread(_read_commit, self._repo, oid)
|
||||
|
||||
async def branches(self) -> list[str]:
|
||||
return await asyncio.to_thread(_branches, self._repo)
|
||||
|
||||
async def log(self, branch: str) -> list[bytes]:
|
||||
return await asyncio.to_thread(_log, self._repo, branch)
|
||||
|
||||
async def diff(self, tree_a: bytes, tree_b: bytes) -> dict[str, list[str]]:
|
||||
return await asyncio.to_thread(_diff, self._repo, tree_a, tree_b)
|
||||
Reference in New Issue
Block a user