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