From 12b75ae6d3c91c3e5ebc51643acf2026004e1e83 Mon Sep 17 00:00:00 2001 From: Azalea <22280294+hykilpikonna@users.noreply.github.com> Date: Thu, 7 May 2026 16:41:18 +0000 Subject: [PATCH] [O] Clean up codebase --- src/provider.rs | 336 +++++++++++++++++++++++------------------------- 1 file changed, 163 insertions(+), 173 deletions(-) diff --git a/src/provider.rs b/src/provider.rs index 3077b4c..f6e1f00 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -29,6 +29,38 @@ pub struct ProviderClient<'a> { http: Client, } +macro_rules! dispatch_provider { + ($provider:expr, github => $github:expr, gitlab => $gitlab:expr, gitea_like => $gitea_like:expr $(,)?) => { + match $provider { + ProviderKind::Github => $github, + ProviderKind::Gitlab => $gitlab, + ProviderKind::Gitea | ProviderKind::Forgejo => $gitea_like, + } + }; +} + +macro_rules! owned_repos { + ($client:expr, $repo:ty, $url:expr, $namespace:expr) => {{ + Ok($client + .paged_get::<$repo>($url)? + .into_iter() + .filter(|repo: &$repo| repo.owner.login.eq_ignore_ascii_case($namespace)) + .map(Into::into) + .collect()) + }}; +} + +macro_rules! json_method { + ($name:ident, $method:literal, $request:ident) => { + fn $name(&self, url: &str, body: &serde_json::Value) -> Result + where + T: for<'de> Deserialize<'de>, + { + self.send_json($method, self.http.$request(url), url, body) + } + }; +} + impl<'a> ProviderClient<'a> { pub fn new(site: &'a SiteConfig) -> Result { let token = site.token()?; @@ -40,12 +72,11 @@ impl<'a> ProviderClient<'a> { } pub fn list_repos(&self, endpoint: &EndpointConfig) -> Result> { - match self.site.provider { - ProviderKind::Github => self.github_list_repos(endpoint), - ProviderKind::Gitlab => self.gitlab_list_repos(endpoint), - ProviderKind::Gitea => self.gitea_list_repos(endpoint), - ProviderKind::Forgejo => self.gitea_list_repos(endpoint), - } + dispatch_provider!(self.site.provider, + github => self.github_list_repos(endpoint), + gitlab => self.gitlab_list_repos(endpoint), + gitea_like => self.gitea_list_repos(endpoint), + ) } pub fn create_repo( @@ -55,18 +86,11 @@ impl<'a> ProviderClient<'a> { visibility: &Visibility, description: Option<&str>, ) -> Result { - match self.site.provider { - ProviderKind::Github => { - self.github_create_repo(endpoint, name, visibility, description) - } - ProviderKind::Gitlab => { - self.gitlab_create_repo(endpoint, name, visibility, description) - } - ProviderKind::Gitea => self.gitea_create_repo(endpoint, name, visibility, description), - ProviderKind::Forgejo => { - self.gitea_create_repo(endpoint, name, visibility, description) - } - } + dispatch_provider!(self.site.provider, + github => self.github_create_repo(endpoint, name, visibility, description), + gitlab => self.gitlab_create_repo(endpoint, name, visibility, description), + gitea_like => self.gitea_create_repo(endpoint, name, visibility, description), + ) } pub fn install_webhook( @@ -76,13 +100,11 @@ impl<'a> ProviderClient<'a> { url: &str, secret: &str, ) -> Result<()> { - match self.site.provider { - ProviderKind::Github => self.github_install_webhook(endpoint, repo, url, secret), - ProviderKind::Gitlab => self.gitlab_install_webhook(endpoint, repo, url, secret), - ProviderKind::Gitea | ProviderKind::Forgejo => { - self.gitea_install_webhook(endpoint, repo, url, secret) - } - } + dispatch_provider!(self.site.provider, + github => self.github_install_webhook(endpoint, repo, url, secret), + gitlab => self.gitlab_install_webhook(endpoint, repo, url, secret), + gitea_like => self.gitea_install_webhook(endpoint, repo, url, secret), + ) } pub fn uninstall_webhook( @@ -91,13 +113,11 @@ impl<'a> ProviderClient<'a> { repo_name: &str, url: &str, ) -> Result { - match self.site.provider { - ProviderKind::Github => self.github_uninstall_webhook(endpoint, repo_name, url), - ProviderKind::Gitlab => self.gitlab_uninstall_webhook(endpoint, repo_name, url), - ProviderKind::Gitea | ProviderKind::Forgejo => { - self.gitea_uninstall_webhook(endpoint, repo_name, url) - } - } + dispatch_provider!(self.site.provider, + github => self.github_uninstall_webhook(endpoint, repo_name, url), + gitlab => self.gitlab_uninstall_webhook(endpoint, repo_name, url), + gitea_like => self.gitea_uninstall_webhook(endpoint, repo_name, url), + ) } pub fn validate_token(&self) -> Result<()> { @@ -106,12 +126,11 @@ impl<'a> ProviderClient<'a> { } pub fn detect_namespace_kind(&self, namespace: &str) -> Result> { - match self.site.provider { - ProviderKind::Github => self.github_detect_namespace_kind(namespace), - ProviderKind::Gitlab => self.gitlab_detect_namespace_kind(namespace), - ProviderKind::Gitea => self.gitea_detect_namespace_kind(namespace), - ProviderKind::Forgejo => self.gitea_detect_namespace_kind(namespace), - } + dispatch_provider!(self.site.provider, + github => self.github_detect_namespace_kind(namespace), + gitlab => self.gitlab_detect_namespace_kind(namespace), + gitea_like => self.gitea_detect_namespace_kind(namespace), + ) } pub fn authenticated_clone_url(&self, clone_url: &str) -> Result { @@ -126,12 +145,7 @@ impl<'a> ProviderClient<'a> { .site .git_username .clone() - .unwrap_or_else(|| match self.site.provider { - ProviderKind::Github => "x-access-token".to_string(), - ProviderKind::Gitlab | ProviderKind::Gitea | ProviderKind::Forgejo => { - "oauth2".to_string() - } - }); + .unwrap_or_else(|| default_git_username(&self.site.provider).to_string()); url.set_username(&username) .map_err(|_| anyhow!("failed to set username on clone URL"))?; url.set_password(Some(&self.token)) @@ -146,14 +160,7 @@ impl<'a> ProviderClient<'a> { "{}/user/repos?affiliation=owner&visibility=all&per_page=100", self.site.api_base() ); - let repos: Vec = self - .paged_get(&url)? - .into_iter() - .filter(|repo: &GithubRepo| { - repo.owner.login.eq_ignore_ascii_case(&endpoint.namespace) - }) - .collect(); - Ok(repos.into_iter().map(Into::into).collect()) + owned_repos!(self, GithubRepo, &url, &endpoint.namespace) } NamespaceKind::Org => { let url = format!( @@ -161,8 +168,7 @@ impl<'a> ProviderClient<'a> { self.site.api_base(), endpoint.namespace ); - let repos: Vec = self.paged_get(&url)?; - Ok(repos.into_iter().map(Into::into).collect()) + self.paged_remote_repos::(&url) } NamespaceKind::Group => bail!("GitHub endpoints use kind 'user' or 'org'"), } @@ -207,15 +213,7 @@ impl<'a> ProviderClient<'a> { url: &str, secret: &str, ) -> Result<()> { - if matches!(endpoint.kind, NamespaceKind::Group) { - bail!("GitHub endpoints use kind 'user' or 'org'"); - } - let hooks_url = format!( - "{}/repos/{}/{}/hooks", - self.site.api_base(), - endpoint.namespace, - repo.name - ); + let hooks_url = self.repo_hooks_url(endpoint, &repo.name, "GitHub")?; let body = json!({ "name": "web", "active": true, @@ -227,13 +225,7 @@ impl<'a> ProviderClient<'a> { "insecure_ssl": "0", }, }); - if let Some(hook) = self.find_existing_hook(&hooks_url, url)? { - let update_url = format!("{hooks_url}/{}", hook.id); - self.patch_json::(&update_url, &body)?; - } else { - self.post_json::(&hooks_url, &body)?; - } - Ok(()) + self.upsert_hook(&hooks_url, url, &body, false) } fn github_uninstall_webhook( @@ -242,15 +234,7 @@ impl<'a> ProviderClient<'a> { repo_name: &str, url: &str, ) -> Result { - if matches!(endpoint.kind, NamespaceKind::Group) { - bail!("GitHub endpoints use kind 'user' or 'org'"); - } - let hooks_url = format!( - "{}/repos/{}/{}/hooks", - self.site.api_base(), - endpoint.namespace, - repo_name - ); + let hooks_url = self.repo_hooks_url(endpoint, repo_name, "GitHub")?; self.delete_matching_hook(&hooks_url, url) } @@ -262,8 +246,7 @@ impl<'a> ProviderClient<'a> { self.site.api_base(), endpoint.namespace ); - let repos: Vec = self.paged_get(&url)?; - Ok(repos.into_iter().map(Into::into).collect()) + self.paged_remote_repos::(&url) } NamespaceKind::Org | NamespaceKind::Group => { let encoded = urlencoding(&endpoint.namespace); @@ -272,8 +255,7 @@ impl<'a> ProviderClient<'a> { self.site.api_base(), encoded ); - let repos: Vec = self.paged_get(&url)?; - Ok(repos.into_iter().map(Into::into).collect()) + self.paged_remote_repos::(&url) } } } @@ -339,12 +321,7 @@ impl<'a> ProviderClient<'a> { url: &str, secret: &str, ) -> Result<()> { - let project = format!("{}/{}", endpoint.namespace, repo.name); - let hooks_url = format!( - "{}/projects/{}/hooks", - self.site.api_base(), - urlencoding(&project) - ); + let hooks_url = self.gitlab_hooks_url(endpoint, &repo.name); let body = json!({ "url": url, "push_events": true, @@ -352,13 +329,7 @@ impl<'a> ProviderClient<'a> { "token": secret, "enable_ssl_verification": true, }); - if let Some(hook) = self.find_existing_hook(&hooks_url, url)? { - let update_url = format!("{hooks_url}/{}", hook.id); - self.put_json::(&update_url, &body)?; - } else { - self.post_json::(&hooks_url, &body)?; - } - Ok(()) + self.upsert_hook(&hooks_url, url, &body, true) } fn gitlab_uninstall_webhook( @@ -367,12 +338,7 @@ impl<'a> ProviderClient<'a> { repo_name: &str, url: &str, ) -> Result { - let project = format!("{}/{}", endpoint.namespace, repo_name); - let hooks_url = format!( - "{}/projects/{}/hooks", - self.site.api_base(), - urlencoding(&project) - ); + let hooks_url = self.gitlab_hooks_url(endpoint, repo_name); self.delete_matching_hook(&hooks_url, url) } @@ -380,14 +346,7 @@ impl<'a> ProviderClient<'a> { match endpoint.kind { NamespaceKind::User => { let url = format!("{}/user/repos?limit=50", self.site.api_base()); - let repos: Vec = self - .paged_get(&url)? - .into_iter() - .filter(|repo: &GiteaRepo| { - repo.owner.login.eq_ignore_ascii_case(&endpoint.namespace) - }) - .collect(); - Ok(repos.into_iter().map(Into::into).collect()) + owned_repos!(self, GiteaRepo, &url, &endpoint.namespace) } NamespaceKind::Org => { let url = format!( @@ -395,8 +354,7 @@ impl<'a> ProviderClient<'a> { self.site.api_base(), endpoint.namespace ); - let repos: Vec = self.paged_get(&url)?; - Ok(repos.into_iter().map(Into::into).collect()) + self.paged_remote_repos::(&url) } NamespaceKind::Group => bail!("Gitea/Forgejo endpoints use kind 'user' or 'org'"), } @@ -446,15 +404,7 @@ impl<'a> ProviderClient<'a> { url: &str, secret: &str, ) -> Result<()> { - if matches!(endpoint.kind, NamespaceKind::Group) { - bail!("Gitea endpoints use kind 'user' or 'org'"); - } - let hooks_url = format!( - "{}/repos/{}/{}/hooks", - self.site.api_base(), - endpoint.namespace, - repo.name - ); + let hooks_url = self.repo_hooks_url(endpoint, &repo.name, "Gitea/Forgejo")?; let body = json!({ "type": "gitea", "active": true, @@ -465,13 +415,7 @@ impl<'a> ProviderClient<'a> { "secret": secret, }, }); - if let Some(hook) = self.find_existing_hook(&hooks_url, url)? { - let update_url = format!("{hooks_url}/{}", hook.id); - self.patch_json::(&update_url, &body)?; - } else { - self.post_json::(&hooks_url, &body)?; - } - Ok(()) + self.upsert_hook(&hooks_url, url, &body, false) } fn gitea_uninstall_webhook( @@ -480,18 +424,35 @@ impl<'a> ProviderClient<'a> { repo_name: &str, url: &str, ) -> Result { - if matches!(endpoint.kind, NamespaceKind::Group) { - bail!("Gitea endpoints use kind 'user' or 'org'"); - } - let hooks_url = format!( - "{}/repos/{}/{}/hooks", - self.site.api_base(), - endpoint.namespace, - repo_name - ); + let hooks_url = self.repo_hooks_url(endpoint, repo_name, "Gitea/Forgejo")?; self.delete_matching_hook(&hooks_url, url) } + fn repo_hooks_url( + &self, + endpoint: &EndpointConfig, + repo_name: &str, + provider: &str, + ) -> Result { + if matches!(endpoint.kind, NamespaceKind::Group) { + bail!("{provider} endpoints use kind 'user' or 'org'"); + } + Ok(format!( + "{}/repos/{}/{repo_name}/hooks", + self.site.api_base(), + endpoint.namespace + )) + } + + fn gitlab_hooks_url(&self, endpoint: &EndpointConfig, repo_name: &str) -> String { + let project = format!("{}/{repo_name}", endpoint.namespace); + format!( + "{}/projects/{}/hooks", + self.site.api_base(), + urlencoding(&project) + ) + } + fn find_existing_hook(&self, hooks_url: &str, target_url: &str) -> Result> { let hooks: Vec = self.paged_get(hooks_url)?; Ok(hooks @@ -499,6 +460,27 @@ impl<'a> ProviderClient<'a> { .find(|hook| hook.url() == Some(target_url))) } + fn upsert_hook( + &self, + hooks_url: &str, + target_url: &str, + body: &serde_json::Value, + put_on_update: bool, + ) -> Result<()> { + let Some(hook) = self.find_existing_hook(hooks_url, target_url)? else { + self.post_json::(hooks_url, body)?; + return Ok(()); + }; + + let update_url = format!("{hooks_url}/{}", hook.id); + if put_on_update { + self.put_json::(&update_url, body)?; + } else { + self.patch_json::(&update_url, body)?; + } + Ok(()) + } + fn delete_matching_hook(&self, hooks_url: &str, target_url: &str) -> Result { let Some(hook) = self.find_existing_hook(hooks_url, target_url)? else { return Ok(false); @@ -527,6 +509,17 @@ impl<'a> ProviderClient<'a> { Ok(output) } + fn paged_remote_repos(&self, url: &str) -> Result> + where + T: for<'de> Deserialize<'de> + Into, + { + Ok(self + .paged_get::(url)? + .into_iter() + .map(Into::into) + .collect()) + } + fn get_json(&self, url: &str) -> Result where T: for<'de> Deserialize<'de>, @@ -536,57 +529,47 @@ impl<'a> ProviderClient<'a> { .with_context(|| format!("invalid JSON from {url}")) } - fn post_json(&self, url: &str, body: &serde_json::Value) -> Result - where - T: for<'de> Deserialize<'de>, - { - self.request_headers(self.http.post(url))? - .json(body) - .send() - .with_context(|| format!("POST {url} failed")) - .and_then(|response| check_response("POST", url, response))? - .json() - .with_context(|| format!("invalid JSON from {url}")) - } + json_method!(post_json, "POST", post); + json_method!(put_json, "PUT", put); + json_method!(patch_json, "PATCH", patch); - fn put_json(&self, url: &str, body: &serde_json::Value) -> Result + fn send_json( + &self, + method: &str, + request: reqwest::blocking::RequestBuilder, + url: &str, + body: &serde_json::Value, + ) -> Result where T: for<'de> Deserialize<'de>, { - self.request_headers(self.http.put(url))? + self.request_headers(request)? .json(body) .send() - .with_context(|| format!("PUT {url} failed")) - .and_then(|response| check_response("PUT", url, response))? - .json() - .with_context(|| format!("invalid JSON from {url}")) - } - - fn patch_json(&self, url: &str, body: &serde_json::Value) -> Result - where - T: for<'de> Deserialize<'de>, - { - self.request_headers(self.http.patch(url))? - .json(body) - .send() - .with_context(|| format!("PATCH {url} failed")) - .and_then(|response| check_response("PATCH", url, response))? + .with_context(|| format!("{method} {url} failed")) + .and_then(|response| check_response(method, url, response))? .json() .with_context(|| format!("invalid JSON from {url}")) } fn get(&self, url: &str) -> Result { - self.request_headers(self.http.get(url))? - .send() - .with_context(|| format!("GET {url} failed")) - .and_then(|response| check_response("GET", url, response)) + self.send("GET", self.http.get(url), url) } fn delete(&self, url: &str) -> Result { - self.request_headers(self.http.delete(url))? + self.send("DELETE", self.http.delete(url), url) + } + + fn send( + &self, + method: &str, + request: reqwest::blocking::RequestBuilder, + url: &str, + ) -> Result { + self.request_headers(request)? .send() - .with_context(|| format!("DELETE {url} failed")) - .and_then(|response| check_response("DELETE", url, response)) + .with_context(|| format!("{method} {url} failed")) + .and_then(|response| check_response(method, url, response)) } fn request_headers( @@ -656,6 +639,13 @@ fn urlencoding(value: &str) -> String { url::form_urlencoded::byte_serialize(value.as_bytes()).collect() } +fn default_git_username(provider: &ProviderKind) -> &'static str { + match provider { + ProviderKind::Github => "x-access-token", + ProviderKind::Gitlab | ProviderKind::Gitea | ProviderKind::Forgejo => "oauth2", + } +} + #[derive(Deserialize)] struct GithubRepo { name: String,