Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for parent issues and querying #1

Merged
merged 6 commits into from
Nov 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v3
uses: actions/setup-go@v5
with:
go-version: '1.21'
go-version: '1.23'
cache: true
- run: go mod tidy
- run: go test -v ./...
- name: Install Cosign
uses: sigstore/cosign-installer@v3.3.0
uses: sigstore/cosign-installer@v3.7.0
- uses: goreleaser/goreleaser-action@v4
if: success() && startsWith(github.ref, 'refs/tags/')
with:
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1 @@
jt
/jt
68 changes: 56 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# jt

Tiny command-line tool for creating JIRA tickets with a summary/title and optionally a description.
Tiny command-line tool for creating JIRA issues with a summary, description and optionally a parent.

## Installation
If you have go install locally, compile and install with:
Expand All @@ -19,33 +19,77 @@ defaultIssueType: Task
defaultComponentNames:
- Team A
- Development
DefaultParentIssueTypes:
- Epic
- Initiative
```

Then you can create a ticket with:
Then you can create an issue with:
```bash
# Create ticket with only a summary
jt My new ticket
# Create issue with only a summary
jt My new issue

# Create ticket with a summary and a description
jt My new ticket -m "With a description!"
# Create issue with a summary and a description
jt My new issue -m "With a description!"

# Or create a ticket with $EDITOR
# Or create an issue with $EDITOR
jt
# The first line is the ticket summary/title
# The first line is the issue summary/title
#
# The description is everything after a blank line
# which can be multiline.
```

If you want to add the issue to a parent Epic or Initiative, use `-p`:
```bash
jt -p ABC-12345 Add a feature
```

### Setting up JIRA API access
The first time you run it, it will prompt for an access token for JIRA.
You can generate one at https://id.atlassian.com/manage-profile/security/api-tokens.

It will be stored in your system's keyring, so you won't have to enter it again until you restart or lock your keychain.

### gitcommit-style vim highlighting
Add this to your `.vimrc` to get gitcommit-style highlighting for the summary and description:
```vim
" jt syntax highlighting
au BufReadPost *.jt set syntax=gitcommit
```

### Setting up JIRA API access
The first time you run it, it will prompt for an access token for JIRA.
You can generate one at https://id.atlassian.com/manage-profile/security/api-tokens.
### auto-completion and in-line search for parent issues
The provided completion script for `zsh` allows you to not only get completion for available flags, but also
to search easily for parent issues with descriptions.

```bash
jt -p <TAB>
PROJ-12345 [Initiative]: Initiative A
PROJ-12344 [Initiative]: Initiative B
PROJ-12343 [Epic]: Epic A
PROJ-12342 [Epic]: Epic B

# You can also search for a specific description by typing it after `-p`
jt -p image<TAB>
```bash
PROJ-12340 [Initiative]: Immutable Docker Images
PROJ-12341 [Initiative]: Image Storage Project
PROJ-12338 [Epic]: Scan Images
PROJ-12339 [Epic]: Optimize Go image

# Or search for a specific issue key
jt -p PROJ-123<TAB>
PROJ-12346 [Initiative]: Initiative C
PROJ-12347 [Initiative]: Initiative D
PROJ-12348 [Epic]: Epic C
PROJ-12349 [Epic]: Epic D
```

To enable `zsh` completion, run the following:
```bash
source <(jt --completion)
```
> **Note:** Currently only `zsh` is supported. If you want to add support for another shell, feel free to open a PR.

It will be stored in your system's keyring, so you won't have to enter it again until you restart or lock your keychain.
### TODO
- [ ] Add support for creating sub-Tasks with Tasks as parents. Currently we don't know what type the issue passed to `-p` is.
13 changes: 13 additions & 0 deletions cmd/jt/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package main

import (
_ "embed"
"fmt"
)

//go:embed completion/jt_completion.zsh
var completionZSH string

func printCompletionZSH() {
fmt.Println(completionZSH)
}
109 changes: 109 additions & 0 deletions cmd/jt/completion/jt_completion.zsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Zsh completion script for the 'jt' command

