From 2ec2bb025486fdc0806a8bd3ae4a338f65d8e9de Mon Sep 17 00:00:00 2001 From: dm9pZCAq <46228973+dm9pZCAq@users.noreply.github.com> Date: Sun, 6 Oct 2024 18:45:32 +0300 Subject: [PATCH] feat: yazi-cli: support raw git urls --- Cargo.lock | 1 + yazi-cli/Cargo.toml | 1 + yazi-cli/src/package/package.rs | 210 +++++++++++++++++++++++++++----- 3 files changed, 181 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a078b237..e51513af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3258,6 +3258,7 @@ dependencies = [ "clap_complete_nushell", "crossterm", "md-5", + "regex", "serde_json", "tokio", "toml_edit", diff --git a/yazi-cli/Cargo.toml b/yazi-cli/Cargo.toml index cc068dd9..28014290 100644 --- a/yazi-cli/Cargo.toml +++ b/yazi-cli/Cargo.toml @@ -18,6 +18,7 @@ anyhow = { workspace = true } clap = { workspace = true } crossterm = { workspace = true } md-5 = { workspace = true } +regex = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } toml_edit = "0.22.22" diff --git a/yazi-cli/src/package/package.rs b/yazi-cli/src/package/package.rs index d500f0ff..28974a88 100644 --- a/yazi-cli/src/package/package.rs +++ b/yazi-cli/src/package/package.rs @@ -1,13 +1,33 @@ -use std::{borrow::Cow, io::BufWriter, path::PathBuf}; +use std::{borrow::Cow, io::BufWriter, path::PathBuf, sync::OnceLock}; -use anyhow::{Context, Result}; +use anyhow::{Error, Result}; use md5::{Digest, Md5}; +use regex::Regex; use yazi_shared::Xdg; +static PACKAGE_RAW_URL_RE: OnceLock = OnceLock::new(); +static PACKAGE_SHORT_URL_RE: OnceLock = OnceLock::new(); + +#[inline] +fn package_raw_url_re() -> &'static Regex { + PACKAGE_RAW_URL_RE.get_or_init(|| { + Regex::new(r"^(?P[^:]+)://(?P[^/]+)/(?P[^:]+)(:(?P.*))?$") + .unwrap() + }) +} + +#[inline] +fn package_short_url_re() -> &'static Regex { + PACKAGE_SHORT_URL_RE.get_or_init(|| { + Regex::new(r"^((?P[^/]+)/)?(?P[^/]+)/(?P[^:]+)(:(?P.*))?$").unwrap() + }) +} + +#[derive(Debug)] pub(crate) struct Package { + pub(crate) proto: String, pub(crate) host: String, - pub(crate) owner: String, - pub(crate) repo_name: String, + pub(crate) url_path: String, pub(crate) child: String, pub(crate) rev: String, pub(super) is_flavor: bool, @@ -15,41 +35,52 @@ pub(crate) struct Package { impl Package { pub(super) fn new(url: &str, rev: Option<&str>) -> Result { - let mut parts = url.splitn(2, ':'); + let rev = rev.unwrap_or_default().to_owned(); + let is_flavor = false; - let mut repo_part = parts.next().unwrap_or_default().to_owned(); - let child = if let Some(s) = parts.next() { - format!("{s}.yazi") + if let Some(raw_url_match) = package_raw_url_re().captures(url) { + let proto = raw_url_match["proto"].to_owned(); + let host = raw_url_match["host"].to_owned(); + let mut url_path = raw_url_match["url_path"].to_owned(); + let child = if let Some(child) = raw_url_match.name("child") { + format!("{}.yazi", child.as_str()) + } else { + url_path.push_str(".yazi"); + String::new() + }; + + Ok(Self { proto, host, url_path, child, rev, is_flavor }) + } else if let Some(short_url_match) = package_short_url_re().captures(url) { + let proto = "https".to_owned(); + let host = + short_url_match.name("host").map(|m| m.as_str()).unwrap_or("github.com").to_owned(); + let owner = &short_url_match["owner"]; + let repo = &short_url_match["repo"]; + let mut url_path = format!("{owner}/{repo}"); + + let child = if let Some(child) = short_url_match.name("child") { + format!("{}.yazi", child.as_str()) + } else { + url_path.push_str(".yazi"); + String::new() + }; + + Ok(Self { proto, host, url_path, child, rev, is_flavor }) } else { - repo_part.push_str(".yazi"); - String::new() - }; - - let mut repo = repo_part.rsplit('/'); - let repo_name = repo.next().context("failed to get repo name")?.to_owned(); - let owner = repo.next().context("failed to get repo owner")?.to_owned(); - let host = repo.next().unwrap_or("github.com").to_owned(); - - Ok(Self { - repo_name, - owner, - host, - child, - rev: rev.unwrap_or_default().to_owned(), - is_flavor: false, - }) + Err(Error::msg("invalid package url")) + } } #[inline] pub(super) fn use_(&self) -> Cow { if self.child.is_empty() { - format!("{}/{}/{}", self.host, self.owner, self.repo_name.trim_end_matches(".yazi")).into() + format!("{}://{}/{}", self.proto, self.host, self.url_path.trim_end_matches(".yazi")).into() } else { format!( - "{}/{}/{}:{}", + "{}://{}/{}:{}", + self.proto, self.host, - self.owner, - self.repo_name, + self.url_path, self.child.trim_end_matches(".yazi") ) .into() @@ -58,7 +89,11 @@ impl Package { #[inline] pub(super) fn name(&self) -> &str { - if self.child.is_empty() { self.repo_name.as_str() } else { self.child.as_str() } + if self.child.is_empty() { + self.url_path.rsplit('/').next().unwrap_or(&self.url_path) + } else { + self.child.as_str() + } } #[inline] @@ -70,7 +105,7 @@ impl Package { #[inline] pub(super) fn remote(&self) -> String { - format!("https://{}/{}/{}.git", self.host, self.owner, self.repo_name) + format!("{}://{}/{}", self.proto, self.host, self.url_path) } pub(super) fn header(&self, s: &str) -> Result<()> { @@ -90,3 +125,116 @@ impl Package { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn two_component_short_url() -> Result<()> { + let url = "owner/repo"; + + let pkg = Package::new(url, None)?; + + assert_eq!(pkg.proto, "https"); + assert_eq!(pkg.host, "github.com"); + assert_eq!(pkg.url_path, "owner/repo.yazi"); + assert_eq!(pkg.child, ""); + + assert_eq!(pkg.remote(), "https://github.com/owner/repo.yazi"); + + Ok(()) + } + + #[test] + fn three_component_short_url() -> Result<()> { + let url = "codeberg.org/owner/repo"; + + let pkg = Package::new(url, None)?; + + assert_eq!(pkg.proto, "https"); + assert_eq!(pkg.host, "codeberg.org"); + assert_eq!(pkg.url_path, "owner/repo.yazi"); + assert_eq!(pkg.child, ""); + + assert_eq!(pkg.remote(), "https://codeberg.org/owner/repo.yazi"); + + Ok(()) + } + + #[test] + fn two_component_short_url_with_child_path() -> Result<()> { + let url = "owner/repo:my-plugin"; + + let pkg = Package::new(url, None)?; + + assert_eq!(pkg.proto, "https"); + assert_eq!(pkg.host, "github.com"); + assert_eq!(pkg.url_path, "owner/repo"); + assert_eq!(pkg.child, "my-plugin.yazi"); + + assert_eq!(pkg.remote(), "https://github.com/owner/repo"); + assert_eq!(pkg.use_(), "https://github.com/owner/repo:my-plugin"); + + Ok(()) + } + + #[test] + fn raw_ssh_url() -> Result<()> { + let url = "ssh://git@my-host:6969/my-plugin"; + + let pkg = Package::new(url, None)?; + + assert_eq!(pkg.proto, "ssh"); + assert_eq!(pkg.host, "git@my-host:6969"); + assert_eq!(pkg.url_path, "my-plugin.yazi"); + assert_eq!(pkg.child, ""); + + assert_eq!(pkg.remote(), "ssh://git@my-host:6969/my-plugin.yazi"); + + Ok(()) + } + + #[test] + fn raw_ssh_url_with_child_path() -> Result<()> { + let url = "ssh://git@192.168.0.69:2222/~/my-repo.git:my-plugin"; + + let pkg = Package::new(url, None)?; + + assert_eq!(pkg.proto, "ssh"); + assert_eq!(pkg.host, "git@192.168.0.69:2222"); + assert_eq!(pkg.url_path, "~/my-repo.git"); + assert_eq!(pkg.child, "my-plugin.yazi"); + + assert_eq!(pkg.remote(), "ssh://git@192.168.0.69:2222/~/my-repo.git"); + assert_eq!(pkg.use_(), "ssh://git@192.168.0.69:2222/~/my-repo.git:my-plugin"); + + Ok(()) + } + + #[test] + fn raw_http_url_with_non_standard_path() -> Result<()> { + let url = "https://example.com/xxx/yyy/zzz/owner/repo:my-plugin"; + + let pkg = Package::new(url, None)?; + + assert_eq!(pkg.proto, "https"); + assert_eq!(pkg.host, "example.com"); + assert_eq!(pkg.url_path, "xxx/yyy/zzz/owner/repo"); + assert_eq!(pkg.child, "my-plugin.yazi"); + + assert_eq!(pkg.remote(), "https://example.com/xxx/yyy/zzz/owner/repo"); + assert_eq!(pkg.use_(), "https://example.com/xxx/yyy/zzz/owner/repo:my-plugin"); + + Ok(()) + } + + #[test] + fn invalid_url() { + let url = "one-component-url???"; + + let pkg = Package::new(url, None); + + assert!(pkg.is_err()); + } +}