From 96aee5372b8f94cc8f3d540061941b7980d6ed6c Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Mon, 23 Dec 2024 21:50:34 -0600 Subject: [PATCH] Implement support for GitLab This involved a big refactor to split the GitHub-specific code out into its own struct and add a new Forge trait that both the GitHub and GitLab structs implement. I used GitHub Copilot Workspaces to implement a first pass at this, though I ended up making some pretty significant changes to what it produced. --- Cargo.lock | 15 ++- Cargo.toml | 1 + ubi-cli/Cargo.toml | 1 + ubi-cli/src/main.rs | 19 ++- ubi-cli/tests/ubi.rs | 44 ++++++ ubi/Cargo.toml | 1 + ubi/src/fetcher.rs | 69 ---------- ubi/src/forge.rs | 73 ++++++++++ ubi/src/github.rs | 97 +++++++++++++ ubi/src/gitlab.rs | 99 ++++++++++++++ ubi/src/lib.rs | 314 +++++++++++++++++++++---------------------- ubi/src/release.rs | 5 - 12 files changed, 502 insertions(+), 236 deletions(-) delete mode 100644 ubi/src/fetcher.rs create mode 100644 ubi/src/forge.rs create mode 100644 ubi/src/github.rs create mode 100644 ubi/src/gitlab.rs diff --git a/Cargo.lock b/Cargo.lock index 5db3bc5..c45fb80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -124,6 +124,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -2142,6 +2153,7 @@ name = "ubi" version = "0.2.4" dependencies = [ "anyhow", + "async-trait", "binstall-tar", "bzip2", "document-features", @@ -2174,6 +2186,7 @@ dependencies = [ "anyhow", "clap", "log", + "strum", "tempfile", "thiserror", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 940ccf8..f9e74ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ resolver = "2" [workspace.dependencies] anyhow = "1.0.93" +async-trait = "0.1.50" binstall-tar = "0.4.42" bzip2 = "0.4.4" clap = { version = "4.5.21", features = ["wrap_help"] } diff --git a/ubi-cli/Cargo.toml b/ubi-cli/Cargo.toml index c696527..576d286 100644 --- a/ubi-cli/Cargo.toml +++ b/ubi-cli/Cargo.toml @@ -12,6 +12,7 @@ edition.workspace = true anyhow.workspace = true clap.workspace = true log.workspace = true +strum.workspace = true tempfile.workspace = true thiserror.workspace = true tokio.workspace = true diff --git a/ubi-cli/src/main.rs b/ubi-cli/src/main.rs index a8ea3b9..16570df 100644 --- a/ubi-cli/src/main.rs +++ b/ubi-cli/src/main.rs @@ -4,9 +4,11 @@ use log::{debug, error}; use std::{ env, path::{Path, PathBuf}, + str::FromStr, }; +use strum::VariantNames; use thiserror::Error; -use ubi::{Ubi, UbiBuilder}; +use ubi::{ForgeType, Ubi, UbiBuilder}; #[derive(Debug, Error)] enum UbiError { @@ -115,6 +117,18 @@ fn cmd() -> Command { " is only one matching release filename for your OS/arch.", )), ) + .arg( + Arg::new("forge") + .long("forge") + .value_parser(clap::builder::PossibleValuesParser::new( + ForgeType::VARIANTS, + )) + .help(concat!( + "The forge to use. If this isn't set, then the value of --url will be checked", + " for gitlab.com. If --url contains any other domain _or_ if it is not, the", + " default is GitHub.", + )), + ) .arg( Arg::new("verbose") .short('v') @@ -182,6 +196,9 @@ fn make_ubi<'a>( if let Some(e) = matches.get_one::("exe") { builder = builder.exe(e); } + if let Some(ft) = matches.get_one::("forge") { + builder = builder.forge(ForgeType::from_str(ft)?); + } Ok((builder.build()?, None)) } diff --git a/ubi-cli/tests/ubi.rs b/ubi-cli/tests/ubi.rs index 80bcced..7d69d45 100644 --- a/ubi-cli/tests/ubi.rs +++ b/ubi-cli/tests/ubi.rs @@ -427,6 +427,50 @@ fn integration_tests() -> Result<()> { make_exe_pathbuf(&["bin", "wren_cli"]), )?; + run_test( + td.path(), + ubi.as_ref(), + &[ + "--project", + "gitlab-org/cli", + "--exe", + "glab", + "--forge", + "gitlab", + ], + make_exe_pathbuf(&["bin", "glab"]), + )?; + + run_test( + td.path(), + ubi.as_ref(), + &[ + "--project", + "gitlab-org/cli", + "--tag", + "v1.49.0", + "--exe", + "glab", + "--forge", + "gitlab", + ], + make_exe_pathbuf(&["bin", "glab"]), + )?; + + run_test( + td.path(), + ubi.as_ref(), + &[ + "--project", + "https://gitlab.com/gitlab-org/cli/-/releases", + "--tag", + "v1.49.0", + "--exe", + "glab", + ], + make_exe_pathbuf(&["bin", "glab"]), + )?; + Ok(()) } diff --git a/ubi/Cargo.toml b/ubi/Cargo.toml index 3853478..c7b9797 100644 --- a/ubi/Cargo.toml +++ b/ubi/Cargo.toml @@ -10,6 +10,7 @@ edition.workspace = true [dependencies] anyhow.workspace = true +async-trait.workspace = true binstall-tar.workspace = true bzip2.workspace = true document-features.workspace = true diff --git a/ubi/src/fetcher.rs b/ubi/src/fetcher.rs deleted file mode 100644 index c8c5e89..0000000 --- a/ubi/src/fetcher.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::release::{Asset, Release}; -use anyhow::Result; -use reqwest::{header::HeaderValue, header::ACCEPT, Client}; -use url::Url; - -#[derive(Debug)] -pub(crate) struct GitHubAssetFetcher { - project_name: String, - tag: Option, - url: Option, - github_api_base: String, -} - -const GITHUB_API_BASE: &str = "https://api.github.com"; - -impl GitHubAssetFetcher { - pub(crate) fn new( - project_name: String, - tag: Option, - url: Option, - github_api_base: Option, - ) -> Self { - Self { - project_name, - tag, - url, - github_api_base: github_api_base.unwrap_or(GITHUB_API_BASE.to_string()), - } - } - - pub(crate) async fn fetch_assets(&self, client: &Client) -> Result> { - if let Some(url) = &self.url { - return Ok(vec![Asset { - name: url.path().split('/').last().unwrap().to_string(), - url: url.clone(), - }]); - } - - Ok(self.release_info(client).await?.assets) - } - - async fn release_info(&self, client: &Client) -> Result { - let mut parts = self.project_name.split('/'); - let owner = parts.next().unwrap(); - let repo = parts.next().unwrap(); - - let url = match &self.tag { - Some(tag) => format!( - "{}/repos/{owner}/{repo}/releases/tags/{tag}", - self.github_api_base, - ), - None => format!( - "{}/repos/{owner}/{repo}/releases/latest", - self.github_api_base, - ), - }; - let req = client - .get(url) - .header(ACCEPT, HeaderValue::from_str("application/json")?) - .build()?; - let resp = client.execute(req).await?; - - if let Err(e) = resp.error_for_status_ref() { - return Err(anyhow::Error::new(e)); - } - - Ok(resp.json::().await?) - } -} diff --git a/ubi/src/forge.rs b/ubi/src/forge.rs new file mode 100644 index 0000000..b4cfa15 --- /dev/null +++ b/ubi/src/forge.rs @@ -0,0 +1,73 @@ +use crate::release::Asset; +use anyhow::Result; +use async_trait::async_trait; +use reqwest::{ + header::{HeaderValue, ACCEPT}, + Client, RequestBuilder, Response, +}; +// It'd be nice to use clap::ValueEnum here, but then we'd need to add clap as a dependency for the +// library code, which would be annoying for downstream users who just want to use the library. +use strum::{AsRefStr, EnumString, VariantNames}; +use url::Url; + +#[derive(AsRefStr, Clone, Debug, Default, EnumString, PartialEq, Eq, VariantNames)] +#[allow(clippy::module_name_repetitions)] +pub enum ForgeType { + #[strum(serialize = "github")] + #[default] + GitHub, + #[strum(serialize = "gitlab")] + GitLab, +} + +#[async_trait] +pub(crate) trait Forge: std::fmt::Debug { + async fn fetch_assets(&self, client: &Client) -> Result>; + + fn release_info_url(&self) -> Url; + fn maybe_add_token_header(&self, req_builder: RequestBuilder) -> Result; + + async fn make_release_info_request(&self, client: &Client) -> Result { + let mut req_builder = client + .get(self.release_info_url()) + .header(ACCEPT, HeaderValue::from_str("application/json")?); + req_builder = self.maybe_add_token_header(req_builder)?; + let resp = client.execute(req_builder.build()?).await?; + + if let Err(e) = resp.error_for_status_ref() { + return Err(anyhow::Error::new(e)); + } + + Ok(resp) + } +} + +const GITHUB_DOMAIN: &str = "github.com"; +const GITLAB_DOMAIN: &str = "gitlab.com"; + +const GITHUB_API_BASE: &str = "https://api.github.com"; +const GITLAB_API_BASE: &str = "https://gitlab.com/api/v4"; + +impl ForgeType { + pub(crate) fn from_url(url: &Url) -> ForgeType { + if url.domain().unwrap().contains(GITLAB_DOMAIN) { + ForgeType::GitLab + } else { + ForgeType::default() + } + } + + pub(crate) fn url_base(&self) -> Url { + match self { + ForgeType::GitHub => Url::parse(&format!("https://{GITHUB_DOMAIN}")).unwrap(), + ForgeType::GitLab => Url::parse(&format!("https://{GITLAB_DOMAIN}")).unwrap(), + } + } + + pub(crate) fn api_base(&self) -> Url { + match self { + ForgeType::GitHub => Url::parse(GITHUB_API_BASE).unwrap(), + ForgeType::GitLab => Url::parse(GITLAB_API_BASE).unwrap(), + } + } +} diff --git a/ubi/src/github.rs b/ubi/src/github.rs new file mode 100644 index 0000000..92543cd --- /dev/null +++ b/ubi/src/github.rs @@ -0,0 +1,97 @@ +use crate::{ + forge::{Forge, ForgeType}, + release::Asset, +}; +use anyhow::Result; +use async_trait::async_trait; +use log::debug; +use reqwest::{ + header::{HeaderValue, AUTHORIZATION}, + Client, RequestBuilder, +}; +use serde::Deserialize; +use std::env; +use url::Url; + +#[derive(Debug)] +pub(crate) struct GitHub { + project_name: String, + tag: Option, + api_base: Url, + token: Option, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct Release { + pub(crate) assets: Vec, +} + +#[async_trait] +impl Forge for GitHub { + async fn fetch_assets(&self, client: &Client) -> Result> { + Ok(self + .make_release_info_request(client) + .await? + .json::() + .await? + .assets) + } + + fn release_info_url(&self) -> Url { + let mut parts = self.project_name.split('/'); + let owner = parts.next().unwrap(); + let repo = parts.next().unwrap(); + + let mut url = self.api_base.clone(); + url.path_segments_mut() + .expect("could not get path segments for url") + .push("repos") + .push(owner) + .push(repo) + .push("releases"); + if let Some(tag) = &self.tag { + url.path_segments_mut() + .expect("could not get path segments for url") + .push("tags") + .push(tag); + } else { + url.path_segments_mut() + .expect("could not get path segments for url") + .push("latest"); + } + + url + } + + fn maybe_add_token_header(&self, mut req_builder: RequestBuilder) -> Result { + if let Some(token) = self.token.as_deref() { + debug!("Adding GitHub token to GitHub request."); + let bearer = format!("Bearer {token}"); + let mut auth_val = HeaderValue::from_str(&bearer)?; + auth_val.set_sensitive(true); + req_builder = req_builder.header(AUTHORIZATION, auth_val); + } + Ok(req_builder) + } +} + +impl GitHub { + pub(crate) fn new( + project_name: String, + tag: Option, + api_base: Option, + token: Option<&str>, + ) -> Self { + let mut token = token.map(String::from); + if token.is_none() { + token = env::var("GITHUB_TOKEN").ok(); + } + + Self { + project_name, + tag, + api_base: api_base.unwrap_or_else(|| ForgeType::GitHub.api_base()), + token, + } + } +} diff --git a/ubi/src/gitlab.rs b/ubi/src/gitlab.rs new file mode 100644 index 0000000..4152ed8 --- /dev/null +++ b/ubi/src/gitlab.rs @@ -0,0 +1,99 @@ +use crate::{ + forge::{Forge, ForgeType}, + release::Asset, +}; +use anyhow::Result; +use async_trait::async_trait; +use log::debug; +use reqwest::{header::HeaderValue, header::AUTHORIZATION, Client, RequestBuilder}; +use serde::Deserialize; +use std::env; +use url::Url; + +#[derive(Debug)] +pub(crate) struct GitLab { + project_name: String, + tag: Option, + api_base: Url, + token: Option, +} + +#[derive(Debug, Deserialize)] +struct Release { + assets: GitLabAssets, +} + +#[derive(Debug, Deserialize)] +struct GitLabAssets { + links: Vec, +} + +#[async_trait] +impl Forge for GitLab { + async fn fetch_assets(&self, client: &Client) -> Result> { + Ok(self + .make_release_info_request(client) + .await? + .json::() + .await? + .assets + .links) + } + + fn release_info_url(&self) -> Url { + let mut url = self.api_base.clone(); + url.path_segments_mut() + .expect("could not get path segments for url") + .push("projects") + .push(&self.project_name) + .push("releases"); + if let Some(tag) = &self.tag { + url.path_segments_mut() + .expect("could not get path segments for url") + .push(tag); + } else { + url.path_segments_mut() + .expect("could not get path segments for url") + .extend(&["permalink", "latest"]); + } + + url + } + + fn maybe_add_token_header(&self, mut req_builder: RequestBuilder) -> Result { + if let Some(token) = self.token.as_deref() { + debug!("Adding GitLab token to GitLab request."); + let bearer = format!("Bearer {token}"); + let mut auth_val = HeaderValue::from_str(&bearer)?; + auth_val.set_sensitive(true); + req_builder = req_builder.header(AUTHORIZATION, auth_val); + } else { + debug!("No GitLab token found."); + } + Ok(req_builder) + } +} + +impl GitLab { + pub(crate) fn new( + project_name: String, + tag: Option, + api_base: Option, + token: Option<&str>, + ) -> Self { + let mut token = token.map(String::from); + if token.is_none() { + token = env::var("CI_JOB_TOKEN").ok(); + } + if token.is_none() { + token = env::var("GITLAB_TOKEN").ok(); + } + + Self { + project_name, + tag, + api_base: api_base.unwrap_or_else(|| ForgeType::GitLab.api_base()), + token, + } + } +} diff --git a/ubi/src/lib.rs b/ubi/src/lib.rs index 1740182..5d0cdcd 100644 --- a/ubi/src/lib.rs +++ b/ubi/src/lib.rs @@ -30,21 +30,24 @@ mod arch; mod extension; -mod fetcher; +mod forge; +mod github; +mod gitlab; mod installer; mod os; mod picker; mod release; +pub use crate::forge::ForgeType; use crate::{ - fetcher::GitHubAssetFetcher, installer::Installer, picker::AssetPicker, release::Asset, - release::Download, + forge::Forge, github::GitHub, gitlab::GitLab, installer::Installer, picker::AssetPicker, + release::Asset, release::Download, }; use anyhow::{anyhow, Result}; use log::debug; use platforms::{Platform, PlatformReq, OS}; use reqwest::{ - header::{HeaderMap, HeaderValue, ACCEPT, AUTHORIZATION, USER_AGENT}, + header::{HeaderMap, HeaderValue, ACCEPT, USER_AGENT}, Client, StatusCode, }; use std::{ @@ -71,9 +74,11 @@ pub struct UbiBuilder<'a> { matching: Option<&'a str>, exe: Option<&'a str>, github_token: Option<&'a str>, + gitlab_token: Option<&'a str>, platform: Option<&'a Platform>, is_musl: Option, - github_api_url_base: Option, + url_base: Option, + forge: Option, } impl<'a> UbiBuilder<'a> { @@ -149,6 +154,15 @@ impl<'a> UbiBuilder<'a> { self } + /// Set a GitLab token to use for API requests. If this is not set then this will be taken from + ////the `CI_JOB_TOKEN` or `GITLAB_TOKEN` env var if one of these is set. If both are set, then + /// the value `CI_JOB_TOKEN` will be used. + #[must_use] + pub fn gitlab_token(mut self, token: &'a str) -> Self { + self.gitlab_token = Some(token); + self + } + /// Set the platform to download for. If not set it will be determined based on the current /// platform's OS/arch. #[must_use] @@ -166,11 +180,21 @@ impl<'a> UbiBuilder<'a> { self } - /// Set the base URL for the GitHub API. This is useful for testing or if you want to operate - /// against a GitHub Enterprise installation. + /// Set the forge type to use for fetching assets and release information. This determines which + /// REST API is used to get information about releases and to download the release. If this isn't + /// set, then this will be determined from the hostname in the url, if that is set. Otherwise, + /// the default is GitHub. + #[must_use] + pub fn forge(mut self, forge: ForgeType) -> Self { + self.forge = Some(forge); + self + } + + /// Set the base URL for the forge site's API. This is useful for testing or if you want to operate + /// against an Enterprise version of GitHub or GitLab, #[must_use] - pub fn github_api_url_base(mut self, github_api_url_base: String) -> Self { - self.github_api_url_base = Some(github_api_url_base); + pub fn url_base(mut self, url_base: String) -> Self { + self.url_base = Some(url_base); self } @@ -219,12 +243,14 @@ impl<'a> UbiBuilder<'a> { self.matching, self.exe, self.github_token, + self.gitlab_token, platform, match self.is_musl { Some(m) => m, None => platform_is_musl(platform), }, - self.github_api_url_base, + self.url_base.as_deref(), + self.forge, ) } } @@ -251,7 +277,8 @@ fn platform_is_musl(platform: &Platform) -> bool { /// [`UbiBuilder`] struct to create a new `Ubi` instance. #[derive(Debug)] pub struct Ubi<'a> { - asset_fetcher: GitHubAssetFetcher, + forge: Box, + asset_url: Option, asset_picker: AssetPicker<'a>, installer: Installer, reqwest_client: Client, @@ -268,37 +295,52 @@ impl<'a> Ubi<'a> { matching: Option<&'a str>, exe: Option<&str>, github_token: Option<&str>, + gitlab_token: Option<&str>, platform: &'a Platform, is_musl: bool, - github_api_url_base: Option, + url_base: Option<&str>, + forge: Option, ) -> Result> { - let url = if let Some(u) = url { - Some(Url::parse(u)?) - } else { - None - }; - let project_name = Self::parse_project_name(project, url.as_ref())?; + let url = url.map(Url::parse).transpose()?; + let (project_name, forge) = Self::parse_project_name(project, url.as_ref(), forge)?; let exe = Self::exe_name(exe, &project_name, platform); let install_path = Self::install_path(install_dir, &exe)?; - Ok(Ubi { - asset_fetcher: GitHubAssetFetcher::new( + + let api_base = url_base.map(Url::parse).transpose()?; + + let asset_fetcher: Box = match forge { + ForgeType::GitHub => Box::new(GitHub::new( project_name, - tag.map(std::string::ToString::to_string), - url, - github_api_url_base, - ), + tag.map(String::from), + api_base, + github_token, + )), + ForgeType::GitLab => Box::new(GitLab::new( + project_name, + tag.map(String::from), + api_base, + gitlab_token, + )), + }; + Ok(Ubi { + forge: asset_fetcher, + asset_url: url, asset_picker: AssetPicker::new(matching, platform, is_musl), installer: Installer::new(install_path, exe), - reqwest_client: Self::reqwest_client(github_token)?, + reqwest_client: Self::reqwest_client()?, }) } - fn parse_project_name(project: Option<&str>, url: Option<&Url>) -> Result { + fn parse_project_name( + project: Option<&str>, + url: Option<&Url>, + forge: Option, + ) -> Result<(String, ForgeType)> { let (parsed, from) = if let Some(project) = project { if project.starts_with("http") { (Url::parse(project)?, format!("--project {project}")) } else { - let base = Url::parse("https://github.com")?; + let base = forge.unwrap_or_default().url_base(); (base.join(project)?, format!("--project {project}")) } } else if let Some(u) = url { @@ -318,7 +360,12 @@ impl<'a> Ubi<'a> { let (org, proj) = (parts[1], parts[2]); debug!("Parsed {from} = {org} / {proj}"); - Ok(format!("{org}/{proj}")) + Ok(( + format!("{org}/{proj}"), + // If the forge argument was not `None` this is kind of pointless, but it should never + // be _wrong_ in that case. + ForgeType::from_url(&parsed), + )) } fn exe_name(exe: Option<&str>, project_name: &str, platform: &Platform) -> String { @@ -354,7 +401,7 @@ impl<'a> Ubi<'a> { Ok(path) } - fn reqwest_client(github_token: Option<&str>) -> Result { + fn reqwest_client() -> Result { let builder = Client::builder().gzip(true); let mut headers = HeaderMap::new(); @@ -362,20 +409,6 @@ impl<'a> Ubi<'a> { USER_AGENT, HeaderValue::from_str(&format!("ubi version {VERSION}"))?, ); - - let mut github_token = github_token.map(String::from); - if github_token.is_none() { - github_token = env::var("GITHUB_TOKEN").ok(); - } - - if let Some(token) = github_token { - debug!("adding GitHub token to GitHub requests"); - let bearer = format!("Bearer {token}"); - let mut auth_val = HeaderValue::from_str(&bearer)?; - auth_val.set_sensitive(true); - headers.insert(AUTHORIZATION, auth_val); - } - Ok(builder.default_headers(headers).build()?) } @@ -404,10 +437,14 @@ impl<'a> Ubi<'a> { } async fn asset(&mut self) -> Result { - let assets = self - .asset_fetcher - .fetch_assets(&self.reqwest_client) - .await?; + if let Some(url) = &self.asset_url { + return Ok(Asset { + name: url.path().split('/').last().unwrap().to_string(), + url: url.clone(), + }); + } + + let assets = self.forge.fetch_assets(&self.reqwest_client).await?; let asset = self.asset_picker.pick_asset(assets)?; debug!("picked asset named {}", asset.name); Ok(asset) @@ -416,10 +453,12 @@ impl<'a> Ubi<'a> { async fn download_asset(&self, client: &Client, asset: Asset) -> Result { debug!("downloading asset from {}", asset.url); - let req = client + let mut req_builder = client .get(asset.url.clone()) - .header(ACCEPT, HeaderValue::from_str("application/octet-stream")?) - .build()?; + .header(ACCEPT, HeaderValue::from_str("application/octet-stream")?); + req_builder = self.forge.maybe_add_token_header(req_builder)?; + let req = req_builder.build()?; + let mut resp = self.reqwest_client.execute(req).await?; if resp.status() != StatusCode::OK { let mut msg = format!("error requesting {}: {}", asset.url, resp.status()); @@ -512,20 +551,38 @@ mod test { format!("https://github.com/{org_and_repo}/actions/runs/4275745616"), ]; for p in projects { - let project_name = Ubi::parse_project_name(Some(p), None)?; + let (project_name, forge_type) = Ubi::parse_project_name(Some(p), None, None)?; assert_eq!( project_name, org_and_repo, "got the right project from --project {p}", ); + assert_eq!(forge_type, ForgeType::GitHub); + + let (project_name, forge_type) = + Ubi::parse_project_name(Some(p), None, Some(ForgeType::GitHub))?; + assert_eq!( + project_name, org_and_repo, + "got the right project from --project {p}", + ); + assert_eq!(forge_type, ForgeType::GitHub); } { let url = Url::parse("https://github.com/houseabsolute/precious/releases/download/v0.1.7/precious-Linux-x86_64-musl.tar.gz")?; - let project_name = Ubi::parse_project_name(None, Some(&url))?; + let (project_name, forge_type) = Ubi::parse_project_name(None, Some(&url), None)?; assert_eq!( project_name, "houseabsolute/precious", "got the right project from the --url", ); + assert_eq!(forge_type, ForgeType::GitHub); + + let (project_name, forge_type) = + Ubi::parse_project_name(None, Some(&url), Some(ForgeType::GitHub))?; + assert_eq!( + project_name, "houseabsolute/precious", + "got the right project from the --url", + ); + assert_eq!(forge_type, ForgeType::GitHub); } Ok(()) @@ -743,18 +800,11 @@ mod test { let platform = req.matching_platforms().next().unwrap(); if let Some(expect_ubi) = t.expect_ubi { - let mut ubi = Ubi::new( - Some("houseabsolute/ubi"), - None, - None, - None, - None, - None, - None, - platform, - false, - Some(server.url()), - )?; + let mut ubi = UbiBuilder::new() + .project("houseabsolute/ubi") + .platform(platform) + .url_base(server.url()) + .build()?; let asset = ubi.asset().await?; let expect_ubi_url = Url::parse(&format!( "https://api.github.com/repos/houseabsolute/ubi/releases/assets/{}", @@ -772,18 +822,11 @@ mod test { } if let Some(expect_omegasort) = t.expect_omegasort { - let mut ubi = Ubi::new( - Some("houseabsolute/omegasort"), - None, - None, - None, - None, - None, - None, - platform, - false, - Some(server.url()), - )?; + let mut ubi = UbiBuilder::new() + .project("houseabsolute/omegasort") + .platform(platform) + .url_base(server.url()) + .build()?; let asset = ubi.asset().await?; let expect_omegasort_url = Url::parse(&format!( "https://api.github.com/repos/houseabsolute/omegasort/releases/assets/{}", @@ -1110,18 +1153,11 @@ mod test { let req = PlatformReq::from_str(p) .unwrap_or_else(|e| panic!("could not create PlatformReq for {p}: {e}")); let platform = req.matching_platforms().next().unwrap(); - let mut ubi = Ubi::new( - Some("protocolbuffers/protobuf"), - None, - None, - None, - None, - None, - None, - platform, - false, - Some(server.url()), - )?; + let mut ubi = UbiBuilder::new() + .project("protocolbuffers/protobuf") + .platform(platform) + .url_base(server.url()) + .build()?; let asset = ubi.asset().await?; assert_eq!( asset.name, t.expect, @@ -1240,18 +1276,11 @@ mod test { let req = PlatformReq::from_str(p) .unwrap_or_else(|e| panic!("could not create PlatformReq for {p}: {e}")); let platform = req.matching_platforms().next().unwrap(); - let mut ubi = Ubi::new( - Some("FiloSottile/mkcert"), - None, - None, - None, - None, - None, - None, - platform, - false, - Some(server.url()), - )?; + let mut ubi = UbiBuilder::new() + .project("FiloSottile/mkcert") + .platform(platform) + .url_base(server.url()) + .build()?; let asset = ubi.asset().await?; assert_eq!( asset.name, t.expect, @@ -1341,18 +1370,11 @@ mod test { let req = PlatformReq::from_str(p) .unwrap_or_else(|e| panic!("could not create PlatformReq for {p}: {e}")); let platform = req.matching_platforms().next().unwrap(); - let mut ubi = Ubi::new( - Some("stedolan/jq"), - None, - None, - None, - None, - None, - None, - platform, - false, - Some(server.url()), - )?; + let mut ubi = UbiBuilder::new() + .project("stedolan/jq") + .platform(platform) + .url_base(server.url()) + .build()?; let asset = ubi.asset().await?; assert_eq!( asset.name, t.expect, @@ -1419,18 +1441,11 @@ mod test { let req = PlatformReq::from_str(p) .unwrap_or_else(|e| panic!("could not create PlatformReq for {p}: {e}")); let platform = req.matching_platforms().next().unwrap(); - let mut ubi = Ubi::new( - Some("test/multiple-matches"), - None, - None, - None, - None, - None, - None, - platform, - false, - Some(server.url()), - )?; + let mut ubi = UbiBuilder::new() + .project("test/multiple-matches") + .platform(platform) + .url_base(server.url()) + .build()?; let asset = ubi.asset().await?; let expect = "mm-i686-pc-windows-gnu.zip"; assert_eq!(asset.name, expect, "picked {expect} as protobuf asset name"); @@ -1471,18 +1486,11 @@ mod test { let req = PlatformReq::from_str(p) .unwrap_or_else(|e| panic!("could not create PlatformReq for {p}: {e}")); let platform = req.matching_platforms().next().unwrap(); - let mut ubi = Ubi::new( - Some("test/macos"), - None, - None, - None, - None, - None, - None, - platform, - false, - Some(server.url()), - )?; + let mut ubi = UbiBuilder::new() + .project("test/macos") + .platform(platform) + .url_base(server.url()) + .build()?; { let asset = ubi.asset().await?; @@ -1567,18 +1575,11 @@ mod test { let req = PlatformReq::from_str(p) .unwrap_or_else(|e| panic!("could not create PlatformReq for {p}: {e}")); let platform = req.matching_platforms().next().unwrap(); - let mut ubi = Ubi::new( - Some("test/os-without-arch"), - None, - None, - None, - None, - None, - None, - platform, - false, - Some(server.url()), - )?; + let mut ubi = UbiBuilder::new() + .project("test/os-without-arch") + .platform(platform) + .url_base(server.url()) + .build()?; let asset = ubi.asset().await?; let expect = "gvproxy-darwin"; assert_eq!(asset.name, expect, "picked {expect} as protobuf asset name"); @@ -1601,18 +1602,11 @@ mod test { let req = PlatformReq::from_str(p) .unwrap_or_else(|e| panic!("could not create PlatformReq for {p}: {e}")); let platform = req.matching_platforms().next().unwrap(); - let mut ubi = Ubi::new( - Some("test/os-without-arch"), - None, - None, - None, - None, - None, - None, - platform, - false, - Some(server.url()), - )?; + let mut ubi = UbiBuilder::new() + .project("test/os-without-arch") + .platform(platform) + .url_base(server.url()) + .build()?; let asset = ubi.asset().await; assert!( asset.is_err(), diff --git a/ubi/src/release.rs b/ubi/src/release.rs index 179f955..3b69e9e 100644 --- a/ubi/src/release.rs +++ b/ubi/src/release.rs @@ -3,11 +3,6 @@ use std::path::PathBuf; use tempfile::TempDir; use url::Url; -#[derive(Debug, Deserialize)] -pub(crate) struct Release { - pub(crate) assets: Vec, -} - #[derive(Clone, Debug, Deserialize, PartialEq, Eq)] pub(crate) struct Asset { pub(crate) name: String,