Add a view command

This commit is contained in:
Daan Boerlage 2025-01-21 21:32:23 +01:00
parent 5233ef19fc
commit b229ce9c5c
Signed by: daan
GPG key ID: FCE070E1E4956606
5 changed files with 165 additions and 6 deletions

View file

@ -40,6 +40,15 @@ pub enum Commands {
#[arg(value_name = "JQL")] #[arg(value_name = "JQL")]
jql: String, 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 /// Set up the configuration
Init { Init {
/// Jira instance URL /// Jira instance URL

View file

@ -1,2 +1,3 @@
pub mod create; pub mod create;
pub mod search; pub mod search;
pub mod view;

148
src/cmd/view.rs Normal file
View file

@ -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<JiraIssue, Box<dyn std::error::Error>> {
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::<JiraIssue>().await?;
Ok(issue_response)
}
fn pretty_print(issue: &JiraIssue) -> Result<(), Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
let j = serde_json::to_string_pretty(issues)?;
println!("{}", j);
Ok(())
}
pub async fn exec(json: bool, issue_key: &str) -> Result<(), Box<dyn std::error::Error>> {
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(())
}

View file

@ -21,8 +21,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Commands::List { json } => { Commands::List { json } => {
let jql = "assignee = currentUser() AND resolution = Unresolved order by updated DESC"; let jql = "assignee = currentUser() AND resolution = Unresolved order by updated DESC";
cmd::search::exec(json, jql).await? cmd::search::exec(json, jql).await?
}, }
Commands::Search { json, jql } => 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 } => { Commands::Init { url, email, token } => {
JiraConfig::init(url, email, token).await?; JiraConfig::init(url, email, token).await?;
println!("Configuration initialized successfully!"); println!("Configuration initialized successfully!");

View file

@ -13,6 +13,7 @@ pub struct JiraIssue {
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct JiraIssueResponseFields { pub struct JiraIssueResponseFields {
pub summary: String, pub summary: String,
pub description: Option<String>,
pub status: Status, pub status: Status,
pub created: chrono::DateTime<chrono::Utc>, pub created: chrono::DateTime<chrono::Utc>,
pub priority: Priority, pub priority: Priority,
@ -36,7 +37,7 @@ pub struct Priority {
pub id: String, pub id: String,
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Person { pub struct Person {
#[serde(rename = "self")] #[serde(rename = "self")]
pub href: String, pub href: String,
@ -48,10 +49,8 @@ pub struct Person {
pub email_address: Option<String>, pub email_address: Option<String>,
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct Comments { pub struct Comments {
#[serde(rename = "self")]
pub href: String,
pub total: u32, pub total: u32,
#[serde(rename = "maxResults")] #[serde(rename = "maxResults")]
pub max_results: u32, pub max_results: u32,
@ -60,7 +59,7 @@ pub struct Comments {
pub comments: Vec<Comment>, pub comments: Vec<Comment>,
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Comment { pub struct Comment {
#[serde(rename = "self")] #[serde(rename = "self")]
pub href: String, pub href: String,
@ -69,6 +68,7 @@ pub struct Comment {
pub body: String, pub body: String,
#[serde(rename = "updateAuthor")] #[serde(rename = "updateAuthor")]
pub update_author: Person, pub update_author: Person,
pub created: chrono::DateTime<chrono::Utc>,
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]