use std::path::{Path, PathBuf};
use std::fs;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::error::{Result, ToriiError};
use crate::core::GitRepo;
#[derive(Debug, Serialize, Deserialize)]
pub struct SnapshotMetadata {
pub id: String,
pub timestamp: DateTime<Utc>,
pub name: Option<String>,
pub branch: String,
pub commit_hash: Option<String>,
}
pub struct SnapshotManager {
repo_path: PathBuf,
snapshots_dir: PathBuf,
}
impl SnapshotManager {
pub fn new<P: AsRef<Path>>(repo_path: P) -> Result<Self> {
let repo_path = repo_path.as_ref().to_path_buf();
let snapshots_dir = repo_path.join(".torii").join("snapshots");
fs::create_dir_all(&snapshots_dir)?;
Ok(Self {
repo_path,
snapshots_dir,
})
}
pub fn create_snapshot(&self, name: Option<&str>) -> Result<String> {
let repo = GitRepo::open(&self.repo_path)?;
let timestamp = Utc::now();
let id = timestamp.format("%Y%m%d_%H%M%S").to_string();
let snapshot_dir = self.snapshots_dir.join(&id);
fs::create_dir_all(&snapshot_dir)?;
let branch = repo.get_current_branch()?;
let metadata = SnapshotMetadata {
id: id.clone(),
timestamp,
name: name.map(String::from),
branch,
commit_hash: None,
};
let metadata_path = snapshot_dir.join("metadata.json");
let metadata_json = serde_json::to_string_pretty(&metadata)?;
fs::write(metadata_path, metadata_json)?;
self.create_bundle(&snapshot_dir, &repo)?;
Ok(id)
}
fn create_bundle(&self, snapshot_dir: &Path, repo: &GitRepo) -> Result<()> {
let mut revwalk = repo.repository().revwalk()?;
revwalk.push_head()?;
let git_dir = self.repo_path.join(".git");
let snapshot_git = snapshot_dir.join("git_backup");
self.copy_dir_recursive(&git_dir, &snapshot_git)?;
Ok(())
}
fn copy_dir_recursive(&self, src: &Path, dst: &Path) -> Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let file_type = entry.file_type()?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if file_type.is_dir() {
self.copy_dir_recursive(&src_path, &dst_path)?;
} else {
fs::copy(&src_path, &dst_path)?;
}
}
Ok(())
}
pub fn list_snapshots(&self) -> Result<()> {
let entries = fs::read_dir(&self.snapshots_dir)?;
println!("📸 Snapshots:");
println!();
for entry in entries {
let entry = entry?;
if entry.file_type()?.is_dir() {
let metadata_path = entry.path().join("metadata.json");
if metadata_path.exists() {
let metadata_json = fs::read_to_string(metadata_path)?;
let metadata: SnapshotMetadata = serde_json::from_str(&metadata_json)?;
let name_str = metadata.name
.as_ref()
.map(|n| format!(" ({})", n))
.unwrap_or_default();
println!(" {} - {}{}",
metadata.id,
metadata.timestamp.format("%Y-%m-%d %H:%M:%S"),
name_str
);
println!(" Branch: {}", metadata.branch);
}
}
}
Ok(())
}
pub fn restore_snapshot(&self, id: &str) -> Result<()> {
let snapshot_dir = self.snapshots_dir.join(id);
if !snapshot_dir.exists() {
return Err(ToriiError::Snapshot(format!("Snapshot not found: {}", id)));
}
let snapshot_git = snapshot_dir.join("git_backup");
let git_dir = self.repo_path.join(".git");
fs::remove_dir_all(&git_dir)?;
self.copy_dir_recursive(&snapshot_git, &git_dir)?;
std::process::Command::new("git")
.args(&["reset", "--hard", "HEAD"])
.current_dir(&self.repo_path)
.output()?;
Ok(())
}
pub fn delete_snapshot(&self, id: &str) -> Result<()> {
let snapshot_dir = self.snapshots_dir.join(id);
if !snapshot_dir.exists() {
return Err(ToriiError::Snapshot(format!("Snapshot not found: {}", id)));
}
fs::remove_dir_all(snapshot_dir)?;
Ok(())
}
pub fn configure_auto_snapshot(&self, enable: bool, interval: Option<u32>) -> Result<()> {
let config_path = self.repo_path.join(".torii").join("config.json");
#[derive(Serialize, Deserialize)]
struct Config {
auto_snapshot_enabled: bool,
auto_snapshot_interval_minutes: u32,
}
let config = Config {
auto_snapshot_enabled: enable,
auto_snapshot_interval_minutes: interval.unwrap_or(30),
};
let config_json = serde_json::to_string_pretty(&config)?;
fs::write(config_path, config_json)?;
Ok(())
}
pub fn stash(&self, name: Option<&str>, include_untracked: bool) -> Result<()> {
let stash_name = name.unwrap_or("WIP");
if include_untracked {
let repo_path = std::path::Path::new(".");
let output = std::process::Command::new("git")
.args(["add", "--intent-to-add", "."])
.current_dir(repo_path)
.output()?;
if !output.status.success() {
let err = String::from_utf8_lossy(&output.stderr);
eprintln!("⚠️ Could not stage untracked files: {}", err);
}
}
let snapshot_id = self.create_snapshot(Some(&format!("stash-{}", stash_name)))?;
std::process::Command::new("git")
.args(["reset", "--hard", "HEAD"])
.current_dir(".")
.output()?;
if include_untracked {
std::process::Command::new("git")
.args(["clean", "-fd"])
.current_dir(".")
.output()?;
}
println!("📦 Stashed changes");
println!(" ID: {}", snapshot_id);
println!(" Name: {}", stash_name);
if include_untracked {
println!(" Untracked files included");
}
println!();
println!("💡 To restore: torii snapshot unstash");
Ok(())
}
pub fn unstash(&self, id: Option<&str>, keep: bool) -> Result<()> {
let snapshot_id = if let Some(id) = id {
id.to_string()
} else {
let mut snapshots: Vec<_> = fs::read_dir(&self.snapshots_dir)?
.filter_map(|e| e.ok())
.filter(|e| {
e.file_name().to_string_lossy().contains("stash-")
})
.collect();
snapshots.sort_by_key(|e| e.metadata().ok().and_then(|m| m.modified().ok()));
let latest = snapshots.last()
.ok_or_else(|| ToriiError::Snapshot("No stash found".to_string()))?;
latest.file_name().to_string_lossy().to_string()
};
println!("🔄 Restoring stash: {}", snapshot_id);
self.restore_snapshot(&snapshot_id)?;
if !keep {
self.delete_snapshot(&snapshot_id)?;
println!(" Stash removed");
}
println!("✅ Stash restored");
Ok(())
}
pub fn undo(&self) -> Result<()> {
let mut snapshots: Vec<_> = fs::read_dir(&self.snapshots_dir)?
.filter_map(|e| e.ok())
.filter(|e| {
let name = e.file_name().to_string_lossy().to_string();
name.starts_with("before-") || name.contains("auto-")
})
.collect();
snapshots.sort_by_key(|e| e.metadata().ok().and_then(|m| m.modified().ok()));
let latest = snapshots.last()
.ok_or_else(|| ToriiError::Snapshot("No operation to undo".to_string()))?;
let snapshot_id = latest.file_name().to_string_lossy().to_string();
println!("🔄 Undoing last operation...");
println!(" Restoring snapshot: {}", snapshot_id);
self.restore_snapshot(&snapshot_id)?;
println!("✅ Operation undone");
Ok(())
}
}