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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:30:44 +08:00
commit bcbd1bdb22
5748 changed files with 562488 additions and 0 deletions
+13
View File
@@ -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. =========
+152
View File
@@ -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))
+40
View File
@@ -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))
+30
View File
@@ -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`")
+172
View File
@@ -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: [],
}
+181
View File
@@ -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)