From 3108ed58cad0255b8d893c8b767ed8b02b7d1638 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Wed, 4 Feb 2026 09:53:00 +0000 Subject: [PATCH] Initial upload: DotMigrate dotfiles migration tool with CI/CD --- src/cli/commands.rs | 285 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 src/cli/commands.rs diff --git a/src/cli/commands.rs b/src/cli/commands.rs new file mode 100644 index 0000000..4d24b94 --- /dev/null +++ b/src/cli/commands.rs @@ -0,0 +1,285 @@ +use crate::cli::{Args, SyncBackend, MergeStrategy}; +use crate::config::{Config, Dotfile}; +use crate::detect::Detector; +use crate::backup::BackupManager; +use crate::sync::SyncManager; +use crate::merge::Merger; +use anyhow::{Context, Result}; +use std::path::PathBuf; + +#[cfg(feature = "dialoguer")] +use dialoguer::Confirm; + +pub fn init(_args: Args, backend: Option) -> Result<()> { + println!("Initializing DotMigrate configuration..."); + + let backend = backend.unwrap_or_else(|| { + #[cfg(feature = "dialoguer")] + { + if Confirm::new() + .with_prompt("Use git repository for sync?") + .default(true) + .interact() + .unwrap() + { + SyncBackend::Git + } else { + SyncBackend::Direct + } + } + #[cfg(not(feature = "dialoguer"))] + { + SyncBackend::Git + } + }); + + let config = Config::default_for_backend(backend); + let config_path = Config::default_path()?; + + config.save(&config_path) + .with_context(|| format!("Failed to save config to {:?}", config_path))?; + + println!("Configuration created at {:?}", config_path); + println!("Run 'dotmigrate detect' to find existing dotfiles."); + + Ok(()) +} + +pub fn detect(args: Args, output: Option, include_system: bool) -> Result<()> { + println!("Detecting dotfiles..."); + + let config_path = args.config.clone() + .or_else(|| Config::default_path().ok()) + .unwrap_or_else(|| PathBuf::from(".dotmigrate/config.yml")); + + let config = if config_path.exists() { + Config::load(&config_path)? + } else { + Config::default() + }; + + let detector = Detector::new(&config); + let dotfiles = detector.detect_dotfiles(include_system) + .context("Failed to detect dotfiles")?; + + if args.dry_run { + println!("[DRY-RUN] Would detect {} dotfiles:", dotfiles.len()); + for dotfile in &dotfiles { + println!(" - {:?}", dotfile.path); + } + } else { + println!("Detected {} dotfiles:", dotfiles.len()); + for dotfile in &dotfiles { + println!(" - {:?}", dotfile.path); + } + } + + if let Some(output_path) = output { + let content = serde_yaml::to_string(&dotfiles) + .context("Failed to serialize dotfiles")?; + + if args.dry_run { + println!("[DRY-RUN] Would save detected dotfiles to {:?}", output_path); + } else { + std::fs::write(&output_path, content) + .context("Failed to write dotfiles to output")?; + println!("Detected dotfiles saved to {:?}", output_path); + } + } + + Ok(()) +} + +pub fn backup(args: Args, output: Option, backup_dir: Option) -> Result<()> { + println!("Creating dotfiles backup..."); + + let config_path = args.config.clone() + .or_else(|| Config::default_path().ok()) + .unwrap_or_else(|| PathBuf::from(".dotmigrate/config.yml")); + + let config = if config_path.exists() { + Config::load(&config_path)? + } else { + Config::default() + }; + + let detector = Detector::new(&config); + let dotfiles = detector.detect_dotfiles(false)?; + + let backup_path = backup_dir + .or_else(|| dirs::home_dir().map(|p| p.join(".dotmigrate/backups"))) + .unwrap_or_else(|| PathBuf::from("backups")); + + let backup_manager = BackupManager::new(&backup_path)?; + + if args.dry_run { + println!("[DRY-RUN] Would create backup of {} dotfiles", dotfiles.len()); + println!("[DRY-RUN] Backup directory: {:?}", backup_path); + } else { + let backup = backup_manager.create_backup(&dotfiles) + .context("Failed to create backup")?; + println!("Backup created: {}", backup.archive_path.display()); + + if let Some(output_path) = output { + let manifest = backup_manager.get_manifest(&backup); + let content = serde_yaml::to_string(&manifest) + .context("Failed to serialize backup manifest")?; + std::fs::write(&output_path, content) + .context("Failed to write backup manifest")?; + println!("Backup manifest saved to {:?}", output_path); + } + } + + Ok(()) +} + +pub fn sync(args: Args, remote: Option, branch: Option, strategy: Option) -> Result<()> { + println!("Syncing dotfiles..."); + + let config_path = args.config.clone() + .or_else(|| Config::default_path().ok()) + .unwrap_or_else(|| PathBuf::from(".dotmigrate/config.yml")); + + let mut config = if config_path.exists() { + Config::load(&config_path)? + } else { + Config::default() + }; + + if let Some(r) = remote { + config.sync.remote = Some(r); + } + if let Some(b) = branch { + config.sync.branch = b; + } + + let backend = args.backend + .or(config.sync.backend) + .unwrap_or(SyncBackend::Git); + + let detector = Detector::new(&config); + let local_dotfiles = detector.detect_dotfiles(false)?; + + let sync_manager = SyncManager::new(&config, backend); + + if args.dry_run { + println!("[DRY-RUN] Would sync with backend: {:?}", backend); + if let Some(ref remote_url) = config.sync.remote { + println!("[DRY-RUN] Remote: {}", remote_url); + } + println!("[DRY-RUN] Local dotfiles: {}", local_dotfiles.len()); + } else { + sync_manager.sync(&local_dotfiles, strategy) + .context("Failed to sync")?; + println!("Sync completed successfully."); + } + + Ok(()) +} + +pub fn merge(args: Args, base: PathBuf, local: PathBuf, remote: PathBuf, output: PathBuf, strategy: Option) -> Result<()> { + println!("Merging files..."); + + let merger = Merger::new(); + + if args.dry_run { + println!("[DRY-RUN] Would merge files:"); + println!(" Base: {:?}", base); + println!(" Local: {:?}", local); + println!(" Remote: {:?}", remote); + println!(" Output: {:?}", output); + } else { + let result = merger.three_way_merge(&base, &local, &remote, &output, strategy) + .context("Failed to merge files")?; + + match result { + merge::MergeResult::Success => { + println!("Merge completed successfully. Output: {:?}", output); + } + merge::MergeResult::Conflict(conflicts) => { + println!("Merge completed with {} conflicts:", conflicts.len()); + for conflict in &conflicts { + println!(" - {}", conflict.path.display()); + } + } + merge::MergeResult::AutoMerged => { + println!("Auto-merge completed successfully. Output: {:?}", output); + } + } + } + + Ok(()) +} + +pub fn status(args: Args, detailed: bool) -> Result<()> { + println!("DotMigrate Status"); + + let config_path = args.config.clone() + .or_else(|| Config::default_path().ok()) + .unwrap_or_else(|| PathBuf::from(".dotmigrate/config.yml")); + + if config_path.exists() { + println!("Configuration: {:?}", config_path); + let config = Config::load(&config_path)?; + println!(" Backend: {:?}", config.sync.backend.unwrap_or(SyncBackend::Git)); + if let Some(ref remote) = config.sync.remote { + println!(" Remote: {}", remote); + } + println!(" Branch: {}", config.sync.branch); + } else { + println!("No configuration found. Run 'dotmigrate init' first."); + } + + let detector = Detector::new(&Config::default()); + let dotfiles = detector.detect_dotfiles(false)?; + + println!("Detected {} dotfiles", dotfiles.len()); + + if detailed { + println!("\nDotfiles:"); + for dotfile in &dotfiles { + println!(" - {:?}", dotfile.path); + if let Some(ref hash) = dotfile.content_hash { + println!(" Hash: {}", hash); + } + } + } + + let backup_dir = dirs::home_dir() + .map(|p| p.join(".dotmigrate/backups")) + .filter(|p| p.exists()); + + if let Some(ref backup_path) = backup_dir { + println!("\nBackup directory: {:?}", backup_path); + let entries = std::fs::read_dir(backup_path) + .map(|r| r.map(|iter| iter.count()).unwrap_or(0)) + .unwrap_or(0); + println!(" {} backup archives", entries); + } + + Ok(()) +} + +pub fn diff(args: Args, local: PathBuf, remote: PathBuf) -> Result<()> { + println!("Comparing files..."); + + let merger = Merger::new(); + let diff = merger.diff(&local, &remote)?; + + if diff.is_empty() { + println!("Files are identical."); + } else { + println!("Differences found:"); + for line in diff { + println!("{}", line); + } + } + + Ok(()) +} + +pub fn completions(shell: clap_complete::Shell) -> Result<()> { + let mut cmd = crate::cli::Args::command(); + clap_complete::generate(shell, &mut cmd, "dotmigrate", &mut std::io::stdout()); + Ok(()) +}