use std::path::PathBuf;
use async_trait::async_trait;
use git2::Repository;
use super::{SkillSource, SourceType};
use crate::cache::{CacheMetadata, CachePaths};
use crate::error::{Result, SkillsetError};
use crate::skill::types::{FetchedSkill, SkillMetadata};
pub struct GitSource {
cache: CachePaths,
}
impl GitSource {
pub fn new() -> Result<Self> {
let cache = CachePaths::new()?;
cache.ensure_directories()?;
Ok(Self { cache })
}
fn parse_reference(&self, reference: &str) -> Result<(String, Option<String>)> {
let git_url = reference
.strip_prefix("git:")
.unwrap_or(reference)
.to_string();
Ok((git_url, None))
}
fn extract_skill_name_from_url(&self, git_url: &str) -> Result<String> {
let parts: Vec<&str> = git_url.split('/').collect();
if parts.len() >= 2 {
let last_part = parts.last().unwrap();
let name = last_part.trim_end_matches(".git");
Ok(name.to_string())
} else {
Err(SkillsetError::Source(format!(
"Unable to extract skill name from git URL: {}",
git_url
)))
}
}
async fn get_or_clone(
&self,
url: &str,
reference: Option<&str>,
skill_name: &str,
) -> Result<PathBuf> {
let cache_key = self.cache.git_cache_key(url, reference);
let checkout_path = self.cache.git_checkout_path(skill_name);
let url_clone = url.to_string();
let checkout_path_clone = checkout_path.clone();
tokio::task::spawn_blocking(move || {
if checkout_path_clone.exists() {
std::fs::remove_dir_all(&checkout_path_clone)?;
}
if checkout_path_clone.exists() {
std::fs::remove_dir_all(&checkout_path_clone).map_err(|e| SkillsetError::Io(e))?;
}
Repository::clone(&url_clone, &checkout_path_clone)
.map_err(|e| SkillsetError::Git(e))?;
Ok::<PathBuf, SkillsetError>(checkout_path_clone)
})
.await
.map_err(|e| SkillsetError::Source(format!("Task execution failed: {}", e)))??;
let metadata = CacheMetadata {
url: url.to_string(),
reference: reference.map(|r| r.to_string()),
skill_name: skill_name.to_string(),
source_type: "git".to_string(),
};
let metadata_path = self.cache.metadata_path(&cache_key);
metadata.save(&metadata_path).await?;
Ok(checkout_path)
}
}
#[async_trait]
impl SkillSource for GitSource {
async fn fetch(&self, reference: &str) -> Result<FetchedSkill> {
let (url, ref_spec) = self.parse_reference(reference)?;
let skill_name = self.extract_skill_name_from_url(&url)?;
let checkout_path = self
.get_or_clone(&url, ref_spec.as_deref(), &skill_name)
.await?;
Ok(FetchedSkill {
name: skill_name,
version: ref_spec.unwrap_or_else(|| "latest".to_string()),
source_path: checkout_path.clone(),
metadata: SkillMetadata {
installed_at: chrono::Utc::now().to_rfc3339(),
repo_path: checkout_path,
convention: "unknown".to_string(), checksum: None,
description: None,
author: None,
dependencies: Vec::new(),
},
})
}
async fn get_metadata(&self, reference: &str) -> Result<SkillMetadata> {
let fetched_skill = self.fetch(reference).await?;
Ok(fetched_skill.metadata)
}
fn source_type(&self) -> SourceType {
SourceType::Git
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_parse_reference() {
let source = GitSource::new().unwrap();
let (url, reference) = source
.parse_reference("https://github.com/user/repo.git")
.unwrap();
assert_eq!(url, "https://github.com/user/repo.git");
assert_eq!(reference, None);
let (url, reference) = source
.parse_reference("git:https://github.com/user/repo.git")
.unwrap();
assert_eq!(url, "https://github.com/user/repo.git");
assert_eq!(reference, None);
}
#[test]
fn test_extract_skill_name_from_url() {
let source = GitSource::new().unwrap();
let name = source
.extract_skill_name_from_url("https://github.com/user/skill-name.git")
.unwrap();
assert_eq!(name, "skill-name");
let name = source
.extract_skill_name_from_url("https://github.com/user/skill-name")
.unwrap();
assert_eq!(name, "skill-name");
let result = source.extract_skill_name_from_url("invalid-url");
assert!(result.is_err());
}
}