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)}
--
⑨