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 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 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 let tree_hash = build_tree_from_index(&index, &store)?;
40
41 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 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 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}