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), nilcase "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)--
⑨