From 2b37c5b18cfab48a07ddb0c5cb68728e195e5a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 25 Oct 2024 00:29:34 +0200 Subject: [PATCH] Add Gitea Action support for issue-to-work conversion Implemented Gitea actions to support converting Gitea issues into work packages in OpenProject. Modified various modules to include necessary structs, functions, and tests to handle Gitea issues, ensure URL validation, and adapt work package creation based on Gitea issues. --- src/gitea/action.rs | 63 ++++++++++++++++++++++++++++++++++++++ src/gitea/client.rs | 46 +++++++++++++++++++++++++++- src/gitea/issue.rs | 44 +++++++++++++++++++++++--- src/gitea/mod.rs | 1 + src/gitea/repository.rs | 2 +- src/gitlab/action.rs | 2 +- src/gitlab/client.rs | 2 +- src/openproject/user.rs | 3 ++ src/openproject/work.rs | 12 ++++++++ src/planning/issue2work.rs | 19 ++++-------- 10 files changed, 172 insertions(+), 22 deletions(-) create mode 100644 src/gitea/action.rs diff --git a/src/gitea/action.rs b/src/gitea/action.rs new file mode 100644 index 0000000..c93a473 --- /dev/null +++ b/src/gitea/action.rs @@ -0,0 +1,63 @@ +use url::Url; +use crate::cli::Issue2Work; +use crate::config::Config; +use crate::error::GeneralError; +use crate::gitea::issue::{issue_html_url_to_api, Issue}; +use crate::gitea::client::has_client_for_url; +use crate::openproject::user::{GetMe, User}; +use crate::openproject::work::{WorkPackage, WorkPackageWriter, WorkPackageWriterAssignee}; +use crate::planning::Issue2WorkActionTrait; + +pub(crate) struct GiteaAction {} + +impl Issue2WorkActionTrait for GiteaAction { + async fn run(&self, url: &Url, config: &Config, args: &Issue2Work) -> Result<(), GeneralError> { + let gitea_client = crate::gitea::client::Client::from_config(config.gitea.first().unwrap()); + let issue: Issue = gitea_client + .get(issue_html_url_to_api(url)?) + .await?; + let open_project_client = crate::openproject::client::Client::from_config(&config.openproject); + + let work_package = create_work_package_from_issue( + &issue, + match args.assign_to_me { + true => { + let u = open_project_client.me().await?; + Some(u) + } + false => None, + } + ); + + let work_package = open_project_client + .create_work_package(&work_package, &args.project_id) + .await?; + + println!( + "new work package created: {:?}, edit at {}/projects/{}/work_packages/{}", + work_package.subject, config.openproject.base_url, args.project_id, work_package.id + ); + + Ok(()) + } + + fn supports(&self, url: &Url, config: &Config, _args: &Issue2Work) -> bool { + has_client_for_url(&url, &config) + } +} + +fn create_work_package_from_issue(issue: &Issue, assignee: Option) -> WorkPackageWriter { + WorkPackageWriter { + subject: format!( + "{} ({})", + issue.title, + issue.repository.full_name + ), + work_type: "TASK".into(), + description: crate::openproject::work::DescriptionWriter { + format: "markdown".into(), + raw: format!("From Gitea issue: {} \n\n{}", issue.html_url, issue.body), + }, + assignee: assignee.into() + } +} \ No newline at end of file diff --git a/src/gitea/client.rs b/src/gitea/client.rs index cfce202..65dc5f9 100644 --- a/src/gitea/client.rs +++ b/src/gitea/client.rs @@ -1,6 +1,5 @@ use crate::config::{Config, GiteaConfig, GitlabConfig}; use crate::error::GeneralError; -use gitlab::AsyncGitlab; use reqwest::header::{HeaderMap, ACCEPT, AUTHORIZATION}; use reqwest::{ClientBuilder, StatusCode}; use serde::de::DeserializeOwned; @@ -12,6 +11,35 @@ pub struct Client { base_uri: String, } +fn is_client_for_url(url: &Url, config: &GiteaConfig) -> bool { + if url.domain() == Some(config.domain.as_str()) { + return true; + } + false +} + +pub(crate) fn has_client_for_url(url: &Url, config: &Config) -> bool { + for c in &config.gitea { + if is_client_for_url(url, c) { + return true; + } + } + + false +} + +fn client_for_url(url: &Url, config: &Config) -> Result { + for c in &config.gitea { + if is_client_for_url(url, c) { + return Ok(Client::from_config(&c)); + } + } + + Err(GeneralError { + description: format!("No gitea client found for url {}", url), + }) +} + impl Client { pub fn from_config(config: &GiteaConfig) -> Self { Self::new(&config.token, &config.domain) @@ -52,3 +80,19 @@ impl Client { } } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_is_client_for_url() { + let config = GiteaConfig { + domain: "gitea.champs-libres.be".into(), + token: "".into(), + }; + + assert_eq!(is_client_for_url(&Url::parse("https://gitea.champs-libres.be/something/somewhere").unwrap(), &config), true); + assert_eq!(is_client_for_url(&Url::parse("https://somewhere.else/something/somewhere").unwrap(), &config), false); + } +} diff --git a/src/gitea/issue.rs b/src/gitea/issue.rs index 416365c..e087049 100644 --- a/src/gitea/issue.rs +++ b/src/gitea/issue.rs @@ -1,14 +1,17 @@ use crate::gitea::client::Client; use crate::gitea::repository::Repository; use serde::Deserialize; +use url::Url; +use crate::error::GeneralError; #[derive(Debug, Deserialize)] pub struct Issue { - id: u64, + pub id: u64, number: u64, - title: String, - body: String, - repository: Repository, + pub title: String, + pub body: String, + pub repository: Repository, + pub html_url: String, } pub trait IssueClient { @@ -16,7 +19,38 @@ pub trait IssueClient { } impl IssueClient for Client { - fn get_issue(owner_or_organisation: &String, repo: &String, number: &u64) -> Option { + fn get_issue(_owner_or_organisation: &String, _repo: &String, number: &u64) -> Option { todo!() } } + +pub fn issue_html_url_to_api(url: &Url) -> Result { + let mut parts = url.path_segments().unwrap(); + let domain = parts.next().unwrap(); + let repo = parts.next().unwrap(); + let issue = parts.next().unwrap(); + let iid = parts.next().unwrap(); + + if (!issue.eq("issues")) { + return Err(GeneralError { + description: format!("Issue url is not valid: {}", url), + }); + } + + let url = Url::parse(format!("{}://{}/api/v1/repos/{}/{}/issues/{}", url.scheme(), url.host().unwrap(), domain, repo, iid).as_str()).unwrap(); + + Ok(url) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_issue_html_url_to_api() { + let url = Url::parse("https://gitea.champs-libres.be/champs-libres/test/issues/1").unwrap(); + let result = issue_html_url_to_api(&url).unwrap(); + + assert_eq!(result.as_str(), "https://gitea.champs-libres.be/api/v1/repos/champs-libres/test/issues/1"); + } +} diff --git a/src/gitea/mod.rs b/src/gitea/mod.rs index c47c4ac..32c6c76 100644 --- a/src/gitea/mod.rs +++ b/src/gitea/mod.rs @@ -1,3 +1,4 @@ pub mod client; pub mod issue; pub mod repository; +pub(crate) mod action; diff --git a/src/gitea/repository.rs b/src/gitea/repository.rs index 0e0ba74..80d50db 100644 --- a/src/gitea/repository.rs +++ b/src/gitea/repository.rs @@ -5,5 +5,5 @@ pub struct Repository { id: u64, name: String, owner: String, - full_name: String, + pub full_name: String, } diff --git a/src/gitlab/action.rs b/src/gitlab/action.rs index 1b9822f..ef3c4d7 100644 --- a/src/gitlab/action.rs +++ b/src/gitlab/action.rs @@ -74,7 +74,7 @@ impl Issue2WorkActionTrait for GitlabAction { Ok(()) } - fn supports(&self, url: &Url, config: &Config, args: &Issue2Work) -> bool { + fn supports(&self, url: &Url, config: &Config, _args: &Issue2Work) -> bool { has_client_for_url(&url, &config) } } diff --git a/src/gitlab/client.rs b/src/gitlab/client.rs index 11a3fd0..4b3d2a6 100644 --- a/src/gitlab/client.rs +++ b/src/gitlab/client.rs @@ -39,7 +39,7 @@ pub async fn client_for_url(url: &Url, config: &Config) -> Result bool { for c in &config.gitlab { - if (is_client_for_url(url, c)) { + if is_client_for_url(url, c) { return true; } } diff --git a/src/openproject/user.rs b/src/openproject/user.rs index 3e29a3f..54965a1 100644 --- a/src/openproject/user.rs +++ b/src/openproject/user.rs @@ -11,8 +11,11 @@ pub struct UserLink { #[derive(Deserialize, Debug, Clone)] pub struct User { #[serde(rename = "_type")] + #[allow(unused_variables)] pub d_type: String, + #[allow(unused_variables)] pub id: u64, + #[allow(unused_variables)] pub name: String, #[serde(rename = "_links")] pub d_links: UserLink, diff --git a/src/openproject/work.rs b/src/openproject/work.rs index d9aca46..5520698 100644 --- a/src/openproject/work.rs +++ b/src/openproject/work.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use crate::openproject::user::User; #[derive(Serialize, Debug)] pub struct WorkPackageWriterAssignee { @@ -24,3 +25,14 @@ pub struct WorkPackage { pub id: u64, pub subject: String, } + +impl From> for WorkPackageWriterAssignee { + fn from(value: Option) -> Self { + WorkPackageWriterAssignee { + href: match value { + None => None, + Some(w) => Some(w.clone().d_links.d_self.href), + }, + } + } +} diff --git a/src/planning/issue2work.rs b/src/planning/issue2work.rs index 695728c..2e1189b 100644 --- a/src/planning/issue2work.rs +++ b/src/planning/issue2work.rs @@ -1,34 +1,27 @@ use crate::cli::Issue2Work; use crate::config::Config; use crate::error::GeneralError; +use crate::gitea::action::GiteaAction; use crate::gitlab::action::GitlabAction; use crate::planning::Issue2WorkActionTrait; use url::Url; struct App { gitlab_issue2work_action: GitlabAction, -} - -trait Issue2WorkPackageAction { - fn issue2work_package_action( - &self, - config: &Config, - args: &Issue2Work, - ) -> Result<(), GeneralError>; + gitea_issue2work_action: GiteaAction, } pub(crate) async fn issue2work(config: Config, args: &Issue2Work) -> Result<(), GeneralError> { let url = Url::parse(&*args.issue_url).expect("issue_url is not valid"); let app = App { gitlab_issue2work_action: GitlabAction {}, + gitea_issue2work_action: GiteaAction {}, }; - if (app - .gitlab_issue2work_action - .supports(&url, &config, args) - ) - { + if app.gitlab_issue2work_action.supports(&url, &config, args) { app.gitlab_issue2work_action.run(&url, &config, args).await + } else if app.gitea_issue2work_action.supports(&url, &config, args) { + app.gitea_issue2work_action.run(&url, &config, args).await } else { Err(GeneralError { description: format!("This action is not supported for this url: {}", url),