Initial upload: DotMigrate dotfiles migration tool with CI/CD
Some checks failed
CI / test (push) Has been cancelled
CI / release (push) Has been cancelled

This commit is contained in:
2026-02-04 09:53:00 +00:00
parent 78b5a6cd66
commit 3108ed58ca

285
src/cli/commands.rs Normal file
View File

@@ -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<SyncBackend>) -> 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<PathBuf>, 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<PathBuf>, backup_dir: Option<PathBuf>) -> 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<String>, branch: Option<String>, strategy: Option<MergeStrategy>) -> 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<MergeStrategy>) -> 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(())
}