From b229ce9c5c5a4c662640ef5d5103aa82cef4c093 Mon Sep 17 00:00:00 2001 From: Daan Boerlage Date: Tue, 21 Jan 2025 21:32:23 +0100 Subject: [PATCH] Add a view command --- src/cli.rs | 9 +++ src/cmd.rs | 1 + src/cmd/view.rs | 148 +++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 3 +- src/types/issue.rs | 10 +-- 5 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 src/cmd/view.rs diff --git a/src/cli.rs b/src/cli.rs index 818a9fb..28817fb 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -40,6 +40,15 @@ pub enum Commands { #[arg(value_name = "JQL")] jql: String, }, + View { + /// Print JSON rather than pretty print + #[arg(long)] + json: bool, + + /// An issue key, for example, KEY-123 + #[arg(value_name = "ISSUE")] + issue: String, + }, /// Set up the configuration Init { /// Jira instance URL diff --git a/src/cmd.rs b/src/cmd.rs index eede39f..992cc62 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -1,2 +1,3 @@ pub mod create; pub mod search; +pub mod view; diff --git a/src/cmd/view.rs b/src/cmd/view.rs new file mode 100644 index 0000000..68d1021 --- /dev/null +++ b/src/cmd/view.rs @@ -0,0 +1,148 @@ +use crate::jira_config::JiraConfig; +use crate::types::issue::JiraIssue; +use crossterm::style::{Color, Stylize}; +use std::io::Write; + +async fn fetch_issue( + config: &JiraConfig, + href: &str, +) -> Result> { + let client = reqwest::Client::new(); + + let response = client + .get(href) + .basic_auth(&config.email, Some(&config.api_token)) + .send() + .await?; + + if !response.status().is_success() { + let error_text = response.text().await?; + return Err(format!("Failed to fetch issue: {}", error_text).into()); + } + + let issue_response = response.json::().await?; + + Ok(issue_response) +} + +fn pretty_print(issue: &JiraIssue) -> Result<(), Box> { + println!("\n== Title {:=<71}", ""); + println!( + "{}: {}", + issue.key.clone().green(), + issue.fields.summary.clone().bold().green() + ); + + println!("\n== Details {:=<69}", ""); + let mut tw = tabwriter::TabWriter::new(vec![]); + writeln!(tw, "{}:\t{}", "Priority".blue(), issue.fields.priority.name)?; + writeln!(tw, "{}:\t{}", "Status".blue(), issue.fields.status.name)?; + writeln!( + tw, + "{}:\t{} <{}>", + "Assignee".blue(), + issue.fields.assignee.display_name, + issue + .fields + .assignee + .email_address + .clone() + .unwrap_or("invalid@example.com".to_string()) + )?; + writeln!( + tw, + "{}:\t{} <{}>", + "Reporter".blue(), + issue.fields.reporter.display_name, + issue + .fields + .reporter + .email_address + .clone() + .unwrap_or("invalid@example.com".to_string()) + )?; + writeln!( + tw, + "{}:\t{}", + "Created".blue(), + issue.fields.created.with_timezone(&chrono::Local) + )?; + writeln!( + tw, + "{}:\t{}", + "Due Date".blue(), + match issue.fields.due_date { + None => "None".to_string(), + Some(x) => x.to_string(), + } + )?; + + tw.flush().unwrap(); + + let written = String::from_utf8(tw.into_inner().unwrap()).unwrap(); + print!("{}", written); + println!("\n== Description {:=<65}", ""); + match issue.fields.description.clone() { + Some(x) => println!("{}", x), + None => println!("(Issue does not have a description)"), + } + + println!("\n== Comments {:=<68}", ""); + for comment in issue.fields.comment.clone().unwrap_or_default().comments { + println!( + "{} at {}", + comment.author.display_name.red(), + comment.created.with_timezone(&chrono::Local) + ); + println!("{}", comment.body); + } + println!("\n== Actions {:=<69}", ""); + println!( + "\u{1b}]8;;{}\u{7}{}\u{1b}]8;;\u{7}", + issue.href, + "Open Issue".green().underline(Color::Green) + ); + + Ok(()) +} + +pub fn json_print(issues: &JiraIssue) -> Result<(), Box> { + let j = serde_json::to_string_pretty(issues)?; + println!("{}", j); + Ok(()) +} + +pub async fn exec(json: bool, issue_key: &str) -> Result<(), Box> { + let config = JiraConfig::load().map_err(|e| format!("Configuration error: {}", e))?; + if !json { + println!("Loading issue data"); + } + + let jql = format!("key = '{}'", issue_key); + + let matched_issues = match crate::jql::run(&config, &jql).await { + Ok(x) => x, + Err(reason) => { + eprintln!("Error fetching issue: {}", reason); + std::process::exit(1); + } + }; + + let matched_issue = match matched_issues.issues.first() { + Some(x) => x, + None => { + eprintln!("No issue found with that key"); + std::process::exit(1); + } + }; + + let fetched_issue = fetch_issue(&config, &matched_issue.href).await?; + + if json { + json_print(&fetched_issue)?; + } else { + pretty_print(&fetched_issue)?; + }; + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 4d70d9b..b42e90b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,8 +21,9 @@ async fn main() -> Result<(), Box> { Commands::List { json } => { let jql = "assignee = currentUser() AND resolution = Unresolved order by updated DESC"; cmd::search::exec(json, jql).await? - }, + } Commands::Search { json, jql } => cmd::search::exec(json, &jql).await?, + Commands::View { json, issue } => cmd::view::exec(json, &issue).await?, Commands::Init { url, email, token } => { JiraConfig::init(url, email, token).await?; println!("Configuration initialized successfully!"); diff --git a/src/types/issue.rs b/src/types/issue.rs index 8683c1c..455cb1b 100644 --- a/src/types/issue.rs +++ b/src/types/issue.rs @@ -13,6 +13,7 @@ pub struct JiraIssue { #[derive(Debug, Deserialize, Serialize)] pub struct JiraIssueResponseFields { pub summary: String, + pub description: Option, pub status: Status, pub created: chrono::DateTime, pub priority: Priority, @@ -36,7 +37,7 @@ pub struct Priority { pub id: String, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct Person { #[serde(rename = "self")] pub href: String, @@ -48,10 +49,8 @@ pub struct Person { pub email_address: Option, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Default, Clone, Deserialize, Serialize)] pub struct Comments { - #[serde(rename = "self")] - pub href: String, pub total: u32, #[serde(rename = "maxResults")] pub max_results: u32, @@ -60,7 +59,7 @@ pub struct Comments { pub comments: Vec, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct Comment { #[serde(rename = "self")] pub href: String, @@ -69,6 +68,7 @@ pub struct Comment { pub body: String, #[serde(rename = "updateAuthor")] pub update_author: Person, + pub created: chrono::DateTime, } #[derive(Debug, Deserialize, Serialize)]