shithub: jirafs

Download patch

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)