1use crate::core::{find_repo_root, read_head, Object, ObjectHash};
2use crate::response::StashResponse;
3use crate::storage::{Index, ObjectStore};
4use serde::{Deserialize, Serialize};
5use std::fs;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8struct StashEntry {
9 pub head_commit: String,
11 pub branch: Option<String>,
13 pub index_tree: String,
15 pub worktree_tree: String,
17 pub message: String,
19 pub timestamp: i64,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24struct StashList {
25 entries: Vec<StashEntry>,
26}
27
28impl StashList {
29 fn load(repo_root: &std::path::Path) -> Result<Self, crate::errors::LitError> {
30 let path = repo_root.join(".lit").join("stash");
31 if !path.exists() {
32 return Ok(StashList {
33 entries: Vec::new(),
34 });
35 }
36 let data = fs::read_to_string(&path).map_err(|e| format!("Failed to read stash: {}", e))?;
37 serde_json::from_str(&data).map_err(|e| format!("Failed to parse stash: {}", e).into())
38 }
39
40 fn save(&self, repo_root: &std::path::Path) -> Result<(), crate::errors::LitError> {
41 let path = repo_root.join(".lit").join("stash");
42 let data = serde_json::to_string_pretty(self)
43 .map_err(|e| format!("Failed to serialize stash: {}", e))?;
44 fs::write(&path, data).map_err(|e| format!("Failed to write stash: {}", e).into())
45 }
46}
47
48pub fn execute(
49 command: Option<crate::StashCommands>,
50) -> Result<StashResponse, crate::errors::LitError> {
51 let repo_root = find_repo_root()?;
52
53 match command {
54 None | Some(crate::StashCommands::Push { message: None }) => stash_push(&repo_root, None),
55 Some(crate::StashCommands::Push { message }) => stash_push(&repo_root, message),
56 Some(crate::StashCommands::Pop) => stash_pop(&repo_root),
57 Some(crate::StashCommands::Apply { index }) => stash_apply(&repo_root, index),
58 Some(crate::StashCommands::List) => stash_list(&repo_root),
59 Some(crate::StashCommands::Drop { index }) => stash_drop(&repo_root, index),
60 }
61}
62
63fn build_tree_from_working_dir(
64 repo_root: &std::path::Path,
65 store: &ObjectStore,
66) -> Result<ObjectHash, crate::errors::LitError> {
67 use crate::core::Tree;
68
69 let mut tree = Tree::new();
70 collect_files_to_tree(repo_root, repo_root, &mut tree, store)?;
71
72 let tree_obj = Object::Tree(tree);
73 store.write(&tree_obj).map_err(Into::into)
74}
75
76fn collect_files_to_tree(
77 repo_root: &std::path::Path,
78 dir: &std::path::Path,
79 tree: &mut crate::core::Tree,
80 store: &ObjectStore,
81) -> Result<(), crate::errors::LitError> {
82 let entries = fs::read_dir(dir).map_err(|e| format!("Failed to read directory: {}", e))?;
83
84 for entry in entries {
85 let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
86 let path = entry.path();
87 let name = entry.file_name().to_string_lossy().to_string();
88
89 if name == ".lit" {
91 continue;
92 }
93
94 if path.is_file() {
95 let content = fs::read(&path).map_err(|e| format!("Failed to read file: {}", e))?;
96 let blob = crate::core::Blob::new(content);
97 let blob_hash = store.write(&Object::Blob(blob))?;
98 let rel_path = path
99 .strip_prefix(repo_root)
100 .unwrap()
101 .to_string_lossy()
102 .replace('\\', "/");
103 tree.add_entry(
104 "100644".to_string(),
105 rel_path,
106 blob_hash,
107 "blob".to_string(),
108 );
109 }
110 }
111 Ok(())
112}
113
114fn build_tree_from_index(
115 repo_root: &std::path::Path,
116 store: &ObjectStore,
117) -> Result<ObjectHash, crate::errors::LitError> {
118 let index = Index::load(repo_root)?;
119 let mut tree = crate::core::Tree::new();
120
121 for entry in index.sorted_entries() {
122 tree.add_entry(
123 entry.mode.clone(),
124 entry.path.clone(),
125 ObjectHash::from_hex(entry.hash.clone()),
126 "blob".to_string(),
127 );
128 }
129
130 let tree_obj = Object::Tree(tree);
131 store.write(&tree_obj).map_err(Into::into)
132}
133
134fn stash_push(
135 repo_root: &std::path::Path,
136 message: Option<String>,
137) -> Result<StashResponse, crate::errors::LitError> {
138 let store = ObjectStore::new(repo_root);
139 let head_commit = read_head(repo_root)?;
140 let branch = crate::core::get_current_branch(repo_root).ok();
141
142 let index_tree_hash = build_tree_from_index(repo_root, &store)?;
144
145 let worktree_tree_hash = build_tree_from_working_dir(repo_root, &store)?;
147
148 let msg = message
149 .unwrap_or_else(|| format!("WIP on {}", branch.as_deref().unwrap_or("detached HEAD")));
150
151 let entry = StashEntry {
152 head_commit: head_commit.clone(),
153 branch: branch.clone(),
154 index_tree: index_tree_hash.to_string(),
155 worktree_tree: worktree_tree_hash.to_string(),
156 message: msg.clone(),
157 timestamp: chrono::Utc::now().timestamp(),
158 };
159
160 let mut stash_list = StashList::load(repo_root)?;
161 stash_list.entries.push(entry);
162 stash_list.save(repo_root)?;
163
164 restore_to_commit(repo_root, &head_commit)?;
166
167 let index = stash_list.entries.len() - 1;
168 Ok(StashResponse::Push {
169 index,
170 message: format!("Saved working directory and index state: {}", msg),
171 })
172}
173
174fn stash_pop(repo_root: &std::path::Path) -> Result<StashResponse, crate::errors::LitError> {
175 let mut stash_list = StashList::load(repo_root)?;
176
177 if stash_list.entries.is_empty() {
178 return Err("No stash entries".into());
179 }
180
181 let entry = stash_list.entries.pop().unwrap();
182 let index = stash_list.entries.len();
183 stash_list.save(repo_root)?;
184
185 apply_stash_entry(repo_root, &entry)?;
187
188 Ok(StashResponse::Pop {
189 index,
190 message: format!("Restored stash@{{{}}}: {}", index, entry.message),
191 })
192}
193
194fn stash_apply(
195 repo_root: &std::path::Path,
196 idx: Option<usize>,
197) -> Result<StashResponse, crate::errors::LitError> {
198 let stash_list = StashList::load(repo_root)?;
199
200 if stash_list.entries.is_empty() {
201 return Err("No stash entries".into());
202 }
203
204 let index = idx.unwrap_or(stash_list.entries.len() - 1);
205
206 if index >= stash_list.entries.len() {
207 return Err(format!("stash@{{{}}} does not exist", index).into());
208 }
209
210 let entry = &stash_list.entries[index];
211 apply_stash_entry(repo_root, entry)?;
212
213 Ok(StashResponse::Apply {
214 index,
215 message: format!("Applied stash@{{{}}}: {}", index, entry.message),
216 })
217}
218
219fn stash_list(repo_root: &std::path::Path) -> Result<StashResponse, crate::errors::LitError> {
220 let stash_list = StashList::load(repo_root)?;
221
222 let entries: Vec<crate::response::StashEntryInfo> = stash_list
223 .entries
224 .iter()
225 .enumerate()
226 .map(|(i, e)| crate::response::StashEntryInfo {
227 index: i,
228 message: e.message.clone(),
229 branch: e.branch.clone(),
230 timestamp: e.timestamp,
231 })
232 .collect();
233
234 Ok(StashResponse::List { entries })
235}
236
237fn stash_drop(
238 repo_root: &std::path::Path,
239 idx: Option<usize>,
240) -> Result<StashResponse, crate::errors::LitError> {
241 let mut stash_list = StashList::load(repo_root)?;
242
243 if stash_list.entries.is_empty() {
244 return Err("No stash entries".into());
245 }
246
247 let index = idx.unwrap_or(stash_list.entries.len() - 1);
248
249 if index >= stash_list.entries.len() {
250 return Err(format!("stash@{{{}}} does not exist", index).into());
251 }
252
253 stash_list.entries.remove(index);
254 stash_list.save(repo_root)?;
255
256 Ok(StashResponse::Drop {
257 index,
258 message: format!("Dropped stash@{{{}}}", index),
259 })
260}
261
262fn apply_stash_entry(
263 repo_root: &std::path::Path,
264 entry: &StashEntry,
265) -> Result<(), crate::errors::LitError> {
266 let store = ObjectStore::new(repo_root);
267 let worktree_hash = ObjectHash::from_hex(entry.worktree_tree.clone());
268
269 let tree = match store.read(&worktree_hash)? {
270 Object::Tree(t) => t,
271 _ => return Err("Invalid stash: not a tree".into()),
272 };
273
274 for te in &tree.entries {
276 if te.object_type == "blob" {
277 let blob = match store.read(&te.hash)? {
278 Object::Blob(b) => b,
279 _ => continue,
280 };
281
282 let full_path = repo_root.join(&te.name);
283 if let Some(parent) = full_path.parent() {
284 fs::create_dir_all(parent)
285 .map_err(|e| format!("Failed to create directory: {}", e))?;
286 }
287
288 fs::write(&full_path, &blob.content)
289 .map_err(|e| format!("Failed to write file: {}", e))?;
290 }
291 }
292
293 let index_hash = ObjectHash::from_hex(entry.index_tree.clone());
295 let index_tree = match store.read(&index_hash)? {
296 Object::Tree(t) => t,
297 _ => return Err("Invalid stash: index not a tree".into()),
298 };
299
300 let mut index = Index::new();
301 for te in &index_tree.entries {
302 index.add(te.name.clone(), te.hash.to_string(), te.mode.clone());
303 }
304 index.save(repo_root)?;
305
306 Ok(())
307}
308
309fn restore_to_commit(
310 repo_root: &std::path::Path,
311 commit_hash: &str,
312) -> Result<(), crate::errors::LitError> {
313 let store = ObjectStore::new(repo_root);
314 let hash = ObjectHash::from_hex(commit_hash.to_string());
315
316 let commit = match store.read(&hash)? {
317 Object::Commit(c) => c,
318 _ => return Err("Not a commit".into()),
319 };
320
321 let tree = match store.read(&commit.tree)? {
322 Object::Tree(t) => t,
323 _ => return Err("Not a tree".into()),
324 };
325
326 for te in &tree.entries {
328 if te.object_type == "blob" {
329 let blob = match store.read(&te.hash)? {
330 Object::Blob(b) => b,
331 _ => continue,
332 };
333 let full_path = repo_root.join(&te.name);
334 if let Some(parent) = full_path.parent() {
335 fs::create_dir_all(parent)
336 .map_err(|e| format!("Failed to create directory: {}", e))?;
337 }
338 fs::write(&full_path, &blob.content)
339 .map_err(|e| format!("Failed to write file: {}", e))?;
340 }
341 }
342
343 let mut index = Index::new();
345 for te in &tree.entries {
346 index.add(te.name.clone(), te.hash.to_string(), te.mode.clone());
347 }
348 index.save(repo_root)?;
349
350 Ok(())
351}