-
-
Notifications
You must be signed in to change notification settings - Fork 948
feat(spm): add support for self-hosted and GitLab repositories #6358
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,7 @@ use crate::config::{Config, Settings}; | |
| use crate::git::{CloneOptions, Git}; | ||
| use crate::install_context::InstallContext; | ||
| use crate::toolset::ToolVersion; | ||
| use crate::{dirs, file, github}; | ||
| use crate::{dirs, file, github, gitlab}; | ||
| use async_trait::async_trait; | ||
| use eyre::WrapErr; | ||
| use serde::Deserializer; | ||
|
|
@@ -17,6 +17,7 @@ use std::{ | |
| fmt::{self, Debug}, | ||
| sync::Arc, | ||
| }; | ||
| use strum::{AsRefStr, EnumString, VariantNames}; | ||
| use url::Url; | ||
| use xx::regex; | ||
|
|
||
|
|
@@ -40,13 +41,26 @@ impl Backend for SPMBackend { | |
| } | ||
|
|
||
| async fn _list_remote_versions(&self, _config: &Arc<Config>) -> eyre::Result<Vec<String>> { | ||
| let repo = SwiftPackageRepo::new(&self.tool_name())?; | ||
| Ok(github::list_releases(repo.shorthand.as_str()) | ||
| .await? | ||
| .into_iter() | ||
| .map(|r| r.tag_name) | ||
| .rev() | ||
| .collect()) | ||
| let provider = GitProvider::from_ba(&self.ba); | ||
| let repo = SwiftPackageRepo::new(&self.tool_name(), &provider)?; | ||
| let releases = match provider.kind { | ||
| GitProviderKind::GitLab => { | ||
| gitlab::list_releases_from_url(&provider.api_url, repo.shorthand.as_str()) | ||
| .await? | ||
| .into_iter() | ||
| .map(|r| r.tag_name) | ||
| .rev() | ||
| .collect() | ||
| } | ||
| _ => github::list_releases_from_url(&provider.api_url, repo.shorthand.as_str()) | ||
| .await? | ||
| .into_iter() | ||
| .map(|r| r.tag_name) | ||
| .rev() | ||
| .collect(), | ||
| }; | ||
|
|
||
| Ok(releases) | ||
| } | ||
|
|
||
| async fn install_version_( | ||
|
|
@@ -66,8 +80,8 @@ impl Backend for SPMBackend { | |
| Or install Swift via https://swift.org/download/", | ||
| ) | ||
| .await; | ||
|
|
||
| let repo = SwiftPackageRepo::new(&self.tool_name())?; | ||
| let provider = GitProvider::from_ba(&self.ba); | ||
| let repo = SwiftPackageRepo::new(&self.tool_name(), &provider)?; | ||
| let revision = if tv.version == "latest" { | ||
| self.latest_stable_version(&ctx.config) | ||
| .await? | ||
|
|
@@ -208,6 +222,56 @@ impl SPMBackend { | |
| } | ||
| } | ||
|
|
||
| #[derive(Clone, Debug, Eq, PartialEq)] | ||
| pub struct GitProvider { | ||
| pub api_url: String, | ||
| pub kind: GitProviderKind, | ||
| } | ||
|
|
||
| impl Default for GitProvider { | ||
| fn default() -> Self { | ||
| Self { | ||
| api_url: github::API_URL.to_string(), | ||
| kind: GitProviderKind::GitHub, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[derive(AsRefStr, Clone, Debug, Eq, PartialEq, EnumString, VariantNames)] | ||
| pub enum GitProviderKind { | ||
| #[strum(serialize = "github")] | ||
| GitHub, | ||
| #[strum(serialize = "gitlab")] | ||
| GitLab, | ||
| } | ||
|
|
||
| impl GitProvider { | ||
| fn from_ba(ba: &BackendArg) -> Self { | ||
| let opts = ba.opts(); | ||
|
|
||
| let default_provider = GitProviderKind::GitHub.as_ref().to_string(); | ||
| let provider = opts.get("provider").unwrap_or(&default_provider); | ||
| let kind = if ba.tool_name.contains("gitlab.com") { | ||
| GitProviderKind::GitLab | ||
| } else { | ||
| match provider.to_lowercase().as_str() { | ||
| "gitlab" => GitProviderKind::GitLab, | ||
| _ => GitProviderKind::GitHub, | ||
| } | ||
| }; | ||
|
|
||
| let api_url = match opts.get("api_url") { | ||
| Some(api_url) => api_url.trim_end_matches('/').to_string(), | ||
| None => match kind { | ||
| GitProviderKind::GitHub => github::API_URL.to_string(), | ||
| GitProviderKind::GitLab => gitlab::API_URL.to_string(), | ||
| }, | ||
| }; | ||
|
|
||
| Self { api_url, kind } | ||
| } | ||
| } | ||
|
|
||
| #[derive(Debug)] | ||
| struct SwiftPackageRepo { | ||
| /// https://github.com/owner/repo.git | ||
|
|
@@ -218,26 +282,31 @@ struct SwiftPackageRepo { | |
|
|
||
| impl SwiftPackageRepo { | ||
| /// Parse the slug or the full URL of a GitHub package repository. | ||
| fn new(name: &str) -> Result<Self, eyre::Error> { | ||
| fn new(name: &str, provider: &GitProvider) -> Result<Self, eyre::Error> { | ||
| let name = name.strip_prefix("spm:").unwrap_or(name); | ||
| let shorthand_regex = regex!(r"^[a-zA-Z0-9_-]+/[a-zA-Z0-9._-]+$"); | ||
| let shorthand_in_url_regex = | ||
| regex!(r"https://github.com/([a-zA-Z0-9_-]+/[a-zA-Z0-9._-]+)\.git"); | ||
|
|
||
| let shorthand = if let Some(Some(m)) = | ||
| shorthand_in_url_regex.captures(name).map(|c| c.get(1)) | ||
| { | ||
| m.as_str() | ||
| let shorthand_regex = regex!(r"^(?:[a-zA-Z0-9_-]+/)+[a-zA-Z0-9._-]+$"); | ||
| let shorthand_in_url_regex = regex!( | ||
| r"^https://(?P<domain>[^/]+)/(?P<shorthand>(?:[a-zA-Z0-9_-]+/)+[a-zA-Z0-9._-]+)\.git" | ||
| ); | ||
|
|
||
| let (shorthand, url) = if let Some(caps) = shorthand_in_url_regex.captures(name) { | ||
| let shorthand = caps.name("shorthand").unwrap().as_str(); | ||
| let url = Url::parse(name)?; | ||
| (shorthand, url) | ||
| } else if shorthand_regex.is_match(name) { | ||
| name | ||
| let host = match provider.kind { | ||
| GitProviderKind::GitHub => "github.com", | ||
| GitProviderKind::GitLab => "gitlab.com", | ||
| }; | ||
| let url_str = format!("https://{}/{}.git", host, name); | ||
| let url = Url::parse(&url_str)?; | ||
| (name, url) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: SPM Incorrectly Constructs URLs for Custom Git ProvidersShorthand SPM package notation with self-hosted Git providers constructs incorrect clone URLs. The
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this a problem @roele ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did not account for supporting shorthand notation for self-hosted repositories. Not sure we can reliably derive the repository URL from the API URL, so i rather only have support for self-hosted via https:// notation. This should probably be stated in the docs though. |
||
| } else { | ||
| Err(eyre::eyre!( | ||
| "Invalid Swift package repository: {}. The repository should either be a GitHub repository slug, owner/name, or the complete URL, https://github.com/owner/name.", | ||
| "Invalid Swift package repository: {}. The repository should either be a repository slug (owner/name), or the complete URL (e.g. https://github.com/owner/name.git).", | ||
| name | ||
| ))? | ||
| }; | ||
| let url_str = format!("https://github.com/{shorthand}.git"); | ||
| let url = Url::parse(&url_str)?; | ||
|
|
||
| Ok(Self { | ||
| url, | ||
|
|
@@ -248,54 +317,134 @@ impl SwiftPackageRepo { | |
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use crate::config::Config; | ||
| use crate::{config::Config, toolset::ToolVersionOptions}; | ||
|
|
||
| use super::*; | ||
| use indexmap::indexmap; | ||
| use pretty_assertions::assert_str_eq; | ||
|
|
||
| #[tokio::test] | ||
| async fn test_git_provider_from_ba() { | ||
| // Example of defining a capture (closure) in Rust: | ||
| let get_ba = |tool: String, opts: Option<ToolVersionOptions>| { | ||
| BackendArg::new_raw("spm".to_string(), Some(tool.clone()), tool, opts) | ||
| }; | ||
|
|
||
| assert_eq!( | ||
| GitProvider::from_ba(&get_ba("tool".to_string(), None)), | ||
| GitProvider { | ||
| api_url: github::API_URL.to_string(), | ||
| kind: GitProviderKind::GitHub | ||
| } | ||
| ); | ||
|
|
||
| assert_eq!( | ||
| GitProvider::from_ba(&get_ba( | ||
| "tool".to_string(), | ||
| Some(ToolVersionOptions { | ||
| opts: indexmap![ | ||
| "provider".to_string() => "gitlab".to_string() | ||
| ], | ||
| ..Default::default() | ||
| }) | ||
| )), | ||
| GitProvider { | ||
| api_url: gitlab::API_URL.to_string(), | ||
| kind: GitProviderKind::GitLab | ||
| } | ||
| ); | ||
|
|
||
| assert_eq!( | ||
| GitProvider::from_ba(&get_ba( | ||
| "tool".to_string(), | ||
| Some(ToolVersionOptions { | ||
| opts: indexmap![ | ||
| "api_url".to_string() => "https://gitlab.acme.com/api/v4".to_string(), | ||
| "provider".to_string() => "gitlab".to_string(), | ||
| ], | ||
| ..Default::default() | ||
| }) | ||
| )), | ||
| GitProvider { | ||
| api_url: "https://gitlab.acme.com/api/v4".to_string(), | ||
| kind: GitProviderKind::GitLab | ||
| } | ||
| ); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn test_spm_repo_init_by_shorthand() { | ||
| let _config = Config::get().await.unwrap(); | ||
| let package_name = "nicklockwood/SwiftFormat"; | ||
| let package_repo = SwiftPackageRepo::new(package_name).unwrap(); | ||
| let package_repo = | ||
| SwiftPackageRepo::new("nicklockwood/SwiftFormat", &GitProvider::default()).unwrap(); | ||
| assert_str_eq!( | ||
| package_repo.url.as_str(), | ||
| "https://github.com/nicklockwood/SwiftFormat.git" | ||
| ); | ||
| assert_str_eq!(package_repo.shorthand, "nicklockwood/SwiftFormat"); | ||
|
|
||
| let package_repo = SwiftPackageRepo::new( | ||
| "acme/nicklockwood/SwiftFormat", | ||
| &GitProvider { | ||
| api_url: gitlab::API_URL.to_string(), | ||
| kind: GitProviderKind::GitLab, | ||
| }, | ||
| ) | ||
| .unwrap(); | ||
| assert_str_eq!( | ||
| package_repo.url.as_str(), | ||
| "https://gitlab.com/acme/nicklockwood/SwiftFormat.git" | ||
| ); | ||
| assert_str_eq!(package_repo.shorthand, "acme/nicklockwood/SwiftFormat"); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn test_spm_repo_init_name() { | ||
| let _config = Config::get().await.unwrap(); | ||
| assert!( | ||
| SwiftPackageRepo::new("owner/name.swift").is_ok(), | ||
| SwiftPackageRepo::new("owner/name.swift", &GitProvider::default()).is_ok(), | ||
| "name part can contain ." | ||
| ); | ||
| assert!( | ||
| SwiftPackageRepo::new("owner/name_swift").is_ok(), | ||
| SwiftPackageRepo::new("owner/name_swift", &GitProvider::default()).is_ok(), | ||
| "name part can contain _" | ||
| ); | ||
| assert!( | ||
| SwiftPackageRepo::new("owner/name-swift").is_ok(), | ||
| SwiftPackageRepo::new("owner/name-swift", &GitProvider::default()).is_ok(), | ||
| "name part can contain -" | ||
| ); | ||
| assert!( | ||
| SwiftPackageRepo::new("owner/name$swift").is_err(), | ||
| SwiftPackageRepo::new("owner/name$swift", &GitProvider::default()).is_err(), | ||
| "name part cannot contain characters other than a-zA-Z0-9._-" | ||
| ); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn test_spm_repo_init_by_url() { | ||
| let _config = Config::get().await.unwrap(); | ||
| let package_name = "https://github.com/nicklockwood/SwiftFormat.git"; | ||
| let package_repo = SwiftPackageRepo::new(package_name).unwrap(); | ||
| let package_repo = SwiftPackageRepo::new( | ||
| "https://github.com/nicklockwood/SwiftFormat.git", | ||
| &GitProvider::default(), | ||
| ) | ||
| .unwrap(); | ||
| assert_str_eq!( | ||
| package_repo.url.as_str(), | ||
| "https://github.com/nicklockwood/SwiftFormat.git" | ||
| ); | ||
| assert_str_eq!(package_repo.shorthand, "nicklockwood/SwiftFormat"); | ||
|
|
||
| let package_repo = SwiftPackageRepo::new( | ||
| "https://gitlab.acme.com/acme/someuser/SwiftTool.git", | ||
| &GitProvider { | ||
| api_url: "https://api.gitlab.acme.com/api/v4".to_string(), | ||
| kind: GitProviderKind::GitLab, | ||
| }, | ||
| ) | ||
| .unwrap(); | ||
| assert_str_eq!( | ||
| package_repo.url.as_str(), | ||
| "https://gitlab.acme.com/acme/someuser/SwiftTool.git" | ||
| ); | ||
| assert_str_eq!(package_repo.shorthand, "acme/someuser/SwiftTool"); | ||
| } | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.