>(config_path: Option) -> Result<()
log::info!("Loading init config from: {}", path.as_ref().display());
InitConfig::load(path.as_ref())?
} else {
- InitConfig::load_default()?
- .ok_or_else(|| anyhow::anyhow!("No init config file found"))?
+ InitConfig::load_default()?.ok_or_else(|| anyhow::anyhow!("No init config file found"))?
};
- println!(
- "{}",
- "📄 Initializing from config file...".cyan().bold()
- );
+ println!("{}", "📄 Initializing from config file...".cyan().bold());
// Convert to onboarding config
let onboarding_config = init_config.to_onboarding_config()?;
@@ -122,7 +118,11 @@ pub fn run_init_from_config>(config_path: Option) -> Result<()
.context("Failed to save filter configuration")?;
println!("{}", "✓ Initialization complete!".green().bold());
- println!(" {} {}", "Repo:".cyan(), onboarding_config.repo_path.display());
+ println!(
+ " {} {}",
+ "Repo:".cyan(),
+ onboarding_config.repo_path.display()
+ );
if let Some(ref url) = onboarding_config.remote_url {
println!(" {} {}", "Remote:".cyan(), url);
}
diff --git a/src/logger.rs b/src/logger.rs
index b6898b16..b1e1b14b 100644
--- a/src/logger.rs
+++ b/src/logger.rs
@@ -61,9 +61,7 @@ pub fn init_logger() -> Result<()> {
.ok(); // Ignore error if logger is already initialized
// Also log initialization to file
- log_to_file(&format!(
- "Logger initialized with level: {default_level:?}"
- ))?;
+ log_to_file(&format!("Logger initialized with level: {default_level:?}"))?;
Ok(())
}
diff --git a/src/main.rs b/src/main.rs
index d4329cdc..87c9e050 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -403,7 +403,12 @@ fn main() -> Result<()> {
}
match command {
- Commands::Init { local, remote, clone, config } => {
+ Commands::Init {
+ local,
+ remote,
+ clone,
+ config,
+ } => {
// If config file is provided, use non-interactive init
if config.is_some() {
run_init_from_config(config)?;
@@ -434,10 +439,7 @@ fn main() -> Result<()> {
filter::FilterConfig::default().save()?;
}
- println!(
- "{}",
- "Clone and initialization complete!".green().bold()
- );
+ println!("{}", "Clone and initialization complete!".green().bold());
} else if let Some(local_path) = local {
// Use CLI args for init (local path)
sync::init_sync_repo(&local_path, remote.as_deref())?;
@@ -447,7 +449,12 @@ fn main() -> Result<()> {
println!(
"{}",
- format!("Cloning from {} to {}...", remote_url, default_path.display()).cyan()
+ format!(
+ "Cloning from {} to {}...",
+ remote_url,
+ default_path.display()
+ )
+ .cyan()
);
scm::clone(&remote_url, &default_path)?;
@@ -459,10 +466,7 @@ fn main() -> Result<()> {
filter::FilterConfig::default().save()?;
}
- println!(
- "{}",
- "Clone and initialization complete!".green().bold()
- );
+ println!("{}", "Clone and initialization complete!".green().bold());
} else {
// No args provided, try config file first, then fall back to interactive onboarding
if !try_init_from_config()? {
@@ -622,7 +626,11 @@ fn main() -> Result<()> {
sync::remove_remote(&name)?;
}
},
- Commands::Undo { operation, verbose, quiet } => {
+ Commands::Undo {
+ operation,
+ verbose,
+ quiet,
+ } => {
// Determine verbosity level
let verbosity = if verbose {
VerbosityLevel::Verbose
@@ -640,7 +648,7 @@ fn main() -> Result<()> {
handle_undo_push(preview, verbosity)?;
}
}
- },
+ }
Commands::History { action } => match action {
HistoryAction::List { limit } => {
handle_history_list(limit)?;
diff --git a/src/merge.rs b/src/merge.rs
index d9ae21f8..aa7eb01f 100644
--- a/src/merge.rs
+++ b/src/merge.rs
@@ -275,8 +275,7 @@ impl<'a> SmartMerger<'a> {
) -> MessageNode {
// Phase 1: BFS to collect all UUIDs reachable from this root
let mut processing_order: Vec = Vec::new();
- let mut queue: std::collections::VecDeque =
- std::collections::VecDeque::new();
+ let mut queue: std::collections::VecDeque = std::collections::VecDeque::new();
queue.push_back(root_uuid.to_string());
while let Some(uuid) = queue.pop_front() {
diff --git a/src/onboarding.rs b/src/onboarding.rs
index 8c3244b3..badd96a6 100644
--- a/src/onboarding.rs
+++ b/src/onboarding.rs
@@ -128,7 +128,10 @@ impl InitConfig {
if let Ok(path) = std::env::var("CLAUDE_CODE_SYNC_INIT_CONFIG") {
let path = PathBuf::from(&path);
if path.exists() {
- log::info!("Loading init config from CLAUDE_CODE_SYNC_INIT_CONFIG: {}", path.display());
+ log::info!(
+ "Loading init config from CLAUDE_CODE_SYNC_INIT_CONFIG: {}",
+ path.display()
+ );
return Ok(Some(Self::load(&path)?));
}
}
@@ -553,7 +556,10 @@ mod tests {
"#;
let config: InitConfig = toml::from_str(toml).unwrap();
assert_eq!(config.repo_path, "~/claude-sync");
- assert_eq!(config.remote_url, Some("https://github.com/user/repo.git".to_string()));
+ assert_eq!(
+ config.remote_url,
+ Some("https://github.com/user/repo.git".to_string())
+ );
assert!(config.clone);
assert!(config.exclude_attachments);
assert_eq!(config.exclude_older_than_days, Some(30));
@@ -629,7 +635,10 @@ mod tests {
};
let onboarding = config.to_onboarding_config().unwrap();
assert_eq!(onboarding.repo_path, PathBuf::from("/tmp/test"));
- assert_eq!(onboarding.remote_url, Some("https://github.com/user/repo.git".to_string()));
+ assert_eq!(
+ onboarding.remote_url,
+ Some("https://github.com/user/repo.git".to_string())
+ );
assert!(onboarding.is_cloned);
assert!(onboarding.exclude_attachments);
assert_eq!(onboarding.exclude_older_than_days, Some(30));
diff --git a/src/scm/git.rs b/src/scm/git.rs
index be90d8c9..e8510215 100644
--- a/src/scm/git.rs
+++ b/src/scm/git.rs
@@ -60,8 +60,9 @@ impl GitScm {
/// Clone a remote repository.
pub fn clone(url: &str, path: &Path) -> Result {
if let Some(parent) = path.parent() {
- std::fs::create_dir_all(parent)
- .with_context(|| format!("Failed to create parent directory for '{}'", path.display()))?;
+ std::fs::create_dir_all(parent).with_context(|| {
+ format!("Failed to create parent directory for '{}'", path.display())
+ })?;
}
let output = Command::new("git")
@@ -184,7 +185,8 @@ impl Scm for GitScm {
4. Remote branch protection rules\n\n\
For HTTPS: Run 'git config --global credential.helper store' and try again\n\
For SSH: Ensure SSH keys are set up with 'ssh -T git@github.com'",
- remote, stderr
+ remote,
+ stderr
));
}
@@ -202,7 +204,8 @@ impl Scm for GitScm {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!(
"Failed to pull from remote '{}': {}",
- remote, stderr
+ remote,
+ stderr
));
}
@@ -275,7 +278,8 @@ mod tests {
assert!(!scm.has_remote("origin"));
- scm.add_remote("origin", "https://github.com/test/repo.git").unwrap();
+ scm.add_remote("origin", "https://github.com/test/repo.git")
+ .unwrap();
assert!(scm.has_remote("origin"));
assert!(!scm.has_remote("upstream"));
}
diff --git a/src/scm/hg.rs b/src/scm/hg.rs
index b2100552..f38d0d92 100644
--- a/src/scm/hg.rs
+++ b/src/scm/hg.rs
@@ -86,7 +86,6 @@ impl HgScm {
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
-
/// Get path to .hg/hgrc config file.
fn hgrc_path(&self) -> PathBuf {
self.path.join(".hg").join("hgrc")
@@ -348,13 +347,15 @@ mod tests {
assert!(!scm.has_remote("origin"));
- scm.add_remote("origin", "https://example.com/repo").unwrap();
+ scm.add_remote("origin", "https://example.com/repo")
+ .unwrap();
assert!(scm.has_remote("origin"));
let url = scm.get_remote_url("origin").unwrap();
assert_eq!(url, "https://example.com/repo");
- scm.set_remote_url("origin", "https://example.com/new").unwrap();
+ scm.set_remote_url("origin", "https://example.com/new")
+ .unwrap();
let new_url = scm.get_remote_url("origin").unwrap();
assert_eq!(new_url, "https://example.com/new");
@@ -374,8 +375,10 @@ mod tests {
assert!(scm.list_remotes().unwrap().is_empty());
- scm.add_remote("origin", "https://example.com/origin").unwrap();
- scm.add_remote("upstream", "https://example.com/upstream").unwrap();
+ scm.add_remote("origin", "https://example.com/origin")
+ .unwrap();
+ scm.add_remote("upstream", "https://example.com/upstream")
+ .unwrap();
let remotes = scm.list_remotes().unwrap();
assert_eq!(remotes.len(), 2);
diff --git a/src/sync/discovery.rs b/src/sync/discovery.rs
index eb435cf7..8a8e00f4 100644
--- a/src/sync/discovery.rs
+++ b/src/sync/discovery.rs
@@ -12,6 +12,14 @@ pub(crate) const LARGE_FILE_WARNING_THRESHOLD: u64 = 10 * 1024 * 1024;
/// Get the Claude Code home directory (`~/.claude`)
pub(crate) fn claude_home_dir() -> Result {
+ // Override for tests/automation, mirroring CLAUDE_CODE_SYNC_CONFIG_DIR:
+ // faking HOME cannot redirect dirs::home_dir() on Windows, where the
+ // profile comes from the known-folder API rather than the environment.
+ if let Ok(override_dir) = std::env::var("CLAUDE_CODE_SYNC_CLAUDE_DIR") {
+ if !override_dir.is_empty() {
+ return Ok(PathBuf::from(override_dir));
+ }
+ }
let home = dirs::home_dir().context("Failed to get home directory")?;
Ok(home.join(".claude"))
}
@@ -118,7 +126,10 @@ pub fn extract_project_name(encoded_path: &str) -> &str {
/// # Returns
/// - `Some(PathBuf)` if exactly one matching project directory is found
/// - `None` if no match found or multiple matches (ambiguous)
-pub fn find_local_project_by_name(claude_projects_dir: &Path, project_name: &str) -> Option {
+pub fn find_local_project_by_name(
+ claude_projects_dir: &Path,
+ project_name: &str,
+) -> Option {
let entries = std::fs::read_dir(claude_projects_dir).ok()?;
let matches: Vec = entries
diff --git a/src/sync/mod.rs b/src/sync/mod.rs
index d1014eb4..268935a1 100644
--- a/src/sync/mod.rs
+++ b/src/sync/mod.rs
@@ -46,7 +46,14 @@ pub fn sync_bidirectional(
}
// Then, push local changes
- push_history(commit_message, true, branch, exclude_attachments, interactive, verbosity)?;
+ push_history(
+ commit_message,
+ true,
+ branch,
+ exclude_attachments,
+ interactive,
+ verbosity,
+ )?;
if verbosity == VerbosityLevel::Quiet {
println!("Sync complete");
diff --git a/src/sync/pull.rs b/src/sync/pull.rs
index 4375dc18..ac4bd48a 100644
--- a/src/sync/pull.rs
+++ b/src/sync/pull.rs
@@ -91,11 +91,8 @@ pub fn pull_history(
// ============================================================================
// ARTIFACT PULL PLAN (read-only, so the snapshot below can cover it)
// ============================================================================
- let artifact_plan = crate::artifacts::engine::plan_pull(
- &claude_home_dir()?,
- &state.sync_repo_path,
- &filter,
- )?;
+ let artifact_plan =
+ crate::artifacts::engine::plan_pull(&claude_home_dir()?, &state.sync_repo_path, &filter)?;
// ============================================================================
// SNAPSHOT CREATION: Only backup files that will actually change
@@ -162,7 +159,11 @@ pub fn pull_history(
println!();
println!("{}", "Pull Summary:".bold().cyan());
println!(" {} Local sessions: {}", "•".cyan(), local_sessions.len());
- println!(" {} Remote sessions: {}", "•".cyan(), remote_sessions.len());
+ println!(
+ " {} Remote sessions: {}",
+ "•".cyan(),
+ remote_sessions.len()
+ );
println!();
}
@@ -174,7 +175,12 @@ pub fn pull_history(
.strip_prefix(&remote_projects_dir)
.unwrap_or(Path::new(&session.file_path));
- println!(" {}. {} ({} messages)", idx + 1, relative_path.display(), session.message_count());
+ println!(
+ " {}. {} ({} messages)",
+ idx + 1,
+ relative_path.display(),
+ session.message_count()
+ );
}
if remote_sessions.len() > 20 {
println!(" ... and {} more", remote_sessions.len() - 20);
@@ -184,11 +190,14 @@ pub fn pull_history(
// Interactive confirmation
if interactive && interactive_conflict::is_interactive() {
- let confirm = Confirm::new("Do you want to proceed with pulling and merging these changes?")
- .with_default(true)
- .with_help_message("This will merge remote sessions into your local Claude Code history")
- .prompt()
- .context("Failed to get confirmation")?;
+ let confirm =
+ Confirm::new("Do you want to proceed with pulling and merging these changes?")
+ .with_default(true)
+ .with_help_message(
+ "This will merge remote sessions into your local Claude Code history",
+ )
+ .prompt()
+ .context("Failed to get confirmation")?;
if !confirm {
println!("\n{}", "Pull cancelled.".yellow());
@@ -465,7 +474,10 @@ pub fn pull_history(
.unwrap_or_else(|_| remote_relative.to_path_buf());
(dest, tracking_path)
} else {
- log::warn!("Could not extract filename from remote path: {:?}", remote_relative);
+ log::warn!(
+ "Could not extract filename from remote path: {:?}",
+ remote_relative
+ );
skipped_no_local_match += 1;
continue; // Skip this session
}
@@ -534,10 +546,7 @@ pub fn pull_history(
artifact_report.total_modified()
);
if artifact_report.total_modified() > 0 {
- println!(
- " {}",
- "Undo with: claude-code-sync undo pull".dimmed()
- );
+ println!(" {}", "Undo with: claude-code-sync undo pull".dimmed());
}
}
diff --git a/src/sync/push.rs b/src/sync/push.rs
index 5f8ad051..cac6167b 100644
--- a/src/sync/push.rs
+++ b/src/sync/push.rs
@@ -186,12 +186,15 @@ pub fn push_history(
println!();
println!(
"{}",
- "Warning: Multiple projects map to the same name:".yellow().bold()
+ "Warning: Multiple projects map to the same name:"
+ .yellow()
+ .bold()
);
for (name, paths) in &collisions {
println!(" {} -> {} locations:", name.cyan(), paths.len());
for path in paths.iter().take(3) {
- let display_path = path.file_name()
+ let display_path = path
+ .file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
println!(" - {}", display_path);
@@ -243,11 +246,7 @@ pub fn push_history(
entry.operation,
) {
Ok(summary) => pushed_conversations.push(summary),
- Err(e) => log::warn!(
- "Failed to create summary for {}: {}",
- relative_path_str,
- e
- ),
+ Err(e) => log::warn!("Failed to create summary for {}: {}", relative_path_str, e),
}
}
@@ -342,11 +341,7 @@ pub fn push_history(
if let Some(ref hash) = commit_before_push {
if verbosity != VerbosityLevel::Quiet {
- println!(
- " {} Recorded commit {} for undo",
- "✓".green(),
- &hash[..8]
- );
+ println!(" {} Recorded commit {} for undo", "✓".green(), &hash[..8]);
}
} else if verbosity != VerbosityLevel::Quiet {
println!(
diff --git a/src/sync/remote.rs b/src/sync/remote.rs
index 8bda7100..f9cfc138 100644
--- a/src/sync/remote.rs
+++ b/src/sync/remote.rs
@@ -63,7 +63,11 @@ pub fn set_remote(name: &str, url: &str) -> Result<()> {
let repo = scm::open(&state.sync_repo_path)?;
// Validate URL format
- if !url.starts_with("http://") && !url.starts_with("https://") && !url.starts_with("git@") && !url.starts_with("ssh://") {
+ if !url.starts_with("http://")
+ && !url.starts_with("https://")
+ && !url.starts_with("git@")
+ && !url.starts_with("ssh://")
+ {
return Err(anyhow!(
"Invalid URL format: {url}\n\
\n\
diff --git a/src/undo/cleanup.rs b/src/undo/cleanup.rs
index 1ab0fd59..ce04c5e8 100644
--- a/src/undo/cleanup.rs
+++ b/src/undo/cleanup.rs
@@ -3,8 +3,8 @@ use log::warn;
use std::fs;
use std::path::Path;
-use crate::history::OperationType;
use super::snapshot::Snapshot;
+use crate::history::OperationType;
/// Configuration for snapshot cleanup
pub struct SnapshotCleanupConfig {
diff --git a/src/undo/mod.rs b/src/undo/mod.rs
index 7f9a2d86..8cdf5f94 100644
--- a/src/undo/mod.rs
+++ b/src/undo/mod.rs
@@ -4,28 +4,30 @@
//! Snapshots enable undoing pull operations (by restoring files) and push operations
//! (by resetting Git commits). Includes validation and security checks for safe restoration.
-mod snapshot;
-mod restore;
-mod preview;
-mod operations;
mod cleanup;
+mod operations;
+mod preview;
+mod restore;
+mod snapshot;
// Re-export public types and functions to maintain API compatibility
-pub use snapshot::Snapshot;
-pub use preview::{VerbosityLevel, preview_undo_pull, preview_undo_push};
+pub use cleanup::{cleanup_old_snapshots, SnapshotCleanupConfig};
pub use operations::{undo_pull, undo_push};
-pub use cleanup::{SnapshotCleanupConfig, cleanup_old_snapshots};
+pub use preview::{preview_undo_pull, preview_undo_push, VerbosityLevel};
+pub use snapshot::Snapshot;
// These are part of the public API but currently only used in tests
#[allow(unused_imports)]
-pub use preview::UndoPreview;
-#[allow(unused_imports)]
pub use cleanup::cleanup_old_snapshots_with_dir;
+#[allow(unused_imports)]
+pub use preview::UndoPreview;
#[cfg(test)]
mod tests {
use super::*;
- use crate::history::{ConversationSummary, OperationRecord, OperationType, SyncOperation, OperationHistory};
+ use crate::history::{
+ ConversationSummary, OperationHistory, OperationRecord, OperationType, SyncOperation,
+ };
use crate::scm::{self, Scm};
use std::collections::HashMap;
use std::fs;
@@ -707,7 +709,6 @@ mod tests {
);
}
-
#[test]
fn test_undo_pull_transaction_safety() {
// This test verifies that history is updated FIRST, then files are restored.
@@ -897,9 +898,19 @@ mod tests {
snapshot.save_to_disk(Some(&snapshots_dir)).unwrap();
// Verify it's a full snapshot
- assert!(snapshot.base_snapshot_id.is_none(), "First snapshot should not have a base");
- assert_eq!(snapshot.files.len(), 2, "First snapshot should contain all files");
- assert!(snapshot.deleted_files.is_empty(), "First snapshot should have no deleted files");
+ assert!(
+ snapshot.base_snapshot_id.is_none(),
+ "First snapshot should not have a base"
+ );
+ assert_eq!(
+ snapshot.files.len(),
+ 2,
+ "First snapshot should contain all files"
+ );
+ assert!(
+ snapshot.deleted_files.is_empty(),
+ "First snapshot should have no deleted files"
+ );
}
#[test]
@@ -939,7 +950,10 @@ mod tests {
snapshot2.save_to_disk(Some(&snapshots_dir)).unwrap();
// Verify it's a differential snapshot
- assert!(snapshot2.base_snapshot_id.is_some(), "Second snapshot should have a base");
+ assert!(
+ snapshot2.base_snapshot_id.is_some(),
+ "Second snapshot should have a base"
+ );
assert_eq!(
snapshot2.base_snapshot_id.as_ref().unwrap(),
&snapshot1.snapshot_id,
@@ -947,10 +961,20 @@ mod tests {
);
// Should only contain changed file (file1) and new file (file3), not file2
- assert_eq!(snapshot2.files.len(), 2, "Should only contain changed and new files");
- assert!(snapshot2.files.contains_key(&file1.to_string_lossy().to_string()));
- assert!(snapshot2.files.contains_key(&file3.to_string_lossy().to_string()));
- assert!(!snapshot2.files.contains_key(&file2.to_string_lossy().to_string()));
+ assert_eq!(
+ snapshot2.files.len(),
+ 2,
+ "Should only contain changed and new files"
+ );
+ assert!(snapshot2
+ .files
+ .contains_key(&file1.to_string_lossy().to_string()));
+ assert!(snapshot2
+ .files
+ .contains_key(&file3.to_string_lossy().to_string()));
+ assert!(!snapshot2
+ .files
+ .contains_key(&file2.to_string_lossy().to_string()));
}
#[test]
@@ -988,9 +1012,15 @@ mod tests {
snapshot2.save_to_disk(Some(&snapshots_dir)).unwrap();
// Verify deletion tracking
- assert_eq!(snapshot2.deleted_files.len(), 1, "Should track one deleted file");
+ assert_eq!(
+ snapshot2.deleted_files.len(),
+ 1,
+ "Should track one deleted file"
+ );
assert!(
- snapshot2.deleted_files.contains(&file2.to_string_lossy().to_string()),
+ snapshot2
+ .deleted_files
+ .contains(&file2.to_string_lossy().to_string()),
"Should track file2 as deleted"
);
}
@@ -1030,13 +1060,34 @@ mod tests {
snapshot2.save_to_disk(Some(&snapshots_dir)).unwrap();
// Reconstruct full state from differential snapshot
- let full_state = snapshot2.reconstruct_full_state_with_dir(Some(&snapshots_dir)).unwrap();
+ let full_state = snapshot2
+ .reconstruct_full_state_with_dir(Some(&snapshots_dir))
+ .unwrap();
// Should contain all three files with correct content
- assert_eq!(full_state.len(), 3, "Should have 3 files after reconstruction");
- assert_eq!(full_state.get(&file1.to_string_lossy().to_string()).unwrap(), b"v2");
- assert_eq!(full_state.get(&file2.to_string_lossy().to_string()).unwrap(), b"v1"); // Unchanged from base
- assert_eq!(full_state.get(&file3.to_string_lossy().to_string()).unwrap(), b"v2"); // New file
+ assert_eq!(
+ full_state.len(),
+ 3,
+ "Should have 3 files after reconstruction"
+ );
+ assert_eq!(
+ full_state
+ .get(&file1.to_string_lossy().to_string())
+ .unwrap(),
+ b"v2"
+ );
+ assert_eq!(
+ full_state
+ .get(&file2.to_string_lossy().to_string())
+ .unwrap(),
+ b"v1"
+ ); // Unchanged from base
+ assert_eq!(
+ full_state
+ .get(&file3.to_string_lossy().to_string())
+ .unwrap(),
+ b"v2"
+ ); // New file
}
#[test]
@@ -1075,7 +1126,9 @@ mod tests {
fs::write(&file2, b"should_be_deleted").unwrap();
// Restore snapshot2 which should delete file2
- snapshot2.restore_with_base_and_snapshots(Some(temp_dir.path()), Some(&snapshots_dir)).unwrap();
+ snapshot2
+ .restore_with_base_and_snapshots(Some(temp_dir.path()), Some(&snapshots_dir))
+ .unwrap();
// Verify file1 exists and file2 was deleted
assert!(file1.exists(), "file1 should exist after restore");
@@ -1107,7 +1160,10 @@ mod tests {
let result = snapshot.reconstruct_full_state();
assert!(result.is_err(), "Should fail when base snapshot is missing");
assert!(
- result.unwrap_err().to_string().contains("Base snapshot not found"),
+ result
+ .unwrap_err()
+ .to_string()
+ .contains("Base snapshot not found"),
"Error should mention missing base snapshot"
);
}
@@ -1136,19 +1192,28 @@ mod tests {
}
// Verify chain structure
- assert!(snapshots[0].base_snapshot_id.is_none(), "First should have no base");
+ assert!(
+ snapshots[0].base_snapshot_id.is_none(),
+ "First should have no base"
+ );
for i in 1..5 {
assert_eq!(
snapshots[i].base_snapshot_id.as_ref().unwrap(),
&snapshots[i - 1].snapshot_id,
- "Snapshot {} should reference snapshot {}", i, i - 1
+ "Snapshot {} should reference snapshot {}",
+ i,
+ i - 1
);
}
// Reconstruct from the last snapshot
- let full_state = snapshots[4].reconstruct_full_state_with_dir(Some(&snapshots_dir)).unwrap();
+ let full_state = snapshots[4]
+ .reconstruct_full_state_with_dir(Some(&snapshots_dir))
+ .unwrap();
assert_eq!(
- full_state.get(&file1.to_string_lossy().to_string()).unwrap(),
+ full_state
+ .get(&file1.to_string_lossy().to_string())
+ .unwrap(),
b"version_5",
"Should reconstruct to the latest version"
);
@@ -1185,7 +1250,8 @@ mod tests {
max_age_days: 0, // Only count matters, not age
};
- let deleted = cleanup_old_snapshots_with_dir(Some(config), false, Some(&snapshots_dir)).unwrap();
+ let deleted =
+ cleanup_old_snapshots_with_dir(Some(config), false, Some(&snapshots_dir)).unwrap();
assert_eq!(deleted, 5, "Should delete 5 old snapshots");
// Count remaining snapshots
@@ -1224,7 +1290,8 @@ mod tests {
max_age_days: 50,
};
- let deleted = cleanup_old_snapshots_with_dir(Some(config), false, Some(&snapshots_dir)).unwrap();
+ let deleted =
+ cleanup_old_snapshots_with_dir(Some(config), false, Some(&snapshots_dir)).unwrap();
// Snapshots are at days: 5, 15, 25, 35, 45, 55, 65, 75, 85, 95
// Age threshold is now - 50 days
@@ -1275,14 +1342,18 @@ mod tests {
max_age_days: 0, // Don't keep by age
};
- let deleted = cleanup_old_snapshots_with_dir(Some(config), false, Some(&snapshots_dir)).unwrap();
+ let deleted =
+ cleanup_old_snapshots_with_dir(Some(config), false, Some(&snapshots_dir)).unwrap();
// Should delete 7 pull + 7 push = 14 total
assert_eq!(deleted, 14, "Should delete 14 old snapshots");
// Should keep 3 pull + 3 push = 6 total
let remaining = fs::read_dir(&snapshots_dir).unwrap().count();
- assert_eq!(remaining, 6, "Should have 6 snapshots remaining (3 per type)");
+ assert_eq!(
+ remaining, 6,
+ "Should have 6 snapshots remaining (3 per type)"
+ );
}
#[test]
@@ -1312,11 +1383,15 @@ mod tests {
};
// Dry run should report but not delete
- let deleted = cleanup_old_snapshots_with_dir(Some(config), true, Some(&snapshots_dir)).unwrap();
+ let deleted =
+ cleanup_old_snapshots_with_dir(Some(config), true, Some(&snapshots_dir)).unwrap();
assert_eq!(deleted, 7, "Should report 7 snapshots would be deleted");
// All snapshots should still exist
let remaining = fs::read_dir(&snapshots_dir).unwrap().count();
- assert_eq!(remaining, 10, "All snapshots should still exist after dry run");
+ assert_eq!(
+ remaining, 10,
+ "All snapshots should still exist after dry run"
+ );
}
}
diff --git a/src/undo/operations.rs b/src/undo/operations.rs
index dea3af03..d3e75343 100644
--- a/src/undo/operations.rs
+++ b/src/undo/operations.rs
@@ -2,9 +2,9 @@ use anyhow::{anyhow, Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
+use super::snapshot::Snapshot;
use crate::history::{OperationHistory, OperationType};
use crate::scm;
-use super::snapshot::Snapshot;
/// Undo the last pull operation
///
@@ -140,9 +140,9 @@ pub fn undo_push(repo_path: &Path, history_path: Option) -> Result "Pull",
OperationType::Push => "Push",
};
- println!("Undo {}: {} conversations affected", op_type, self.conversation_count);
+ println!(
+ "Undo {}: {} conversations affected",
+ op_type, self.conversation_count
+ );
if !self.affected_files.is_empty() {
println!(" {} files will be restored", self.affected_files.len());
}
@@ -151,7 +154,12 @@ impl UndoPreview {
} else {
commit.as_str()
};
- println!("{} {} (full: {})", "Will reset to:".bold(), short_hash.yellow(), commit.dimmed());
+ println!(
+ "{} {} (full: {})",
+ "Will reset to:".bold(),
+ short_hash.yellow(),
+ commit.dimmed()
+ );
}
println!(
@@ -161,7 +169,11 @@ impl UndoPreview {
);
if !self.affected_files.is_empty() {
- println!("\n{} ({} total)", "Files to be restored:".bold(), self.affected_files.len());
+ println!(
+ "\n{} ({} total)",
+ "Files to be restored:".bold(),
+ self.affected_files.len()
+ );
for (idx, file) in self.affected_files.iter().enumerate() {
println!(" {}. {}", idx + 1, file);
@@ -187,9 +199,12 @@ impl UndoPreview {
let days = time_diff.num_days();
let hours = time_diff.num_hours() % 24;
let mins = time_diff.num_minutes() % 60;
- println!(" {} {} days, {} hours, {} minutes ago",
+ println!(
+ " {} {} days, {} hours, {} minutes ago",
"Age:".dimmed(),
- days, hours, mins
+ days,
+ hours,
+ mins
);
}
diff --git a/src/undo/restore.rs b/src/undo/restore.rs
index dc12d12f..5508a766 100644
--- a/src/undo/restore.rs
+++ b/src/undo/restore.rs
@@ -51,9 +51,8 @@ impl Snapshot {
// Validate the path is within allowed directory
if let Ok(canonical) = path.canonicalize() {
if canonical.starts_with(&allowed_base) && path.exists() {
- fs::remove_file(&path).with_context(|| {
- format!("Failed to delete file: {}", path.display())
- })?;
+ fs::remove_file(&path)
+ .with_context(|| format!("Failed to delete file: {}", path.display()))?;
}
}
}
diff --git a/src/undo/snapshot.rs b/src/undo/snapshot.rs
index d372b6dd..e172d914 100644
--- a/src/undo/snapshot.rs
+++ b/src/undo/snapshot.rs
@@ -310,7 +310,10 @@ impl Snapshot {
/// # Returns
/// The most recent snapshot, or None if no snapshots exist
#[allow(dead_code)] // used via the library target; the bin compiles this module separately
- pub(crate) fn find_latest_snapshot(operation_type: OperationType, custom_dir: Option<&Path>) -> Result