Compare commits

..

No commits in common. "0c459e2770ae299ab34ad2302f3ab5b1bbd88515" and "86fe96f0cde83ee98fb5ecaad492cfe319ff2925" have entirely different histories.

11 changed files with 143 additions and 325 deletions

87
Cargo.lock generated
View file

@ -256,6 +256,16 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "colored"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.59.0",
]
[[package]]
name = "config"
version = "0.15.6"
@ -329,31 +339,6 @@ dependencies = [
"libc",
]
[[package]]
name = "crossterm"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [
"bitflags",
"crossterm_winapi",
"mio",
"parking_lot",
"rustix",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
dependencies = [
"winapi",
]
[[package]]
name = "crunchy"
version = "0.2.2"
@ -935,8 +920,8 @@ version = "0.1.0"
dependencies = [
"chrono",
"clap",
"colored",
"config",
"crossterm",
"directories",
"gray_matter",
"open",
@ -970,6 +955,12 @@ dependencies = [
"serde",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.169"
@ -1042,7 +1033,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys 0.52.0",
]
@ -1555,27 +1545,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-mio"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
dependencies = [
"libc",
"mio",
"signal-hook",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
@ -2084,28 +2053,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.52.0"

View file

@ -9,6 +9,7 @@ reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
colored = "2.0"
toml = "0.8"
config = "0.15"
directories = "6.0"
@ -17,4 +18,3 @@ gray_matter = { version = "0.2", default-features = false, features = ["toml"] }
open = "5.2"
chrono = { version = "0.4", features = ["serde"] }
tabwriter = "1.4"
crossterm = "0.28"

View file

@ -24,10 +24,9 @@ cargo install jirac
Usage: jirac <COMMAND>
Commands:
create Create an issue
list Find issues currently assigned to you
search Search for issues
init Set up the configuration
create Create a ticket
list Find tickets currently assigned to you
init Setup the configuration
help Print this message or the help of the given subcommand(s)
Options:
@ -69,7 +68,7 @@ jirac create ticket.md
jirac create --project KEY
```
### Listing tickets
## Listing tickets
```
Find tickets currently assigned to you
@ -81,29 +80,6 @@ Options:
-h, --help Print help
```
### Search for tickets
```
Search for issues
Usage: jirac search [OPTIONS] <JQL>
Arguments:
<JQL> A JQL string
Options:
--json Print JSON rather than pretty print
-h, --help Print help
```
Use [JQL](https://support.atlassian.com/jira-software-cloud/docs/use-advanced-search-with-jira-query-language-jql/) to search for Issues.
*Find all in-progress tickets in a project*
```
jirac search 'project = KEY AND status = "In Progress" ORDER BY created DESC'
```
## Configuration
Get the following information:
@ -112,7 +88,7 @@ Get the following information:
* You email
* [An API key](https://id.atlassian.com/manage-profile/security/api-tokens)
Then run the `jirac init` command
Then run the the `jirac init` command
```
Setup the configuration

View file

@ -10,37 +10,27 @@ pub struct Cli {
#[derive(Subcommand)]
pub enum Commands {
/// Create an issue
/// Create a ticket
Create {
/// The project key in which to create the issue
/// The project key in which to create the ticket
#[arg(long)]
project: Option<String>,
/// Open the new issue in a browser
/// Open the new ticket in a browser
#[arg(long)]
open: bool,
/// A Markdown file
/// A markdown file
#[arg(value_name = "MARKDOWN_FILE")]
markdown_file: Option<PathBuf>,
},
/// Find issues currently assigned to you
/// Find tickets currently assigned to you
List {
/// Print JSON rather than pretty print
/// Print json rather than pretty print
#[arg(long)]
json: bool,
},
/// Search for issues
Search {
/// Print JSON rather than pretty print
#[arg(long)]
json: bool,
/// A JQL string
#[arg(value_name = "JQL")]
jql: String,
},
/// Set up the configuration
/// Setup the configuration
Init {
/// Jira instance URL
#[arg(long)]

View file

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

View file

@ -1,5 +1,115 @@
use crate::jira_config::JiraConfig;
use crate::types::issue::{display_issues_json, display_issues_pretty};
use colored::Colorize;
use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
use serde::{Deserialize, Serialize};
use std::io::Write;
#[derive(Debug, Deserialize, Serialize)]
struct JiraIssue {
key: String,
#[serde(rename = "self")]
href: String,
fields: JiraIssueResponseFields,
}
#[derive(Debug, Deserialize, Serialize)]
struct JiraIssueResponseFields {
summary: String,
status: Status,
created: chrono::DateTime<chrono::Utc>,
priority: Priority,
assignee: Person,
reporter: Person,
creator: Person,
#[serde(rename = "duedate")]
due_date: Option<chrono::NaiveDate>,
}
#[derive(Deserialize)]
struct JiraSearchResponse {
issues: Vec<JiraIssue>,
total: u32,
}
#[derive(Debug, Deserialize, Serialize)]
struct Status {
name: String,
}
#[derive(Debug, Deserialize, Serialize)]
struct Priority {
name: String,
id: String,
}
#[derive(Debug, Deserialize, Serialize)]
struct Person {
#[serde(rename = "self")]
href: String,
#[serde(rename = "displayName")]
display_name: String,
#[serde(rename = "accountId")]
account_id: String,
#[serde(rename = "emailAddress")]
email_address: String,
}
async fn list_jira_issues(
config: &JiraConfig,
) -> Result<JiraSearchResponse, Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
// JQL query to find issues assigned to the specified user
let jql =
"assignee = currentUser() AND resolution = Unresolved order by updated DESC".to_string();
let query = [("jql", jql.as_str())];
let response = client
.get(format!("{}/rest/api/2/search", config.url))
.basic_auth(&config.email, Some(&config.api_token))
.headers(headers)
.query(&query)
.send()
.await?;
if !response.status().is_success() {
let error_text = response.text().await?;
return Err(format!("Failed to fetch issues: {}", error_text).into());
}
Ok(response.json::<JiraSearchResponse>().await?)
}
fn display_issues_pretty(issues: &[JiraIssue]) -> Result<(), Box<dyn std::error::Error>> {
println!("Found {} issues:", issues.len());
println!("{:-<80}", "");
for issue in issues {
let mut tw = tabwriter::TabWriter::new(vec![]);
writeln!(tw, "{}:\t{}", "Key".blue(), issue.key)?;
writeln!(tw, "{}:\t{}", "Summary".blue(), issue.fields.summary)?;
writeln!(tw, "{}:\t{}", "Status".blue(), issue.fields.status.name)?;
writeln!(tw, "{}:\t{}", "Created".blue(), issue.fields.created)?;
writeln!(tw, "{}:\t{:?}", "Due Date".blue(), issue.fields.due_date)?;
writeln!(tw, "{}:\t{}", "URL".blue(), issue.href.underline())?;
tw.flush().unwrap();
let written = String::from_utf8(tw.into_inner().unwrap()).unwrap();
print!("{}", written);
println!("{:-<80}", "");
}
Ok(())
}
fn display_issues_json(issues: &[JiraIssue]) -> Result<(), Box<dyn std::error::Error>> {
let j = serde_json::to_string_pretty(issues)?;
println!("{}", j);
Ok(())
}
pub async fn list(json: bool) -> Result<(), Box<dyn std::error::Error>> {
let config = JiraConfig::load().map_err(|e| format!("Configuration error: {}", e))?;
@ -7,9 +117,7 @@ pub async fn list(json: bool) -> Result<(), Box<dyn std::error::Error>> {
println!("Fetching issues assigned...");
}
let jql = "assignee = currentUser() AND resolution = Unresolved order by updated DESC";
match crate::jql::run(&config, jql).await {
match list_jira_issues(&config).await {
Ok(response) => {
if json {
if response.issues.is_empty() {

View file

@ -1,32 +0,0 @@
use crate::jira_config::JiraConfig;
use crate::types::issue::{display_issues_json, display_issues_pretty};
pub async fn exec(json: bool, jql: String) -> Result<(), Box<dyn std::error::Error>> {
let config = JiraConfig::load().map_err(|e| format!("Configuration error: {}", e))?;
if !json {
println!("Searching for issues...");
}
match crate::jql::run(&config, &jql).await {
Ok(response) => {
if json {
if response.issues.is_empty() {
println!("[]");
} else {
display_issues_json(&response.issues)?;
}
} else if response.issues.is_empty() {
println!("No results found for query.");
} else {
display_issues_pretty(&response.issues)?;
println!("Total issues: {}", response.total);
}
}
Err(e) => {
eprintln!("Error fetching issues: {}", e);
std::process::exit(1);
}
}
Ok(())
}

View file

@ -1,37 +0,0 @@
use crate::jira_config::JiraConfig;
use crate::types::issue::JiraIssue;
use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
use serde::Deserialize;
#[derive(Deserialize)]
pub struct JiraSearchResponse {
pub issues: Vec<JiraIssue>,
pub total: u32,
}
pub async fn run(
config: &JiraConfig,
jql: &str,
) -> Result<JiraSearchResponse, Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
let query = [("jql", jql)];
let response = client
.get(format!("{}/rest/api/2/search", config.url))
.basic_auth(&config.email, Some(&config.api_token))
.headers(headers)
.query(&query)
.send()
.await?;
if !response.status().is_success() {
let error_text = response.text().await?;
return Err(format!("Failed to fetch issues: {}", error_text).into());
}
Ok(response.json::<JiraSearchResponse>().await?)
}

View file

@ -1,8 +1,6 @@
mod cli;
mod cmd;
mod jira_config;
mod jql;
mod types;
use clap::Parser;
use cli::{Cli, Commands};
@ -19,7 +17,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
markdown_file,
} => cmd::create::create(project, open, markdown_file).await?,
Commands::List { json } => cmd::list::list(json).await?,
Commands::Search { json, jql } => cmd::search::exec(json, jql).await?,
Commands::Init { url, email, token } => {
JiraConfig::init(url, email, token).await?;
println!("Configuration initialized successfully!");

View file

@ -1 +0,0 @@
pub mod issue;

View file

@ -1,129 +0,0 @@
use crossterm::style::Stylize;
use serde::{Deserialize, Serialize};
use std::io::Write;
#[derive(Debug, Deserialize, Serialize)]
pub struct JiraIssue {
pub key: String,
#[serde(rename = "self")]
pub href: String,
pub fields: JiraIssueResponseFields,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct JiraIssueResponseFields {
pub summary: String,
pub status: Status,
pub created: chrono::DateTime<chrono::Utc>,
pub priority: Priority,
pub assignee: Person,
pub reporter: Person,
pub creator: Person,
#[serde(rename = "duedate")]
pub due_date: Option<chrono::NaiveDate>,
pub comment: Option<Comments>,
pub votes: Votes,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Status {
pub name: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Priority {
pub name: String,
pub id: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Person {
#[serde(rename = "self")]
pub href: String,
#[serde(rename = "displayName")]
pub display_name: String,
#[serde(rename = "accountId")]
pub account_id: String,
#[serde(rename = "emailAddress")]
pub email_address: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Comments {
#[serde(rename = "self")]
pub href: String,
pub total: u32,
#[serde(rename = "maxResults")]
pub max_results: u32,
#[serde(rename = "startAt")]
pub start_at: u32,
pub comments: Vec<Comment>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Comment {
#[serde(rename = "self")]
pub href: String,
pub id: String,
pub author: Person,
pub body: String,
#[serde(rename = "updateAuthor")]
pub update_author: Person,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Votes {
#[serde(rename = "self")]
pub href: String,
#[serde(rename = "votes")]
pub count: i32,
#[serde(rename = "hasVoted")]
pub has_voted: bool,
}
pub fn display_issues_pretty(issues: &[JiraIssue]) -> Result<(), Box<dyn std::error::Error>> {
println!("Found {} issues:", issues.len());
println!("{:-<80}", "");
for issue in issues {
let mut tw = tabwriter::TabWriter::new(vec![]);
writeln!(tw, "{}:\t{}", "Key".blue(), issue.key)?;
writeln!(tw, "{}:\t{}", "Summary".blue(), issue.fields.summary)?;
writeln!(tw, "{}:\t{}", "Status".blue(), issue.fields.status.name)?;
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(),
}
)?;
writeln!(
tw,
"\u{1b}]8;;{}\u{7}{}\u{1b}]8;;\u{7}",
issue.href,
"Open Issue".green()
)?;
tw.flush().unwrap();
let written = String::from_utf8(tw.into_inner().unwrap()).unwrap();
print!("{}", written);
println!("{:-<80}", "");
}
Ok(())
}
pub fn display_issues_json(issues: &[JiraIssue]) -> Result<(), Box<dyn std::error::Error>> {
let j = serde_json::to_string_pretty(issues)?;
println!("{}", j);
Ok(())
}