Support Forgejo servers
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
use std::{error, fmt};
|
||||
use std::{error, fmt, str::FromStr};
|
||||
|
||||
use reqwest::{
|
||||
Client as HttpClient,
|
||||
header::{ACCEPT, AUTHORIZATION, HeaderMap, HeaderValue},
|
||||
};
|
||||
pub use reqwest::{Method, RequestBuilder, Response, StatusCode, Url};
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use gitea_openapi::apis;
|
||||
pub use gitea_openapi::models;
|
||||
@@ -30,6 +30,37 @@ pub use pulls::{PullFileSource, pull_file_source, pull_state};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Provider {
|
||||
#[default]
|
||||
Gitea,
|
||||
Forgejo,
|
||||
}
|
||||
|
||||
impl fmt::Display for Provider {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str(match self {
|
||||
Self::Gitea => "gitea",
|
||||
Self::Forgejo => "forgejo",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Provider {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self> {
|
||||
match value.to_ascii_lowercase().as_str() {
|
||||
"gitea" => Ok(Self::Gitea),
|
||||
"forgejo" => Ok(Self::Forgejo),
|
||||
_ => Err(Error::InvalidInput(
|
||||
"provider must be gitea or forgejo".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn validate_page(page: i32, limit: i32) -> Result<()> {
|
||||
if page < 1 || limit < 1 {
|
||||
return Err(Error::InvalidInput(
|
||||
@@ -67,7 +98,7 @@ impl fmt::Display for Error {
|
||||
Self::Transport(error) => error.fmt(formatter),
|
||||
Self::Generated(message) => formatter.write_str(message),
|
||||
Self::Api { status, message } => {
|
||||
write!(formatter, "Gitea returned {status}: {message}")
|
||||
write!(formatter, "Server returned {status}: {message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,22 +128,32 @@ impl Error {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Client {
|
||||
api_url: Url,
|
||||
forgejo_api_url: Url,
|
||||
http: HttpClient,
|
||||
provider: Provider,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(instance_url: &str, token: Option<&str>) -> Result<Self> {
|
||||
Self::with_provider(instance_url, token, Provider::Gitea)
|
||||
}
|
||||
|
||||
pub fn with_provider(
|
||||
instance_url: &str,
|
||||
token: Option<&str>,
|
||||
provider: Provider,
|
||||
) -> Result<Self> {
|
||||
let mut instance_url = Url::parse(instance_url)
|
||||
.map_err(|error| Error::Configuration(format!("invalid Gitea URL: {error}")))?;
|
||||
.map_err(|error| Error::Configuration(format!("invalid server URL: {error}")))?;
|
||||
|
||||
if !matches!(instance_url.scheme(), "http" | "https") {
|
||||
return Err(Error::Configuration(
|
||||
"Gitea URL must use http or https".into(),
|
||||
"Server URL must use http or https".into(),
|
||||
));
|
||||
}
|
||||
if !instance_url.username().is_empty() || instance_url.password().is_some() {
|
||||
return Err(Error::Configuration(
|
||||
"Gitea URL must not contain credentials".into(),
|
||||
"Server URL must not contain credentials".into(),
|
||||
));
|
||||
}
|
||||
if !instance_url.path().ends_with('/') {
|
||||
@@ -121,25 +162,61 @@ impl Client {
|
||||
|
||||
let api_url = instance_url
|
||||
.join("api/v1/")
|
||||
.map_err(|error| Error::Configuration(format!("invalid Gitea URL: {error}")))?;
|
||||
.map_err(|error| Error::Configuration(format!("invalid server URL: {error}")))?;
|
||||
let forgejo_api_url = instance_url
|
||||
.join("api/forgejo/v1/")
|
||||
.map_err(|error| Error::Configuration(format!("invalid server URL: {error}")))?;
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
|
||||
if let Some(token) = token {
|
||||
let value = HeaderValue::from_str(&format!("token {token}"))
|
||||
.map_err(|_| Error::Configuration("invalid Gitea token".into()))?;
|
||||
.map_err(|_| Error::Configuration("invalid server token".into()))?;
|
||||
headers.insert(AUTHORIZATION, value);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
api_url,
|
||||
forgejo_api_url,
|
||||
http: HttpClient::builder().default_headers(headers).build()?,
|
||||
provider,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn discover(
|
||||
instance_url: &str,
|
||||
token: Option<&str>,
|
||||
fallback: Provider,
|
||||
) -> Result<Self> {
|
||||
let mut client = Self::with_provider(instance_url, token, fallback)?;
|
||||
let version = client.gitea_version().await?;
|
||||
if is_forgejo_version(&version) {
|
||||
client.provider = Provider::Forgejo;
|
||||
return Ok(client);
|
||||
}
|
||||
|
||||
match client.forgejo_version().await {
|
||||
Ok(_) => client.provider = Provider::Forgejo,
|
||||
Err(Error::Api { status, .. }) if status == StatusCode::NOT_FOUND => {
|
||||
if fallback == Provider::Forgejo {
|
||||
return Err(Error::Configuration(
|
||||
"Server does not expose the Forgejo API".into(),
|
||||
));
|
||||
}
|
||||
client.provider = Provider::Gitea;
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
pub fn api_url(&self) -> &Url {
|
||||
&self.api_url
|
||||
}
|
||||
|
||||
pub fn provider(&self) -> Provider {
|
||||
self.provider
|
||||
}
|
||||
|
||||
/// Returns an authenticated configuration for every generated typed API.
|
||||
pub(crate) fn configuration(&self) -> apis::configuration::Configuration {
|
||||
apis::configuration::Configuration {
|
||||
@@ -188,11 +265,27 @@ impl Client {
|
||||
}
|
||||
|
||||
pub async fn version(&self) -> Result<models::ServerVersion> {
|
||||
match self.provider {
|
||||
Provider::Gitea => self.gitea_version().await,
|
||||
Provider::Forgejo => self.forgejo_version().await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn gitea_version(&self) -> Result<models::ServerVersion> {
|
||||
apis::miscellaneous_api::get_version(&self.configuration())
|
||||
.await
|
||||
.map_err(|error| Error::Generated(error.to_string()))
|
||||
}
|
||||
|
||||
async fn forgejo_version(&self) -> Result<models::ServerVersion> {
|
||||
let request =
|
||||
self.http
|
||||
.get(self.forgejo_api_url.join("version").map_err(|error| {
|
||||
Error::Configuration(format!("invalid Forgejo endpoint: {error}"))
|
||||
})?);
|
||||
Ok(self.execute(request).await?.json().await?)
|
||||
}
|
||||
|
||||
pub async fn current_user(&self) -> Result<models::User> {
|
||||
apis::user_api::user_get_current(&self.configuration())
|
||||
.await
|
||||
@@ -218,6 +311,13 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_forgejo_version(version: &models::ServerVersion) -> bool {
|
||||
version
|
||||
.version
|
||||
.as_deref()
|
||||
.is_some_and(|version| version.contains("+gitea-"))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ApiError {
|
||||
#[serde(default)]
|
||||
@@ -230,7 +330,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn keeps_instance_subpaths_and_rejects_endpoint_escapes() {
|
||||
let client = Client::new("https://example.com/gitea", Some("secret")).unwrap();
|
||||
let client = Client::with_provider(
|
||||
"https://example.com/gitea",
|
||||
Some("secret"),
|
||||
Provider::Forgejo,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
client.api_url().as_str(),
|
||||
@@ -247,6 +352,20 @@ mod tests {
|
||||
client.configuration().base_path,
|
||||
"https://example.com/gitea/api/v1"
|
||||
);
|
||||
assert_eq!(client.provider(), Provider::Forgejo);
|
||||
assert_eq!(
|
||||
client.forgejo_api_url.as_str(),
|
||||
"https://example.com/gitea/api/forgejo/v1/"
|
||||
);
|
||||
assert_eq!("GITEA".parse::<Provider>().unwrap(), Provider::Gitea);
|
||||
assert_eq!(Provider::Forgejo.to_string(), "forgejo");
|
||||
assert!("github".parse::<Provider>().is_err());
|
||||
assert!(is_forgejo_version(&models::ServerVersion {
|
||||
version: Some("16.0.1+gitea-1.24.6".into()),
|
||||
}));
|
||||
assert!(!is_forgejo_version(&models::ServerVersion {
|
||||
version: Some("1.25.2".into()),
|
||||
}));
|
||||
let _typed_model = models::Issue::default();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user