Compare commits
	
		
			22 Commits
		
	
	
		
			1d8a70768f
			...
			fix-releas
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e49b539e61 | |||
| c1e8019688 | |||
| b648599088 | |||
| ca46a9eccc | |||
| 81350f38e4 | |||
| 1f2d42e1d5 | |||
| a0f67464b5 | |||
| ff5699e627 | |||
| dc0040c2d1 | |||
| 9950282d6e | |||
| 7c8e8eb236 | |||
| 442b5d25b4 | |||
| 4763d48290 | |||
| 1af6596b51 | |||
| c5f4f9fcf9 | |||
| 9f544c66c2 | |||
| 7c1cdb64ec | |||
| 4bb787488d | |||
| ab0df54893 | |||
| 7b6cc33ecb | |||
| 696fd15cfa | |||
| 957c5b91bc | 
| @@ -1,4 +0,0 @@ | ||||
| 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 | ||||
| @@ -1,3 +0,0 @@ | ||||
| kind: Added | ||||
| body: Allow to configure multiple gitlab instances | ||||
| time: 2024-03-17T21:35:56.447907065+01:00 | ||||
							
								
								
									
										5
									
								
								.changes/v0.2.0.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.changes/v0.2.0.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| ## 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
									
								
								.changes/v0.3.0.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/v0.3.0.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| ## v0.3.0 - 2024-11-17 | ||||
| ### Added | ||||
| * Open the newly create work package after the wp has been created | ||||
							
								
								
									
										3
									
								
								.changes/v0.4.0.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/v0.4.0.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| ## 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" | ||||
							
								
								
									
										1
									
								
								.changes/v_release_test_4.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.changes/v_release_test_4.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| ok | ||||
| @@ -20,17 +20,14 @@ jobs: | ||||
|         id: read_release | ||||
|         with: | ||||
|           path: .changes/${{ github.ref_name }}.md | ||||
|       - name: Setup go for using go gitea actions | ||||
|         uses: https://github.com/actions/setup-go@v4 | ||||
|         with: | ||||
|           go-version: '>=1.20.1' | ||||
|       - name: Use Go Action to release | ||||
|         id: use-go-action | ||||
|         uses: https://gitea.com/actions/release-action@main | ||||
|       - name: Release | ||||
|         uses: https://gitea.com/akkuman/gitea-release-action@v1 | ||||
|         env: | ||||
|           NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18 | ||||
|         with: | ||||
|           files: |- | ||||
|             target/release/cl-cli | ||||
|             config.toml.dist | ||||
|           api_key: '${{secrets.RELEASE_TOKEN}}' | ||||
|           body: | | ||||
|             ${{ steps.read_release.outputs.content }} | ||||
|           md5sum: true | ||||
|           sha256sum: true | ||||
|           body_path: .changes/${{ github.ref_name }}.md | ||||
|   | ||||
							
								
								
									
										25
									
								
								.gitea/workflows/release/check.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								.gitea/workflows/release/check.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| name: Check go code | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - '**' | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - '**' | ||||
|   pull_request_target: | ||||
|     branches: | ||||
|       - '**' | ||||
|  | ||||
| jobs: | ||||
|   build-and-release: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: https://github.com/actions/checkout@v4 | ||||
|       - name: Install rust toolchain | ||||
|         uses: https://github.com/dtolnay/rust-toolchain@stable | ||||
|       - name: Build binaries | ||||
|         run: cargo build | ||||
|       - name: Run tests | ||||
|         run: cargo test | ||||
							
								
								
									
										14
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -6,6 +6,20 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), | ||||
| 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 | ||||
| ### Added | ||||
| * Initiate changie versioning | ||||
|   | ||||
							
								
								
									
										2056
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2056
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										15
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								Cargo.toml
									
									
									
									
									
								
							| @@ -7,11 +7,12 @@ edition = "2021" | ||||