# Always show the list, even if there's only one option.
zstyle ':completion:*:jt:*' force-list always

# Disable sorting to preserve the custom order from jt -q.
zstyle ':completion::complete:jt::' sort false

# Define the jt completion function for Zsh
_jt_completions() {
# Define the arguments with exclusivity
_arguments -C \
'(-m --msg)'{-m,--msg}'[Issue description, optional]:description' \
'(-e --edit)'{-e,--edit}'[Open default editor for summary and description, optional]' \
'(-p --parent)'{-p,--parent}'[Assign the issue to a parent Epic or Initiative, optional]:project:->parent_completion' \
'(-c --completion)'{-c,--completion}'[Print zsh shell completion script to stdout and exit]' \
'(-q --query)'{-q,--query}'[Query issues and exit. Options: parents, epics, initiatives, tasks, bugs. Comma followed by string for description search]:query:->query_completion' \
'(-h --help)'{-h,--help}'[Show help]' &&
return 0

# Handle completion based on the context
case $state in
parent_completion)
# Define the completion options for the -p/--parent flag

# Enable immediate menu display and prevent sorting
compstate[insert]=menu
# Prevent sorting to preserve custom order
compstate[nosort]=true

local -a insertions descriptions
local search_term="${words[CURRENT]}"
local query_param="parents"
local has_matches=0 # Flag to check if matches are found in this section

# If the search term is empty, or an issue ID prefix (e.g., PLT-77), fetch the full list
if [[ -z "$search_term" || "$search_term" =~ '^([[:alpha:]]{3,4})-[0-9]*' ]]; then
# Fetch the full list for normal Zsh prefix-based filtering
while IFS= read -r line; do
local id="${line%% *}"
local description="${line#* }"
insertions+=("$id")
descriptions+=("${id} ${description}")
done < <(jt -q "$query_param")

# Use compadd to display results with the following flags:
# -Q: Suppresses quoting of completions with special characters. The returned IDs don't need to be quoted.
# -V jt_issues: Groups completions under the label "jt_issues". This seems to allow
# zstyle ':completion::complete:jt::' sort false to work correctly and return the list in the order
# that jt -q parents returns them.
# -d descriptions: Provides descriptions alongside each completion item. Length of insertions and descriptions
# must match.
# -l: Forces a single-column list display regardless of terminal width or number of items
if compadd -Q -V jt_issues -d descriptions -l -- "${insertions[@]}"; then
has_matches=1
fi
else
# Perform a text search by appending the search term with a comma.
query_param+=",$search_term"
while IFS= read -r line; do
local id="${line%% *}"
local description="${line#* }"
insertions+=("$id")
descriptions+=("${id} ${description}")
done < <(jt -q "$query_param")

# Use compadd to display results. The same flags as above but with and
# important difference. The -U flag:
# -U: Ensures Zsh doesn't further filter returned values.
# This would filter out the results since the search string would not match the returned IDs.
if compadd -Q -U -V jt_issues -d descriptions -l-- "${insertions[@]}"; then
has_matches=1
fi
fi

# Show "No issues found" message if no matches were added
((!has_matches)) && compadd -x 'No issues found'
return 0
;;
query_completion)
# Define completion options for the -q/--query flag
local -a query_options
query_options=("parents" "epics" "initiatives" "tasks" "bugs")

# Check if the user has entered a comma
if [[ "$words[CURRENT]" == *,* ]]; then
# After a comma, allow free text input (no specific completion)
compadd -U "$words[CURRENT]"
else
local current_word="$words[CURRENT]"
# Filter options based on the current input
local -a filtered_options
for opt in "${query_options[@]}"; do
if [[ -z "$current_word" || "$opt" = ${current_word}* ]]; then
filtered_options+=("$opt")
fi
done

# Provide filtered options with no space after completion
# -S '': Adds an empty string suffix to avoid adding a space after the completion since we support ,<text>
compadd -Q -U -S '' -- "${filtered_options[@]}"
fi
return 0
;;
esac
}

