Skip to main content

lit/commands/
stash.rs

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    /// Commit HEAD was pointing to when stash was created
10    pub head_commit: String,
11    /// Branch name (if on a branch)
12    pub branch: Option<String>,
13    /// Hash of the tree object representing index state
14    pub index_tree: String,
15    /// Hash of the tree object representing working tree state
16    pub worktree_tree: String,
17    /// Message describing the stash
18    pub message: String,
19    /// Timestamp
20    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        // Skip .lit directory
90        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    // Save the current index tree
143    let index_tree_hash = build_tree_from_index(repo_root, &store)?;
144
145    // Save the current working tree
146    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 working tree to HEAD state
165    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    // Restore working tree from stash
186    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    // Restore files from the stashed working tree
275    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    // Restore index from stashed index tree
294    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    // Update working directory files
327    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    // Reset index
344    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}