|  | ||||
| [dependencies] | ||||
| clap = { version = "4.1.13", features = ["derive"] } | ||||
| gitlab = "0.1607.0" | ||||
| reqwest = "0.11.23" | ||||
| serde = { version = "1.0.158", features = ["derive"] } | ||||
| toml = "0.7.3" | ||||
| url = "2.3.1" | ||||
| gitlab = "0.1804.0" | ||||
| reqwest = "0.12.24" | ||||
| serde = { version = "1.0.228", features = ["derive"] } | ||||
| toml = "0.9.8" | ||||
| url = "2.5.7" | ||||
| tokio = { version = "1.0.0", features = ["rt", "rt-multi-thread", "macros"] } | ||||
| simple-home-dir = "0.2.1" | ||||
| log = "0.4.17" | ||||
| simple-home-dir = "0.5.2" | ||||
| log = "0.4.28" | ||||
| open = "5.3.2" | ||||
|   | ||||
| @@ -1,6 +1,11 @@ | ||||
| [gitlab] | ||||
| [[gitlab]] | ||||
| # generate from https://gitlab.com/-/user_settings/personal_access_tokens | ||||
| token = "glpat-example" | ||||
| domain = "gitlab.com" | ||||
|  | ||||
| [[gitea]] | ||||
| token = "abcdexempletoken" | ||||
| domain = "gitea.champs-libres.be" | ||||
|  | ||||
| [openproject] | ||||
| # generate api token from https://champs-libres.openproject.com/my/access_token | ||||
|   | ||||
| @@ -1,9 +1,6 @@ | ||||
| use crate::config::Config; | ||||
| use crate::error::GeneralError; | ||||
| use crate::gitea::issue::Issue; | ||||
| use crate::openproject::client::Client; | ||||
| use crate::openproject::root::RootClient; | ||||
| use crate::openproject::user::GetMe; | ||||
| use url::Url; | ||||
|  | ||||
| pub(crate) async fn debug(config: Config) -> Result<(), GeneralError> { | ||||
| @@ -19,8 +16,9 @@ pub(crate) async fn debug(config: Config) -> Result<(), GeneralError> { | ||||
|  | ||||
|     println!("issue: {:?}", issue); | ||||
|  | ||||
|     return Ok(()); | ||||
|     Ok(()) | ||||
|  | ||||
|     /* | ||||
|     let open_project_client = Client::from_config(&config.openproject); | ||||
|     println!("base_url: {}", open_project_client.base_url); | ||||
|     println!("base_url: will get root"); | ||||
| @@ -30,4 +28,6 @@ pub(crate) async fn debug(config: Config) -> Result<(), GeneralError> { | ||||
|     println!("me: {:?}", u); | ||||
|  | ||||
|     Ok(()) | ||||
|  | ||||
|      */ | ||||
| } | ||||
|   | ||||
| @@ -7,7 +7,7 @@ pub struct GeneralError { | ||||
| } | ||||
|  | ||||
| impl From<InvalidHeaderValue> for GeneralError { | ||||
|     fn from(value: InvalidHeaderValue) -> Self { | ||||
|     fn from(_value: InvalidHeaderValue) -> Self { | ||||
|         GeneralError { | ||||
|             description: "Unable to convert the token into header value".to_string(), | ||||
|         } | ||||
|   | ||||
| @@ -1,22 +1,22 @@ | ||||
| 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::gitea::issue::{issue_html_url_to_api, Issue, IssueWriteSetBody}; | ||||
| use crate::openproject::user::{GetMe, User}; | ||||
| use crate::openproject::work::{WorkPackage, WorkPackageWriter, WorkPackageWriterAssignee}; | ||||
| use crate::openproject::work::WorkPackageWriter; | ||||
| use crate::planning::utils::{append_related_issues, IssueRelated}; | ||||
| use crate::planning::Issue2WorkActionTrait; | ||||
| use url::Url; | ||||
|  | ||||
| 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 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, | ||||
| @@ -26,18 +26,38 @@ impl Issue2WorkActionTrait for GiteaAction { | ||||
|                     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 | ||||
|         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!( | ||||
|             "new work package created: {:?}, edit at {}", | ||||
|             work_package.subject, url_wp | ||||
|         ); | ||||
|  | ||||
|         if let Err(e) = open::that(url_wp) { | ||||
|             println!("failed to open work package in browser: {}", e); | ||||
|         }; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
| @@ -49,15 +69,14 @@ impl Issue2WorkActionTrait for GiteaAction { | ||||
| fn create_work_package_from_issue(issue: &Issue, assignee: Option<User>) -> WorkPackageWriter { | ||||
|     WorkPackageWriter { | ||||
|         subject: format!( | ||||
|             "{} ({})", | ||||
|             issue.title, | ||||
|             issue.repository.full_name | ||||
|             "{} ({}/{})", | ||||
|             issue.title, issue.repository.full_name, issue.number | ||||
|         ), | ||||
|         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() | ||||
|         assignee: assignee.into(), | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,14 +1,15 @@ | ||||
| use crate::config::{Config, GiteaConfig, GitlabConfig}; | ||||
| use crate::config::{Config, GiteaConfig}; | ||||
| use crate::error::GeneralError; | ||||
| use reqwest::header::{HeaderMap, ACCEPT, AUTHORIZATION}; | ||||
| use reqwest::{ClientBuilder, StatusCode}; | ||||
| use serde::de::DeserializeOwned; | ||||
| use serde::Serialize; | ||||
| use url::Url; | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct Client { | ||||
|     token: String, | ||||
|     base_uri: String, | ||||
|     // base_uri: String, | ||||
| } | ||||
|  | ||||
| fn is_client_for_url(url: &Url, config: &GiteaConfig) -> bool { | ||||
| @@ -28,6 +29,7 @@ pub(crate) fn has_client_for_url(url: &Url, config: &Config) -> bool { | ||||
|     false | ||||
| } | ||||
|  | ||||
| #[allow(dead_code)] | ||||
| fn client_for_url(url: &Url, config: &Config) -> Result<Client, GeneralError> { | ||||
|     for c in &config.gitea { | ||||
|         if is_client_for_url(url, c) { | ||||
| @@ -45,16 +47,16 @@ impl Client { | ||||
|         Self::new(&config.token, &config.domain) | ||||
|     } | ||||
|  | ||||
|     pub fn new(token: &String, domain: &String) -> Self { | ||||
|     pub fn new(token: &String, _domain: &String) -> Self { | ||||
|         Client { | ||||
|             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> { | ||||
|         let mut headers = HeaderMap::new(); | ||||
|         headers.append(AUTHORIZATION, self.token.parse()?); | ||||
|         headers.append(AUTHORIZATION, format!("token {}", self.token).parse()?); | ||||
|         headers.append(ACCEPT, "application/json".parse()?); | ||||
|  | ||||
|         let client = ClientBuilder::new() | ||||
| @@ -79,6 +81,38 @@ 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)] | ||||
| @@ -92,7 +126,19 @@ mod test { | ||||
|             token: "<PASSWORD>".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); | ||||
|         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 | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,27 +1,21 @@ | ||||
| use crate::gitea::client::Client; | ||||
| use crate::gitea::repository::Repository; | ||||
| use serde::Deserialize; | ||||
| use url::Url; | ||||
| use crate::error::GeneralError; | ||||
| use crate::gitea::repository::Repository; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use url::Url; | ||||
|  | ||||
| #[derive(Debug, Deserialize)] | ||||
| pub struct Issue { | ||||
|     pub id: u64, | ||||
|     number: u64, | ||||
|     // pub id: u64, | ||||
|     pub number: u64, | ||||
|     pub title: String, | ||||
|     pub body: String, | ||||
|     pub repository: Repository, | ||||
|     pub html_url: String, | ||||
| } | ||||
|  | ||||
| pub trait IssueClient { | ||||
|     fn get_issue(owner_or_organisation: &String, repo: &String, number: &u64) -> Option<Issue>; | ||||
| } | ||||
|  | ||||
| impl IssueClient for Client { | ||||
|     fn get_issue(_owner_or_organisation: &String, _repo: &String, number: &u64) -> Option<Issue> { | ||||
|         todo!() | ||||
|     } | ||||
| #[derive(Debug, Serialize)] | ||||
| pub struct IssueWriteSetBody { | ||||
|     pub body: String, | ||||
| } | ||||
|  | ||||
| pub fn issue_html_url_to_api(url: &Url) -> Result<Url, GeneralError> { | ||||
| @@ -31,13 +25,24 @@ pub fn issue_html_url_to_api(url: &Url) -> Result<Url, GeneralError> { | ||||
|     let issue = parts.next().unwrap(); | ||||
|     let iid = parts.next().unwrap(); | ||||
|  | ||||
|     if (!issue.eq("issues")) { | ||||
|     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(); | ||||
|     let url = Url::parse( | ||||
|         format!( | ||||
|             "{}://{}/api/v1/repos/{}/{}/issues/{}", | ||||
|             url.scheme(), | ||||
|             url.host().unwrap(), | ||||
|             domain, | ||||
|             repo, | ||||
|             iid | ||||
|         ) | ||||
|         .as_str(), | ||||
|     ) | ||||
|     .unwrap(); | ||||
|  | ||||
|     Ok(url) | ||||
| } | ||||
| @@ -51,6 +56,9 @@ mod tests { | ||||
|         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"); | ||||
|         assert_eq!( | ||||
|             result.as_str(), | ||||
|             "https://gitea.champs-libres.be/api/v1/repos/champs-libres/test/issues/1" | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| pub(crate) mod action; | ||||
| pub mod client; | ||||
| pub mod issue; | ||||
| pub mod repository; | ||||
| pub(crate) mod action; | ||||
|   | ||||
| @@ -2,8 +2,8 @@ use serde::Deserialize; | ||||
|  | ||||
| #[derive(Debug, Deserialize)] | ||||
| pub struct Repository { | ||||
|     id: u64, | ||||
|     name: String, | ||||
|     owner: String, | ||||
|     // id: u64, | ||||
|     // name: String, | ||||
|     // owner: String, | ||||
|     pub full_name: String, | ||||
| } | ||||
|   | ||||
| @@ -2,13 +2,12 @@ use crate::cli::Issue2Work; | ||||
| use crate::config::Config; | ||||
| use crate::error::GeneralError; | ||||
| use crate::gitlab::client::{client_for_url, has_client_for_url}; | ||||
| use crate::gitlab::issue::IssueBundle; | ||||
| 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::Issue2WorkActionTrait; | ||||
| use gitlab::api::{issues, projects, AsyncQuery}; | ||||
| use gitlab::{Issue, Project}; | ||||
| use url::Url; | ||||
|  | ||||
| #[derive(Debug)] | ||||
| @@ -41,7 +40,7 @@ impl Issue2WorkActionTrait for GitlabAction { | ||||
|         let issue = issues.first().unwrap(); | ||||
|  | ||||
|         let project_endpoint = projects::Project::builder() | ||||
|             .project(issue.project_id.value()) | ||||
|             .project(issue.project_id) | ||||
|             .build() | ||||
|             .unwrap(); | ||||
|  | ||||
|   | ||||
| @@ -3,10 +3,6 @@ use crate::error::GeneralError; | ||||
| use gitlab::{AsyncGitlab, GitlabBuilder}; | ||||
| use url::Url; | ||||
|  | ||||
| pub struct ClientProvider {} | ||||
|  | ||||
| impl ClientProvider {} | ||||
|  | ||||
| fn is_client_for_url(url: &Url, config: &GitlabConfig) -> bool { | ||||
|     if url.domain() == Some(config.domain.as_str()) { | ||||
|         return true; | ||||
|   | ||||
| @@ -1,5 +1,17 @@ | ||||
| use gitlab::Issue; | ||||
| use gitlab::Project; | ||||
| use serde::Deserialize; | ||||
|  | ||||
| #[derive(Clone, Debug, Deserialize)] | ||||
| pub struct Issue { | ||||
|     pub iid: u64, | ||||
|     pub title: String, | ||||
|     pub web_url: String, | ||||
|     pub project_id: u64, | ||||
| } | ||||
|  | ||||
| #[derive(Clone, Debug, Deserialize)] | ||||
| pub struct Project { | ||||
|     pub name_with_namespace: String, | ||||
| } | ||||
|  | ||||
| /// A struct which contains Issue and Project | ||||
| #[derive(Debug)] | ||||
|   | ||||
| @@ -1,13 +1,13 @@ | ||||
| use serde::Deserialize; | ||||
|  | ||||
| #[derive(Deserialize, Debug)] | ||||
| pub struct HalEntity { | ||||
|     #[serde(rename = "_type")] | ||||
|     pub d_type: String, | ||||
| } | ||||
| // #[derive(Deserialize, Debug)] | ||||
| // pub struct HalEntity { | ||||
| //     // #[serde(rename = "_type")] | ||||
| //     // pub d_type: String, | ||||
| // } | ||||
|  | ||||
| #[derive(Deserialize, Debug, Clone)] | ||||
| pub struct Link { | ||||
|     pub href: String, | ||||
|     pub title: Option<String>, | ||||
|     // pub title: Option<String>, | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| use crate::openproject::client::{handle_response_status, Client, OpenProjectError}; | ||||
| use crate::openproject::hal::HalEntity; | ||||
| // use crate::openproject::hal::HalEntity; | ||||
| use serde::Deserialize; | ||||
|  | ||||
| #[derive(Deserialize, Debug)] | ||||
| pub struct User { | ||||
|     pub href: String, | ||||
|     pub title: String, | ||||
|     // pub title: String, | ||||
| } | ||||
| #[derive(Deserialize, Debug)] | ||||
| pub struct Links { | ||||
| @@ -13,12 +13,12 @@ pub struct Links { | ||||
| } | ||||
| #[derive(Deserialize, Debug)] | ||||
| pub struct Root { | ||||
|     #[serde(rename = "instanceName")] | ||||
|     pub instance_name: String, | ||||
|     // #[serde(rename = "instanceName")] | ||||
|     // pub instance_name: String, | ||||
|     #[serde(rename = "_links")] | ||||
|     pub links: Links, | ||||
|     #[serde(flatten)] | ||||
|     pub hal_entity: HalEntity, | ||||
|     // #[serde(flatten)] | ||||
|     // pub hal_entity: HalEntity, | ||||
| } | ||||
|  | ||||
| pub trait RootClient { | ||||
|   | ||||
| @@ -10,13 +10,10 @@ 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 = "_type")] | ||||
|     // pub d_type: String, | ||||
|     // pub id: u64, | ||||
|     // pub name: String, | ||||
|     #[serde(rename = "_links")] | ||||
|     pub d_links: UserLink, | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use crate::openproject::user::User; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| #[derive(Serialize, Debug)] | ||||
| pub struct WorkPackageWriterAssignee { | ||||
|   | ||||
| @@ -1,11 +1,10 @@ | ||||
| use crate::cli::Issue2Work; | ||||
| use crate::config::Config; | ||||
| use crate::error::GeneralError; | ||||
| use crate::gitlab::client::ClientProvider; | ||||
| use gitlab::{AsyncGitlab, GitlabBuilder}; | ||||
| use url::Url; | ||||
|  | ||||
| pub(crate) mod issue2work; | ||||
| pub mod utils; | ||||
|  | ||||
| pub trait Issue2WorkActionTrait { | ||||
|     async fn run(&self, url: &Url, config: &Config, args: &Issue2Work) -> Result<(), GeneralError>; | ||||
|   | ||||
							
								
								
									
										180
									
								
								src/planning/utils.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								src/planning/utils.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,180 @@ | ||||
| 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() | ||||
|             ), | ||||
|         ); | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user