ref: e01deb2174a3dad91e6d6dbe0946036a0c0da228
dir: /jira.go/
package main
import (
"encoding/json"
"errors"
"fmt"
"log"
"strconv"
"strings"
"sync"
"time"
"github.com/andygrunwald/go-jira"
"github.com/joushou/qp"
"github.com/joushou/qptools/fileserver/trees"
)
type WorklogView struct {
issueNo string
worklog string
}
func (wv *WorklogView) Walk(jc *Client, file string) (trees.File, error) {
w, err := GetSpecificWorklogForIssue(jc, wv.issueNo, wv.worklog)
if err != nil {
return nil, err
}
sf := trees.NewSyntheticFile(file, 0555, "jira", "jira")
switch file {
case "comment":
sf.SetContent([]byte(w.Comment + "\n"))
case "author":
sf.SetContent([]byte(w.Author.Name + "\n"))
case "time":
t := time.Duration(w.TimeSpentSeconds) * time.Second
sf.SetContent([]byte(t.String() + "\n"))
case "started":
sf.SetContent([]byte(time.Time(*w.Started).String() + "\n"))
default:
return nil, nil
}
return sf, nil
}
func (wv *WorklogView) List(jc *Client) ([]qp.Stat, error) {
return StringsToStats([]string{"comment", "author", "time", "started"}, 0555, "jira", "jira"), nil
}
type IssueWorklogView struct {
issueNo string
}
func (iwv *IssueWorklogView) Walk(jc *Client, file string) (trees.File, error) {
w, err := GetWorklogForIssue(jc, iwv.issueNo)
if err != nil {
return nil, err
}
for _, wr := range w.Worklogs {
if wr.ID == file {
return NewJiraDir(file,
0555|qp.DMDIR,
"jira",
"jira",
jc,
&WorklogView{issueNo: iwv.issueNo, worklog: file})
}
}
return nil, nil
}
func (iwv *IssueWorklogView) List(jc *Client) ([]qp.Stat, error) {
w, err := GetWorklogForIssue(jc, iwv.issueNo)
if err != nil {
return nil, err
}
var s []string
for _, wr := range w.Worklogs {
s = append(s, wr.ID)
}
return StringsToStats(s, 0555|qp.DMDIR, "jira", "jira"), nil
}
type CommentView struct {
issueNo string
comment string
}
func (cw *CommentView) Walk(jc *Client, file string) (trees.File, error) {
if !StringExistsInSets(file, []string{"author", "comment", "updated", "created"}) {
return nil, nil
}
cmt, err := GetComment(jc, cw.issueNo, cw.comment)
if err != nil {
return nil, err
}
var cnt []byte
writable := false
forceTrunc := true
switch file {
case "author":
cnt = []byte(cmt.Author.Name + "\n")
case "comment":
cnt = []byte(cmt.Body)
forceTrunc = false
writable = true
case "updated":
cnt = []byte(cmt.Updated + "\n")
case "created":
cnt = []byte(cmt.Created + "\n")
}
var perm qp.FileMode
if writable {
perm = 0777
} else {
perm = 0555
}
sf := trees.NewSyntheticFile(file, perm, "jira", "jira")
sf.SetContent(cnt)
onClose := func() error {
sf.RLock()
str := string(sf.Content)
sf.RUnlock()
switch file {
case "comment":
return SetComment(jc, cw.issueNo, cw.comment, str)
}
return nil
}
if writable {
cs := NewCloseSaver(sf, onClose)
cs.forceTrunc = forceTrunc
return cs, nil
}
return sf, nil
}
func (cw *CommentView) List(jc *Client) ([]qp.Stat, error) {
a := StringsToStats([]string{"comment"}, 0777, "jira", "jira")
b := StringsToStats([]string{"author", "updated", "created"}, 0555, "jira", "jira")
return append(a, b...), nil
}
type IssueCommentView struct {
issueNo string
}
func (icv *IssueCommentView) Walk(jc *Client, file string) (trees.File, error) {
switch file {
case "comment":
sf := trees.NewSyntheticFile(file, 0777, "jira", "jira")
onClose := func() error {
sf.Lock()
body := string(sf.Content)
sf.Unlock()
return AddComment(jc, icv.issueNo, body)
}
return NewCloseSaver(sf, onClose), nil
default:
_, err := GetComment(jc, icv.issueNo, file)
if err != nil {
return nil, err
}
cv := &CommentView{issueNo: icv.issueNo, comment: file}
return NewJiraDir(file, 0777|qp.DMDIR, "jira", "jira", jc, cv)
}
}
func (icv *IssueCommentView) List(jc *Client) ([]qp.Stat, error) {
strs, err := GetCommentsForIssue(jc, icv.issueNo)
if err != nil {
return nil, err
}
a := StringsToStats(strs, 0777|qp.DMDIR, "jira", "jira")
b := StringsToStats([]string{"comment"}, 0777, "jira", "jira")
return append(a, b...), nil
}
func (icv *IssueCommentView) Remove(jc *Client, name string) error {
switch name {
case "comment":
return trees.ErrPermissionDenied
default:
return RemoveComment(jc, icv.issueNo, name)
}
}
type IssueView struct {
project string
issueNo string
issueLock sync.Mutex
newIssue bool
values map[string]string
}
func (iw *IssueView) normalFiles() (files, dirs []string) {
files = []string{"assignee", "creator", "ctl", "description", "type", "key", "reporter", "status",
"summary", "labels", "transition", "priority", "resolution", "raw", "progress", "links", "components",
"project"}
dirs = []string{"comments", "worklog"}
return
}
func (iw *IssueView) newFiles() (files, dirs []string) {
files = []string{"ctl", "description", "type", "summary", "project"}
return
}
func (iw *IssueView) newWalk(jc *Client, file string) (trees.File, error) {
files, dirs := iw.newFiles()
if !StringExistsInSets(file, files, dirs) {
return nil, nil
}
switch file {
case "ctl":
cmds := map[string]func([]string) error{
"commit": func(args []string) error {
var issuetype, summary, description, project string
iw.issueLock.Lock()
isNew := iw.newIssue
if iw.values != nil {
issuetype = strings.Replace(string(iw.values["type"]), "\n", "", -1)
summary = strings.Replace(string(iw.values["summary"]), "\n", "", -1)
description = string(iw.values["description"])
project = strings.Replace(string(iw.values["project"]), "\n", "", -1)
}
iw.issueLock.Unlock()
if project == "" && iw.project != "" {
project = iw.project
}
if !isNew {
return errors.New("issue already committed")
}
issue := jira.Issue{
Fields: &jira.IssueFields{
Type: jira.IssueType{
Name: issuetype,
},
Project: jira.Project{
Key: project,
},
Summary: summary,
Description: description,
},
}
key, err := CreateIssue(jc, &issue)
if err != nil {
log.Printf("Create failed: %v", err)
return err
}
iw.issueLock.Lock()
iw.issueNo = key
iw.project = project
iw.newIssue = false
iw.issueLock.Unlock()
return nil
},
}
return NewCommandFile("ctl", 0777, "jira", "jira", cmds), nil
default:
sf := trees.NewSyntheticFile(file, 0777, "jira", "jira")
iw.issueLock.Lock()
defer iw.issueLock.Unlock()
if iw.values == nil {
iw.values = make(map[string]string)
}
value := iw.values[file]
sf.SetContent([]byte(value))
onClose := func() error {
iw.issueLock.Lock()
defer iw.issueLock.Unlock()
iw.values[file] = string(sf.Content)
return nil
}
return NewCloseSaver(sf, onClose), nil
}
}
func renderIssueLink(l *jira.IssueLink, key string) string {
switch {
case l.OutwardIssue != nil:
return fmt.Sprintf("%s %s %s", key, l.OutwardIssue.Key, l.Type.Name)
case l.InwardIssue != nil:
return fmt.Sprintf("%s %s %s", l.InwardIssue.Key, key, l.Type.Name)
default:
return ""
}
}
func (iw *IssueView) normalWalk(jc *Client, file string) (trees.File, error) {
files, dirs := iw.normalFiles()
if !StringExistsInSets(file, files, dirs) {
return nil, nil
}
issue, err := GetIssue(jc, iw.issueNo)
if err != nil {
return nil, err
}
forceTrunc := true
writable := true
var cnt []byte
switch file {
case "assignee":
if issue.Fields != nil && issue.Fields.Assignee != nil {
cnt = []byte(issue.Fields.Assignee.Name + "\n")
}
case "reporter":
if issue.Fields != nil && issue.Fields.Reporter != nil {
cnt = []byte(issue.Fields.Reporter.Name + "\n")
}
case "creator":
if issue.Fields != nil && issue.Fields.Creator != nil {
cnt = []byte(issue.Fields.Creator.Name + "\n")
}
case "summary":
if issue.Fields != nil {
cnt = []byte(issue.Fields.Summary + "\n")
}
forceTrunc = false
case "description":
if issue.Fields != nil {
cnt = []byte(issue.Fields.Description + "\n")
}
forceTrunc = false
case "type":
if issue.Fields != nil {
cnt = []byte(issue.Fields.Type.Name + "\n")
}
case "status":
if issue.Fields != nil && issue.Fields.Status != nil {
cnt = []byte(issue.Fields.Status.Name + "\n")
}
case "priority":
if issue.Fields != nil && issue.Fields.Priority != nil {
cnt = []byte(issue.Fields.Priority.Name + "\n")
}
case "resolution":
if issue.Fields != nil && issue.Fields.Resolution != nil {
cnt = []byte(issue.Fields.Resolution.Name + "\n")
}
case "progress":
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
r := t - p
cnt = []byte(fmt.Sprintf("Progress: %v, Remaining: %v, Total: %v\n", p, r, t))
}
writable = false
case "project":
if issue.Fields != nil {
cnt = []byte(issue.Fields.Project.Key + "\n")
}
writable = false
case "key":
cnt = []byte(issue.Key + "\n")
writable = false
case "components":
if issue.Fields != nil {
var s string
for _, comp := range issue.Fields.Components {
s += comp.Name + "\n"
}
cnt = []byte(s)
}
forceTrunc = false
case "labels":
if issue.Fields != nil {
var s string
for _, lbl := range issue.Fields.Labels {
s += lbl + "\n"
}
cnt = []byte(s)
}
forceTrunc = false
case "transition":
trs, err := GetTransitionsForIssue(jc, issue.Key)
if err != nil {
log.Printf("Could not get transitions for issue %s: %v", issue.Key, err)
return nil, err
}
var s string
for _, tr := range trs {
s += tr.Name + "\n"
}
cnt = []byte(s)
case "links":
var s string
if issue.Fields != nil {
for _, l := range issue.Fields.IssueLinks {
s += renderIssueLink(l, issue.Key) + "\n"
}
cnt = []byte(s)
}
forceTrunc = false
case "comments":
return NewJiraDir(file,
0555|qp.DMDIR,
"jira",
"jira",
jc,
&IssueCommentView{issueNo: iw.issueNo})
case "worklog":
return NewJiraDir(file,
0555|qp.DMDIR,
"jira",
"jira",
jc,
&IssueWorklogView{issueNo: iw.issueNo})
case "raw":
b, err := json.MarshalIndent(issue, "", " ")
if err != nil {
return nil, err
}
cnt = b
case "ctl":
cmds := map[string]func([]string) error{
"delete": func(args []string) error {
return DeleteIssue(jc, issue.Key)
},
}
return NewCommandFile("ctl", 0777, "jira", "jira", cmds), nil
}
var perm qp.FileMode
if writable {
perm = 0777
} else {
perm = 0555
}
sf := trees.NewSyntheticFile(file, perm, "jira", "jira")
sf.SetContent(cnt)
onClose := func() error {
switch file {
case "raw":
sf.RLock()
defer sf.RUnlock()
return SetIssueRaw(jc, issue.Key, sf.Content)
case "links":
cur := make(map[string]string)
for _, l := range issue.Fields.IssueLinks {
cur[renderIssueLink(l, issue.Key)] = l.ID
}
sf.RLock()
str := string(sf.Content)
sf.RUnlock()
// Figure out which issue links are new, and which are old.
var new []string
input := strings.Split(str, "\n")
for _, s := range input {
if s == "" {
continue
}
if _, exists := cur[s]; !exists {
new = append(new, s)
} else {
delete(cur, s)
}
}
// Delete the remaining old issue links
for k, v := range cur {
err := DeleteIssueLink(jc, v)
if err != nil {
log.Printf("Could not delete issue link %s (%s): %v", v, k, err)
}
}
for _, k := range new {
args := strings.Split(k, " ")
if len(args) != 3 {
continue
}
if args[0] != issue.Key && args[1] != issue.Key {
continue
}
err := LinkIssues(jc, args[0], args[1], args[2])
if err != nil {
log.Printf("Could not create issue link (%s): %v", k, err)
}
}
return nil
case "transition":
sf.RLock()
str := string(sf.Content)
sf.RUnlock()
str = strings.Replace(str, "\n", "", -1)
return TransitionIssue(jc, issue.Key, str)
case "status":
sf.RLock()
str := string(sf.Content)
sf.RUnlock()
str = strings.Replace(str, "\n", "", -1)
issue, err := GetIssue(jc, iw.issueNo)
if err != nil {
log.Printf("Could not fetch issue: %v", err)
return err
}
if issue.Fields == nil {
log.Printf("Issue missing fields")
return errors.New("oops")
}
if issue.Fields.Status == nil {
log.Printf("Issue missing status")
return errors.New("oops2")
}
wg, err := BuildWorkflow2(jc, iw.project, issue.Fields.Type.ID)
if err != nil {
log.Printf("Could not build workflow: %v", err)
return err
}
p, err := wg.Path(issue.Fields.Status.Name, str, 500)
if err != nil {
log.Printf("Could not find path: %v", err)
log.Printf("Workflow: \n%s\n", wg.Dump())
return err
}
log.Printf("Workflow path: %s", strings.Join(p, ", "))
for _, s := range p {
err = TransitionIssue(jc, issue.Key, s)
if err != nil {
log.Printf("Could not transition issue: %v", err)
return err
}
}
return nil
default:
sf.RLock()
str := string(sf.Content)
sf.RUnlock()
switch file {
case "description", "labels", "components":
default:
str = strings.Replace(str, "\n", "", -1)
}
return SetFieldInIssue(jc, issue.Key, file, str)
}
}
if writable {
cs := NewCloseSaver(sf, onClose)
cs.forceTrunc = forceTrunc
return cs, nil
}
return sf, nil
}
func (iw *IssueView) Walk(jc *Client, file string) (trees.File, error) {
iw.issueLock.Lock()
isNew := iw.newIssue
iw.issueLock.Unlock()
if isNew {
return iw.newWalk(jc, file)
} else {
return iw.normalWalk(jc, file)
}
}
func (iw *IssueView) List(jc *Client) ([]qp.Stat, error) {
iw.issueLock.Lock()
isNew := iw.newIssue
iw.issueLock.Unlock()
var files, dirs []string
if isNew {
files, dirs = iw.newFiles()
} else {
files, dirs = iw.normalFiles()
}
var stats []qp.Stat
stats = append(stats, StringsToStats(files, 0777, "jira", "jira")...)
stats = append(stats, StringsToStats(dirs, 0777|qp.DMDIR, "jira", "jira")...)
return stats, nil
}
type SearchView struct {
query string
resultLock sync.Mutex
results []string
}
func (sw *SearchView) search(jc *Client) error {
keys, err := GetKeysForSearch(jc, sw.query, jc.maxlisting)
if err != nil {
return err
}
sw.resultLock.Lock()
sw.results = keys
sw.resultLock.Unlock()
return nil
}
func (sw *SearchView) Walk(jc *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")
}
iw := &IssueView{
project: issue.Fields.Project.Key,
issueNo: issue.Key,
}
return NewJiraDir(file, 0555|qp.DMDIR, "jira", "jira", jc, iw)
}
func (sw *SearchView) List(jc *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 ProjectIssuesView struct {
project string
}
func (piw *ProjectIssuesView) Walk(jc *Client, issueNo string) (trees.File, error) {
iw := &IssueView{
project: piw.project,
}
if issueNo == "new" {
iw.newIssue = true
} else {
// Check if the thing is a valid issue number.
if _, err := strconv.ParseUint(issueNo, 10, 64); err != nil {
return nil, nil
}
issueKey := fmt.Sprintf("%s-%s", piw.project, issueNo)
_, err := GetIssue(jc, issueKey)
if err != nil {
log.Printf("Could not get issue details: %v", err)
return nil, err
}
iw.issueNo = issueKey
}
return NewJiraDir(issueNo, 0555|qp.DMDIR, "jira", "jira", jc, iw)
}
func (piw *ProjectIssuesView) List(jc *Client) ([]qp.Stat, error) {
keys, err := GetKeysForNIssuesInProject(jc, piw.project, jc.maxlisting)
if err != nil {
log.Printf("Could not generate issue list: %v", err)
return nil, err
}
keys = append(keys, "new")
return StringsToStats(keys, 0555|qp.DMDIR, "jira", "jira"), nil
}
type ProjectView struct {
project string
}
func (pw *ProjectView) Walk(jc *Client, file string) (trees.File, error) {
switch file {
case "issues":
piw := &ProjectIssuesView{project: pw.project}
return NewJiraDir(file, 0555|qp.DMDIR, "jira", "jira", jc, piw)
case "components":
project, err := GetProject(jc, pw.project)
if err != nil {
return nil, err
}
var components string
for _, c := range project.Components {
components += c.Name + "\n"
}
sf := trees.NewSyntheticFile(file, 0555, "jira", "jira")
sf.SetContent([]byte(components))
return sf, nil
case "issuetypes":
project, err := GetProject(jc, pw.project)
if err != nil {
return nil, err
}
var issuetypes string
for _, tp := range project.IssueTypes {
issuetypes += tp.Name + "\n"
}
sf := trees.NewSyntheticFile(file, 0555, "jira", "jira")
sf.SetContent([]byte(issuetypes))
return sf, nil
case "raw":
project, err := GetProject(jc, pw.project)
if err != nil {
return nil, err
}
b, err := json.MarshalIndent(project, "", " ")
if err != nil {
return nil, err
}
sf := trees.NewSyntheticFile(file, 0555, "jira", "jira")
sf.SetContent([]byte(b))
return sf, nil
default:
return nil, nil
}
}
func (pw *ProjectView) List(jc *Client) ([]qp.Stat, error) {
return StringsToStats([]string{"issues", "issuetypes", "components", "raw"}, 0555|qp.DMDIR, "jira", "jira"), nil
}
type AllProjectsView struct{}
func (apw *AllProjectsView) Walk(jc *Client, projectName string) (trees.File, error) {
projectName = strings.ToUpper(projectName)
projects, err := GetProjects(jc)
if err != nil {
log.Printf("Could not generate project list: %v", err)
return nil, err
}
pw := &ProjectView{project: projectName}
for _, project := range projects {
if project.Key == projectName {
return NewJiraDir(projectName, 0555|qp.DMDIR, "jira", "jira", jc, pw)
}
}
return nil, nil
}
func (apw *AllProjectsView) List(jc *Client) ([]qp.Stat, error) {
projects, err := GetProjects(jc)
if err != nil {
log.Printf("Could not generate project list: %v", err)
return nil, err
}
var strs []string
for _, p := range projects {
strs = append(strs, p.Key)
}
return StringsToStats(strs, 0555|qp.DMDIR, "jira", "jira"), nil
}
type AllIssuesView struct{}
func (aiv *AllIssuesView) Walk(jc *Client, issueKey string) (trees.File, error) {
iw := &IssueView{}
if issueKey == "new" {
iw.newIssue = true
} else {
s := strings.Split(strings.ToUpper(issueKey), "-")
if len(s) != 2 {
return nil, nil
}
if _, err := strconv.ParseUint(s[1], 10, 64); err != nil {
return nil, nil
}
issue, err := GetIssue(jc, issueKey)
if err != nil {
log.Printf("Could not get issue details: %v", err)
return nil, err
}
if issue.Fields == nil {
return nil, errors.New("no fields")
}
iw.issueNo = issueKey
iw.project = issue.Fields.Project.Key
}
return NewJiraDir(issueKey, 0555|qp.DMDIR, "jira", "jira", jc, iw)
}
func (aiv *AllIssuesView) List(jc *Client) ([]qp.Stat, error) {
keys, err := GetKeysForSearch(jc, "", jc.maxlisting)
if err != nil {
log.Printf("Could not generate issue list: %v", err)
return nil, err
}
keys = append(keys, "new")
return StringsToStats(keys, 0555|qp.DMDIR, "jira", "jira"), nil
}
type JiraView struct {
searchLock sync.Mutex
searches map[string]*SearchView
}
func (jw *JiraView) Walk(jc *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 "ctl":
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 {
return err
}
jw.searchLock.Lock()
jw.searches[args[0]] = sw
jw.searchLock.Unlock()
return nil
},
"pass-login": func(args []string) error {
if len(args) == 2 {
jc.user = args[0]
jc.pass = args[1]
}
return nil
},
"set": func(args []string) error {
if len(args) != 2 {
return errors.New("invalid arguments")
}
switch args[0] {
case "max-listing":
mi, err := strconv.ParseInt(args[1], 10, 64)
if err != nil {
return err
}
jc.maxlisting = int(mi)
return nil
default:
return errors.New("unknown variable")
}
},
}
return NewCommandFile("ctl", 0777, "jira", "jira", cmds), nil
case "projects":
return NewJiraDir(file, 0555|qp.DMDIR, "jira", "jira", jc, &AllProjectsView{})
case "issues":
return NewJiraDir(file, 0555|qp.DMDIR, "jira", "jira", jc, &AllIssuesView{})
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 *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", "issues"}, 0555|qp.DMDIR, "jira", "jira")
b := StringsToStats([]string{"ctl"}, 0777, "jira", "jira")
c := StringsToStats(strs, 0777|qp.DMDIR, "jira", "jira")
return append(append(a, b...), c...), nil
}
func (jw *JiraView) Remove(jc *Client, file string) error {
switch file {
case "ctl", "projects", "issues":
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
}
}