use super::*;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RenameBackupManifest {
pub operation_id: String,
pub timestamp: String,
pub files: HashMap<String, String>,
}
pub fn create_rename_backup(
workspace_root: &Path,
symbol_id: &str,
files_to_backup: &[PathBuf],
) -> Result<PathBuf> {
let timestamp = Utc::now().format("%Y%m%d-%H%M%S").to_string();
let operation_id = format!("rename-{}-{}", symbol_id, timestamp);
let backups_base = workspace_root.join(".splice/backups");
fs::create_dir_all(&backups_base).map_err(|e| SpliceError::Io {
path: backups_base.clone(),
source: e,
})?;
let backup_dir = backups_base.join(&operation_id);
fs::create_dir(&backup_dir).map_err(|e| SpliceError::Io {
path: backup_dir.clone(),
source: e,
})?;
let mut manifest = RenameBackupManifest {
operation_id: operation_id.clone(),
timestamp: Utc::now().to_rfc3339(),
files: HashMap::new(),
};
for file_path in files_to_backup {
let relative_path = file_path.strip_prefix(workspace_root).map_err(|_| {
SpliceError::Other(format!(
"File {} is not under workspace root {}",
file_path.display(),
workspace_root.display()
))
})?;
let backup_file_path = backup_dir.join(relative_path);
if let Some(parent) = backup_file_path.parent() {
fs::create_dir_all(parent).map_err(|e| SpliceError::Io {
path: parent.to_path_buf(),
source: e,
})?;
}
fs::copy(file_path, &backup_file_path).map_err(|e| SpliceError::Io {
path: backup_file_path.clone(),
source: e,
})?;
let checksum = sha256_checksum(file_path)?;
manifest
.files
.insert(relative_path.display().to_string(), checksum);
}
let manifest_path = backup_dir.join("manifest.json");
let manifest_json = serde_json::to_string_pretty(&manifest)
.map_err(|e| SpliceError::Other(format!("Failed to serialize backup manifest: {}", e)))?;
fs::write(&manifest_path, manifest_json).map_err(|e| SpliceError::Io {
path: manifest_path.clone(),
source: e,
})?;
Ok(backup_dir)
}
pub(crate) fn sha256_checksum(file_path: &Path) -> Result<String> {
let mut hasher = Sha256::new();
let content = fs::read(file_path).map_err(|e| SpliceError::Io {
path: file_path.to_path_buf(),
source: e,
})?;
hasher.update(&content);
Ok(format!("{:x}", hasher.finalize()))
}
#[derive(Debug)]
pub struct RenameTransaction {
pub(crate) backup_dir: Option<PathBuf>,
pub(crate) workspace_root: Option<PathBuf>,
modified_files: Vec<PathBuf>,
}
impl RenameTransaction {
pub fn new() -> Self {
Self {
backup_dir: None,
workspace_root: None,
modified_files: Vec::new(),
}
}
pub fn with_backup(mut self, backup_dir: PathBuf, workspace_root: PathBuf) -> Self {
self.backup_dir = Some(backup_dir);
self.workspace_root = Some(workspace_root);
self
}
pub fn track_modified(&mut self, file: PathBuf) {
self.modified_files.push(file);
}
pub fn rollback(self) -> Result<()> {
if let (Some(backup_dir), Some(workspace_root)) = (self.backup_dir, self.workspace_root) {
let manifest_path = backup_dir.join("manifest.json");
if manifest_path.exists() {
let manifest_json =
fs::read_to_string(&manifest_path).map_err(|e| SpliceError::Io {
path: manifest_path.clone(),
source: e,
})?;
let manifest: RenameBackupManifest =
serde_json::from_str(&manifest_json).map_err(|e| {
SpliceError::Other(format!("Failed to parse backup manifest: {}", e))
})?;
for (relative_path, _checksum) in manifest.files {
let backup_file = backup_dir.join(&relative_path);
let original_file = workspace_root.join(&relative_path);
if backup_file.exists() {
fs::copy(&backup_file, &original_file).map_err(|e| SpliceError::Io {
path: original_file.clone(),
source: e,
})?;
}
}
}
}
Ok(())
}
pub fn modified_count(&self) -> usize {
self.modified_files.len()
}
pub fn modified_files(&self) -> &[PathBuf] {
&self.modified_files
}
}
impl Default for RenameTransaction {
fn default() -> Self {
Self::new()
}
}