Compare commits
9 Commits
v0.5.0-alp
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
eb730f4ae0
|
|||
| 69eafc509a | |||
| f72f03e7d4 | |||
| 255e7fd5f0 | |||
|
58422f926e
|
|||
|
986bb4e2c2
|
|||
|
606486c3fe
|
|||
|
0b9099d2d3
|
|||
| af1b805616 |
@@ -1,3 +0,0 @@
|
|||||||
kind: Added
|
|
||||||
body: Add a web extension
|
|
||||||
time: 2025-11-12T20:13:11.961655215+01:00
|
|
||||||
4
.changes/v0.5.0.md
Normal file
4
.changes/v0.5.0.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
## v0.5.0 - 2025-11-13
|
||||||
|
### Added
|
||||||
|
* Add a web extension
|
||||||
|
* Install using package manager, with CI generating the package
|
||||||
3
.changes/v0.6.0.md
Normal file
3
.changes/v0.6.0.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## v0.6.0 - 2025-11-18
|
||||||
|
### Added
|
||||||
|
* Append work package link to gitlab description after creating a WP from a gitlab issue
|
||||||
@@ -80,5 +80,4 @@ jobs:
|
|||||||
md5sum: true
|
md5sum: true
|
||||||
sha256sum: true
|
sha256sum: true
|
||||||
body: Test release
|
body: Test release
|
||||||
prerelease: true
|
body_path: .changes/${{ github.ref_name }}.md
|
||||||
#body_path: .changes/${{ github.ref_name }}.md
|
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ 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.5.0 - 2025-11-13
|
||||||
|
### Added
|
||||||
|
* Add a web extension
|
||||||
|
* Install using package manager, with CI generating the package
|
||||||
|
|
||||||
## v0.4.1 - 2025-10-25
|
## v0.4.1 - 2025-10-25
|
||||||
### Fixed
|
### Fixed
|
||||||
* Fix dependencies
|
* Fix dependencies
|
||||||
|
|||||||
41
Cargo.lock
generated
41
Cargo.lock
generated
@@ -17,6 +17,15 @@ version = "2.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
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]]
|
[[package]]
|
||||||
name = "android_system_properties"
|
name = "android_system_properties"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -174,12 +183,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cl-cli"
|
name = "cl-cli"
|
||||||
version = "0.5.0-alpha16"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"gitlab",
|
"gitlab",
|
||||||
"log",
|
"log",
|
||||||
"open",
|
"open",
|
||||||
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -1155,6 +1165,35 @@ dependencies = [
|
|||||||
"getrandom 0.3.4",
|
"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]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.12.24"
|
version = "0.12.24"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cl-cli"
|
name = "cl-cli"
|
||||||
version = "0.5.0-alpha16"
|
version = "0.6.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
description = "Some helpers scripts for Champs-Libres"
|
description = "Some helpers scripts for Champs-Libres"
|
||||||
@@ -27,6 +27,7 @@ simple-home-dir = "0.5.2"
|
|||||||
log = "0.4.28"
|
log = "0.4.28"
|
||||||
open = "5.3.2"
|
open = "5.3.2"
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.145"
|
||||||
|
regex = "1.12.2"
|
||||||
|
|
||||||
[package.metadata.deb]
|
[package.metadata.deb]
|
||||||
maintainer = "Julien Fastré <julien.fastre@champs-libres.coop>"
|
maintainer = "Julien Fastré <julien.fastre@champs-libres.coop>"
|
||||||
|
|||||||
59
README.md
59
README.md
@@ -6,23 +6,51 @@ Currently:
|
|||||||
|
|
||||||
- convert a gitlab issue to a work package
|
- convert a gitlab issue to a work package
|
||||||
|
|
||||||
## Install & configure
|
## Install
|
||||||
|
|
||||||
### Install
|
There are two components to install:
|
||||||
|
|
||||||
Download the most recent binaries at https://gitea.champs-libres.be/julienfastre/cl-cli/releases
|
- a firefox add-ons;
|
||||||
|
- some binaries on the host machine
|
||||||
|
|
||||||
Once downloaded, install it:
|
### Install firefox add-on
|
||||||
|
|
||||||
```
|
Using firefox, download the `.xpi` file from the [release page](https://gitea.champs-libres.be/julienfastre/cl-cli/releases) of the `julienfastre/cl-cli` repository.
|
||||||
# this will install the command globally with the name "cl-cli"
|
|
||||||
sudo install cl-cli /usr/local/bin/cl-cli
|
|
||||||
|
|
||||||
# this will install the command globally with the name "cl"
|
[Go to the most recent release](https://gitea.champs-libres.be/julienfastre/cl-cli/releases/latest)
|
||||||
sudo install cl-cli /usr/local/bin/cl
|
|
||||||
|
### Install cl-cli binaries
|
||||||
|
|
||||||
|
#### Using package manager
|
||||||
|
|
||||||
|
A debian package is published and stored on gitea.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# download the gpg key
|
||||||
|
sudo curl https://gitea.champs-libres.be/api/packages/julienfastre/debian/repository.key -o /etc/apt/keyrings/gitea-julienfastre.asc
|
||||||
|
|
||||||
|
# add the package repository. You must adapt the $distribution (see below)
|
||||||
|
echo "deb [signed-by=/etc/apt/keyrings/gitea-julienfastre.asc] https://gitea.champs-libres.be/api/packages/julienfastre/debian $distribution main" | sudo tee -a /etc/apt/sources.list.d/gitea.list
|
||||||
|
|
||||||
|
# update
|
||||||
|
sudo apt update
|
||||||
|
|
||||||
|
# install
|
||||||
|
sudo apt install cl-cli
|
||||||
```
|
```
|
||||||
|
|
||||||
### Configure
|
Available distributions:
|
||||||
|
|
||||||
|
- Ubuntu 24..04: noble
|
||||||
|
- Ubuntu 25.04: plucky
|
||||||
|
- Ubuntu: 25.10: questing
|
||||||
|
|
||||||
|
**Note:** The package is very simple, there are very few dependencies, and there isn't any difference between them for now.
|
||||||
|
|
||||||
|
|
||||||
|
## Configure
|
||||||
|
|
||||||
Copy the file [config.toml.dist](./config.toml.dist) as a template, and
|
Copy the file [config.toml.dist](./config.toml.dist) as a template, and
|
||||||
save it at the path `$HOME/.config/cl-cli/config.toml`:
|
save it at the path `$HOME/.config/cl-cli/config.toml`:
|
||||||
@@ -33,14 +61,19 @@ cp config.toml $HOME/.config/cl-cli/config.toml
|
|||||||
editor $HOME/.config/cl-cli/config.toml
|
editor $HOME/.config/cl-cli/config.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, fill it with the required configuration options (gitlab and openproject token).
|
Then, fill it with the required configuration options:
|
||||||
|
|
||||||
|
- gitea personal access token ([Access the page on gitea.champs-libres.be](https://gitea.champs-libres.be/user/settings/applications)):
|
||||||
|
|
||||||
|

|
||||||
|
- Gitlab: [Create an access token](https://gitlab.com/-/user_settings/personal_access_tokens?name=cl-cli&scopes=api,read_user) With the "api" and "read_user" scopes
|
||||||
|
- [Openproject](https://champs-libres.openproject.com/my/access_tokens)
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
## Usage as CLI
|
||||||
|
|
||||||
### Convert a gitlab issue into a work package
|
### Convert a gitlab issue into a work package
|
||||||
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cl-cli planning i2work https://gitlab.com/Chill-Projet/chill-bundles/-/issues/240 chill
|
cl-cli planning i2work https://gitlab.com/Chill-Projet/chill-bundles/-/issues/240 chill
|
||||||
```
|
```
|
||||||
|
|||||||
BIN
docs/configuration/gitea_screenshot_configure_pat.png
Normal file
BIN
docs/configuration/gitea_screenshot_configure_pat.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
@@ -6,9 +6,12 @@ use crate::gitlab::issue::{Issue, IssueBundle, Project};
|
|||||||
use crate::openproject::client::Client;
|
use crate::openproject::client::Client;
|
||||||
use crate::openproject::user::{GetMe, User};
|
use crate::openproject::user::{GetMe, User};
|
||||||
use crate::openproject::work::WorkPackageWriterAssignee;
|
use crate::openproject::work::WorkPackageWriterAssignee;
|
||||||
|
use crate::planning::utils::{append_related_issues, IssueRelated};
|
||||||
use crate::planning::Issue2WorkActionTrait;
|
use crate::planning::Issue2WorkActionTrait;
|
||||||
use crate::planning::Issue2WorkResult;
|
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;
|
use url::Url;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -71,6 +74,27 @@ impl Issue2WorkActionTrait for GitlabAction {
|
|||||||
.create_work_package(&(&dto).into(), &args.project_id)
|
.create_work_package(&(&dto).into(), &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), &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<Issue, ApiError<RestError>> = endpoint_put.query_async(&client).await;
|
||||||
|
if r.is_err() {
|
||||||
|
eprintln!("Error while updating the issue description: {:?}", r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let result = Issue2WorkResult {
|
let result = Issue2WorkResult {
|
||||||
work_package_url: format!(
|
work_package_url: format!(
|
||||||
"{}/projects/{}/work_packages/{}",
|
"{}/projects/{}/work_packages/{}",
|
||||||
|
|||||||
@@ -65,10 +65,30 @@ fn add_related_issues_section(issue: &IssueRelated) -> Vec<String> {
|
|||||||
|
|
||||||
fn convert_issue_link_items(issue: &IssueRelated) -> String {
|
fn convert_issue_link_items(issue: &IssueRelated) -> String {
|
||||||
match issue {
|
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<u32> {
|
||||||
|
// Cette regex capture le dernier segment numérique si l’URL se termine par :
|
||||||
|
// - /work_packages/<id>
|
||||||
|
// - /wp/<id>
|
||||||
|
// 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::<u32>().ok())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::planning::utils::{append_related_issues, IssueRelated};
|
use crate::planning::utils::{append_related_issues, IssueRelated};
|
||||||
@@ -80,7 +100,8 @@ mod tests {
|
|||||||
r#"
|
r#"
|
||||||
## Related issues
|
## 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()))
|
append_related_issues(&issue, &("".to_string()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -97,7 +118,8 @@ Some content
|
|||||||
|
|
||||||
## Related issues
|
## Related issues
|
||||||
|
|
||||||
- [https://example/wp/1](https://example/wp/1)"#,
|
- [https://example/wp/1](https://example/wp/1)
|
||||||
|
- OP#1"#,
|
||||||
append_related_issues(
|
append_related_issues(
|
||||||
&issue,
|
&issue,
|
||||||
&"Something happens.\n\
|
&"Something happens.\n\
|
||||||
@@ -123,7 +145,8 @@ Some content
|
|||||||
## Related issues
|
## Related issues
|
||||||
|
|
||||||
- [https://example/wp/2](https://example/wp/2)
|
- [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(
|
append_related_issues(
|
||||||
&issue,
|
&issue,
|
||||||
&r#"Something happens.
|
&r#"Something happens.
|
||||||
@@ -154,6 +177,7 @@ Some content
|
|||||||
|
|
||||||
- [https://example/wp/2](https://example/wp/2)
|
- [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
|
||||||
|
|
||||||
## Other content
|
## Other content
|
||||||
|
|
||||||
|
|||||||
@@ -172,10 +172,10 @@ mod tests {
|
|||||||
fn deserializes_issue2work_variant() {
|
fn deserializes_issue2work_variant() {
|
||||||
// Given: a JSON with the external tag "type" matching the enum variant name
|
// Given: a JSON with the external tag "type" matching the enum variant name
|
||||||
let payload = json!({
|
let payload = json!({
|
||||||
"type": "Issue2Work",
|
"type": "Issue2Work",
|
||||||
"url": "https://example.com/issues/123",
|
"url": "https://example.com/issues/123",
|
||||||
"project": "test"
|
"project": "test"
|
||||||
});
|
});
|
||||||
|
|
||||||
// When: deserializing to InputMessage
|
// When: deserializing to InputMessage
|
||||||
let msg: InputMessage = serde_json::from_value(payload).expect("valid enum JSON");
|
let msg: InputMessage = serde_json::from_value(payload).expect("valid enum JSON");
|
||||||
@@ -192,10 +192,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn fails_on_unknown_type_tag() {
|
fn fails_on_unknown_type_tag() {
|
||||||
let payload = json!({
|
let payload = json!({
|
||||||
"type": "Unknown",
|
"type": "Unknown",
|
||||||
"url": "https://example.com/issues/123",
|
"url": "https://example.com/issues/123",
|
||||||
"project": "test"
|
"project": "test"
|
||||||
});
|
});
|
||||||
|
|
||||||
let err = serde_json::from_value::<InputMessage>(payload).unwrap_err();
|
let err = serde_json::from_value::<InputMessage>(payload).unwrap_err();
|
||||||
// Basic sanity check that it's a data error mentioning the unrecognized variant
|
// 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() {
|
fn fails_when_missing_required_fields() {
|
||||||
// Missing "url" and "project"
|
// Missing "url" and "project"
|
||||||
let payload = json!({
|
let payload = json!({
|
||||||
"type": "Issue2Work"
|
"type": "Issue2Work"
|
||||||
});
|
});
|
||||||
|
|
||||||
let err = serde_json::from_value::<InputMessage>(payload).unwrap_err();
|
let err = serde_json::from_value::<InputMessage>(payload).unwrap_err();
|
||||||
let msg = err.to_string();
|
let msg = err.to_string();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Champs-Libres Helper",
|
"name": "Champs-Libres Helper",
|
||||||
"version": "0.4",
|
"version": "0.6.0",
|
||||||
"browser_specific_settings": {
|
"browser_specific_settings": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
"id": "helper@champs-libres-coop",
|
"id": "helper@champs-libres-coop",
|
||||||
@@ -26,4 +26,4 @@
|
|||||||
"scripts": ["dist/background/background.js"],
|
"scripts": ["dist/background/background.js"],
|
||||||
"type": "module"
|
"type": "module"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "web-extension",
|
"name": "web-extension",
|
||||||
"version": "0.4.0",
|
"version": "0.6.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Reference in New Issue
Block a user