ref: 4f25d18a917065101ed25244638156421f11fc49
parent: dea4abf84413766eeb039b26c3c12c64d6d4e147
author: Kenny Levinsen <w@kl.wtf>
date: Tue Jun 7 18:28:28 EDT 2016
More features Saves searches, issue links, issuetype->type, unauthenticated access
--- a/jira.go
+++ b/jira.go
@@ -98,13 +98,13 @@
}
func (iw *IssueView) normalFiles() (files, dirs []string) {
- files = []string{"assignee", "creator", "ctl", "description", "issuetype", "key", "reporter", "status", "summary", "labels", "transitions", "priority", "resolution", "raw", "progress"}
+ files = []string{"assignee", "creator", "ctl", "description", "type", "key", "reporter", "status", "summary", "labels", "transitions", "priority", "resolution", "raw", "progress", "links"}
dirs = []string{"comments"}
return
}
func (iw *IssueView) newFiles() (files, dirs []string) {
- files = []string{"ctl", "description", "issuetype", "summary"}
+ files = []string{"ctl", "description", "type", "summary"}
return
}
@@ -123,7 +123,7 @@
iw.issueLock.Lock()
isNew := iw.newIssue
if iw.values != nil {
- issuetype = strings.Replace(string(iw.values["issuetype"]), "\n", "", -1)
+ issuetype = strings.Replace(string(iw.values["type"]), "\n", "", -1)
summary = strings.Replace(string(iw.values["summary"]), "\n", "", -1)
description = strings.Replace(string(iw.values["description"]), "\n", "", -1)
}
@@ -225,7 +225,7 @@
if issue.Fields != nil {
sf.SetContent([]byte(issue.Fields.Description + "\n"))
}
- case "issuetype":
+ case "type":
if issue.Fields != nil {
sf.SetContent([]byte(issue.Fields.Type.Name + "\n"))
}
@@ -245,8 +245,8 @@
if issue.Fields != nil && issue.Fields.Progress != nil {
p := time.Duration(issue.Fields.Progress.Progress) * time.Second
t := time.Duration(issue.Fields.Progress.Total) * time.Second
- percent := int((1 - float64(issue.Fields.Progress.Total-issue.Fields.Progress.Progress)/float64(issue.Fields.Progress.Total)) * 100)
- sf.SetContent([]byte(fmt.Sprintf("Progress: %v, Total: %v, Percent: %d%%\n", p, t, percent)))
+ r := t - p
+ sf.SetContent([]byte(fmt.Sprintf("Progress: %v, Remaining: %v, Total: %v\n", p, r, t)))
}
case "key":
sf.SetContent([]byte(issue.Key + "\n"))
@@ -270,6 +270,19 @@
s += tr.Name + "\n"
}
sf.SetContent([]byte(s))
+ case "links":
+ var s string
+ if issue.Fields != nil {
+ for _, l := range issue.Fields.IssueLinks {
+ switch {
+ case l.OutwardIssue != nil:
+ s += fmt.Sprintf("%s %s\n", l.OutwardIssue.Key, l.Type.Outward)
+ case l.InwardIssue != nil:
+ s += fmt.Sprintf("%s %s\n", l.InwardIssue.Key, l.Type.Inward)
+ }
+ }
+ }
+ sf.SetContent([]byte(s))
case "comments":
return NewJiraDir(file,
0555|qp.DMDIR,
@@ -295,7 +308,7 @@
onClose := func() error {
switch file {
- case "key", "raw":
+ case "key", "raw", "progress", "links":
return nil
case "transitions":
sf.Lock()
@@ -395,6 +408,68 @@
return stats, nil
}
+type SearchView struct {
+ query string
+ resultLock sync.Mutex
+ results []string
+}
+
+func (sw *SearchView) search(jc *jira.Client) error {
+ keys, err := GetKeysForSearch(jc, sw.query, 250)
+ if err != nil {
+ return err
+ }
+
+ sw.resultLock.Lock()
+ sw.results = keys
+ sw.resultLock.Unlock()
+ return nil
+}
+
+func (sw *SearchView) Walk(jc *jira.Client, file string) (trees.File, error) {
+ sw.resultLock.Lock()
+ keys := sw.results
+ sw.resultLock.Unlock()
+
+ if !StringExistsInSets(file, keys) {
+ return nil, trees.ErrNoSuchFile
+ }
+
+ issue, err := GetIssue(jc, file)
+ if err != nil {
+ return nil, err
+ }
+
+ if issue.Fields == nil {
+ return nil, errors.New("nil fields in issue")
+ }
+
+ s := strings.Split(issue.Key, "-")
+ if len(s) != 2 {
+ return nil, errors.New("funky issue key")
+ }
+ issueNo := s[1]
+
+ iw := &IssueView{
+ project: issue.Fields.Project.Key,
+ issueNo: issueNo,
+ }
+
+ return NewJiraDir(file, 0555|qp.DMDIR, "jira", "jira", jc, iw)
+}
+
+func (sw *SearchView) List(jc *jira.Client) ([]qp.Stat, error) {
+ if err := sw.search(jc); err != nil {
+ return nil, err
+ }
+
+ sw.resultLock.Lock()
+ keys := sw.results
+ sw.resultLock.Unlock()
+
+ return StringsToStats(keys, 0555|qp.DMDIR, "jira", "jira"), nil
+}
+
type ProjectView struct {
project string
}
@@ -432,9 +507,9 @@
return StringsToStats(keys, 0555|qp.DMDIR, "jira", "jira"), nil
}
-type JiraView struct{}
+type AllProjectsView struct{}
-func (jw *JiraView) Walk(jc *jira.Client, projectName string) (trees.File, error) {
+func (apw *AllProjectsView) Walk(jc *jira.Client, projectName string) (trees.File, error) {
projectName = strings.ToUpper(projectName)
projects, err := GetProjects(jc)
if err != nil {
@@ -441,25 +516,18 @@
return nil, err
}
+ pw := &ProjectView{project: projectName}
+
for _, project := range projects {
if project.Key == projectName {
- goto found
+ return NewJiraDir(projectName, 0555|qp.DMDIR, "jira", "jira", jc, pw)
}
}
return nil, nil
-
-found:
-
- return NewJiraDir(projectName,
- 0555|qp.DMDIR,
- "jira",
- "jira",
- jc,
- &ProjectView{project: projectName})
}
-func (jw *JiraView) List(jc *jira.Client) ([]qp.Stat, error) {
+func (apw *AllProjectsView) List(jc *jira.Client) ([]qp.Stat, error) {
projects, err := GetProjects(jc)
if err != nil {
return nil, err
@@ -471,4 +539,88 @@
}
return StringsToStats(strs, 0555|qp.DMDIR, "jira", "jira"), nil
+}
+
+type JiraView struct {
+ searchLock sync.Mutex
+ searches map[string]*SearchView
+}
+
+func (jw *JiraView) Walk(jc *jira.Client, file string) (trees.File, error) {
+ jw.searchLock.Lock()
+ defer jw.searchLock.Unlock()
+ if jw.searches == nil {
+ jw.searches = make(map[string]*SearchView)
+ }
+
+ switch file {
+ case "search":
+ cmds := map[string]func([]string) error{
+ "search": func(args []string) error {
+ if len(args) < 2 {
+ return errors.New("query missing")
+ }
+
+ sw := &SearchView{query: strings.Join(args[1:], " ")}
+ if err := sw.search(jc); err != nil {
+ log.Printf("search failed: %v", err)
+ return err
+ }
+
+ jw.searchLock.Lock()
+ jw.searches[args[0]] = sw
+ jw.searchLock.Unlock()
+ return nil
+ },
+ }
+ return NewCommandFile("search", 0777, "jira", "jira", cmds), nil
+ case "projects":
+ return NewJiraDir(file, 0555|qp.DMDIR, "jira", "jira", jc, &AllProjectsView{})
+ default:
+ search, exists := jw.searches[file]
+
+ if !exists {
+ return nil, nil
+ }
+
+ return NewJiraDir(file, 0555|qp.DMDIR, "jira", "jira", jc, search)
+ }
+}
+
+func (jw *JiraView) List(jc *jira.Client) ([]qp.Stat, error) {
+ jw.searchLock.Lock()
+ defer jw.searchLock.Unlock()
+ if jw.searches == nil {
+ jw.searches = make(map[string]*SearchView)
+ }
+
+ var strs []string
+ for k := range jw.searches {
+ strs = append(strs, k)
+ }
+
+ a := StringsToStats([]string{"projects"}, 0555|qp.DMDIR, "jira", "jira")
+ b := StringsToStats([]string{"search"}, 0777, "jira", "jira")
+ c := StringsToStats(strs, 0777|qp.DMDIR, "jira", "jira")
+ return append(append(a, b...), c...), nil
+}
+
+func (jw *JiraView) Remove(jc *jira.Client, file string) error {
+ switch file {
+ case "search", "projects":
+ return trees.ErrPermissionDenied
+ default:
+ jw.searchLock.Lock()
+ defer jw.searchLock.Unlock()
+ if jw.searches == nil {
+ jw.searches = make(map[string]*SearchView)
+ }
+
+ if _, exists := jw.searches[file]; exists {
+ delete(jw.searches, file)
+ return nil
+ }
+
+ return trees.ErrNoSuchFile
+ }
}
--- a/main.go
+++ b/main.go
@@ -21,34 +21,35 @@
var user, password string
fmt.Printf("Username: ")
- _, err = fmt.Scan(&user)
- if err != nil {
- fmt.Printf("Could not read username: %v", err)
- return
- }
- fmt.Printf("Password: ")
- pass, err := gopass.GetPasswdMasked()
- if err != nil {
- fmt.Printf("Could not read password: %v", err)
- return
- }
- password = string(pass)
+ _, err = fmt.Scanln(&user)
+ if err == nil {
- auth := func() {
- res, err := jiraClient.Authentication.AcquireSessionCookie(user, password)
- if err != nil || res == false {
- fmt.Printf("Could not authenticate to JIRA: %v\n", err)
+ fmt.Printf("Password: ")
+ pass, err := gopass.GetPasswdMasked()
+ if err != nil {
+ fmt.Printf("Could not read password: %v", err)
return
}
- }
- auth()
+ password = string(pass)
- go func() {
- t := time.NewTicker(5 * time.Minute)
- for range t.C {
- auth()
+ auth := func() {
+ res, err := jiraClient.Authentication.AcquireSessionCookie(user, password)
+ if err != nil || res == false {
+ fmt.Printf("Could not authenticate to JIRA: %v\n", err)
+ return
+ }
}
- }()
+ auth()
+
+ go func() {
+ t := time.NewTicker(5 * time.Minute)
+ for range t.C {
+ auth()
+ }
+ }()
+ } else {
+ fmt.Printf("Continuing without authentication.\n")
+ }
root, err := NewJiraDir("", 0555|qp.DMDIR, "jira", "jira", jiraClient, &JiraView{})
if err != nil {
--- a/utils.go
+++ b/utils.go
@@ -3,6 +3,7 @@
import (
"errors"
"fmt"
+ "net/url"
"strings"
"time"
@@ -48,8 +49,8 @@
return ss, nil
}
-func GetKeysForNIssues(jc *jira.Client, project string, n int) ([]string, error) {
- cmd := fmt.Sprintf("/rest/api/2/search?fields=key&maxResults=%d&jql=project=%s", n, project)
+func GetKeysForSearch(jc *jira.Client, query string, max int) ([]string, error) {
+ cmd := fmt.Sprintf("/rest/api/2/search?fields=key&maxResults=%d&jql=%s", max, url.QueryEscape(query))
req, err := jc.NewRequest("GET", cmd, nil)
if err != nil {
@@ -63,6 +64,27 @@
ss := make([]string, len(s.Issues))
for i, issue := range s.Issues {
+ ss[i] = issue.Key
+ }
+
+ return ss, nil
+}
+
+func GetKeysForNIssues(jc *jira.Client, project string, max int) ([]string, error) {
+ cmd := fmt.Sprintf("/rest/api/2/search?fields=key&maxResults=%d&jql=project=%s", max, project)
+
+ req, err := jc.NewRequest("GET", cmd, nil)
+ if err != nil {
+ return nil, fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ var s SearchResult
+ if _, err := jc.Do(req, &s); err != nil {
+ return nil, fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ ss := make([]string, len(s.Issues))
+ for i, issue := range s.Issues {
s := strings.Split(issue.Key, "-")
if len(s) != 2 {
continue
@@ -177,6 +199,11 @@
}
func SetFieldInIssue(jc *jira.Client, issue, field, val string) error {
+ switch field {
+ case "type":
+ field = "issuetype"
+ }
+
cmd := fmt.Sprintf("/rest/api/2/issue/%s", issue)
method := "PUT"
@@ -222,9 +249,7 @@
}
type CommentResult struct {
- Comments []struct {
- ID string `json:"id"`
- } `json:"comments,omitempty"`
+ Comments []jira.Comment `json:"comments,omitempty"`
}
func GetCommentsForIssue(jc *jira.Client, issue string) ([]string, error) {
@@ -497,7 +522,7 @@
func NewJiraDir(name string, perm qp.FileMode, user, group string, jc *jira.Client, thing interface{}) (*JiraDir, error) {
switch thing.(type) {
- case trees.File, jiraWalker, jiraLister, jiraRemover:
+ case trees.Dir, jiraWalker, jiraLister, jiraRemover:
default:
return nil, fmt.Errorf("unsupported type: %T", thing)
}