Skip to main content

memstead_git_branch/storage/
mod.rs

1//! Re-export shim over `memstead_base::storage` (the [`MemWriter`] trait
2//! and [`MemWriterError`]) plus the git-tree adapter that stays in
3//! this crate.
4//!
5//! The git-tree adapter ([`git_tree::GitTreeMemWriter`]) buffers
6//! mutations and applies them via `gix::object::tree::Editor` against a
7//! multi-root `mem-repo-git` repository, one branch per mem.
8
9pub mod git_tree;
10
11use std::path::PathBuf;
12
13pub use memstead_base::storage::{CommitId, MemWriter, MemWriterError};
14
15/// Construct a `Box<dyn MemWriter>` for the git-object-backed path.
16/// `gitdir` points at the multi-root `mem-repo-git` repo; `ref_name`
17/// is the per-mem branch (fully-qualified, e.g.
18/// `refs/heads/<mem>`). The first commit creates the ref if it does
19/// not yet exist.
20#[cfg(feature = "git-object-storage")]
21pub fn git_tree_mem_writer(gitdir: PathBuf, ref_name: String) -> Box<dyn MemWriter> {
22    Box::new(git_tree::GitTreeMemWriter::new(gitdir, ref_name))
23}
24
25/// Full counterpart of [`memstead_base::instantiate_lean_backend`]: turns
26/// any [`memstead_base::Mount`] into a `Box<dyn MemBackend>`, including
27/// the git-branch variant that the lean flavour cannot construct.
28///
29/// Folder and Archive variants delegate to the lean function so the
30/// instantiation paths share one implementation. The git-branch
31/// variant constructs a [`git_tree::GitTreeMemWriter`] using the
32/// mount's `gitdir` + `branch`, fully-qualifying the ref-name as
33/// `refs/heads/<branch>` so the per-branch mutex inside the writer
34/// keys consistently with what `agent_notes_since` and
35/// `read_branch_blobs` expect.
36pub fn instantiate_full_backend(
37    mount: &memstead_base::Mount,
38) -> Result<Box<dyn memstead_base::MemBackend>, memstead_base::InstantiateError> {
39    use memstead_base::MountStorage;
40    match &mount.storage {
41        MountStorage::Folder { .. } | MountStorage::Archive { .. } | MountStorage::InMemory => {
42            memstead_base::instantiate_lean_backend(mount)
43        }
44        MountStorage::GitBranch { gitdir, branch } => {
45            let ref_name = if branch.starts_with("refs/") {
46                branch.clone()
47            } else {
48                format!("refs/heads/{branch}")
49            };
50            Ok(Box::new(git_tree::GitTreeMemWriter::new(
51                gitdir.clone(),
52                ref_name,
53            )))
54        }
55    }
56}
57
58/// The git-branch ops bundle installed on `memstead_base::Engine` by full
59/// boot. Wraps `crate::ops::changes::changes_since` and
60/// `crate::ops::export::export_mem_from_branch` so the engine can
61/// dispatch from a [`MountStorage::GitBranch`] mount without an extra
62/// trait or downcast.
63pub const FULL_GIT_BRANCH_OPS: memstead_base::GitBranchOps = memstead_base::GitBranchOps {
64    changes_since: changes_since_dispatch,
65    diff: diff_dispatch,
66    branch_reset: branch_reset_dispatch,
67    fetch: fetch_dispatch,
68    pull: pull_dispatch,
69    push: push_dispatch,
70    remote_add: remote_add_dispatch,
71    read_tree: read_tree_dispatch,
72    export: export_dispatch,
73    export_to_bytes: export_to_bytes_dispatch,
74    prune_residue: prune_residue_dispatch,
75    rename_mem_storage: rename_mem_storage_dispatch,
76    write_schema: write_schema_dispatch,
77    read_schema_file: read_schema_file_dispatch,
78    read_ref_schemas: read_ref_schemas_dispatch,
79};
80
81/// Dispatcher for `Engine::install_schema` on git-branch workspaces.
82/// Writes the schema package onto the unified `__MEMSTEAD:schemas/` ref
83/// and returns the resulting commit sha.
84fn write_schema_dispatch(
85    gitdir: &std::path::Path,
86    name: &str,
87    version: &str,
88    files: &[(String, Vec<u8>)],
89) -> Result<String, memstead_base::backend::BackendError> {
90    crate::storage_memstead::write_schema_to_memstead_ref(gitdir, name, version, files)
91        .map(|outcome| outcome.commit_sha)
92        .map_err(|e| {
93            memstead_base::backend::BackendError::Other(format!(
94                "schema install onto __MEMSTEAD ref at {}: {e}",
95                gitdir.display(),
96            ))
97        })
98}
99
100/// Dispatcher for `Engine::full_refresh`: re-read every schema sealed
101/// on the `__MEMSTEAD:schemas/` ref so an out-of-band install becomes
102/// resolvable warm. Absent ref/subtree resolves to empty.
103fn read_ref_schemas_dispatch(
104    workspace_root: &std::path::Path,
105) -> Result<Vec<std::sync::Arc<memstead_schema::Schema>>, memstead_base::backend::BackendError> {
106    use crate::mem_repo_schemas::LoadOutcome;
107    match crate::mem_repo_schemas::load_schemas_from_ref(workspace_root) {
108        Ok(LoadOutcome::Schemas(schemas)) => Ok(schemas),
109        Ok(_) => Ok(Vec::new()),
110        Err(e) => Err(memstead_base::backend::BackendError::Other(format!(
111            "schema re-read from __MEMSTEAD ref at {}: {e}",
112            workspace_root.display(),
113        ))),
114    }
115}
116
117/// Dispatcher for the authoring-drift health axis: read one file from
118/// a sealed package on the `__MEMSTEAD:schemas/` ref. Absence is
119/// `Ok(None)`, never an error.
120fn read_schema_file_dispatch(
121    gitdir: &std::path::Path,
122    name: &str,
123    version: &str,
124    rel: &str,
125) -> Result<Option<Vec<u8>>, memstead_base::backend::BackendError> {
126    crate::storage_memstead::read_schema_file_from_memstead_ref(gitdir, name, version, rel).map_err(
127        |e| {
128            memstead_base::backend::BackendError::Other(format!(
129                "schema file read from __MEMSTEAD ref at {}: {e}",
130                gitdir.display(),
131            ))
132        },
133    )
134}
135
136/// Dispatcher for
137/// `RecoveryAction::ForceOverwrite` in `create_mem`. Drops the
138/// per-mem branch + `__MEMSTEAD` config blob in one ref-edit
139/// transaction by delegating to `delete_mem_artifacts_at_gitdir`
140/// (the same helper `MemBackend::delete_artifacts` already wraps
141/// for delete-files flows). Operates on an unmounted gitdir —
142/// callers don't need an instantiated backend, which is why the
143/// orchestrator reaches for this through `Engine::git_branch_ops()`
144/// rather than constructing a backend just to call `delete_artifacts`.
145fn prune_residue_dispatch(
146    gitdir: &std::path::Path,
147    branch_full_path: &str,
148) -> Result<(), memstead_base::backend::BackendError> {
149    let ctx = memstead_base::vcs::CommitContext {
150        actor: memstead_base::vcs::Actor::Agent,
151        client: None,
152        tool: Some("memstead_mem_create (force_overwrite)"),
153        note: None,
154        role: Default::default(),
155        logical_operation_id: None,
156        entity_ids: None,
157    };
158    crate::storage_memstead::delete_mem_artifacts_at_gitdir(gitdir, branch_full_path, &ctx).map_err(
159        |e| {
160            memstead_base::backend::BackendError::Other(format!(
161                "force_overwrite prune at {}: {e}",
162                branch_full_path,
163            ))
164        },
165    )
166}
167
168/// Dispatcher for `memstead_engine::rename_mem` on git-branch
169/// workspaces: branch move + `__MEMSTEAD:mems/` config relocation in
170/// one ref-edit transaction, history preserved.
171fn rename_mem_storage_dispatch(
172    gitdir: &std::path::Path,
173    old_leaf: &str,
174    new_leaf: &str,
175) -> Result<(), memstead_base::backend::BackendError> {
176    let ctx = memstead_base::vcs::CommitContext {
177        actor: memstead_base::vcs::Actor::Agent,
178        client: None,
179        tool: Some("memstead mem rename"),
180        note: None,
181        role: Default::default(),
182        logical_operation_id: None,
183        entity_ids: None,
184    };
185    crate::storage_memstead::rename_mem_artifacts_at_gitdir(gitdir, old_leaf, new_leaf, &ctx)
186        .map_err(|e| {
187            memstead_base::backend::BackendError::Other(format!(
188                "mem rename {old_leaf} -> {new_leaf}: {e}",
189            ))
190        })
191}
192
193fn changes_since_dispatch(
194    gitdir: &std::path::Path,
195    branch: &str,
196    mem: &str,
197    since: &str,
198    rename_similarity: f32,
199) -> Result<memstead_base::ops::BackendChanges, memstead_base::backend::BackendError> {
200    let ref_name = if branch.starts_with("refs/") {
201        branch.to_string()
202    } else {
203        format!("refs/heads/{branch}")
204    };
205    let empty_store = memstead_base::Store::new();
206    let report = crate::ops::changes::changes_since(
207        &empty_store,
208        mem,
209        gitdir,
210        since,
211        rename_similarity,
212        Some(&ref_name),
213    )
214    .map_err(|e| {
215        // A bad `since`
216        // SHA (malformed or absent) is a recoverable caller-argument
217        // fault, not a backend fault. Encode it as a typed prefix the
218        // engine lifts to `COMMIT_NOT_FOUND` (carrying the untruncated
219        // SHA), reserving the `MEM_ERROR` catch-all for genuine faults.
220        match e {
221            crate::vcs::VcsError::ObjectNotFound(_) => {
222                memstead_base::backend::BackendError::Other(format!("COMMIT_NOT_FOUND:{since}"))
223            }
224            other => memstead_base::backend::BackendError::Other(format!(
225                "git-branch changes_since: {other}"
226            )),
227        }
228    })?;
229    Ok(memstead_base::ops::BackendChanges {
230        since: report.since,
231        head: report.head,
232        changes: report.changes,
233        notes: report.notes.unwrap_or_default(),
234        memstead_ref: report.memstead_ref,
235    })
236}
237
238// Signature (arity included) is pinned by the `GitBranchOps.export`
239// fn-pointer contract declared in memstead-base.
240#[allow(clippy::too_many_arguments)]
241fn export_dispatch(
242    gitdir: &std::path::Path,
243    branch: &str,
244    mem: &str,
245    config: &memstead_schema::MemConfig,
246    output_path: &std::path::Path,
247    workspace_root: Option<&std::path::Path>,
248    workspace_schemas_dir: Option<&std::path::Path>,
249    provenance_bytes: Option<&[u8]>,
250    anchors_bytes: Option<&[u8]>,
251) -> Result<memstead_base::ops::MemExportResult, memstead_base::backend::BackendError> {
252    let _ = branch;
253    crate::ops::export::export_mem_from_branch(
254        gitdir,
255        mem,
256        config,
257        output_path,
258        workspace_root,
259        workspace_schemas_dir,
260        provenance_bytes,
261        anchors_bytes,
262    )
263    .map_err(|e| {
264        memstead_base::backend::BackendError::Other(format!("export_mem_from_branch: {e}"))
265    })
266}
267
268fn read_tree_dispatch(
269    gitdir: &std::path::Path,
270    ref_name: &str,
271) -> Result<Vec<(String, String)>, memstead_base::backend::BackendError> {
272    #[cfg(feature = "git-object-storage")]
273    {
274        crate::ops::transport::read_md_blobs_at_ref(gitdir, ref_name)
275    }
276    #[cfg(not(feature = "git-object-storage"))]
277    {
278        let _ = (gitdir, ref_name);
279        Err(memstead_base::backend::BackendError::Other(
280            "read_tree: git-object-storage feature not enabled".to_string(),
281        ))
282    }
283}
284
285fn fetch_dispatch(
286    gitdir: &std::path::Path,
287    remote: &str,
288    refspecs: &[String],
289) -> Result<memstead_base::ops::FetchOutcome, memstead_base::backend::BackendError> {
290    #[cfg(feature = "git-object-storage")]
291    {
292        crate::ops::transport::fetch_in_gitdir(gitdir, remote, refspecs)
293    }
294    #[cfg(not(feature = "git-object-storage"))]
295    {
296        let _ = (gitdir, remote, refspecs);
297        Err(memstead_base::backend::BackendError::Other(
298            "fetch: git-object-storage feature not enabled".to_string(),
299        ))
300    }
301}
302
303fn pull_dispatch(
304    gitdir: &std::path::Path,
305    remote: &str,
306    mem: &str,
307) -> Result<memstead_base::ops::PullOutcome, memstead_base::backend::BackendError> {
308    #[cfg(feature = "git-object-storage")]
309    {
310        crate::ops::transport::pull_in_gitdir(gitdir, remote, mem)
311    }
312    #[cfg(not(feature = "git-object-storage"))]
313    {
314        let _ = (gitdir, remote, mem);
315        Err(memstead_base::backend::BackendError::Other(
316            "pull: git-object-storage feature not enabled".to_string(),
317        ))
318    }
319}
320
321fn push_dispatch(
322    gitdir: &std::path::Path,
323    remote: &str,
324    mem: &str,
325    force: bool,
326) -> Result<memstead_base::ops::PushOutcome, memstead_base::backend::BackendError> {
327    #[cfg(feature = "git-object-storage")]
328    {
329        crate::ops::transport::push_in_gitdir(gitdir, remote, mem, force)
330    }
331    #[cfg(not(feature = "git-object-storage"))]
332    {
333        let _ = (gitdir, remote, mem, force);
334        Err(memstead_base::backend::BackendError::Other(
335            "push: git-object-storage feature not enabled".to_string(),
336        ))
337    }
338}
339
340fn remote_add_dispatch(
341    gitdir: &std::path::Path,
342    name: &str,
343    url: &str,
344) -> Result<memstead_base::ops::RemoteAddOutcome, memstead_base::backend::BackendError> {
345    #[cfg(feature = "git-object-storage")]
346    {
347        crate::ops::transport::remote_add_in_gitdir(gitdir, name, url)
348    }
349    #[cfg(not(feature = "git-object-storage"))]
350    {
351        let _ = (gitdir, name, url);
352        Err(memstead_base::backend::BackendError::Other(
353            "remote_add: git-object-storage feature not enabled".to_string(),
354        ))
355    }
356}
357
358fn branch_reset_dispatch(
359    gitdir: &std::path::Path,
360    branch: &str,
361    target_sha: &str,
362    expected_head: Option<&str>,
363) -> Result<memstead_base::ops::BranchResetOutcome, memstead_base::backend::BackendError> {
364    #[cfg(feature = "git-object-storage")]
365    {
366        crate::ops::branch_reset::branch_reset_in_gitdir(gitdir, branch, target_sha, expected_head)
367    }
368    #[cfg(not(feature = "git-object-storage"))]
369    {
370        let _ = (gitdir, branch, target_sha);
371        Err(memstead_base::backend::BackendError::Other(
372            "branch_reset: git-object-storage feature not enabled".to_string(),
373        ))
374    }
375}
376
377fn diff_dispatch(
378    gitdir: &std::path::Path,
379    mem: &str,
380    ref_a: &str,
381    ref_b: &str,
382    config: &memstead_base::ops::DiffConfig,
383) -> Result<memstead_base::ops::Diff, memstead_base::backend::BackendError> {
384    #[cfg(feature = "git-object-storage")]
385    {
386        crate::ops::diff::diff_two_refs(gitdir, mem, ref_a, ref_b, config)
387    }
388    #[cfg(not(feature = "git-object-storage"))]
389    {
390        let _ = (gitdir, mem, ref_a, ref_b, config);
391        Err(memstead_base::backend::BackendError::Other(
392            "diff_two_refs: git-object-storage feature not enabled".to_string(),
393        ))
394    }
395}
396
397#[allow(clippy::too_many_arguments)]
398fn export_to_bytes_dispatch(
399    gitdir: &std::path::Path,
400    branch: &str,
401    mem: &str,
402    config: &memstead_schema::MemConfig,
403    workspace_root: Option<&std::path::Path>,
404    workspace_schemas_dir: Option<&std::path::Path>,
405    provenance_bytes: Option<&[u8]>,
406    anchors_bytes: Option<&[u8]>,
407) -> Result<memstead_base::ops::MemExportBytes, memstead_base::backend::BackendError> {
408    let _ = branch;
409    crate::ops::export::export_mem_from_branch_to_bytes(
410        gitdir,
411        mem,
412        config,
413        workspace_root,
414        workspace_schemas_dir,
415        provenance_bytes,
416        anchors_bytes,
417    )
418    .map_err(|e| {
419        memstead_base::backend::BackendError::Other(format!("export_mem_from_branch_to_bytes: {e}"))
420    })
421}