shithub: jirafs

Download patch

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