Skip to content

Commit

Permalink
Merge pull request #1 from leosunmo/issue-parents
Browse files Browse the repository at this point in the history
Add support for parent issues and querying
  • Loading branch information
leosunmo authored Nov 3, 2024
2 parents e876f85 + c947c87 commit 7963311
Show file tree
Hide file tree
Showing 12 changed files with 983 additions and 54 deletions.
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

0 comments on commit 7963311

Please sign in to comment.