use crate::core::{find_repo_root, read_head, Object, ObjectHash};
use crate::response::StashResponse;
use crate::storage::{Index, ObjectStore};
use serde::{Deserialize, Serialize};
use std::fs;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct StashEntry {
pub head_commit: String,
pub branch: Option<String>,
pub index_tree: String,
pub worktree_tree: String,
pub message: String,
pub timestamp: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct StashList {
entries: Vec<StashEntry>,
}
impl StashList {
fn load(repo_root: &std::path::Path) -> Result<Self, crate::errors::LitError> {
let path = repo_root.join(".lit").join("stash");
if !path.exists() {
return Ok(StashList {
entries: Vec::new(),
});
}
let data = fs::read_to_string(&path).map_err(|e| format!("Failed to read stash: {}", e))?;
serde_json::from_str(&data).map_err(|e| format!("Failed to parse stash: {}", e).into())
}
fn save(&self, repo_root: &std::path::Path) -> Result<(), crate::errors::LitError> {
let path = repo_root.join(".lit").join("stash");
let data = serde_json::to_string_pretty(self)
.map_err(|e| format!("Failed to serialize stash: {}", e))?;
fs::write(&path, data).map_err(|e| format!("Failed to write stash: {}", e).into())
}
}
pub fn execute(
command: Option<crate::StashCommands>,
) -> Result<StashResponse, crate::errors::LitError> {
let repo_root = find_repo_root()?;
match command {
None | Some(crate::StashCommands::Push { message: None }) => stash_push(&repo_root, None),
Some(crate::StashCommands::Push { message }) => stash_push(&repo_root, message),
Some(crate::StashCommands::Pop) => stash_pop(&repo_root),
Some(crate::StashCommands::Apply { index }) => stash_apply(&repo_root, index),
Some(crate::StashCommands::List) => stash_list(&repo_root),
Some(crate::StashCommands::Drop { index }) => stash_drop(&repo_root, index),
}
}
fn build_tree_from_working_dir(
repo_root: &std::path::Path,
store: &ObjectStore,
) -> Result<ObjectHash, crate::errors::LitError> {
use crate::core::Tree;
let mut tree = Tree::new();
collect_files_to_tree(repo_root, repo_root, &mut tree, store)?;
let tree_obj = Object::Tree(tree);
store.write(&tree_obj).map_err(Into::into)
}
fn collect_files_to_tree(
repo_root: &std::path::Path,
dir: &std::path::Path,
tree: &mut crate::core::Tree,
store: &ObjectStore,
) -> Result<(), crate::errors::LitError> {
let entries = fs::read_dir(dir).map_err(|e| format!("Failed to read directory: {}", e))?;
for entry in entries {
let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
let path = entry.path();
let name = entry.file_name().to_string_lossy().to_string();
if name == ".lit" {
continue;
}
if path.is_file() {
let content = fs::read(&path).map_err(|e| format!("Failed to read file: {}", e))?;
let blob = crate::core::Blob::new(content);
let blob_hash = store.write(&Object::Blob(blob))?;
let rel_path = path
.strip_prefix(repo_root)
.unwrap()
.to_string_lossy()
.replace('\\', "/");
tree.add_entry(
"100644".to_string(),
rel_path,
blob_hash,
"blob".to_string(),
);
}
}
Ok(())
}
fn build_tree_from_index(
repo_root: &std::path::Path,
store: &ObjectStore,
) -> Result<ObjectHash, crate::errors::LitError> {
let index = Index::load(repo_root)?;
let mut tree = crate::core::Tree::new();
for entry in index.sorted_entries() {
tree.add_entry(
entry.mode.clone(),
entry.path.clone(),
ObjectHash::from_hex(entry.hash.clone()),
"blob".to_string(),
);
}
let tree_obj = Object::Tree(tree);
store.write(&tree_obj).map_err(Into::into)
}
fn stash_push(
repo_root: &std::path::Path,
message: Option<String>,
) -> Result<StashResponse, crate::errors::LitError> {
let store = ObjectStore::new(repo_root);
let head_commit = read_head(repo_root)?;
let branch = crate::core::get_current_branch(repo_root).ok();
let index_tree_hash = build_tree_from_index(repo_root, &store)?;
let worktree_tree_hash = build_tree_from_working_dir(repo_root, &store)?;
let msg = message
.unwrap_or_else(|| format!("WIP on {}", branch.as_deref().unwrap_or("detached HEAD")));
let entry = StashEntry {
head_commit: head_commit.clone(),
branch: branch.clone(),
index_tree: index_tree_hash.to_string(),
worktree_tree: worktree_tree_hash.to_string(),
message: msg.clone(),
timestamp: chrono::Utc::now().timestamp(),
};
let mut stash_list = StashList::load(repo_root)?;
stash_list.entries.push(entry);
stash_list.save(repo_root)?;
restore_to_commit(repo_root, &head_commit)?;
let index = stash_list.entries.len() - 1;
Ok(StashResponse::Push {
index,
message: format!("Saved working directory and index state: {}", msg),
})
}
fn stash_pop(repo_root: &std::path::Path) -> Result<StashResponse, crate::errors::LitError> {
let mut stash_list = StashList::load(repo_root)?;
if stash_list.entries.is_empty() {
return Err("No stash entries".into());
}
let entry = stash_list.entries.pop().unwrap();
let index = stash_list.entries.len();
stash_list.save(repo_root)?;
apply_stash_entry(repo_root, &entry)?;
Ok(StashResponse::Pop {
index,
message: format!("Restored stash@{{{}}}: {}", index, entry.message),
})
}
fn stash_apply(
repo_root: &std::path::Path,
idx: Option<usize>,
) -> Result<StashResponse, crate::errors::LitError> {
let stash_list = StashList::load(repo_root)?;
if stash_list.entries.is_empty() {
return Err("No stash entries".into());
}
let index = idx.unwrap_or(stash_list.entries.len() - 1);
if index >= stash_list.entries.len() {
return Err(format!("stash@{{{}}} does not exist", index).into());
}
let entry = &stash_list.entries[index];
apply_stash_entry(repo_root, entry)?;
Ok(StashResponse::Apply {
index,
message: format!("Applied stash@{{{}}}: {}", index, entry.message),
})
}
fn stash_list(repo_root: &std::path::Path) -> Result<StashResponse, crate::errors::LitError> {
let stash_list = StashList::load(repo_root)?;
let entries: Vec<crate::response::StashEntryInfo> = stash_list
.entries
.iter()
.enumerate()
.map(|(i, e)| crate::response::StashEntryInfo {
index: i,
message: e.message.clone(),
branch: e.branch.clone(),
timestamp: e.timestamp,
})
.collect();
Ok(StashResponse::List { entries })
}
fn stash_drop(
repo_root: &std::path::Path,
idx: Option<usize>,
) -> Result<StashResponse, crate::errors::LitError> {
let mut stash_list = StashList::load(repo_root)?;
if stash_list.entries.is_empty() {
return Err("No stash entries".into());
}
let index = idx.unwrap_or(stash_list.entries.len() - 1);
if index >= stash_list.entries.len() {
return Err(format!("stash@{{{}}} does not exist", index).into());
}
stash_list.entries.remove(index);
stash_list.save(repo_root)?;
Ok(StashResponse::Drop {
index,
message: format!("Dropped stash@{{{}}}", index),
})
}
fn apply_stash_entry(
repo_root: &std::path::Path,
entry: &StashEntry,
) -> Result<(), crate::errors::LitError> {
let store = ObjectStore::new(repo_root);
let worktree_hash = ObjectHash::from_hex(entry.worktree_tree.clone());
let tree = match store.read(&worktree_hash)? {
Object::Tree(t) => t,
_ => return Err("Invalid stash: not a tree".into()),
};
for te in &tree.entries {
if te.object_type == "blob" {
let blob = match store.read(&te.hash)? {
Object::Blob(b) => b,
_ => continue,
};
let full_path = repo_root.join(&te.name);
if let Some(parent) = full_path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create directory: {}", e))?;
}
fs::write(&full_path, &blob.content)
.map_err(|e| format!("Failed to write file: {}", e))?;
}
}
let index_hash = ObjectHash::from_hex(entry.index_tree.clone());
let index_tree = match store.read(&index_hash)? {
Object::Tree(t) => t,
_ => return Err("Invalid stash: index not a tree".into()),
};
let mut index = Index::new();
for te in &index_tree.entries {
index.add(te.name.clone(), te.hash.to_string(), te.mode.clone());
}
index.save(repo_root)?;
Ok(())
}
fn restore_to_commit(
repo_root: &std::path::Path,
commit_hash: &str,
) -> Result<(), crate::errors::LitError> {
let store = ObjectStore::new(repo_root);
let hash = ObjectHash::from_hex(commit_hash.to_string());
let commit = match store.read(&hash)? {
Object::Commit(c) => c,
_ => return Err("Not a commit".into()),
};
let tree = match store.read(&commit.tree)? {
Object::Tree(t) => t,
_ => return Err("Not a tree".into()),
};
for te in &tree.entries {
if te.object_type == "blob" {
let blob = match store.read(&te.hash)? {
Object::Blob(b) => b,
_ => continue,
};
let full_path = repo_root.join(&te.name);
if let Some(parent) = full_path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create directory: {}", e))?;
}
fs::write(&full_path, &blob.content)
.map_err(|e| format!("Failed to write file: {}", e))?;
}
}
let mut index = Index::new();
for te in &tree.entries {
index.add(te.name.clone(), te.hash.to_string(), te.mode.clone());
}
index.save(repo_root)?;
Ok(())
}