Initial upload: DotMigrate dotfiles migration tool with CI/CD
This commit is contained in:
362
src/sync/mod.rs
Normal file
362
src/sync/mod.rs
Normal file
@@ -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<git2::Repository>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "git2"))]
|
||||
pub struct GitBackend {
|
||||
config: Config,
|
||||
}
|
||||
|
||||
#[cfg(feature = "git2")]
|
||||
impl GitBackend {
|
||||
pub fn new(config: &Config) -> Result<Self> {
|
||||
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<DetectedDotfile>) -> Result<()> {
|
||||
if let Some(ref _repo) = self.repo {
|
||||
Ok(())
|
||||
} else {
|
||||
anyhow::bail!("Git repository not initialized");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_remote(&self) -> Result<Vec<PathBuf>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
pub fn get_name(&self) -> SyncBackend {
|
||||
SyncBackend::Git
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "git2"))]
|
||||
impl GitBackend {
|
||||
pub fn new(config: &Config) -> Result<Self> {
|
||||
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<DetectedDotfile>) -> Result<()> {
|
||||
anyhow::bail!("Git backend requires git2 feature. Rebuild with --features git2");
|
||||
}
|
||||
|
||||
pub fn list_remote(&self) -> Result<Vec<PathBuf>> {
|
||||
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<rusoto_s3::S3Client>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "rusoto_s3"))]
|
||||
pub struct S3Backend {
|
||||
config: Config,
|
||||
bucket: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "rusoto_s3")]
|
||||
impl S3Backend {
|
||||
pub fn new(config: &Config) -> Result<Self> {
|
||||
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<DetectedDotfile>) -> Result<()> {
|
||||
anyhow::bail!("S3 backend implementation pending");
|
||||
}
|
||||
|
||||
pub fn list_remote(&self) -> Result<Vec<PathBuf>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
pub fn get_name(&self) -> SyncBackend {
|
||||
SyncBackend::S3
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "rusoto_s3"))]
|
||||
impl S3Backend {
|
||||
pub fn new(config: &Config) -> Result<Self> {
|
||||
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<DetectedDotfile>) -> Result<()> {
|
||||
anyhow::bail!("S3 backend requires rusoto_s3 feature. Rebuild with --features rusoto_s3");
|
||||
}
|
||||
|
||||
pub fn list_remote(&self) -> Result<Vec<PathBuf>> {
|
||||
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<PathBuf>,
|
||||
}
|
||||
|
||||
impl DirectBackend {
|
||||
pub fn new(config: &Config) -> Result<Self> {
|
||||
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<DetectedDotfile>) -> 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<Vec<PathBuf>> {
|
||||
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<crate::cli::MergeStrategy>,
|
||||
) -> 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<DetectedDotfile>) -> 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user