Skip to content

Commit

Permalink
Implement support for GitLab
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
autarch committed Dec 24, 2024
1 parent 78c5371 commit 96aee53
Show file tree
Hide file tree
Showing 12 changed files with 502 additions and 236 deletions.
15 changes: 14 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
1 change: 1 addition & 0 deletions ubi-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 18 additions & 1 deletion ubi-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -182,6 +196,9 @@ fn make_ubi<'a>(
if let Some(e) = matches.get_one::<String>("exe") {
builder = builder.exe(e);
}
if let Some(ft) = matches.get_one::<String>("forge") {
builder = builder.forge(ForgeType::from_str(ft)?);
}

Ok((builder.build()?, None))
}
Expand Down
44 changes: 44 additions & 0 deletions ubi-cli/tests/ubi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}

Expand Down
1 change: 1 addition & 0 deletions ubi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
69 changes: 0 additions & 69 deletions ubi/src/fetcher.rs

This file was deleted.

73 changes: 73 additions & 0 deletions ubi/src/forge.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<Asset>>;

fn release_info_url(&self) -> Url;
fn maybe_add_token_header(&self, req_builder: RequestBuilder) -> Result<RequestBuilder>;

async fn make_release_info_request(&self, client: &Client) -> Result<Response> {
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(),
}
}
}
Loading

0 comments on commit 96aee53

Please sign in to comment.