Skip to main content

cadvm_core/
snapshot.rs

1//! Creating snapshots (commits) from the working tree.
2
3use cadvm_store::ObjectId;
4
5use crate::config;
6use crate::error::Result;
7use crate::format::CadFormat;
8use crate::mesh;
9use crate::model::{CommitBody, FileEntry, Manifest};
10use crate::repo::{Head, Repository};
11use crate::step;
12use crate::worktree;
13
14/// Result of a successful snapshot.
15#[derive(Debug, Clone)]
16pub struct SnapshotOutcome {
17    pub commit_id: ObjectId,
18    pub manifest_id: ObjectId,
19    pub file_count: usize,
20    /// The branch the snapshot advanced, if HEAD was attached.
21    pub branch: Option<String>,
22}
23
24/// Build a manifest from the current working tree (no commit written).
25pub fn build_manifest(repo: &Repository) -> Result<Manifest> {
26    let mut manifest = Manifest::empty();
27    for rel in worktree::scan_step_files(repo)? {
28        let content = worktree::read_working_file(repo, &rel)?;
29        let format = CadFormat::from_path(&rel).expect("scan only yields tracked formats");
30        let blob_ref = repo.store().put_file_content(&content)?;
31        let raw_hash = blob_ref.raw_hash.clone();
32
33        // B-Rep (STEP) and mesh (STL/OBJ) carry different metadata.
34        let (line_count, step_metadata, mesh_metadata) = if format.is_brep() {
35            (Some(count_lines(&content)), step::extract(&content), None)
36        } else {
37            (None, None, mesh::extract(&content, format))
38        };
39
40        let entry = FileEntry {
41            path: rel.clone(),
42            format,
43            raw_hash,
44            blob_ref,
45            size_bytes: content.len() as u64,
46            line_count,
47            step_metadata,
48            mesh_metadata,
49        };
50        manifest.files.insert(rel, entry);
51    }
52    Ok(manifest)
53}
54
55/// Create a snapshot: scan the working tree, store content, write a manifest and
56/// commit, and advance the current branch (or detached HEAD).
57///
58/// `timestamp_unix` is supplied by the caller (the CLI passes the current time)
59/// so the core stays deterministic and easily testable.
60pub fn snapshot(repo: &Repository, message: &str, timestamp_unix: i64) -> Result<SnapshotOutcome> {
61    let manifest = build_manifest(repo)?;
62    let file_count = manifest.file_count();
63    let manifest_id = repo.write_manifest(&manifest)?;
64
65    // Warm the hash cache so the next `status` is a cache hit (no re-hashing).
66    let mut cache = crate::index::HashCache::load(repo);
67    let mut tracked = std::collections::BTreeSet::new();
68    for (rel, entry) in &manifest.files {
69        let _ = cache.record(repo, rel, entry.raw_hash.clone());
70        tracked.insert(rel.clone());
71    }
72    cache.retain(&tracked);
73    let _ = cache.save(repo);
74
75    let parents = match repo.head_commit_id()? {
76        Some(parent) => vec![parent],
77        None => Vec::new(),
78    };
79
80    let body = CommitBody {
81        parents,
82        manifest: manifest_id.clone(),
83        message: message.to_string(),
84        timestamp_unix,
85        author: Some(config::resolve_author(repo)?),
86    };
87    let commit_id = repo.write_commit(&body)?;
88
89    // Advance whatever HEAD points at.
90    let branch = match repo.read_head()? {
91        Head::Branch(name) => {
92            repo.write_ref(&name, &commit_id)?;
93            Some(name)
94        }
95        Head::Detached(_) => {
96            repo.write_head(&Head::Detached(commit_id.clone()))?;
97            None
98        }
99    };
100
101    Ok(SnapshotOutcome {
102        commit_id,
103        manifest_id,
104        file_count,
105        branch,
106    })
107}
108
109/// Count lines in file content (number of `\n`, plus one if the last line is
110/// unterminated). Empty content has zero lines.
111fn count_lines(content: &[u8]) -> u64 {
112    if content.is_empty() {
113        return 0;
114    }
115    let newlines = content.iter().filter(|&&b| b == b'\n').count() as u64;
116    if content.last() == Some(&b'\n') {
117        newlines
118    } else {
119        newlines + 1
120    }
121}