# Register the _jt_completions function for the jt command in Zsh
compdef _jt_completions jt
63 changes: 46 additions & 17 deletions cmd/jt/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,16 @@ import (
)

var (
msg = pflag.StringP("msg", "m", "", "issue description, optional")
edit = pflag.BoolP("edit", "e", false, "open the issue in your default editor, optional")
issueFlags = pflag.NewFlagSet("issues", pflag.ContinueOnError)
exclusiveFlags = pflag.NewFlagSet("exclusive", pflag.ContinueOnError)
msg = issueFlags.StringP("msg", "m", "", "Issue description, optional")
edit = issueFlags.BoolP("edit", "e", false, "Open default editor for summary and description, optional")
parent = issueFlags.StringP("parent", "p", "", "Assign the issue to a parent Epic or Initiative, optional")
query = exclusiveFlags.StringSliceP("query", "q", []string{}, `Query issues and exit. Available queries are: "parents", "epics", "initiatives", "tasks", and "bugs".
The "parents" query will search for parent issues (Epics, Initiatives by default).
A wildcard text search term can also be provided after a comma.
For example: jt -q "parents,some issue". Double quote if the search text contains spaces.`)
completion = exclusiveFlags.BoolP("completion", "c", false, "Print zsh shell completion script to stdout and exit")
)

func main() {
Expand All @@ -24,33 +32,45 @@ func main() {
}

func run() error {
fl := pflag.NewFlagSet("jt", pflag.ContinueOnError)
fl.AddFlagSet(pflag.CommandLine)
fl.Usage = func() {
rootFlags := pflag.NewFlagSet("root", pflag.ContinueOnError)
rootFlags.Usage = func() {
fmt.Println("Usage: jt [flags] [summary]")
fmt.Println("\nIf summary is not provided, jt will open your default editor and prompt you for a summary and description.")
fmt.Println("\nFlags:")
pflag.PrintDefaults()
fmt.Println("\nIssue Creation Flags:")
issueFlags.PrintDefaults()
fmt.Println("\nExclusive Flags:")
exclusiveFlags.PrintDefaults()
}

rootFlags.AddFlagSet(issueFlags)
rootFlags.AddFlagSet(exclusiveFlags)
// Parse flags
err := fl.Parse(os.Args[1:])
err := rootFlags.Parse(os.Args[1:])
if err != nil {
if !errors.Is(err, pflag.ErrHelp) {
fl.Usage()
rootFlags.Usage()
fmt.Printf("\n%s\n", err)
}
return nil
}

if *completion {
printCompletionZSH()
return nil
}

// If a query is provided, run the query and return.
if len(*query) > 0 {
return runQuery(*query)
}

var desc string
// Check if msg is set
if *msg != "" {
desc = *msg
}

// Read the issue summary from the command line arguments.
summary := strings.Join(fl.Args(), " ")
summary := strings.Join(rootFlags.Args(), " ")

if summary == "" || *edit {
var err error
Expand All @@ -77,20 +97,29 @@ func run() error {
}

jc := jt.JiraConfig{
URL: parsedURL.String(),
Email: conf.Email,
Token: t,
URL: parsedURL.String(),
Email: conf.Email,
Token: t,
}

ic := jt.IssueConfig{
Summary: summary,
Description: desc,
ProjectKey: conf.DefaultProjectKey,
IssueType: conf.DefaultIssueType,
ComponentNames: conf.DefaultComponentNames,
}

if parent != nil && *parent != "" {
ic.ParentIssueKey = *parent
}

c := jt.NewJiraClient(jc)
key, err := c.NewJIRATicket(summary, desc)
key, err := c.NewJIRAIssue(ic)
if err != nil {
return fmt.Errorf("failed to create ticket: %s\n", err)
return fmt.Errorf("failed to create issue: %s\n", err)
}

fmt.Printf("created ticket: %s\tURL: %s\n", key, parsedURL.String()+"/browse/"+key)
fmt.Printf("created issue: %s\tURL: %s\n", key, parsedURL.String()+"/browse/"+key)
return nil
}
Loading