From 3f714a309c485eb148568a7b9c750a548c7bfd8f Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Wed, 4 Feb 2026 09:53:04 +0000 Subject: [PATCH] Initial upload: DotMigrate dotfiles migration tool with CI/CD --- src/sync/mod.rs | 362 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 src/sync/mod.rs diff --git a/src/sync/mod.rs b/src/sync/mod.rs new file mode 100644 index 0000000..13c96ee --- /dev/null +++ b/src/sync/mod.rs @@ -0,0 +1,362 @@ +use crate::config::Config; +use crate::detect::DetectedDotfile; +use crate::cli::SyncBackend; +use anyhow::{Context, Result}; +use std::path::PathBuf; +use std::fs; + +#[cfg(feature = "git2")] +pub struct GitBackend { + config: Config, + repo: Option, +} + +#[cfg(not(feature = "git2"))] +pub struct GitBackend { + config: Config, +} + +#[cfg(feature = "git2")] +impl GitBackend { + pub fn new(config: &Config) -> Result { + let repo = match config.sync.remote { + Some(ref remote) if remote.starts_with("http") || remote.starts_with("git@") => { + let clone_path = config.sync.path.parent() + .map(|p| p.join("dotfiles")) + .unwrap_or_else(|| std::path::PathBuf::from("dotfiles")); + Some(git2::Repository::clone(remote, clone_path)?) + } + _ => None, + }; + Ok(Self { + config: config.clone(), + repo, + }) + } + + pub fn push(&self, dotfiles: &[DetectedDotfile]) -> Result<()> { + if let Some(ref repo) = self.repo { + let mut index = repo.index()?; + + for dotfile in dotfiles { + if dotfile.path.exists() { + match repo.status_file(&dotfile.path) { + Ok(_) => { + index.add_path(&dotfile.path)?; + } + Err(_) => { + index.add_path(&dotfile.path)?; + } + } + } + } + + let oid = index.write_tree()?; + let tree = repo.find_tree(oid)?; + + let sig = git2::Signature::now("DotMigrate", "dotmigrate@example.com")?; + + let commit_message = format!("Update {} dotfiles", dotfiles.len()); + + if let Ok(head) = repo.head() { + if let Ok(target) = head.target() { + if let Ok(commit) = repo.find_commit(target) { + repo.commit(Some("HEAD"), &sig, &sig, &commit_message, &tree, std::slice::from_ref(&commit))?; + } else { + repo.commit(Some("HEAD"), &sig, &sig, &commit_message, &tree, &[])?; + } + } else { + repo.commit(Some("HEAD"), &sig, &sig, &commit_message, &tree, &[])?; + } + } else { + repo.commit(Some("HEAD"), &sig, &sig, &commit_message, &tree, &[])?; + } + + if let Ok(mut remote) = repo.find_remote("origin") { + remote.push(&["refs/heads/main:refs/heads/main"], None)?; + } + + Ok(()) + } else { + anyhow::bail!("Git repository not initialized"); + } + } + + pub fn pull(&self, dotfiles: &mut Vec) -> Result<()> { + if let Some(ref _repo) = self.repo { + Ok(()) + } else { + anyhow::bail!("Git repository not initialized"); + } + } + + pub fn list_remote(&self) -> Result> { + Ok(Vec::new()) + } + + pub fn get_name(&self) -> SyncBackend { + SyncBackend::Git + } +} + +#[cfg(not(feature = "git2"))] +impl GitBackend { + pub fn new(config: &Config) -> Result { + Ok(Self { + config: config.clone(), + }) + } + + pub fn push(&self, _dotfiles: &[DetectedDotfile]) -> Result<()> { + anyhow::bail!("Git backend requires git2 feature. Rebuild with --features git2"); + } + + pub fn pull(&self, _dotfiles: &mut Vec) -> Result<()> { + anyhow::bail!("Git backend requires git2 feature. Rebuild with --features git2"); + } + + pub fn list_remote(&self) -> Result> { + anyhow::bail!("Git backend requires git2 feature. Rebuild with --features git2"); + } + + pub fn get_name(&self) -> SyncBackend { + SyncBackend::Git + } +} + +#[cfg(feature = "rusoto_s3")] +pub struct S3Backend { + config: Config, + bucket: String, + client: Option, +} + +#[cfg(not(feature = "rusoto_s3"))] +pub struct S3Backend { + config: Config, + bucket: String, +} + +#[cfg(feature = "rusoto_s3")] +impl S3Backend { + pub fn new(config: &Config) -> Result { + let bucket = config.sync.remote.clone() + .unwrap_or_else(|| "dotmigrate-bucket".to_string()); + + let client = None; + + Ok(Self { + config: config.clone(), + bucket, + client, + }) + } + + pub fn push(&self, _dotfiles: &[DetectedDotfile]) -> Result<()> { + anyhow::bail!("S3 backend implementation pending"); + } + + pub fn pull(&self, _dotfiles: &mut Vec) -> Result<()> { + anyhow::bail!("S3 backend implementation pending"); + } + + pub fn list_remote(&self) -> Result> { + Ok(Vec::new()) + } + + pub fn get_name(&self) -> SyncBackend { + SyncBackend::S3 + } +} + +#[cfg(not(feature = "rusoto_s3"))] +impl S3Backend { + pub fn new(config: &Config) -> Result { + let bucket = config.sync.remote.clone() + .unwrap_or_else(|| "dotmigrate-bucket".to_string()); + + Ok(Self { + config: config.clone(), + bucket, + }) + } + + pub fn push(&self, _dotfiles: &[DetectedDotfile]) -> Result<()> { + anyhow::bail!("S3 backend requires rusoto_s3 feature. Rebuild with --features rusoto_s3"); + } + + pub fn pull(&self, _dotfiles: &mut Vec) -> Result<()> { + anyhow::bail!("S3 backend requires rusoto_s3 feature. Rebuild with --features rusoto_s3"); + } + + pub fn list_remote(&self) -> Result> { + anyhow::bail!("S3 backend requires rusoto_s3 feature. Rebuild with --features rusoto_s3"); + } + + pub fn get_name(&self) -> SyncBackend { + SyncBackend::S3 + } +} + +pub struct DirectBackend { + config: Config, + remote_path: Option, +} + +impl DirectBackend { + pub fn new(config: &Config) -> Result { + let remote_path = config.sync.remote.clone() + .map(PathBuf::from); + + Ok(Self { + config: config.clone(), + remote_path, + }) + } + + pub fn push(&self, dotfiles: &[DetectedDotfile]) -> Result<()> { + let target_dir = self.remote_path + .as_ref() + .or_else(|| self.config.sync.path.parent()) + .context("No remote path specified")?; + + if !target_dir.exists() { + std::fs::create_dir_all(target_dir) + .context("Failed to create target directory")?; + } + + for dotfile in dotfiles { + let file_name = dotfile.path.file_name() + .context("Dotfile has no file name")?; + + let target_path = target_dir.join(file_name); + + std::fs::copy(&dotfile.path, &target_path) + .context(format!("Failed to copy {:?} to {:?}", dotfile.path, target_path))?; + } + + Ok(()) + } + + pub fn pull(&self, dotfiles: &mut Vec) -> Result<()> { + let source_dir = self.remote_path + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Remote path not specified"))?; + + if !source_dir.exists() { + return Ok(()); + } + + for entry in std::fs::read_dir(source_dir)? { + let entry = entry?; + let path = entry.path(); + + if path.is_file() { + let detected = DetectedDotfile::new(path); + dotfiles.push(detected); + } + } + + Ok(()) + } + + pub fn list_remote(&self) -> Result> { + let source_dir = self.remote_path + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Remote path not specified"))?; + + let mut files = Vec::new(); + + if source_dir.exists() { + for entry in std::fs::read_dir(source_dir)? { + let entry = entry?; + if entry.path().is_file() { + files.push(entry.path()); + } + } + } + + Ok(files) + } + + pub fn get_name(&self) -> SyncBackend { + SyncBackend::Direct + } +} + +pub struct SyncManager { + config: Config, + backend_type: SyncBackend, +} + +impl SyncManager { + pub fn new(config: &Config, backend_type: SyncBackend) -> Self { + Self { + config: config.clone(), + backend_type, + } + } + + pub fn sync( + &self, + local_dotfiles: &[DetectedDotfile], + _strategy: Option, + ) -> Result<()> { + match self.backend_type { + SyncBackend::Git => { + let backend = GitBackend::new(&self.config)?; + backend.push(local_dotfiles).context("Push failed")?; + let mut dotfiles = local_dotfiles.to_vec(); + backend.pull(&mut dotfiles).context("Pull failed")?; + } + SyncBackend::S3 => { + let backend = S3Backend::new(&self.config)?; + backend.push(local_dotfiles).context("Push failed")?; + let mut dotfiles = local_dotfiles.to_vec(); + backend.pull(&mut dotfiles).context("Pull failed")?; + } + SyncBackend::Direct => { + let backend = DirectBackend::new(&self.config)?; + backend.push(local_dotfiles).context("Push failed")?; + let mut dotfiles = local_dotfiles.to_vec(); + backend.pull(&mut dotfiles).context("Pull failed")?; + } + } + + Ok(()) + } + + pub fn push(&self, dotfiles: &[DetectedDotfile]) -> Result<()> { + match self.backend_type { + SyncBackend::Git => { + let backend = GitBackend::new(&self.config)?; + backend.push(dotfiles) + } + SyncBackend::S3 => { + let backend = S3Backend::new(&self.config)?; + backend.push(dotfiles) + } + SyncBackend::Direct => { + let backend = DirectBackend::new(&self.config)?; + backend.push(dotfiles) + } + } + } + + pub fn pull(&self, dotfiles: &mut Vec) -> Result<()> { + match self.backend_type { + SyncBackend::Git => { + let backend = GitBackend::new(&self.config)?; + backend.pull(dotfiles) + } + SyncBackend::S3 => { + let backend = S3Backend::new(&self.config)?; + backend.pull(dotfiles) + } + SyncBackend::Direct => { + let backend = DirectBackend::new(&self.config)?; + backend.pull(dotfiles) + } + } + } +}