ref: 0c729fb8280bf2bc2dd12952873bd82f20247c69
parent: 4f25d18a917065101ed25244638156421f11fc49
author: Kenny Levinsen <kl@codesealer.com>
date: Wed Jun 8 14:00:09 EDT 2016
Overhaul, add links, components OAuth * OAuth 1.0 support has been added to avoid the constant logouts that JIRA has a tendency for. * Issue links can now be assigned. * Component support added - both reading and assigning. * Probably other things as well.
--- a/jira.go
+++ b/jira.go
@@ -16,15 +16,15 @@
)
type jiraWalker interface {
- Walk(jc *jira.Client, name string) (trees.File, error)
+ Walk(jc *Client, name string) (trees.File, error)
}
type jiraLister interface {
- List(jc *jira.Client) ([]qp.Stat, error)
+ List(jc *Client) ([]qp.Stat, error)
}
type jiraRemover interface {
- Remove(jc *jira.Client, name string) error
+ Remove(jc *Client, name string) error
}
type CommentView struct {
@@ -32,7 +32,7 @@
issueNo string
}
-func (cw *CommentView) Walk(jc *jira.Client, name string) (trees.File, error) {
+func (cw *CommentView) Walk(jc *Client, name string) (trees.File, error) {
switch name {
case "comment":
sf := trees.NewSyntheticFile(name, 0777, "jira", "jira")
@@ -68,7 +68,7 @@
}
}
-func (cw *CommentView) List(jc *jira.Client) ([]qp.Stat, error) {
+func (cw *CommentView) List(jc *Client) ([]qp.Stat, error) {
strs, err := GetCommentsForIssue(jc, fmt.Sprintf("%s-%s", cw.project, cw.issueNo))
if err != nil {
return nil, err
@@ -79,7 +79,7 @@
return StringsToStats(strs, 0777, "jira", "jira"), nil
}
-func (cw *CommentView) Remove(jc *jira.Client, name string) error {
+func (cw *CommentView) Remove(jc *Client, name string) error {
switch name {
case "comment":
return trees.ErrPermissionDenied
@@ -98,7 +98,7 @@
}
func (iw *IssueView) normalFiles() (files, dirs []string) {
- files = []string{"assignee", "creator", "ctl", "description", "type", "key", "reporter", "status", "summary", "labels", "transitions", "priority", "resolution", "raw", "progress", "links"}
+ files = []string{"assignee", "creator", "ctl", "description", "type", "key", "reporter", "status", "summary", "labels", "transitions", "priority", "resolution", "raw", "progress", "links", "components"}
dirs = []string{"comments"}
return
}
@@ -108,7 +108,7 @@
return
}
-func (iw *IssueView) newWalk(jc *jira.Client, file string) (trees.File, error) {
+func (iw *IssueView) newWalk(jc *Client, file string) (trees.File, error) {
files, dirs := iw.newFiles()
if !StringExistsInSets(file, files, dirs) {
return nil, nil
@@ -191,7 +191,18 @@
}
-func (iw *IssueView) normalWalk(jc *jira.Client, file string) (trees.File, error) {
+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
@@ -250,6 +261,14 @@
}
case "key":
sf.SetContent([]byte(issue.Key + "\n"))
+ case "components":
+ if issue.Fields != nil {
+ var s string
+ for _, comp := range issue.Fields.Components {
+ s += comp.Name + "\n"
+ }
+ sf.SetContent([]byte(s))
+ }
case "labels":
if issue.Fields != nil {
var s string
@@ -274,12 +293,7 @@
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)
- }
+ s += renderIssueLink(l, issue.Key) + "\n"
}
}
sf.SetContent([]byte(s))
@@ -303,13 +317,60 @@
},
}
return NewCommandFile("ctl", 0777, "jira", "jira", cmds), nil
-
}
onClose := func() error {
switch file {
- case "key", "raw", "progress", "links":
+ case "key", "raw", "progress":
return nil
+
+ case "links":
+ cur := make(map[string]string)
+ for _, l := range issue.Fields.IssueLinks {
+ cur[renderIssueLink(l, issue.Key)] = l.ID
+ }
+
+ sf.Lock()
+ str := string(sf.Content)
+ sf.Unlock()
+
+ // 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 "transitions":
sf.Lock()
str := string(sf.Content)
@@ -367,7 +428,9 @@
sf.Lock()
str := string(sf.Content)
sf.Unlock()
- if file != "description" && file != "labels" {
+ switch file {
+ case "description", "labels", "components":
+ default:
str = strings.Replace(str, "\n", "", -1)
}
return SetFieldInIssue(jc, issue.Key, file, str)
@@ -377,7 +440,7 @@
return NewCloseSaver(sf, onClose), nil
}
-func (iw *IssueView) Walk(jc *jira.Client, file string) (trees.File, error) {
+func (iw *IssueView) Walk(jc *Client, file string) (trees.File, error) {
iw.issueLock.Lock()
isNew := iw.newIssue
iw.issueLock.Unlock()
@@ -389,7 +452,7 @@
}
}
-func (iw *IssueView) List(jc *jira.Client) ([]qp.Stat, error) {
+func (iw *IssueView) List(jc *Client) ([]qp.Stat, error) {
iw.issueLock.Lock()
isNew := iw.newIssue
iw.issueLock.Unlock()
@@ -414,7 +477,7 @@
results []string
}
-func (sw *SearchView) search(jc *jira.Client) error {
+func (sw *SearchView) search(jc *Client) error {
keys, err := GetKeysForSearch(jc, sw.query, 250)
if err != nil {
return err
@@ -426,7 +489,7 @@
return nil
}
-func (sw *SearchView) Walk(jc *jira.Client, file string) (trees.File, error) {
+func (sw *SearchView) Walk(jc *Client, file string) (trees.File, error) {
sw.resultLock.Lock()
keys := sw.results
sw.resultLock.Unlock()
@@ -458,7 +521,7 @@
return NewJiraDir(file, 0555|qp.DMDIR, "jira", "jira", jc, iw)
}
-func (sw *SearchView) List(jc *jira.Client) ([]qp.Stat, error) {
+func (sw *SearchView) List(jc *Client) ([]qp.Stat, error) {
if err := sw.search(jc); err != nil {
return nil, err
}
@@ -474,7 +537,7 @@
project string
}
-func (pw *ProjectView) Walk(jc *jira.Client, issueNo string) (trees.File, error) {
+func (pw *ProjectView) Walk(jc *Client, issueNo string) (trees.File, error) {
iw := &IssueView{
project: pw.project,
}
@@ -489,6 +552,7 @@
_, err := GetIssue(jc, fmt.Sprintf("%s-%s", pw.project, issueNo))
if err != nil {
+ log.Printf("Could not get issue details: %v", err)
return nil, err
}
iw.issueNo = issueNo
@@ -497,9 +561,10 @@
return NewJiraDir(issueNo, 0555|qp.DMDIR, "jira", "jira", jc, iw)
}
-func (pw *ProjectView) List(jc *jira.Client) ([]qp.Stat, error) {
+func (pw *ProjectView) List(jc *Client) ([]qp.Stat, error) {
keys, err := GetKeysForNIssues(jc, pw.project, 250)
if err != nil {
+ log.Printf("Could not generate issue list: %v", err)
return nil, err
}
@@ -509,10 +574,11 @@
type AllProjectsView struct{}
-func (apw *AllProjectsView) Walk(jc *jira.Client, projectName string) (trees.File, error) {
+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
}
@@ -527,9 +593,10 @@
return nil, nil
}
-func (apw *AllProjectsView) List(jc *jira.Client) ([]qp.Stat, error) {
+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
}
@@ -546,7 +613,7 @@
searches map[string]*SearchView
}
-func (jw *JiraView) Walk(jc *jira.Client, file string) (trees.File, error) {
+func (jw *JiraView) Walk(jc *Client, file string) (trees.File, error) {
jw.searchLock.Lock()
defer jw.searchLock.Unlock()
if jw.searches == nil {
@@ -554,7 +621,7 @@
}
switch file {
- case "search":
+ case "ctl":
cmds := map[string]func([]string) error{
"search": func(args []string) error {
if len(args) < 2 {
@@ -572,8 +639,11 @@
jw.searchLock.Unlock()
return nil
},
+ "relogin": func(args []string) error {
+ return jc.login()
+ },
}
- return NewCommandFile("search", 0777, "jira", "jira", cmds), nil
+ return NewCommandFile("ctl", 0777, "jira", "jira", cmds), nil
case "projects":
return NewJiraDir(file, 0555|qp.DMDIR, "jira", "jira", jc, &AllProjectsView{})
default:
@@ -587,7 +657,7 @@
}
}
-func (jw *JiraView) List(jc *jira.Client) ([]qp.Stat, error) {
+func (jw *JiraView) List(jc *Client) ([]qp.Stat, error) {
jw.searchLock.Lock()
defer jw.searchLock.Unlock()
if jw.searches == nil {
@@ -600,14 +670,14 @@
}
a := StringsToStats([]string{"projects"}, 0555|qp.DMDIR, "jira", "jira")
- b := StringsToStats([]string{"search"}, 0777, "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 *jira.Client, file string) error {
+func (jw *JiraView) Remove(jc *Client, file string) error {
switch file {
- case "search", "projects":
+ case "ctl", "projects":
return trees.ErrPermissionDenied
default:
jw.searchLock.Lock()
--- a/main.go
+++ b/main.go
@@ -1,57 +1,71 @@
package main
import (
+ "flag"
"fmt"
"net"
- "os"
+ "net/http"
+ "net/url"
"time"
- "github.com/andygrunwald/go-jira"
"github.com/howeyc/gopass"
"github.com/joushou/qp"
"github.com/joushou/qptools/fileserver"
)
+var (
+ usingOAuth = flag.Bool("oauth", false, "use OAuth 1.0 for authorization")
+ ckey = flag.String("ckey", "", "consumer key for OAuth")
+ pkey = flag.String("pkey", "", "private key file for OAuth")
+ pass = flag.Bool("pass", false, "use password for authorization")
+ jiraURLStr = flag.String("url", "", "jira URL")
+)
+
func main() {
- jiraClient, err := jira.NewClient(nil, os.Args[1])
+ flag.Parse()
+
+ jiraURL, err := url.Parse(*jiraURLStr)
if err != nil {
- fmt.Printf("Could not connect to JIRA: %v\n", err)
+ fmt.Printf("Could not parse JIRA URL: %v\n", err)
return
}
- var user, password string
- fmt.Printf("Username: ")
- _, err = fmt.Scanln(&user)
- if err == nil {
+ client := &Client{Client: &http.Client{}, usingOAuth: *usingOAuth, jiraURL: jiraURL}
- fmt.Printf("Password: ")
- pass, err := gopass.GetPasswdMasked()
- if err != nil {
- fmt.Printf("Could not read password: %v", err)
- return
- }
- password = string(pass)
-
- auth := func() {
- res, err := jiraClient.Authentication.AcquireSessionCookie(user, password)
- if err != nil || res == false {
- fmt.Printf("Could not authenticate to JIRA: %v\n", err)
+ switch {
+ case *pass:
+ var user string
+ fmt.Printf("Username: ")
+ _, err = fmt.Scanln(&user)
+ if err == nil {
+ fmt.Printf("Password: ")
+ pass, err := gopass.GetPasswdMasked()
+ if err != nil {
+ fmt.Printf("Could not read password: %v", err)
return
}
- }
- auth()
- go func() {
- t := time.NewTicker(5 * time.Minute)
- for range t.C {
- auth()
- }
- }()
- } else {
- fmt.Printf("Continuing without authentication.\n")
+ client.user = user
+ client.pass = string(pass)
+ client.login()
+
+ go func() {
+ t := time.NewTicker(5 * time.Minute)
+ for range t.C {
+ client.login()
+ }
+ }()
+ } else {
+ fmt.Printf("Continuing without authentication.\n")
+ }
+ case *usingOAuth:
+ if err := client.oauth(*ckey, *pkey); err != nil {
+ fmt.Printf("Could not complete oauth handshake: %v\n", err)
+ return
+ }
}
- root, err := NewJiraDir("", 0555|qp.DMDIR, "jira", "jira", jiraClient, &JiraView{})
+ root, err := NewJiraDir("", 0555|qp.DMDIR, "jira", "jira", client, &JiraView{})
if err != nil {
fmt.Printf("Could not create JIRA view")
return
--- a/utils.go
+++ b/utils.go
@@ -1,8 +1,18 @@
package main
import (
+ "bytes"
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/json"
+ "encoding/pem"
"errors"
"fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "net/http/httputil"
"net/url"
"strings"
"time"
@@ -10,6 +20,7 @@
"github.com/andygrunwald/go-jira"
"github.com/joushou/qp"
"github.com/joushou/qptools/fileserver/trees"
+ "github.com/mrjones/oauth"
)
type SearchResult struct {
@@ -16,7 +27,7 @@
Issues []jira.Issue `json:"issues"`
}
-func GetProjects(jc *jira.Client) ([]jira.Project, error) {
+func GetProjects(jc *Client) ([]jira.Project, error) {
req, err := jc.NewRequest("GET", "/rest/api/2/project", nil)
if err != nil {
return nil, fmt.Errorf("could not query JIRA: %v", err)
@@ -30,7 +41,7 @@
return projects, nil
}
-func GetTypesForProject(jc *jira.Client, project string) ([]string, error) {
+func GetTypesForProject(jc *Client, project string) ([]string, error) {
req, err := jc.NewRequest("GET", "/rest/api/2/issuetype", nil)
if err != nil {
return nil, fmt.Errorf("could not query JIRA: %v", err)
@@ -49,7 +60,7 @@
return ss, nil
}
-func GetKeysForSearch(jc *jira.Client, query string, max int) ([]string, error) {
+func GetKeysForSearch(jc *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)
@@ -70,7 +81,7 @@
return ss, nil
}
-func GetKeysForNIssues(jc *jira.Client, project string, max int) ([]string, error) {
+func GetKeysForNIssues(jc *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)
@@ -95,7 +106,7 @@
return ss, nil
}
-func GetIssue(jc *jira.Client, key string) (*jira.Issue, error) {
+func GetIssue(jc *Client, key string) (*jira.Issue, error) {
req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/api/2/issue/%s", key), nil)
if err != nil {
return nil, fmt.Errorf("could not query JIRA: %v", err)
@@ -113,7 +124,7 @@
Key string `json:"key,omitempty"`
}
-func CreateIssue(jc *jira.Client, issue *jira.Issue) (string, error) {
+func CreateIssue(jc *Client, issue *jira.Issue) (string, error) {
req, err := jc.NewRequest("POST", "/rest/api/2/issue", issue)
if err != nil {
return "", fmt.Errorf("could not query JIRA: %v", err)
@@ -126,7 +137,7 @@
return cir.Key, nil
}
-func DeleteIssue(jc *jira.Client, issue string) error {
+func DeleteIssue(jc *Client, issue string) error {
req, err := jc.NewRequest("DELETE", fmt.Sprintf("/rest/api/2/issue/%s", issue), nil)
if err != nil {
return fmt.Errorf("could not query JIRA: %v", err)
@@ -138,6 +149,42 @@
return nil
}
+func DeleteIssueLink(jc *Client, issueLinkID string) error {
+ req, err := jc.NewRequest("DELETE", fmt.Sprintf("/rest/api/2/issueLink/%s", issueLinkID), nil)
+ if err != nil {
+ return fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ if _, err = jc.Do(req, nil); err != nil {
+ return fmt.Errorf("could not query JIRA: %v", err)
+ }
+ return nil
+}
+
+func LinkIssues(jc *Client, inwardKey, outwardKey, relation string) error {
+ issueLink := &jira.IssueLink{
+ Type: jira.IssueLinkType{
+ Name: relation,
+ },
+ InwardIssue: &jira.Issue{
+ Key: inwardKey,
+ },
+ OutwardIssue: &jira.Issue{
+ Key: outwardKey,
+ },
+ }
+
+ req, err := jc.NewRequest("POST", "/rest/api/2/issueLink", issueLink)
+ if err != nil {
+ return fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ if _, err = jc.Do(req, nil); err != nil {
+ return fmt.Errorf("could not query JIRA: %v", err)
+ }
+ return nil
+}
+
type Transition struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
@@ -148,7 +195,7 @@
Transitions []Transition `json:"transitions,omitempty"`
}
-func GetTransitionsForIssue(jc *jira.Client, issue string) ([]Transition, error) {
+func GetTransitionsForIssue(jc *Client, issue string) ([]Transition, error) {
req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/api/2/issue/%s/transitions", issue), nil)
if err != nil {
return nil, fmt.Errorf("could not query JIRA: %v", err)
@@ -162,7 +209,7 @@
return tr.Transitions, nil
}
-func TransitionIssue(jc *jira.Client, issue, transition string) error {
+func TransitionIssue(jc *Client, issue, transition string) error {
transition = strings.Replace(transition, "\n", "", -1)
transitions, err := GetTransitionsForIssue(jc, issue)
if err != nil {
@@ -198,7 +245,7 @@
return nil
}
-func SetFieldInIssue(jc *jira.Client, issue, field, val string) error {
+func SetFieldInIssue(jc *Client, issue, field, val string) error {
switch field {
case "type":
field = "issuetype"
@@ -229,6 +276,19 @@
}
}
fields[field] = labels
+ case "components":
+ componentThing := []map[string]string{}
+ components := strings.Split(val, "\n")
+ for _, s := range components {
+ if s == "" || s == "\n" {
+ continue
+ }
+ thing := map[string]string{
+ "name": s,
+ }
+ componentThing = append(componentThing, thing)
+ }
+ fields[field] = componentThing
case "issuetype", "assignee", "reporter", "creator", "priority", "resolution":
fields[field] = map[string]interface{}{
"name": value,
@@ -241,6 +301,9 @@
return fmt.Errorf("could not query JIRA: %v", err)
}
+ b, _ := httputil.DumpRequestOut(req, true)
+ log.Printf("Set field body: \n%s\n", b)
+
if _, err = jc.Do(req, nil); err != nil {
return fmt.Errorf("could not query JIRA: %v", err)
}
@@ -252,7 +315,7 @@
Comments []jira.Comment `json:"comments,omitempty"`
}
-func GetCommentsForIssue(jc *jira.Client, issue string) ([]string, error) {
+func GetCommentsForIssue(jc *Client, issue string) ([]string, error) {
req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/api/2/issue/%s/comment?maxResults=1000", issue), nil)
if err != nil {
return nil, fmt.Errorf("could not query JIRA: %v", err)
@@ -271,7 +334,7 @@
return ss, nil
}
-func GetComment(jc *jira.Client, issue, id string) (*jira.Comment, error) {
+func GetComment(jc *Client, issue, id string) (*jira.Comment, error) {
req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/api/2/issue/%s/comment/%s", issue, id), nil)
if err != nil {
return nil, fmt.Errorf("could not query JIRA: %v", err)
@@ -285,7 +348,7 @@
return &c, nil
}
-func SetComment(jc *jira.Client, issue, id, body string) error {
+func SetComment(jc *Client, issue, id, body string) error {
c := jira.Comment{
Body: body,
}
@@ -302,7 +365,7 @@
return nil
}
-func AddComment(jc *jira.Client, issue, body string) error {
+func AddComment(jc *Client, issue, body string) error {
c := jira.Comment{
Body: body,
}
@@ -319,7 +382,7 @@
return nil
}
-func RemoveComment(jc *jira.Client, issue, id string) error {
+func RemoveComment(jc *Client, issue, id string) error {
req, err := jc.NewRequest("DELETE", fmt.Sprintf("/rest/api/2/issue/%s/comment/%s", issue, id), nil)
if err != nil {
return fmt.Errorf("could not query JIRA: %v", err)
@@ -441,7 +504,11 @@
args = args[1:]
if f, exists := cf.cmds[cmd]; exists {
- return len(p), f(args)
+ err := f(args)
+ if err != nil {
+ log.Printf("Command %s failed: %v", cmd, err)
+ }
+ return len(p), err
}
return len(p), errors.New("no such command")
}
@@ -463,7 +530,7 @@
type JiraDir struct {
thing interface{}
- client *jira.Client
+ client *Client
*trees.SyntheticDir
}
@@ -520,7 +587,7 @@
}, nil
}
-func NewJiraDir(name string, perm qp.FileMode, user, group string, jc *jira.Client, thing interface{}) (*JiraDir, error) {
+func NewJiraDir(name string, perm qp.FileMode, user, group string, jc *Client, thing interface{}) (*JiraDir, error) {
switch thing.(type) {
case trees.Dir, jiraWalker, jiraLister, jiraRemover:
default:
@@ -532,4 +599,163 @@
client: jc,
SyntheticDir: trees.NewSyntheticDir(name, perm, user, group),
}, nil
+}
+
+type Client struct {
+ user, pass string
+ jiraURL *url.URL
+ cookies []*http.Cookie
+ usingOAuth bool
+ *http.Client
+}
+
+func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) {
+ rel, err := url.Parse(urlStr)
+ if err != nil {
+ return nil, err
+ }
+
+ u := c.jiraURL.ResolveReference(rel)
+
+ var buf io.ReadWriter
+ if body != nil {
+ buf = new(bytes.Buffer)
+ err := json.NewEncoder(buf).Encode(body)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ req, err := http.NewRequest(method, u.String(), buf)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+
+ // Set session cookie if there is one
+ for _, cookie := range c.cookies {
+ req.AddCookie(cookie)
+ }
+
+ return req, nil
+}
+
+func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) {
+ resp, err := c.Client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ respBody, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return resp, err
+ }
+
+ if !(resp.StatusCode >= 200 && resp.StatusCode <= 299) {
+ return resp, fmt.Errorf("Error (status code %d): %s", resp.StatusCode, respBody)
+ }
+
+ if v != nil {
+ // Open a NewDecoder and defer closing the reader only if there is a provided interface to decode to
+ defer resp.Body.Close()
+ err = json.Unmarshal(respBody, v)
+ }
+
+ return resp, err
+}
+
+func (c *Client) AcquireSessionCookie(username, password string) (bool, error) {
+ apiEndpoint := "rest/auth/1/session"
+ body := struct {
+ Username string `json:"username"`
+ Password string `json:"password"`
+ }{
+ username,
+ password,
+ }
+
+ req, err := c.NewRequest("POST", apiEndpoint, body)
+ if err != nil {
+ return false, err
+ }
+
+ resp, err := c.Do(req, nil)
+ c.cookies = resp.Cookies()
+
+ if err != nil {
+ return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). %s", err)
+ }
+ if resp != nil && resp.StatusCode != 200 {
+ return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). Status code: %d", resp.StatusCode)
+ }
+
+ return true, nil
+}
+
+func (c *Client) login() error {
+ if c.usingOAuth {
+ return nil
+ }
+ res, err := c.AcquireSessionCookie(c.user, c.pass)
+ if err != nil || res == false {
+ return fmt.Errorf("Could not authenticate to JIRA: %v\n", err)
+ }
+ return nil
+}
+
+func (c *Client) oauth(consumerKey, privateKeyFile string) error {
+ pvf, err := ioutil.ReadFile(privateKeyFile)
+ if err != nil {
+ return err
+ }
+
+ block, _ := pem.Decode(pvf)
+ privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
+ if err != nil {
+ return err
+ }
+
+ url1, _ := c.jiraURL.Parse("/plugins/servlet/oauth/request-token")
+ url2, _ := c.jiraURL.Parse("/plugins/servlet/oauth/authorize")
+ url3, _ := c.jiraURL.Parse("/plugins/servlet/oauth/access-token")
+
+ t := oauth.NewRSAConsumer(
+ consumerKey,
+ privateKey,
+ oauth.ServiceProvider{
+ RequestTokenUrl: url1.String(),
+ AuthorizeTokenUrl: url2.String(),
+ AccessTokenUrl: url3.String(),
+ HttpMethod: "POST",
+ },
+ )
+
+ t.HttpClient = &http.Client{
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+ },
+ }
+
+ requestToken, url, err := t.GetRequestTokenAndUrl("oob")
+ if err != nil {
+ return err
+ }
+
+ fmt.Printf("OAuth token requested. Please to go the following URL:\n\t%s\n\nEnter verification code: ", url)
+ var verificationCode string
+ fmt.Scanln(&verificationCode)
+ accessToken, err := t.AuthorizeToken(requestToken, verificationCode)
+ if err != nil {
+ return err
+ }
+ fmt.Printf("OAuth token authorized.\n")
+
+ client, err := t.MakeHttpClient(accessToken)
+ if err != nil {
+ return err
+ }
+
+ c.Client = client
+ return nil
}
--- a/workflow.go
+++ b/workflow.go
@@ -5,8 +5,6 @@
"fmt"
"net/url"
"strings"
-
- "github.com/andygrunwald/go-jira"
)
type thing struct {
@@ -13,7 +11,7 @@
Name string `json:"name"`
}
-func BuildWorkflow1(jc *jira.Client, project, issueTypeNo string) (*WorkflowGraph, error) {
+func BuildWorkflow1(jc *Client, project, issueTypeNo string) (*WorkflowGraph, error) {
req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/projectconfig/latest/issuetype/%s/%s/workflow", project, issueTypeNo), nil)
if err != nil {
return nil, fmt.Errorf("could not query JIRA 1: %v", err)
@@ -39,7 +37,7 @@
return &wg, nil
}
-func BuildWorkflow2(jc *jira.Client, project, issueTypeNo string) (*WorkflowGraph, error) {
+func BuildWorkflow2(jc *Client, project, issueTypeNo string) (*WorkflowGraph, error) {
req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/projectconfig/latest/issuetype/%s/%s/workflow", project, issueTypeNo), nil)
if err != nil {
return nil, fmt.Errorf("could not query JIRA 1: %v", err)