diff --git a/yazi-cli/src/package/deploy.rs b/yazi-cli/src/package/deploy.rs index 82739260..1591ae2c 100644 --- a/yazi-cli/src/package/deploy.rs +++ b/yazi-cli/src/package/deploy.rs @@ -1,34 +1,46 @@ use anyhow::{Context, Result, bail}; -use tokio::fs; -use yazi_shared::{Xdg, fs::{copy_dir_all, maybe_exists, must_exists}}; +use tokio::fs::{self, remove_dir_all}; +use yazi_shared::{Xdg, fs::{copy_dir_all, maybe_exists, must_exists, remove_dir_clean}}; -use super::Package; +use super::{DiffAction, Package}; +use crate::package::Git; const TRACKER: &str = "DO_NOT_MODIFY_ANYTHING_IN_THIS_DIRECTORY"; impl Package { pub(super) async fn deploy(&mut self) -> Result<()> { let Some(name) = self.name().map(ToOwned::to_owned) else { bail!("Invalid package url") }; - let from = self.local().join(&self.child); + let origin = self.local().join(&self.child); self.header("Deploying package `{name}`")?; - self.is_flavor = maybe_exists(&from.join("flavor.toml")).await; - let to = if self.is_flavor { + self.is_flavor = maybe_exists(&origin.join("flavor.toml")).await; + let dest = if self.is_flavor { Xdg::config_dir().join(format!("flavors/{name}")) } else { Xdg::config_dir().join(format!("plugins/{name}")) }; - let tracker = to.join(TRACKER); - if maybe_exists(&to).await && !must_exists(&tracker).await { + let tracker = dest.join(TRACKER); + if maybe_exists(&dest).await && !must_exists(&tracker).await { bail!( "A user package with the same name `{name}` already exists. For safety, please manually delete it from your plugin/flavor directory and re-run the command." ); } - fs::create_dir_all(&to).await?; - fs::write(tracker, []).await?; + fs::create_dir_all(&dest).await?; + + let mut dirs = &["assets"][..]; + let mut diff_actions = vec![]; + if tracker.exists() { + let prev_rev = String::from_utf8(fs::read(&tracker).await?)?; + + if prev_rev != self.rev { + diff_actions = Git::diff(&self.local().join(&self.child), &prev_rev, dirs).await?; + } + } + + fs::write(tracker, &self.rev.trim_start_matches('=')).await?; let files = if self.is_flavor { &["flavor.toml", "tmtheme.xml", "README.md", "preview.png", "LICENSE", "LICENSE-tmtheme"][..] @@ -36,20 +48,61 @@ For safety, please manually delete it from your plugin/flavor directory and re-r &["init.lua", "README.md", "LICENSE"][..] }; - let dirs = &["assets"][..]; - for file in files { - let (from, to) = (from.join(file), to.join(file)); + let (from, to) = (origin.join(file), dest.join(file)); fs::copy(&from, &to) .await .with_context(|| format!("failed to copy `{}` to `{}`", from.display(), to.display()))?; } + for action in diff_actions.as_slice() { + match action { + DiffAction::Add { file } => { + let (from, to) = (origin.join(file), dest.join(file)); + fs::create_dir_all(&to).await?; + fs::copy(&from, &to).await.with_context(|| { + format!("failed to copy `{}` to `{}`", from.display(), to.display()) + })?; + } + DiffAction::Delete { file } => { + let file = dest.join(file); + fs::remove_file(&file) + .await + .with_context(|| format!("failed to remove `{}`", file.display()))?; + } + DiffAction::Rename { old, new } => { + let (from, to) = (dest.join(old), dest.join(new)); + fs::create_dir_all(&to).await?; + fs::rename(&from, &to).await.with_context(|| { + format!("failed to rename `{}` to `{}`", from.display(), to.display()) + })?; + } + DiffAction::Copy { old, new } => { + let (from, to) = (dest.join(old), dest.join(new)); + fs::create_dir_all(&to).await?; + fs::copy(&from, &to).await.with_context(|| { + format!("failed to copy `{}` to `{}`", from.display(), to.display()) + })?; + } + }; + } + + remove_dir_clean(&dest).await; + + if !diff_actions.is_empty() { + dirs = &[]; + } + for dir in dirs { - let (from, to) = (from.join(dir), to.join(dir)); + let (from, to) = (origin.join(dir), dest.join(dir)); if !from.exists() { + if to.exists() { + remove_dir_all(&to) + .await + .with_context(|| format!("failed to prune no-longer exist folder {}", to.display()))?; + } continue; } diff --git a/yazi-cli/src/package/git.rs b/yazi-cli/src/package/git.rs index 6be4a4f3..6754941d 100644 --- a/yazi-cli/src/package/git.rs +++ b/yazi-cli/src/package/git.rs @@ -6,6 +6,13 @@ use yazi_shared::strip_trailing_newline; pub(super) struct Git; +pub(super) enum DiffAction { + Add { file: String }, + Delete { file: String }, + Rename { old: String, new: String }, + Copy { old: String, new: String }, +} + impl Git { pub(super) async fn clone(url: &str, path: &Path) -> Result<()> { Self::exec(|c| c.args(["clone", url]).arg(path)).await @@ -42,6 +49,48 @@ impl Git { )) } + pub(super) async fn diff(path: &Path, rev: &str, filter: &[&str]) -> Result> { + let mut command = Command::new("git"); + command.current_dir(path).args(["diff", "-M100", "-C100", rev, "origin/HEAD", "--name-status"]); + if !filter.is_empty() { + command.arg("--"); + } + for f in filter { + command.arg(*f); + } + let output = command.output().await.context("Failed to run git diff")?; + + if !output.status.success() { + bail!("git diff failed: {}", output.status); + } + + let stdout = String::from_utf8(output.stdout).context("Failed to parse git diff output")?; + let mut diff_actions = Vec::new(); + + for line in stdout.lines() { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.is_empty() { + continue; + } + + match parts[0] { + "A" | "M" if parts.len() == 2 => { + diff_actions.push(DiffAction::Add { file: parts[1].to_string() }) + } + "D" if parts.len() == 2 => { + diff_actions.push(DiffAction::Delete { file: parts[1].to_string() }) + } + "R100" if parts.len() == 3 => diff_actions + .push(DiffAction::Rename { old: parts[1].to_string(), new: parts[2].to_string() }), + "C100" if parts.len() == 3 => diff_actions + .push(DiffAction::Copy { old: parts[1].to_string(), new: parts[2].to_string() }), + _ => {} + } + } + + Ok(diff_actions) + } + async fn exec(f: impl FnOnce(&mut Command) -> &mut Command) -> Result<()> { let status = f(&mut Command::new("git")).status().await.context("Failed to execute `git` command")?; @@ -53,3 +102,17 @@ impl Git { Ok(()) } } + +impl DiffAction { + pub(super) fn is_in(&self, dirs: &[&str]) -> bool { + for dir in dirs { + match self { + DiffAction::Add { file } => return file.contains(dir), + DiffAction::Delete { file } => return file.contains(dir), + DiffAction::Rename { old: _, new } => return new.contains(dir), + DiffAction::Copy { old: _, new } => return new.contains(dir), + } + } + false + } +}