use crate::core::{
find_repo_root, get_current_branch, read_head, write_ref, Blob, Commit, Object, ObjectHash,
Tree,
};
use crate::response::SnapshotResponse;
use crate::storage::{Index, ObjectStore};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use walkdir::WalkDir;
pub fn execute(
message: String,
author: Option<String>,
metadata: Option<serde_json::Value>,
) -> Result<SnapshotResponse, crate::errors::LitError> {
let repo_root = find_repo_root()?;
let store = ObjectStore::new(&repo_root);
let mut index = Index::load(&repo_root)?;
let files_added = add_all_files(&repo_root, &store, &mut index)?;
index.save(&repo_root)?;
if index.entries.is_empty() {
return Err("Nothing to snapshot (no files in working directory)".into());
}
let author_name = if let Some(a) = author {
a
} else {
std::env::var("USER")
.or_else(|_| std::env::var("USERNAME"))
.unwrap_or_else(|_| "Unknown".to_string())
};
let tree_hash = build_tree_from_index(&index, &store)?;
let parents = match read_head(&repo_root) {
Ok(parent_hash) => vec![ObjectHash::from_hex(parent_hash)],
Err(_) => vec![],
};
let parent_str = parents.first().map(|p| p.to_string());
let mut commit = Commit::new(
tree_hash.clone(),
parents,
author_name.clone(),
message.clone(),
);
commit.metadata = metadata;
let timestamp = commit.timestamp;
let commit_object = Object::Commit(commit);
let commit_hash = store.write(&commit_object)?;
let branch = get_current_branch(&repo_root).unwrap_or_else(|_| "main".to_string());
write_ref(
&repo_root,
&format!("heads/{}", branch),
commit_hash.as_str(),
)?;
Ok(SnapshotResponse {
hash: commit_hash.to_string(),
short_hash: commit_hash.short().to_string(),
tree: tree_hash.to_string(),
parent: parent_str,
author: author_name,
message,
timestamp,
files_added,
})
}
fn add_all_files(
repo_root: &Path,
store: &ObjectStore,
index: &mut Index,
) -> Result<usize, crate::errors::LitError> {
let mut count = 0usize;
for entry in WalkDir::new(repo_root).into_iter().filter_entry(|e| {
let name = e.file_name().to_string_lossy();
!name.starts_with('.') && name != "target" && name != "node_modules"
}) {
let entry = entry.map_err(|e| format!("Failed to read directory: {}", e))?;
if entry.file_type().is_file() {
let path = entry.path();
if path.starts_with(repo_root.join(".lit")) {
continue;
}
let content =
fs::read(path).map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
let blob = Blob::new(content);
let hash = store.write(&Object::Blob(blob))?;
let rel_path = path
.strip_prefix(repo_root)
.map_err(|e| format!("Path error: {}", e))?
.to_string_lossy()
.replace('\\', "/");
index.add(rel_path, hash.to_string(), "100644".to_string());
count += 1;
}
}
Ok(count)
}
fn build_tree_from_index(
index: &Index,
store: &ObjectStore,
) -> Result<ObjectHash, crate::errors::LitError> {
let mut tree_map: HashMap<String, Vec<(String, String, String)>> = HashMap::new();
for entry in index.sorted_entries() {
let parts: Vec<&str> = entry.path.split('/').collect();
if parts.len() == 1 {
tree_map.entry("".to_string()).or_default().push((
parts[0].to_string(),
entry.hash.clone(),
entry.mode.clone(),
));
} else {
let dir = parts[0].to_string();
tree_map.entry(dir).or_default().push((
parts[1..].join("/"),
entry.hash.clone(),
entry.mode.clone(),
));
}
}
let mut root_tree = Tree::new();
if let Some(root_files) = tree_map.get("") {
for (name, hash, mode) in root_files {
root_tree.add_entry(
mode.clone(),
name.clone(),
ObjectHash::from_hex(hash.clone()),
"blob".to_string(),
);
}
}
for dir in tree_map.keys() {
if !dir.is_empty() {
let mut subtree = Tree::new();
if let Some(files) = tree_map.get(dir) {
for (name, hash, mode) in files {
subtree.add_entry(
mode.clone(),
name.clone(),
ObjectHash::from_hex(hash.clone()),
"blob".to_string(),
);
}
}
let subtree_hash = store.write(&Object::Tree(subtree))?;
root_tree.add_entry(
"040000".to_string(),
dir.clone(),
subtree_hash,
"tree".to_string(),
);
}
}
store.write(&Object::Tree(root_tree)).map_err(Into::into)
}