pub mod bitbucket;
pub mod github;
pub mod gitlab;
use std::fmt;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum SourceError {
#[error("Invalid source format: {0}")]
InvalidFormat(String),
#[error("Unsupported source: {0}")]
Unsupported(String),
#[error("Authentication required for private repository")]
AuthRequired,
}
#[derive(Debug, Clone)]
pub enum RepoSource {
GitHub {
owner: String,
repo: String,
ref_name: Option<String>,
},
GitLab {
owner: String,
repo: String,
ref_name: Option<String>,
},
Bitbucket {
owner: String,
repo: String,
ref_name: Option<String>,
},
DirectUrl {
url: String,
},
}
impl RepoSource {
pub fn parse(input: &str) -> Result<Self, SourceError> {
if let Some(rest) = input.strip_prefix("github:") {
return Self::parse_github_shorthand(rest);
}
if let Some(rest) = input.strip_prefix("gitlab:") {
return Self::parse_gitlab_shorthand(rest);
}
if let Some(rest) = input.strip_prefix("bitbucket:") {
return Self::parse_bitbucket_shorthand(rest);
}
if input.starts_with("https://github.com/") {
return Self::parse_github_url(input);
}
if input.starts_with("https://gitlab.com/") {
return Self::parse_gitlab_url(input);
}
if input.starts_with("http://") || input.starts_with("https://") {
return Ok(Self::DirectUrl {
url: input.to_string(),
});
}
Err(SourceError::InvalidFormat(input.to_string()))
}
fn parse_github_shorthand(input: &str) -> Result<Self, SourceError> {
let (repo_part, ref_name) = if let Some(idx) = input.find('@') {
(&input[..idx], Some(input[idx + 1..].to_string()))
} else {
(input, None)
};
let parts: Vec<&str> = repo_part.split('/').collect();
if parts.len() != 2 {
return Err(SourceError::InvalidFormat(format!(
"Expected owner/repo, got: {}",
input
)));
}
Ok(Self::GitHub {
owner: parts[0].to_string(),
repo: parts[1].to_string(),
ref_name,
})
}
fn parse_gitlab_shorthand(input: &str) -> Result<Self, SourceError> {
let (repo_part, ref_name) = if let Some(idx) = input.find('@') {
(&input[..idx], Some(input[idx + 1..].to_string()))
} else {
(input, None)
};
let parts: Vec<&str> = repo_part.split('/').collect();
if parts.len() < 2 {
return Err(SourceError::InvalidFormat(format!(
"Expected owner/repo, got: {}",
input
)));
}
let repo = parts[parts.len() - 1].to_string();
let owner = parts[..parts.len() - 1].join("/");
Ok(Self::GitLab {
owner,
repo,
ref_name,
})
}
fn parse_bitbucket_shorthand(input: &str) -> Result<Self, SourceError> {
let (repo_part, ref_name) = if let Some(idx) = input.find('@') {
(&input[..idx], Some(input[idx + 1..].to_string()))
} else {
(input, None)
};
let parts: Vec<&str> = repo_part.split('/').collect();
if parts.len() != 2 {
return Err(SourceError::InvalidFormat(format!(
"Expected owner/repo, got: {}",
input
)));
}
Ok(Self::Bitbucket {
owner: parts[0].to_string(),
repo: parts[1].to_string(),
ref_name,
})
}
fn parse_github_url(input: &str) -> Result<Self, SourceError> {
let url = input.trim_end_matches('/').trim_end_matches(".git");
let parts: Vec<&str> = url.split('/').collect();
if parts.len() >= 5 && parts[2] == "github.com" {
Ok(Self::GitHub {
owner: parts[3].to_string(),
repo: parts[4].to_string(),
ref_name: None,
})
} else {
Err(SourceError::InvalidFormat(input.to_string()))
}
}
fn parse_gitlab_url(input: &str) -> Result<Self, SourceError> {
let url = input.trim_end_matches('/').trim_end_matches(".git");
let parts: Vec<&str> = url.split('/').collect();
if parts.len() >= 5 && parts[2] == "gitlab.com" {
let repo = parts[parts.len() - 1].to_string();
let owner = parts[3..parts.len() - 1].join("/");
Ok(Self::GitLab {
owner,
repo,
ref_name: None,
})
} else {
Err(SourceError::InvalidFormat(input.to_string()))
}
}
pub fn zip_url(&self) -> Result<String, SourceError> {
match self {
Self::GitHub {
owner,
repo,
ref_name,
} => {
let ref_str = ref_name.as_deref().unwrap_or("HEAD");
Ok(format!(
"https://github.com/{}/{}/archive/{}.zip",
owner, repo, ref_str
))
}
Self::GitLab {
owner,
repo,
ref_name,
} => {
let ref_str = ref_name.as_deref().unwrap_or("HEAD");
Ok(format!(
"https://gitlab.com/{}/{}/-/archive/{}/{}-{}.zip",
owner, repo, ref_str, repo, ref_str
))
}
Self::Bitbucket {
owner,
repo,
ref_name,
} => {
let ref_str = ref_name.as_deref().unwrap_or("main");
Ok(format!(
"https://bitbucket.org/{}/{}/get/{}.zip",
owner, repo, ref_str
))
}
Self::DirectUrl { url } => Ok(url.clone()),
}
}
pub fn host(&self) -> Option<String> {
match self {
Self::GitHub { .. } => Some("github.com".to_string()),
Self::GitLab { .. } => Some("gitlab.com".to_string()),
Self::Bitbucket { .. } => Some("bitbucket.org".to_string()),
Self::DirectUrl { url } => url.split('/').nth(2).map(|s| s.to_string()),
}
}
pub fn clone_url(&self) -> Result<String, SourceError> {
match self {
Self::GitHub { owner, repo, .. } => {
Ok(format!("https://github.com/{}/{}.git", owner, repo))
}
Self::GitLab { owner, repo, .. } => {
Ok(format!("https://gitlab.com/{}/{}.git", owner, repo))
}
Self::Bitbucket { owner, repo, .. } => {
Ok(format!("https://bitbucket.org/{}/{}.git", owner, repo))
}
Self::DirectUrl { .. } => Err(SourceError::Unsupported(
"Cannot clone from direct URL".to_string(),
)),
}
}
pub fn to_url(&self) -> String {
match self {
Self::GitHub { owner, repo, .. } => {
format!("https://github.com/{}/{}", owner, repo)
}
Self::GitLab { owner, repo, .. } => {
format!("https://gitlab.com/{}/{}", owner, repo)
}
Self::Bitbucket { owner, repo, .. } => {
format!("https://bitbucket.org/{}/{}", owner, repo)
}
Self::DirectUrl { url } => url.clone(),
}
}
pub fn hf_model_id(&self) -> Option<&str> {
None
}
pub fn ref_name(&self) -> Option<&str> {
match self {
Self::GitHub { ref_name, .. } => ref_name.as_deref(),
Self::GitLab { ref_name, .. } => ref_name.as_deref(),
Self::Bitbucket { ref_name, .. } => ref_name.as_deref(),
Self::DirectUrl { .. } => None,
}
}
}
impl fmt::Display for RepoSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::GitHub {
owner,
repo,
ref_name,
} => {
write!(f, "github:{}/{}", owner, repo)?;
if let Some(r) = ref_name {
write!(f, "@{}", r)?;
}
Ok(())
}
Self::GitLab {
owner,
repo,
ref_name,
} => {
write!(f, "gitlab:{}/{}", owner, repo)?;
if let Some(r) = ref_name {
write!(f, "@{}", r)?;
}
Ok(())
}
Self::Bitbucket {
owner,
repo,
ref_name,
} => {
write!(f, "bitbucket:{}/{}", owner, repo)?;
if let Some(r) = ref_name {
write!(f, "@{}", r)?;
}
Ok(())
}
Self::DirectUrl { url } => write!(f, "{}", url),
}
}
}