Skip to main content

opys_engine/commands/
sync.rs

1//! The full auto-sync pass: reconcile cross-references, linkify body prose, and
2//! relocate documents to their canonical layout path. Invoked by `maybe_sync`
3//! after every mutating command and by the `opys sync` command.
4//!
5//! The pass runs over the in-memory store: reconstruct every doc, backfill
6//! timestamps, run the (unchanged) `links` reconcile/linkify engine on the
7//! reconstructed `Doc`s, write the changed ones back via `put_doc`, point every
8//! doc at its canonical path, and flush. Relation-map / linkify logic stays in
9//! Rust — the store is the data layer feeding it.
10
11use crate::doc::Doc;
12use crate::error::{usage, Result};
13use crate::links;
14use crate::project::Project;
15use crate::store::Store;
16use crate::Ctx;
17
18/// The `opys sync` command entry: open the project and run the full pass.
19pub fn run_command(ctx: &Ctx) -> Result<()> {
20    let prj = ctx.open()?;
21    let n = run(&prj, ctx.backend.as_ref())?;
22    println!("synced {n} document(s)");
23    Ok(())
24}
25
26/// Load the corpus, run the sync pass, and flush. Returns the document count.
27/// Errors (without writing) if any document fails to parse.
28pub fn run(prj: &Project, backend: &dyn crate::backend::Backend) -> Result<usize> {
29    let (mut store, errs) = backend.load(prj)?;
30    if !errs.is_empty() {
31        return Err(usage("fix parse errors first (run verify)"));
32    }
33    let n = pass(prj, &mut store)?;
34    backend.flush(prj, store)?;
35    Ok(n)
36}
37
38/// Run the reconcile/linkify/backfill/relocate pass against an already-open
39/// store (no flush). Returns the document count. Used both by [`run`] and, on
40/// the invocation's own store, after a mutating command.
41pub fn pass(prj: &Project, store: &mut Store) -> Result<usize> {
42    // Clean up the index older versions generated at the base; opys no longer
43    // writes one (slice the inventory live with `opys list`/`opys query`).
44    let _ = std::fs::remove_file(prj.base.join("INDEX.md"));
45
46    let rows = store.full_rows()?;
47    let count = rows.len();
48
49    // Reconstruct the working set, backfilling the auto-maintained timestamps on
50    // docs predating the fields from the file's mtime. Housekeeping, so it must
51    // not bump `updated` on docs that already have it — only fill genuine gaps.
52    let mut docs: Vec<Doc> = Vec::with_capacity(count);
53    for r in &rows {
54        let mut doc = r.doc.clone();
55        let need_created = !doc.frontmatter.contains_key("created");
56        let need_updated = !doc.frontmatter.contains_key("updated");
57        if (need_created || need_updated) && r.orig_mtime.is_some() {
58            let ts = r.orig_mtime.clone().unwrap();
59            if need_created {
60                doc.frontmatter.set_str("created", ts.clone());
61            }
62            if need_updated {
63                doc.frontmatter.set_str("updated", ts);
64            }
65        }
66        docs.push(doc);
67    }
68
69    // The (unchanged) relation/linkify engine operates on the reconstructed set.
70    links::reconcile(&mut docs);
71    links::reconcile_blockers(&mut docs);
72    let index = links::build_index(&docs);
73    let prefixes: Vec<String> = prj.pcfg.types.values().map(|t| t.prefix.clone()).collect();
74    let re = links::ref_re(&prefixes);
75    for d in docs.iter_mut() {
76        let dir = d.path.parent().unwrap_or(&prj.base).to_path_buf();
77        d.body = links::linkify(&d.body, &dir, &index, &re);
78    }
79
80    // Write back what the pass changed, and point every mislocated doc at its
81    // canonical path (flush relocates the file even with no content change).
82    // The canonical path is computed in Rust and only written when it differs,
83    // so the common no-op sync issues almost no SQL.
84    for (r, d) in rows.iter().zip(&docs) {
85        if d.to_text() != r.doc.to_text() {
86            store.put_doc(&prj.pcfg, Some(r.dkey), d)?;
87        }
88        if let (Some(id), Some(status)) = (d.id(), d.status()) {
89            let canonical = prj
90                .pcfg
91                .doc_relpath(id, status)
92                .to_string_lossy()
93                .into_owned();
94            if canonical != r.relpath {
95                store.set_path(r.dkey, &canonical)?;
96            }
97        }
98    }
99    Ok(count)
100}