f737b6a24d
Refactor plugin imports to use workshop_engine types, consolidate resource fetching into utils/fetch module with checksum verification, and add interactive notification server using axum.
186 lines
6.2 KiB
Rust
186 lines
6.2 KiB
Rust
// Code in this module PARTIALLY OR FULLY utilized AI Coding Agent.
|
|
use super::error::FetchError;
|
|
use std::path::Path;
|
|
use tokio::process::Command;
|
|
|
|
/// Version specification for a git repository.
|
|
#[derive(Debug, PartialEq)]
|
|
pub enum GitVersion {
|
|
/// No version specified (invalid for fetch operations)
|
|
None,
|
|
/// Single version string (tag or commit hash)
|
|
Single(String),
|
|
/// Mixed: try tag first, fallback to commit hash
|
|
Mixed(String, String),
|
|
}
|
|
|
|
/// Parses a git URL with optional version specification.
|
|
///
|
|
/// Format: `url` (no version), `url@tag` (single version), `url@tag:commit` (mixed version).
|
|
/// Returns (base_url, GitVersion).
|
|
pub fn parse_git_url(url: &str) -> (String, GitVersion) {
|
|
if let Some((base, version)) = url.rsplit_once('@') {
|
|
if version.contains(":") {
|
|
let (tag, commit) = version.split_once(':').unwrap();
|
|
return (
|
|
base.to_string(),
|
|
GitVersion::Mixed(tag.to_string(), commit.to_string()),
|
|
);
|
|
}
|
|
return (base.to_string(), GitVersion::Single(version.to_string()));
|
|
}
|
|
(url.to_string(), GitVersion::None)
|
|
}
|
|
|
|
/// Clones a git repository and checks out the specified version.
|
|
///
|
|
/// Removes existing destination directory, performs shallow or full clone,
|
|
/// then checks out the given version (tag or commit).
|
|
pub async fn git_clone_and_checkout(
|
|
baseurl: &str,
|
|
version: &str,
|
|
dest: &Path,
|
|
name: &str,
|
|
shallow: bool,
|
|
) -> Result<(), FetchError> {
|
|
if dest.exists() {
|
|
tokio::fs::remove_dir_all(dest)
|
|
.await
|
|
.map_err(|e| FetchError::FetchFailed {
|
|
name: name.to_string(),
|
|
url: baseurl.to_string(),
|
|
reason: format!("Failed to remove existing directory: {}", e),
|
|
})?;
|
|
}
|
|
let mut cmd = Command::new("git");
|
|
cmd.arg("clone");
|
|
if shallow {
|
|
cmd.arg("--depth").arg("1");
|
|
}
|
|
cmd.arg(baseurl).arg(dest);
|
|
let output = cmd.output().await.map_err(|e| FetchError::FetchFailed {
|
|
name: name.to_string(),
|
|
url: baseurl.to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
if !output.status.success() {
|
|
return Err(FetchError::FetchFailed {
|
|
name: name.to_string(),
|
|
url: baseurl.to_string(),
|
|
reason: String::from_utf8_lossy(&output.stderr).to_string(),
|
|
});
|
|
}
|
|
let mut cmd = Command::new("git");
|
|
cmd.arg("checkout").arg(version).current_dir(dest);
|
|
let output = cmd.output().await.map_err(|e| FetchError::FetchFailed {
|
|
name: name.to_string(),
|
|
url: baseurl.to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
if !output.status.success() {
|
|
return Err(FetchError::FetchFailed {
|
|
name: name.to_string(),
|
|
url: baseurl.to_string(),
|
|
reason: String::from_utf8_lossy(&output.stderr).to_string(),
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Fetches a plugin from a git repository.
|
|
///
|
|
/// Parses the URL for version info, then clones and checks out the appropriate ref.
|
|
/// For Mixed version, tries the tag first and falls back to the commit hash.
|
|
pub async fn fetch_git_plugin(
|
|
path: &str,
|
|
name: &str,
|
|
basedir: &Path,
|
|
shallow: Option<bool>,
|
|
) -> Result<(), FetchError> {
|
|
let (baseurl, version) = parse_git_url(path);
|
|
match version {
|
|
GitVersion::None => {
|
|
log::error!("Version should be specified for git plugin URL: {}", path);
|
|
Err(FetchError::FetchFailed {
|
|
name: name.to_string(),
|
|
url: path.to_string(),
|
|
reason: "Version should be specified as either a tag or a commit hash.".to_string(),
|
|
})
|
|
}
|
|
GitVersion::Single(version) => {
|
|
log::debug!("Fetching git plugin: {}@{}", name, version);
|
|
let shallow_flag = shallow.unwrap_or(false);
|
|
git_clone_and_checkout(
|
|
&baseurl,
|
|
&version.to_string(),
|
|
&basedir.join(name),
|
|
name,
|
|
shallow_flag,
|
|
)
|
|
.await
|
|
}
|
|
GitVersion::Mixed(tag, commit) => {
|
|
log::debug!("Fetching git plugin: {}@{}/{}", name, tag, commit);
|
|
let shallow_flag = shallow.unwrap_or(false);
|
|
let result =
|
|
git_clone_and_checkout(&baseurl, &tag, &basedir.join(name), name, shallow_flag)
|
|
.await;
|
|
if result.is_err() {
|
|
log::warn!(
|
|
"Failed to clone with tag, trying commit hash. URL: {}",
|
|
path
|
|
);
|
|
return git_clone_and_checkout(&baseurl, &commit, &basedir.join(name), name, false)
|
|
.await;
|
|
}
|
|
result
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_parse_git_url_no_version() {
|
|
let (url, version) = parse_git_url("https://github.com/user/repo");
|
|
assert_eq!(url, "https://github.com/user/repo");
|
|
assert_eq!(version, GitVersion::None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_git_url_single_tag() {
|
|
let (url, version) = parse_git_url("https://github.com/user/repo@v1.0.0");
|
|
assert_eq!(url, "https://github.com/user/repo");
|
|
assert_eq!(version, GitVersion::Single("v1.0.0".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_git_url_mixed_version() {
|
|
let (url, version) = parse_git_url("https://github.com/user/repo@main:abc123");
|
|
assert_eq!(url, "https://github.com/user/repo");
|
|
assert_eq!(
|
|
version,
|
|
GitVersion::Mixed("main".to_string(), "abc123".to_string())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_git_url_complex_url() {
|
|
let (url, version) = parse_git_url("git@github.com:user/repo.git@tag:commit");
|
|
assert_eq!(url, "git@github.com:user/repo.git");
|
|
assert_eq!(
|
|
version,
|
|
GitVersion::Mixed("tag".to_string(), "commit".to_string())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_git_url_single_with_at_in_path() {
|
|
let (url, version) = parse_git_url("https://github.com/user/repo@my-repo@v1.0");
|
|
assert_eq!(url, "https://github.com/user/repo@my-repo");
|
|
assert_eq!(version, GitVersion::Single("v1.0".to_string()));
|
|
}
|
|
}
|