This commit is contained in:
@@ -98,6 +98,21 @@ gitea-kanban --project Kanban owner repo move 123 --column-id 7
|
||||
gitea-kanban --project Kanban owner repo move 123 --column-id 7 --sorting 2
|
||||
```
|
||||
|
||||
### `comment`
|
||||
|
||||
Comments are managed on issues assigned to the selected project. Issue values
|
||||
are repository issue numbers. Comment values are global Gitea comment IDs.
|
||||
|
||||
```sh
|
||||
gitea-kanban --project Kanban owner repo comment list 42
|
||||
gitea-kanban --project Kanban owner repo comment add 42 --body "Investigating"
|
||||
gitea-kanban --project Kanban owner repo comment edit 42 9001 --body "Resolved"
|
||||
gitea-kanban --project Kanban owner repo comment delete 42 9001 --yes
|
||||
```
|
||||
|
||||
The CLI verifies that the issue belongs to the selected project and that the
|
||||
comment belongs to that issue before editing or deleting it.
|
||||
|
||||
### `delete`
|
||||
|
||||
Delete an issue assigned to selected project. This is irreversible and requires
|
||||
|
||||
@@ -58,6 +58,10 @@ gitea-kanban --project Kanban owner repo create --title "Fix issue" --body "Deta
|
||||
gitea-kanban --project Kanban owner repo edit 42 --title "Updated" --body "Changed"
|
||||
gitea-kanban --project Kanban owner repo move 123 --column-id 7
|
||||
gitea-kanban --project Kanban owner repo delete 42 --yes
|
||||
gitea-kanban --project Kanban owner repo comment list 42
|
||||
gitea-kanban --project Kanban owner repo comment add 42 --body "Investigating"
|
||||
gitea-kanban --project Kanban owner repo comment edit 42 9001 --body "Resolved"
|
||||
gitea-kanban --project Kanban owner repo comment delete 42 9001 --yes
|
||||
```
|
||||
|
||||
`delete` requires repository admin permission. `move` takes global issue and
|
||||
|
||||
@@ -9,8 +9,8 @@ use serde::de::DeserializeOwned;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::model::{
|
||||
CreateIssuePayload, EditIssuePayload, Issue, MoveProjectIssuePayload, Project, ProjectBoard,
|
||||
ProjectColumn,
|
||||
Comment, CommentPayload, CreateIssuePayload, EditIssuePayload, Issue, MoveProjectIssuePayload,
|
||||
Project, ProjectBoard, ProjectColumn,
|
||||
};
|
||||
|
||||
pub trait GiteaApi {
|
||||
@@ -30,6 +30,10 @@ pub trait GiteaApi {
|
||||
fn create_issue(&self, payload: &CreateIssuePayload) -> Result<Issue, ApiError>;
|
||||
fn edit_issue(&self, issue_number: u64, payload: &EditIssuePayload) -> Result<Issue, ApiError>;
|
||||
fn delete_issue(&self, issue_number: u64) -> Result<(), ApiError>;
|
||||
fn list_issue_comments(&self, issue_number: u64) -> Result<Vec<Comment>, ApiError>;
|
||||
fn create_issue_comment(&self, issue_number: u64, body: String) -> Result<Comment, ApiError>;
|
||||
fn edit_issue_comment(&self, comment_id: u64, body: String) -> Result<Comment, ApiError>;
|
||||
fn delete_issue_comment(&self, comment_id: u64) -> Result<(), ApiError>;
|
||||
}
|
||||
|
||||
pub struct GiteaClient {
|
||||
@@ -279,6 +283,62 @@ impl GiteaApi for GiteaClient {
|
||||
check_response(response, &format!("deleting issue #{issue_number}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn list_issue_comments(&self, issue_number: u64) -> Result<Vec<Comment>, ApiError> {
|
||||
self.get_all(
|
||||
&format!("issues/{issue_number}/comments"),
|
||||
&format!("listing comments for issue #{issue_number}"),
|
||||
&[],
|
||||
)
|
||||
}
|
||||
|
||||
fn create_issue_comment(&self, issue_number: u64, body: String) -> Result<Comment, ApiError> {
|
||||
let response = self
|
||||
.client
|
||||
.post(self.endpoint(&format!("issues/{issue_number}/comments"))?)
|
||||
.header("Authorization", format!("token {}", self.token))
|
||||
.json(&CommentPayload { body })
|
||||
.send()
|
||||
.map_err(|error| {
|
||||
ApiError(format!(
|
||||
"cannot reach Gitea while creating comment on issue #{issue_number}: {error}"
|
||||
))
|
||||
})?;
|
||||
self.decode(
|
||||
response,
|
||||
&format!("creating comment on issue #{issue_number}"),
|
||||
)
|
||||
}
|
||||
|
||||
fn edit_issue_comment(&self, comment_id: u64, body: String) -> Result<Comment, ApiError> {
|
||||
let response = self
|
||||
.client
|
||||
.patch(self.endpoint(&format!("issues/comments/{comment_id}"))?)
|
||||
.header("Authorization", format!("token {}", self.token))
|
||||
.json(&CommentPayload { body })
|
||||
.send()
|
||||
.map_err(|error| {
|
||||
ApiError(format!(
|
||||
"cannot reach Gitea while editing comment #{comment_id}: {error}"
|
||||
))
|
||||
})?;
|
||||
self.decode(response, &format!("editing comment #{comment_id}"))
|
||||
}
|
||||
|
||||
fn delete_issue_comment(&self, comment_id: u64) -> Result<(), ApiError> {
|
||||
let response = self
|
||||
.client
|
||||
.delete(self.endpoint(&format!("issues/comments/{comment_id}"))?)
|
||||
.header("Authorization", format!("token {}", self.token))
|
||||
.send()
|
||||
.map_err(|error| {
|
||||
ApiError(format!(
|
||||
"cannot reach Gitea while deleting comment #{comment_id}: {error}"
|
||||
))
|
||||
})?;
|
||||
check_response(response, &format!("deleting comment #{comment_id}"))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn check_response(response: Response, operation: &str) -> Result<Response, ApiError> {
|
||||
|
||||
@@ -55,6 +55,37 @@ enum Command {
|
||||
#[arg(long)]
|
||||
sorting: Option<u64>,
|
||||
},
|
||||
/// Manage comments on an issue in selected project.
|
||||
Comment {
|
||||
#[command(subcommand)]
|
||||
command: CommentCommand,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum CommentCommand {
|
||||
/// List comments for an issue.
|
||||
List { issue: u64 },
|
||||
/// Add a comment to an issue.
|
||||
Add {
|
||||
issue: u64,
|
||||
#[arg(long)]
|
||||
body: String,
|
||||
},
|
||||
/// Replace a comment body.
|
||||
Edit {
|
||||
issue: u64,
|
||||
comment_id: u64,
|
||||
#[arg(long)]
|
||||
body: String,
|
||||
},
|
||||
/// Delete a comment.
|
||||
Delete {
|
||||
issue: u64,
|
||||
comment_id: u64,
|
||||
#[arg(long)]
|
||||
yes: bool,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@@ -140,6 +171,81 @@ fn run() -> Result<(), Box<dyn Error>> {
|
||||
)?;
|
||||
println!("moved issue {issue_id} to column {column_id}");
|
||||
}
|
||||
Command::Comment { command } => match command {
|
||||
CommentCommand::List { issue } => {
|
||||
for comment in project_issue_comments(&client, &config, issue)? {
|
||||
let author = comment
|
||||
.user
|
||||
.as_ref()
|
||||
.map(|user| user.login.as_str())
|
||||
.unwrap_or("unknown");
|
||||
println!(
|
||||
"#{} {}\n{}",
|
||||
comment.id,
|
||||
sanitize_terminal_text(author),
|
||||
sanitize_terminal_text(&comment.body)
|
||||
);
|
||||
}
|
||||
}
|
||||
CommentCommand::Add { issue, body } => {
|
||||
project_issue_comments(&client, &config, issue)?;
|
||||
let comment = operations::create_issue_comment(&client, issue, body)?;
|
||||
println!("created comment #{} on issue #{}", comment.id, issue);
|
||||
}
|
||||
CommentCommand::Edit {
|
||||
issue,
|
||||
comment_id,
|
||||
body,
|
||||
} => {
|
||||
ensure_comment(&client, &config, issue, comment_id)?;
|
||||
operations::edit_issue_comment(&client, comment_id, body)?;
|
||||
println!("updated comment #{comment_id}");
|
||||
}
|
||||
CommentCommand::Delete {
|
||||
issue,
|
||||
comment_id,
|
||||
yes,
|
||||
} => {
|
||||
if !yes {
|
||||
return Err("comment deletion requires --yes".into());
|
||||
}
|
||||
ensure_comment(&client, &config, issue, comment_id)?;
|
||||
operations::delete_issue_comment(&client, comment_id)?;
|
||||
println!("deleted comment #{comment_id}");
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn project_issue_comments(
|
||||
client: &GiteaClient,
|
||||
config: &Config,
|
||||
issue: u64,
|
||||
) -> Result<Vec<gitea_kanban_tui::model::Comment>, Box<dyn Error>> {
|
||||
let board = load_project_board(client, config)?;
|
||||
if !board
|
||||
.issues_by_column
|
||||
.iter()
|
||||
.flatten()
|
||||
.any(|candidate| candidate.number == issue)
|
||||
{
|
||||
return Err(format!("issue #{issue} is not assigned to selected project").into());
|
||||
}
|
||||
Ok(operations::list_issue_comments(client, issue)?)
|
||||
}
|
||||
|
||||
fn ensure_comment(
|
||||
client: &GiteaClient,
|
||||
config: &Config,
|
||||
issue: u64,
|
||||
comment_id: u64,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
if !project_issue_comments(client, config, issue)?
|
||||
.iter()
|
||||
.any(|comment| comment.id == comment_id)
|
||||
{
|
||||
return Err(format!("comment #{comment_id} is not attached to issue #{issue}").into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -9,6 +9,24 @@ pub struct Issue {
|
||||
pub body: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
||||
pub struct Comment {
|
||||
pub id: u64,
|
||||
pub body: String,
|
||||
#[serde(default)]
|
||||
pub user: Option<CommentUser>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
||||
pub struct CommentUser {
|
||||
pub login: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct CommentPayload {
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct CreateIssuePayload {
|
||||
pub title: String,
|
||||
@@ -117,4 +135,24 @@ mod tests {
|
||||
serde_json::json!({"title": "Updated", "body": "Changed"})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serializes_and_deserializes_comment_data() {
|
||||
let comment: Comment = serde_json::from_value(serde_json::json!({
|
||||
"id": 9001,
|
||||
"body": "Investigating",
|
||||
"user": {"login": "alice"}
|
||||
}))
|
||||
.expect("comment JSON");
|
||||
let payload = CommentPayload {
|
||||
body: "Resolved".to_owned(),
|
||||
};
|
||||
|
||||
assert_eq!(comment.id, 9001);
|
||||
assert_eq!(comment.user.expect("comment user").login, "alice");
|
||||
assert_eq!(
|
||||
serde_json::to_value(payload).expect("comment payload"),
|
||||
serde_json::json!({"body": "Resolved"})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::api::{ApiError, GiteaApi};
|
||||
use crate::model::{CreateIssuePayload, EditIssuePayload, Issue, MoveProjectIssuePayload};
|
||||
use crate::model::{Comment, CreateIssuePayload, EditIssuePayload, Issue, MoveProjectIssuePayload};
|
||||
|
||||
pub fn create_project_issue(
|
||||
client: &impl GiteaApi,
|
||||
@@ -34,3 +34,30 @@ pub fn move_project_issue(
|
||||
) -> Result<(), ApiError> {
|
||||
client.move_project_issue(project_id, issue_id, payload)
|
||||
}
|
||||
|
||||
pub fn list_issue_comments(
|
||||
client: &impl GiteaApi,
|
||||
issue_number: u64,
|
||||
) -> Result<Vec<Comment>, ApiError> {
|
||||
client.list_issue_comments(issue_number)
|
||||
}
|
||||
|
||||
pub fn create_issue_comment(
|
||||
client: &impl GiteaApi,
|
||||
issue_number: u64,
|
||||
body: String,
|
||||
) -> Result<Comment, ApiError> {
|
||||
client.create_issue_comment(issue_number, body)
|
||||
}
|
||||
|
||||
pub fn edit_issue_comment(
|
||||
client: &impl GiteaApi,
|
||||
comment_id: u64,
|
||||
body: String,
|
||||
) -> Result<Comment, ApiError> {
|
||||
client.edit_issue_comment(comment_id, body)
|
||||
}
|
||||
|
||||
pub fn delete_issue_comment(client: &impl GiteaApi, comment_id: u64) -> Result<(), ApiError> {
|
||||
client.delete_issue_comment(comment_id)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user