Compare commits

..

3 Commits

Author SHA1 Message Date
julienfastre 2b37c5b18c 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.
2024-10-25 00:29:34 +02:00
julienfastre 9ef98e5044 Refactor the issue2work action
- separation the logic to handle issues from gitlab into the gitlab mod
- create a kind of visitor pattern to check between the different providers
2024-10-24 23:14:14 +02:00
julienfastre 9aec267e0a WIP on integrate gitea 2024-05-20 19:16:21 +02:00
24 changed files with 82 additions and 388 deletions
@@ -0,0 +1,4 @@
kind: Added
body: Add an option `--assign-to-me` to automatically assign the openproject's user
to the newly created work package
time: 2024-03-17T21:35:42.724855046+01:00
@@ -0,0 +1,3 @@
kind: Added
body: Allow to configure multiple gitlab instances
time: 2024-03-17T21:35:56.447907065+01:00
-5
View File
@@ -1,5 +0,0 @@
## v0.2.0 - 2024-11-14
### Added
* Add an option `--assign-to-me` to automatically assign the openproject's user to the newly created work package
* Allow to configure multiple gitlab instances
* Create work pakcage from gitea
-3
View File
@@ -1,3 +0,0 @@
## v0.3.0 - 2024-11-17
### Added
* Open the newly create work package after the wp has been created
-3
View File
@@ -1,3 +0,0 @@
## v0.4.0 - 2025-10-24
### Added
* [planning - i2work] Met à jour le body du ticket gitea avec un lien vers le work package, dans une section "related issues"
@@ -21,7 +21,7 @@ jobs:
with: with:
path: .changes/${{ github.ref_name }}.md path: .changes/${{ github.ref_name }}.md
- name: Setup go for using go gitea actions - name: Setup go for using go gitea actions
uses: https://gitea.com/actions/setup-go@v4 uses: https://github.com/actions/setup-go@v4
with: with:
go-version: '>=1.20.1' go-version: '>=1.20.1'
- name: Use Go Action to release - name: Use Go Action to release
-14
View File
@@ -6,20 +6,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie). and is generated by [Changie](https://github.com/miniscruff/changie).
## v0.4.0 - 2025-10-24
### Added
* [planning - i2work] Met à jour le body du ticket gitea avec un lien vers le work package, dans une section "related issues"
## v0.3.0 - 2024-11-17
### Added
* Open the newly create work package after the wp has been created
## v0.2.0 - 2024-11-14
### Added
* Add an option `--assign-to-me` to automatically assign the openproject's user to the newly created work package
* Allow to configure multiple gitlab instances
* Create work pakcage from gitea
## v0.1.0 - 2024-01-08 ## v0.1.0 - 2024-01-08
### Added ### Added
* Initiate changie versioning * Initiate changie versioning
Generated
-37
View File
@@ -111,7 +111,6 @@ dependencies = [
"clap", "clap",
"gitlab", "gitlab",
"log", "log",
"open",
"reqwest", "reqwest",
"serde", "serde",
"simple-home-dir", "simple-home-dir",
@@ -739,15 +738,6 @@ version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146"
[[package]]
name = "is-docker"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
dependencies = [
"once_cell",
]
[[package]] [[package]]
name = "is-terminal" name = "is-terminal"
version = "0.4.5" version = "0.4.5"
@@ -760,16 +750,6 @@ dependencies = [
"windows-sys 0.45.0", "windows-sys 0.45.0",
] ]
[[package]]
name = "is-wsl"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
dependencies = [
"is-docker",
"once_cell",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.10.5" version = "0.10.5"
@@ -929,17 +909,6 @@ version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
[[package]]
name = "open"
version = "5.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ecd52f0b8d15c40ce4820aa251ed5de032e5d91fab27f7db2f40d42a8bdf69c"
dependencies = [
"is-wsl",
"libc",
"pathdiff",
]
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.62" version = "0.10.62"
@@ -990,12 +959,6 @@ version = "6.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267"
[[package]]
name = "pathdiff"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.2.0" version = "2.2.0"
-1
View File
@@ -15,4 +15,3 @@ url = "2.3.1"
tokio = { version = "1.0.0", features = ["rt", "rt-multi-thread", "macros"] } tokio = { version = "1.0.0", features = ["rt", "rt-multi-thread", "macros"] }
simple-home-dir = "0.2.1" simple-home-dir = "0.2.1"
log = "0.4.17" log = "0.4.17"
open = "5.3.1"
+1 -6
View File
@@ -1,11 +1,6 @@
[[gitlab]] [gitlab]
# generate from https://gitlab.com/-/user_settings/personal_access_tokens # generate from https://gitlab.com/-/user_settings/personal_access_tokens
token = "glpat-example" token = "glpat-example"
domain = "gitlab.com"
[[gitea]]
token = "abcdexempletoken"
domain = "gitea.champs-libres.be"
[openproject] [openproject]
# generate api token from https://champs-libres.openproject.com/my/access_token # generate api token from https://champs-libres.openproject.com/my/access_token
+4 -4
View File
@@ -1,6 +1,9 @@
use crate::config::Config; use crate::config::Config;
use crate::error::GeneralError; use crate::error::GeneralError;
use crate::gitea::issue::Issue; use crate::gitea::issue::Issue;
use crate::openproject::client::Client;
use crate::openproject::root::RootClient;
use crate::openproject::user::GetMe;
use url::Url; use url::Url;
pub(crate) async fn debug(config: Config) -> Result<(), GeneralError> { pub(crate) async fn debug(config: Config) -> Result<(), GeneralError> {
@@ -16,9 +19,8 @@ pub(crate) async fn debug(config: Config) -> Result<(), GeneralError> {
println!("issue: {:?}", issue); println!("issue: {:?}", issue);
Ok(()) return Ok(());
/*
let open_project_client = Client::from_config(&config.openproject); let open_project_client = Client::from_config(&config.openproject);
println!("base_url: {}", open_project_client.base_url); println!("base_url: {}", open_project_client.base_url);
println!("base_url: will get root"); println!("base_url: will get root");
@@ -28,6 +30,4 @@ pub(crate) async fn debug(config: Config) -> Result<(), GeneralError> {
println!("me: {:?}", u); println!("me: {:?}", u);
Ok(()) Ok(())
*/
} }
+1 -1
View File
@@ -7,7 +7,7 @@ pub struct GeneralError {
} }
impl From<InvalidHeaderValue> for GeneralError { impl From<InvalidHeaderValue> for GeneralError {
fn from(_value: InvalidHeaderValue) -> Self { fn from(value: InvalidHeaderValue) -> Self {
GeneralError { GeneralError {
description: "Unable to convert the token into header value".to_string(), description: "Unable to convert the token into header value".to_string(),
} }
+15 -34
View File
@@ -1,22 +1,22 @@
use url::Url;
use crate::cli::Issue2Work; use crate::cli::Issue2Work;
use crate::config::Config; use crate::config::Config;
use crate::error::GeneralError; use crate::error::GeneralError;
use crate::gitea::issue::{issue_html_url_to_api, Issue};
use crate::gitea::client::has_client_for_url; use crate::gitea::client::has_client_for_url;
use crate::gitea::issue::{issue_html_url_to_api, Issue, IssueWriteSetBody};
use crate::openproject::user::{GetMe, User}; use crate::openproject::user::{GetMe, User};
use crate::openproject::work::WorkPackageWriter; use crate::openproject::work::{WorkPackage, WorkPackageWriter, WorkPackageWriterAssignee};
use crate::planning::utils::{append_related_issues, IssueRelated};
use crate::planning::Issue2WorkActionTrait; use crate::planning::Issue2WorkActionTrait;
use url::Url;
pub(crate) struct GiteaAction {} pub(crate) struct GiteaAction {}
impl Issue2WorkActionTrait for GiteaAction { impl Issue2WorkActionTrait for GiteaAction {
async fn run(&self, url: &Url, config: &Config, args: &Issue2Work) -> Result<(), GeneralError> { 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 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 issue: Issue = gitea_client
let open_project_client = .get(issue_html_url_to_api(url)?)
crate::openproject::client::Client::from_config(&config.openproject); .await?;
let open_project_client = crate::openproject::client::Client::from_config(&config.openproject);
let work_package = create_work_package_from_issue( let work_package = create_work_package_from_issue(
&issue, &issue,
@@ -26,38 +26,18 @@ impl Issue2WorkActionTrait for GiteaAction {
Some(u) Some(u)
} }
false => None, false => None,
}, }
); );
let work_package = open_project_client let work_package = open_project_client
.create_work_package(&work_package, &args.project_id) .create_work_package(&work_package, &args.project_id)
.await?; .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.to_string()),
&issue.body,
);
let _u: Issue = gitea_client
.patch(
issue_html_url_to_api(url)?,
&IssueWriteSetBody { body: content },
)
.await?;
println!( println!(
"new work package created: {:?}, edit at {}", "new work package created: {:?}, edit at {}/projects/{}/work_packages/{}",
work_package.subject, url_wp work_package.subject, config.openproject.base_url, args.project_id, work_package.id
); );
if let Err(e) = open::that(url_wp) {
println!("failed to open work package in browser: {}", e);
};
Ok(()) Ok(())
} }
@@ -69,14 +49,15 @@ impl Issue2WorkActionTrait for GiteaAction {
fn create_work_package_from_issue(issue: &Issue, assignee: Option<User>) -> WorkPackageWriter { fn create_work_package_from_issue(issue: &Issue, assignee: Option<User>) -> WorkPackageWriter {
WorkPackageWriter { WorkPackageWriter {
subject: format!( subject: format!(
"{} ({}/{})", "{} ({})",
issue.title, issue.repository.full_name, issue.number issue.title,
issue.repository.full_name
), ),
work_type: "TASK".into(), work_type: "TASK".into(),
description: crate::openproject::work::DescriptionWriter { description: crate::openproject::work::DescriptionWriter {
format: "markdown".into(), format: "markdown".into(),
raw: format!("From Gitea issue: {} \n\n{}", issue.html_url, issue.body), raw: format!("From Gitea issue: {} \n\n{}", issue.html_url, issue.body),
}, },
assignee: assignee.into(), assignee: assignee.into()
} }
} }
+7 -53
View File
@@ -1,15 +1,14 @@
use crate::config::{Config, GiteaConfig}; use crate::config::{Config, GiteaConfig, GitlabConfig};
use crate::error::GeneralError; use crate::error::GeneralError;
use reqwest::header::{HeaderMap, ACCEPT, AUTHORIZATION}; use reqwest::header::{HeaderMap, ACCEPT, AUTHORIZATION};
use reqwest::{ClientBuilder, StatusCode}; use reqwest::{ClientBuilder, StatusCode};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::Serialize;
use url::Url; use url::Url;
#[derive(Debug)] #[derive(Debug)]
pub struct Client { pub struct Client {
token: String, token: String,
// base_uri: String, base_uri: String,
} }
fn is_client_for_url(url: &Url, config: &GiteaConfig) -> bool { fn is_client_for_url(url: &Url, config: &GiteaConfig) -> bool {
@@ -29,7 +28,6 @@ pub(crate) fn has_client_for_url(url: &Url, config: &Config) -> bool {
false false
} }
#[allow(dead_code)]
fn client_for_url(url: &Url, config: &Config) -> Result<Client, GeneralError> { fn client_for_url(url: &Url, config: &Config) -> Result<Client, GeneralError> {
for c in &config.gitea { for c in &config.gitea {
if is_client_for_url(url, c) { if is_client_for_url(url, c) {
@@ -47,16 +45,16 @@ impl Client {
Self::new(&config.token, &config.domain) Self::new(&config.token, &config.domain)
} }
pub fn new(token: &String, _domain: &String) -> Self { pub fn new(token: &String, domain: &String) -> Self {
Client { Client {
token: token.clone(), token: token.clone(),
// base_uri: format!("https://{}", domain.clone()), base_uri: format!("https://{}", domain.clone()),
} }
} }
pub async fn get<T: DeserializeOwned>(&self, url: Url) -> Result<T, GeneralError> { pub async fn get<T: DeserializeOwned>(&self, url: Url) -> Result<T, GeneralError> {
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
headers.append(AUTHORIZATION, format!("token {}", self.token).parse()?); headers.append(AUTHORIZATION, self.token.parse()?);
headers.append(ACCEPT, "application/json".parse()?); headers.append(ACCEPT, "application/json".parse()?);
let client = ClientBuilder::new() let client = ClientBuilder::new()
@@ -81,38 +79,6 @@ impl Client {
}), }),
} }
} }
pub async fn patch<T: DeserializeOwned, B: Serialize>(
&self,
url: Url,
body: &B,
) -> Result<T, GeneralError> {
let mut headers = HeaderMap::new();
headers.append(AUTHORIZATION, format!("token {}", self.token).parse()?);
headers.append(ACCEPT, "application/json".parse()?);
let client = ClientBuilder::new()
.default_headers(headers)
.build()
.unwrap();
let response = client.patch(url.clone()).json(body).send().await?;
match response.status() {
StatusCode::OK | StatusCode::CREATED => {
let result: T = response.json().await?;
Ok(result)
}
_ => Err(GeneralError {
description: format!(
"Could not call PATCH on {:?}, error code {}",
url,
response.status()
),
}),
}
}
} }
#[cfg(test)] #[cfg(test)]
@@ -126,19 +92,7 @@ mod test {
token: "<PASSWORD>".into(), token: "<PASSWORD>".into(),
}; };
assert_eq!( assert_eq!(is_client_for_url(&Url::parse("https://gitea.champs-libres.be/something/somewhere").unwrap(), &config), true);
is_client_for_url( assert_eq!(is_client_for_url(&Url::parse("https://somewhere.else/something/somewhere").unwrap(), &config), false);
&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
);
} }
} }
+16 -24
View File
@@ -1,21 +1,27 @@
use crate::error::GeneralError; use crate::gitea::client::Client;
use crate::gitea::repository::Repository; use crate::gitea::repository::Repository;
use serde::{Deserialize, Serialize}; use serde::Deserialize;
use url::Url; use url::Url;
use crate::error::GeneralError;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Issue { pub struct Issue {
// pub id: u64, pub id: u64,
pub number: u64, number: u64,
pub title: String, pub title: String,
pub body: String, pub body: String,
pub repository: Repository, pub repository: Repository,
pub html_url: String, pub html_url: String,
} }
#[derive(Debug, Serialize)] pub trait IssueClient {
pub struct IssueWriteSetBody { fn get_issue(owner_or_organisation: &String, repo: &String, number: &u64) -> Option<Issue>;
pub body: String, }
impl IssueClient for Client {
fn get_issue(_owner_or_organisation: &String, _repo: &String, number: &u64) -> Option<Issue> {
todo!()
}
} }
pub fn issue_html_url_to_api(url: &Url) -> Result<Url, GeneralError> { pub fn issue_html_url_to_api(url: &Url) -> Result<Url, GeneralError> {
@@ -25,24 +31,13 @@ pub fn issue_html_url_to_api(url: &Url) -> Result<Url, GeneralError> {
let issue = parts.next().unwrap(); let issue = parts.next().unwrap();
let iid = parts.next().unwrap(); let iid = parts.next().unwrap();
if !issue.eq("issues") { if (!issue.eq("issues")) {
return Err(GeneralError { return Err(GeneralError {
description: format!("Issue url is not valid: {}", url), description: format!("Issue url is not valid: {}", url),
}); });
} }
let url = Url::parse( let url = Url::parse(format!("{}://{}/api/v1/repos/{}/{}/issues/{}", url.scheme(), url.host().unwrap(), domain, repo, iid).as_str()).unwrap();
format!(
"{}://{}/api/v1/repos/{}/{}/issues/{}",
url.scheme(),
url.host().unwrap(),
domain,
repo,
iid
)
.as_str(),
)
.unwrap();
Ok(url) Ok(url)
} }
@@ -56,9 +51,6 @@ mod tests {
let url = Url::parse("https://gitea.champs-libres.be/champs-libres/test/issues/1").unwrap(); let url = Url::parse("https://gitea.champs-libres.be/champs-libres/test/issues/1").unwrap();
let result = issue_html_url_to_api(&url).unwrap(); let result = issue_html_url_to_api(&url).unwrap();
assert_eq!( assert_eq!(result.as_str(), "https://gitea.champs-libres.be/api/v1/repos/champs-libres/test/issues/1");
result.as_str(),
"https://gitea.champs-libres.be/api/v1/repos/champs-libres/test/issues/1"
);
} }
} }
+1 -1
View File
@@ -1,4 +1,4 @@
pub(crate) mod action;
pub mod client; pub mod client;
pub mod issue; pub mod issue;
pub mod repository; pub mod repository;
pub(crate) mod action;
+3 -3
View File
@@ -2,8 +2,8 @@ use serde::Deserialize;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Repository { pub struct Repository {
// id: u64, id: u64,
// name: String, name: String,
// owner: String, owner: String,
pub full_name: String, pub full_name: String,
} }
+4
View File
@@ -3,6 +3,10 @@ use crate::error::GeneralError;
use gitlab::{AsyncGitlab, GitlabBuilder}; use gitlab::{AsyncGitlab, GitlabBuilder};
use url::Url; use url::Url;
pub struct ClientProvider {}
impl ClientProvider {}
fn is_client_for_url(url: &Url, config: &GitlabConfig) -> bool { fn is_client_for_url(url: &Url, config: &GitlabConfig) -> bool {
if url.domain() == Some(config.domain.as_str()) { if url.domain() == Some(config.domain.as_str()) {
return true; return true;
+6 -6
View File
@@ -1,13 +1,13 @@
use serde::Deserialize; use serde::Deserialize;
// #[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
// pub struct HalEntity { pub struct HalEntity {
// // #[serde(rename = "_type")] #[serde(rename = "_type")]
// // pub d_type: String, pub d_type: String,
// } }
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
pub struct Link { pub struct Link {
pub href: String, pub href: String,
// pub title: Option<String>, pub title: Option<String>,
} }
+6 -6
View File
@@ -1,11 +1,11 @@
use crate::openproject::client::{handle_response_status, Client, OpenProjectError}; use crate::openproject::client::{handle_response_status, Client, OpenProjectError};
// use crate::openproject::hal::HalEntity; use crate::openproject::hal::HalEntity;
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct User { pub struct User {
pub href: String, pub href: String,
// pub title: String, pub title: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Links { pub struct Links {
@@ -13,12 +13,12 @@ pub struct Links {
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Root { pub struct Root {
// #[serde(rename = "instanceName")] #[serde(rename = "instanceName")]
// pub instance_name: String, pub instance_name: String,
#[serde(rename = "_links")] #[serde(rename = "_links")]
pub links: Links, pub links: Links,
// #[serde(flatten)] #[serde(flatten)]
// pub hal_entity: HalEntity, pub hal_entity: HalEntity,
} }
pub trait RootClient { pub trait RootClient {
+7 -4
View File
@@ -10,10 +10,13 @@ pub struct UserLink {
} }
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
pub struct User { pub struct User {
// #[serde(rename = "_type")] #[serde(rename = "_type")]
// pub d_type: String, #[allow(unused_variables)]
// pub id: u64, pub d_type: String,
// pub name: String, #[allow(unused_variables)]
pub id: u64,
#[allow(unused_variables)]
pub name: String,
#[serde(rename = "_links")] #[serde(rename = "_links")]
pub d_links: UserLink, pub d_links: UserLink,
} }
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::openproject::user::User;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::openproject::user::User;
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct WorkPackageWriterAssignee { pub struct WorkPackageWriterAssignee {
+2 -1
View File
@@ -1,10 +1,11 @@
use crate::cli::Issue2Work; use crate::cli::Issue2Work;
use crate::config::Config; use crate::config::Config;
use crate::error::GeneralError; use crate::error::GeneralError;
use crate::gitlab::client::ClientProvider;
use gitlab::{AsyncGitlab, GitlabBuilder};
use url::Url; use url::Url;
pub(crate) mod issue2work; pub(crate) mod issue2work;
pub mod utils;
pub trait Issue2WorkActionTrait { pub trait Issue2WorkActionTrait {
async fn run(&self, url: &Url, config: &Config, args: &Issue2Work) -> Result<(), GeneralError>; async fn run(&self, url: &Url, config: &Config, args: &Issue2Work) -> Result<(), GeneralError>;
-180
View File
@@ -1,180 +0,0 @@
pub enum IssueRelated {
OpenProjectIssue(String),
}
pub fn append_related_issues(issue: &IssueRelated, content: &String) -> String {
let mut splitted = content.lines();
let mut new_content: Vec<String> = Vec::new();
let mut found = false;
let mut iteration_started = false;
while let Some(line) = splitted.next() {
if line.contains("## Related issues") {
found = true;
new_content.push(line.parse().unwrap());
// we go to the end of the section
while let Some(line) = splitted.next() {
if line.starts_with("-") || line.starts_with("*") {
iteration_started = true;
new_content.push(line.parse().unwrap());
} else if (line.trim().is_empty()) && iteration_started {
new_content.append(&mut add_related_issues_section(issue));
iteration_started = false;
found = true;
new_content.push(line.parse().unwrap());
break;
} else if line.starts_with("#") {
new_content.push("new title found".to_string());
found = true;
new_content.append(&mut add_related_issues_section(issue));
new_content.push("".to_string());
new_content.push(line.parse().unwrap());
break;
} else {
new_content.push(line.parse().unwrap());
}
}
} else {
new_content.push(line.parse().unwrap());
}
}
if !found || iteration_started {
if !found {
new_content.append(&mut add_related_issues_title());
}
new_content.append(&mut add_related_issues_section(issue));
}
new_content.join(&"\n")
}
fn add_related_issues_title() -> Vec<String> {
let mut previous: Vec<String> = Vec::new();
previous.push("".to_string());
previous.push("## Related issues".to_string());
previous.push("".to_string());
previous
}
fn add_related_issues_section(issue: &IssueRelated) -> Vec<String> {
let mut previous: Vec<String> = Vec::new();
previous.push(convert_issue_link_items(issue));
previous
}
fn convert_issue_link_items(issue: &IssueRelated) -> String {
match issue {
IssueRelated::OpenProjectIssue(issue_url) => format!("- [{}]({})", issue_url, issue_url),
}
}
#[cfg(test)]
mod tests {
use crate::planning::utils::{append_related_issues, IssueRelated};
#[test]
pub fn test_append_related_issues_content_empty() {
let issue = IssueRelated::OpenProjectIssue("https://example/wp/1".to_string());
assert_eq!(
r#"
## Related issues
- [https://example/wp/1](https://example/wp/1)"#,
append_related_issues(&issue, &("".to_string()))
);
}
#[test]
pub fn test_append_related_issues_content_not_empty() {
let issue = IssueRelated::OpenProjectIssue("https://example/wp/1".to_string());
assert_eq!(
r#"Something happens.
## Some title
Some content
## Related issues
- [https://example/wp/1](https://example/wp/1)"#,
append_related_issues(
&issue,
&"Something happens.\n\
\n\
## Some title\n\
\n\
Some content"
.to_string()
),
);
}
#[test]
pub fn test_append_related_issues_existing_section_related_issues() {
let issue = IssueRelated::OpenProjectIssue("https://example/wp/1".to_string());
assert_eq!(
r#"Something happens.
## Some title
Some content
## Related issues
- [https://example/wp/2](https://example/wp/2)
- [https://example/wp/1](https://example/wp/1)"#,
append_related_issues(
&issue,
&r#"Something happens.
## Some title
Some content
## Related issues
- [https://example/wp/2](https://example/wp/2)"#
.to_string()
),
);
}
#[test]
pub fn test_append_related_issues_existing_section_related_issues_not_last() {
let issue = IssueRelated::OpenProjectIssue("https://example/wp/1".to_string());
assert_eq!(
r#"Something happens.
## Some title
Some content
## Related issues
- [https://example/wp/2](https://example/wp/2)
- [https://example/wp/1](https://example/wp/1)
## Other content
Some other content"#,
append_related_issues(
&issue,
&r#"Something happens.
## Some title
Some content
## Related issues
- [https://example/wp/2](https://example/wp/2)
## Other content
Some other content"#
.to_string()
),
);
}
}