From 69eafc509a1759adeb8efb65bc8ee77d69a1d169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 18 Nov 2025 08:45:31 +0000 Subject: [PATCH] Append link to gitlab to create link between open project and gitlab (#7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed-on: https://gitea.champs-libres.be/julienfastre/cl-cli/pulls/7 Co-authored-by: Julien Fastré Co-committed-by: Julien Fastré --- .../unreleased/Added-20251117-221555.yaml | 3 ++ Cargo.lock | 39 +++++++++++++++++++ Cargo.toml | 1 + src/gitlab/action.rs | 26 ++++++++++++- src/planning/utils.rs | 32 +++++++++++++-- src/webext.rs | 20 +++++----- 6 files changed, 106 insertions(+), 15 deletions(-) create mode 100644 .changes/unreleased/Added-20251117-221555.yaml diff --git a/.changes/unreleased/Added-20251117-221555.yaml b/.changes/unreleased/Added-20251117-221555.yaml new file mode 100644 index 0000000..a37f4e5 --- /dev/null +++ b/.changes/unreleased/Added-20251117-221555.yaml @@ -0,0 +1,3 @@ +kind: Added +body: Append work package link to gitlab description after creating a WP from a gitlab issue +time: 2025-11-17T22:15:55.503651263+01:00 diff --git a/Cargo.lock b/Cargo.lock index 1c031e4..07426af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -180,6 +189,7 @@ dependencies = [ "gitlab", "log", "open", + "regex", "reqwest", "serde", "serde_json", @@ -1155,6 +1165,35 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + [[package]] name = "reqwest" version = "0.12.24" diff --git a/Cargo.toml b/Cargo.toml index 6d7d948..092679d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ simple-home-dir = "0.5.2" log = "0.4.28" open = "5.3.2" serde_json = "1.0.145" +regex = "1.12.2" [package.metadata.deb] maintainer = "Julien Fastré " diff --git a/src/gitlab/action.rs b/src/gitlab/action.rs index 4cf877e..ea0102c 100644 --- a/src/gitlab/action.rs +++ b/src/gitlab/action.rs @@ -6,9 +6,12 @@ use crate::gitlab::issue::{Issue, IssueBundle, Project}; use crate::openproject::client::Client; use crate::openproject::user::{GetMe, User}; use crate::openproject::work::WorkPackageWriterAssignee; +use crate::planning::utils::{append_related_issues, IssueRelated}; use crate::planning::Issue2WorkActionTrait; use crate::planning::Issue2WorkResult; -use gitlab::api::{issues, projects, AsyncQuery}; +use gitlab::api::projects::issues::EditIssue; +use gitlab::api::{issues, projects, ApiError, AsyncQuery}; +use gitlab::RestError; use url::Url; #[derive(Debug)] @@ -71,6 +74,27 @@ impl Issue2WorkActionTrait for GitlabAction { .create_work_package(&(&dto).into(), &args.project_id) .await?; + let url_wp = format!( + "{}/projects/{}/work_packages/{}", + config.openproject.base_url, args.project_id, work_package.id + ); + + let content = + append_related_issues(&IssueRelated::OpenProjectIssue(url_wp), &issue.description); + + let endpoint_put_result = EditIssue::builder() + .project(issue.project_id) + .issue(issue.iid) + .description(content) + .build(); + + if let Ok(endpoint_put) = endpoint_put_result { + let r: Result> = endpoint_put.query_async(&client).await; + if r.is_err() { + eprintln!("Error while updating the issue description: {:?}", r); + } + } + let result = Issue2WorkResult { work_package_url: format!( "{}/projects/{}/work_packages/{}", diff --git a/src/planning/utils.rs b/src/planning/utils.rs index ef8dbf4..243264e 100644 --- a/src/planning/utils.rs +++ b/src/planning/utils.rs @@ -65,10 +65,30 @@ fn add_related_issues_section(issue: &IssueRelated) -> Vec { fn convert_issue_link_items(issue: &IssueRelated) -> String { match issue { - IssueRelated::OpenProjectIssue(issue_url) => format!("- [{}]({})", issue_url, issue_url), + IssueRelated::OpenProjectIssue(issue_url) => { + let m = format!("- [{}]({})", issue_url, issue_url); + match extract_id(issue_url) { + Some(n) => format!("{}\n- OP#{}", m, n), + None => m, + } + } } } +use regex::Regex; + +fn extract_id(url: &str) -> Option { + // Cette regex capture le dernier segment numérique si l’URL se termine par : + // - /work_packages/ + // - /wp/ + // Et accepte n'importe quel segment avant. + let re = Regex::new(r"/(?:work_packages|wp)/(\d+)$").unwrap(); + + re.captures(url) + .and_then(|caps| caps.get(1)) + .and_then(|m| m.as_str().parse::().ok()) +} + #[cfg(test)] mod tests { use crate::planning::utils::{append_related_issues, IssueRelated}; @@ -80,7 +100,8 @@ mod tests { r#" ## Related issues -- [https://example/wp/1](https://example/wp/1)"#, +- [https://example/wp/1](https://example/wp/1) +- OP#1"#, append_related_issues(&issue, &("".to_string())) ); } @@ -97,7 +118,8 @@ Some content ## Related issues -- [https://example/wp/1](https://example/wp/1)"#, +- [https://example/wp/1](https://example/wp/1) +- OP#1"#, append_related_issues( &issue, &"Something happens.\n\ @@ -123,7 +145,8 @@ Some content ## Related issues - [https://example/wp/2](https://example/wp/2) -- [https://example/wp/1](https://example/wp/1)"#, +- [https://example/wp/1](https://example/wp/1) +- OP#1"#, append_related_issues( &issue, &r#"Something happens. @@ -154,6 +177,7 @@ Some content - [https://example/wp/2](https://example/wp/2) - [https://example/wp/1](https://example/wp/1) +- OP#1 ## Other content diff --git a/src/webext.rs b/src/webext.rs index e1156cd..112eaf2 100644 --- a/src/webext.rs +++ b/src/webext.rs @@ -172,10 +172,10 @@ mod tests { fn deserializes_issue2work_variant() { // Given: a JSON with the external tag "type" matching the enum variant name let payload = json!({ - "type": "Issue2Work", - "url": "https://example.com/issues/123", - "project": "test" - }); + "type": "Issue2Work", + "url": "https://example.com/issues/123", + "project": "test" + }); // When: deserializing to InputMessage let msg: InputMessage = serde_json::from_value(payload).expect("valid enum JSON"); @@ -192,10 +192,10 @@ mod tests { #[test] fn fails_on_unknown_type_tag() { let payload = json!({ - "type": "Unknown", - "url": "https://example.com/issues/123", - "project": "test" - }); + "type": "Unknown", + "url": "https://example.com/issues/123", + "project": "test" + }); let err = serde_json::from_value::(payload).unwrap_err(); // Basic sanity check that it's a data error mentioning the unrecognized variant @@ -210,8 +210,8 @@ mod tests { fn fails_when_missing_required_fields() { // Missing "url" and "project" let payload = json!({ - "type": "Issue2Work" - }); + "type": "Issue2Work" + }); let err = serde_json::from_value::(payload).unwrap_err(); let msg = err.to_string();