Skip to main content

lit/commands/
snapshot.rs

1use crate::core::{
2    find_repo_root, get_current_branch, read_head, write_ref, Blob, Commit, Object, ObjectHash,
3    Tree,
4};
5use crate::response::SnapshotResponse;
6use crate::storage::{Index, ObjectStore};
7use std::collections::HashMap;
8use std::fs;
9use std::path::Path;
10use walkdir::WalkDir;
11
12pub fn execute(
13    message: String,
14    author: Option<String>,
15    metadata: Option<serde_json::Value>,
16) -> Result<SnapshotResponse, crate::errors::LitError> {
17    let repo_root = find_repo_root()?;
18    let store = ObjectStore::new(&repo_root);
19    let mut index = Index::load(&repo_root)?;
20
21    // Stage all files (like `lit add .`)
22    let files_added = add_all_files(&repo_root, &store, &mut index)?;
23    index.save(&repo_root)?;
24
25    if index.entries.is_empty() {
26        return Err("Nothing to snapshot (no files in working directory)".into());
27    }
28
29    // Get author
30    let author_name = if let Some(a) = author {
31        a
32    } else {
33        std::env::var("USER")
34            .or_else(|_| std::env::var("USERNAME"))
35            .unwrap_or_else(|_| "Unknown".to_string())
36    };
37
38    // Build tree from index
39    let tree_hash = build_tree_from_index(&index, &store)?;
40
41    // Get parent commit(s)
42    let parents = match read_head(&repo_root) {
43        Ok(parent_hash) => vec![ObjectHash::from_hex(parent_hash)],
44        Err(_) => vec![],
45    };
46    let parent_str = parents.first().map(|p| p.to_string());
47
48    // Create commit object with optional metadata
49    let mut commit = Commit::new(
50        tree_hash.clone(),
51        parents,
52        author_name.clone(),
53        message.clone(),
54    );
55    commit.metadata = metadata;
56
57    let timestamp = commit.timestamp;
58    let commit_object = Object::Commit(commit);
59    let commit_hash = store.write(&commit_object)?;
60
61    // Update the current branch reference
62    let branch = get_current_branch(&repo_root).unwrap_or_else(|_| "main".to_string());
63    write_ref(
64        &repo_root,
65        &format!("heads/{}", branch),
66        commit_hash.as_str(),
67    )?;
68
69    Ok(SnapshotResponse {
70        hash: commit_hash.to_string(),
71        short_hash: commit_hash.short().to_string(),
72        tree: tree_hash.to_string(),
73        parent: parent_str,
74        author: author_name,
75        message,
76        timestamp,
77        files_added,
78    })
79}
80
81fn add_all_files(
82    repo_root: &Path,
83    store: &ObjectStore,
84    index: &mut Index,
85) -> Result<usize, crate::errors::LitError> {
86    let mut count = 0usize;
87    for entry in WalkDir::new(repo_root).into_iter().filter_entry(|e| {
88        let name = e.file_name().to_string_lossy();
89        !name.starts_with('.') && name != "target" && name != "node_modules"
90    }) {
91        let entry = entry.map_err(|e| format!("Failed to read directory: {}", e))?;
92        if entry.file_type().is_file() {
93            let path = entry.path();
94            if path.starts_with(repo_root.join(".lit")) {
95                continue;
96            }
97            let content =
98                fs::read(path).map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
99            let blob = Blob::new(content);
100            let hash = store.write(&Object::Blob(blob))?;
101            let rel_path = path
102                .strip_prefix(repo_root)
103                .map_err(|e| format!("Path error: {}", e))?
104                .to_string_lossy()
105                .replace('\\', "/");
106            index.add(rel_path, hash.to_string(), "100644".to_string());
107            count += 1;
108        }
109    }
110    Ok(count)
111}
112
113fn build_tree_from_index(
114    index: &Index,
115    store: &ObjectStore,
116) -> Result<ObjectHash, crate::errors::LitError> {
117    let mut tree_map: HashMap<String, Vec<(String, String, String)>> = HashMap::new();
118
119    for entry in index.sorted_entries() {
120        let parts: Vec<&str> = entry.path.split('/').collect();
121        if parts.len() == 1 {
122            tree_map.entry("".to_string()).or_default().push((
123                parts[0].to_string(),
124                entry.hash.clone(),
125                entry.mode.clone(),
126            ));
127        } else {
128            let dir = parts[0].to_string();
129            tree_map.entry(dir).or_default().push((
130                parts[1..].join("/"),
131                entry.hash.clone(),
132                entry.mode.clone(),
133            ));
134        }
135    }
136
137    let mut root_tree = Tree::new();
138
139    if let Some(root_files) = tree_map.get("") {
140        for (name, hash, mode) in root_files {
141            root_tree.add_entry(
142                mode.clone(),
143                name.clone(),
144                ObjectHash::from_hex(hash.clone()),
145                "blob".to_string(),
146            );
147        }
148    }
149
150    for dir in tree_map.keys() {
151        if !dir.is_empty() {
152            let mut subtree = Tree::new();
153            if let Some(files) = tree_map.get(dir) {
154                for (name, hash, mode) in files {
155                    subtree.add_entry(
156                        mode.clone(),
157                        name.clone(),
158                        ObjectHash::from_hex(hash.clone()),
159                        "blob".to_string(),
160                    );
161                }
162            }
163            let subtree_hash = store.write(&Object::Tree(subtree))?;
164            root_tree.add_entry(
165                "040000".to_string(),
166                dir.clone(),
167                subtree_hash,
168                "tree".to_string(),
169            );
170        }
171    }
172
173    store.write(&Object::Tree(root_tree)).map_err(Into::into)
174}