Compare commits

..

2 commits

3 changed files with 50 additions and 29 deletions

View file

@ -1,13 +1,13 @@
# Jirac # Jirac
A CLI for creating and managing Jira tickets directly from your terminal. A CLI for creating and managing Jira issues directly from your terminal.
## Features ## Features
- Create Jira tickets from markdown files or using your preferred editor - Create Jira issues from Markdown files or using your preferred editor
- List tickets assigned to specific users - List issues assigned to specific users
- Simple configuration using TOML - Simple configuration using TOML
- Interactive ticket creation with templates - Interactive issue creation with templates
- Execute JQL - Execute JQL
## Installation ## Installation
@ -27,6 +27,7 @@ Commands:
create Create an issue create Create an issue
list Find issues currently assigned to you list Find issues currently assigned to you
search Search for issues search Search for issues
view View an issue
init Set up the configuration init Set up the configuration
help Print this message or the help of the given subcommand(s) help Print this message or the help of the given subcommand(s)
@ -35,10 +36,10 @@ Options:
-V, --version Print version -V, --version Print version
``` ```
### Creating a ticket ### Creating ab issue
``` ```
Create a ticket Create an issue
Usage: jirac create [OPTIONS] [MARKDOWN_FILE] Usage: jirac create [OPTIONS] [MARKDOWN_FILE]
@ -46,8 +47,8 @@ Arguments:
[MARKDOWN_FILE] A markdown file [MARKDOWN_FILE] A markdown file
Options: Options:
--project <PROJECT> The project key in which to create the ticket --project <PROJECT> The project key in which to create the issue
--open Open the new ticket in a browser --open Open the new issue in a browser
-h, --help Print help -h, --help Print help
``` ```
@ -57,10 +58,10 @@ Options:
jirac create jirac create
``` ```
*From a markdown file* *From a Markdown file*
```sh ```sh
jirac create ticket.md jirac create issue.md
``` ```
*Specify a project* *Specify a project*
@ -69,10 +70,10 @@ jirac create ticket.md
jirac create --project KEY jirac create --project KEY
``` ```
### Listing tickets ### Listing issues
``` ```
Find tickets currently assigned to you Find issues currently assigned to you
Usage: jirac list [OPTIONS] Usage: jirac list [OPTIONS]
@ -81,7 +82,7 @@ Options:
-h, --help Print help -h, --help Print help
``` ```
### Search for tickets ### Search for issues
``` ```
Search for issues Search for issues
@ -98,12 +99,31 @@ Options:
Use [JQL](https://support.atlassian.com/jira-software-cloud/docs/use-advanced-search-with-jira-query-language-jql/) to search for Issues. 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* *Find all in-progress issues in a project*
``` ```
jirac search 'project = KEY AND status = "In Progress" ORDER BY created DESC' jirac search 'project = KEY AND status = "In Progress" ORDER BY created DESC'
``` ```
### View an issue
```
View an issue
Usage: jirac view [OPTIONS] <ISSUE>
Arguments:
<ISSUE> An issue key, for example, KEY-123
Options:
--json Print JSON rather than pretty print
-h, --help Print help
```
```sh
./target/release/jirac view KEY-123
```
## Configuration ## Configuration
Get the following information: Get the following information:

View file

@ -40,6 +40,7 @@ pub enum Commands {
#[arg(value_name = "JQL")] #[arg(value_name = "JQL")]
jql: String, jql: String,
}, },
/// View an issue
View { View {
/// Print JSON rather than pretty print /// Print JSON rather than pretty print
#[arg(long)] #[arg(long)]

View file

@ -10,7 +10,7 @@ use std::path::PathBuf;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
struct TicketMetadata { struct IssueMetadata {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
status: Option<String>, status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@ -101,15 +101,15 @@ struct Status {
name: String, name: String,
} }
fn get_ticket_template( fn get_issue_template(
ticket_metadata: &TicketMetadata, issue_metadata: &IssueMetadata,
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<String, Box<dyn std::error::Error>> {
let matter = toml::to_string(ticket_metadata)?; let matter = toml::to_string(issue_metadata)?;
let has_project = ticket_metadata.project.is_some(); let has_project = issue_metadata.project.is_some();
let template = format!( let template = format!(
r#"--- r#"---
# The ticket can contain the following properties: # The issue can contain the following properties:
# - status # - status
# - project (required) # - project (required)
# - assignee # - assignee
@ -139,9 +139,9 @@ fn get_editor() -> String {
fn parse_markdown( fn parse_markdown(
content: &str, content: &str,
) -> Result<(String, String, TicketMetadata), Box<dyn std::error::Error>> { ) -> Result<(String, String, IssueMetadata), Box<dyn std::error::Error>> {
let matter = Matter::<TOML>::new(); let matter = Matter::<TOML>::new();
let result = matter.parse_with_struct::<TicketMetadata>(content); let result = matter.parse_with_struct::<IssueMetadata>(content);
let (metadata, content) = match result { let (metadata, content) = match result {
Some(x) => (x.data, x.content), Some(x) => (x.data, x.content),
@ -153,7 +153,7 @@ fn parse_markdown(
// let metadata = result.data // let metadata = result.data
// .and_then(|d| toml::from_str(d).ok()) // .and_then(|d| toml::from_str(d).ok())
// .unwrap_or_else(|| TicketMetadata { // .unwrap_or_else(|| IssueMetadata {
// status: None, // status: None,
// project: None, // project: None,
// assignee: None, // assignee: None,
@ -182,7 +182,7 @@ fn parse_markdown(
fn create_temp_markdown(project_key: Option<String>) -> Result<String, Box<dyn std::error::Error>> { fn create_temp_markdown(project_key: Option<String>) -> Result<String, Box<dyn std::error::Error>> {
let mut temp_file = NamedTempFile::new()?; let mut temp_file = NamedTempFile::new()?;
let metadata = TicketMetadata { let metadata = IssueMetadata {
status: None, status: None,
project: project_key, project: project_key,
assignee: None, assignee: None,
@ -190,7 +190,7 @@ fn create_temp_markdown(project_key: Option<String>) -> Result<String, Box<dyn s
extra: Default::default(), extra: Default::default(),
}; };
let template = get_ticket_template(&metadata)?; let template = get_issue_template(&metadata)?;
temp_file.write_all(template.as_bytes())?; temp_file.write_all(template.as_bytes())?;
temp_file.flush()?; temp_file.flush()?;
@ -215,7 +215,7 @@ async fn create_jira_issue(
project_key: &str, project_key: &str,
title: &str, title: &str,
description: &str, description: &str,
metadata: &TicketMetadata, metadata: &IssueMetadata,
) -> Result<JiraResponse, Box<dyn std::error::Error>> { ) -> Result<JiraResponse, Box<dyn std::error::Error>> {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
@ -356,7 +356,7 @@ pub async fn create(
}; };
if content.trim().is_empty() { if content.trim().is_empty() {
return Err("Empty content. Aborting ticket creation.".into()); return Err("Empty content. Aborting issue creation.".into());
} }
let (title, description, metadata) = parse_markdown(&content)?; let (title, description, metadata) = parse_markdown(&content)?;
@ -375,7 +375,7 @@ pub async fn create(
} }
// Confirm creation // Confirm creation
println!("\nAbout to create ticket:"); println!("\nAbout to create an issue:");
println!("Project: {}", selected_project); println!("Project: {}", selected_project);
if let Some(status) = &metadata.status { if let Some(status) = &metadata.status {
println!("Status: {}", status); println!("Status: {}", status);
@ -399,7 +399,7 @@ pub async fn create(
let url = format!("{}/browse/{}", config.url, response.key); let url = format!("{}/browse/{}", config.url, response.key);
println!("Successfully created ticket: {}", response.key); println!("Successfully created issue: {}", response.key);
println!("URL: {}", url); println!("URL: {}", url);
if open { if open {