mirror of
https://github.com/Threnklyn/jira.git
synced 2026-06-07 21:43:32 +02:00
initial checkin, work in progress
This commit is contained in:
+124
@@ -0,0 +1,124 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/op/go-logging"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"net/url"
|
||||
"time"
|
||||
"io"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var log = logging.MustGetLogger("jira.cli")
|
||||
|
||||
type Cli struct {
|
||||
endpoint *url.URL
|
||||
opts map[string]string
|
||||
cookieFile string
|
||||
ua *http.Client
|
||||
}
|
||||
|
||||
func New(opts map[string]string) *Cli {
|
||||
homedir := os.Getenv("HOME")
|
||||
cookieJar, _ := cookiejar.New(nil)
|
||||
endpoint, _ := opts["endpoint"]
|
||||
url, _ := url.Parse(endpoint)
|
||||
|
||||
cli := &Cli{
|
||||
endpoint: url,
|
||||
opts: opts,
|
||||
cookieFile: fmt.Sprintf("%s/.jira.d/cookies.js", homedir),
|
||||
ua: &http.Client{Jar: cookieJar},
|
||||
}
|
||||
|
||||
cli.ua.Jar.SetCookies(url, cli.loadCookies())
|
||||
|
||||
return cli
|
||||
}
|
||||
|
||||
func (c *Cli) saveCookies(cookies []*http.Cookie) {
|
||||
// expiry in one week from now
|
||||
expiry := time.Now().Add(24 * 7 * time.Hour)
|
||||
for _, cookie := range cookies {
|
||||
cookie.Expires = expiry
|
||||
}
|
||||
|
||||
if currentCookies := c.loadCookies(); currentCookies != nil {
|
||||
currentCookiesByName := make(map[string]*http.Cookie)
|
||||
for _, cookie := range currentCookies {
|
||||
currentCookiesByName[cookie.Name] = cookie
|
||||
}
|
||||
|
||||
for _, cookie := range cookies {
|
||||
currentCookiesByName[cookie.Name] = cookie
|
||||
}
|
||||
|
||||
mergedCookies := make([]*http.Cookie, 0, len(currentCookiesByName))
|
||||
for _, v := range currentCookiesByName {
|
||||
mergedCookies = append(mergedCookies, v)
|
||||
}
|
||||
jsonWrite(c.cookieFile, mergedCookies)
|
||||
} else {
|
||||
jsonWrite(c.cookieFile, cookies)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cli) loadCookies() []*http.Cookie {
|
||||
bytes, err := ioutil.ReadFile(c.cookieFile)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
// dont load cookies if the file does not exist
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("Failed to open %s: %s", c.cookieFile, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cookies := make([]*http.Cookie,0)
|
||||
err = json.Unmarshal(bytes, &cookies)
|
||||
if err != nil {
|
||||
log.Error("Failed to parse json from file %s: %s", c.cookieFile, err)
|
||||
}
|
||||
log.Debug("Loading Cookies: %s", cookies)
|
||||
return cookies
|
||||
}
|
||||
|
||||
func (c *Cli) post(uri string, content io.Reader) *http.Response {
|
||||
req, _ := http.NewRequest("POST", uri, content)
|
||||
return c.makeRequest(req)
|
||||
}
|
||||
|
||||
func (c *Cli) get(uri string) *http.Response {
|
||||
req, _ := http.NewRequest("GET", uri, nil)
|
||||
return c.makeRequest(req)
|
||||
}
|
||||
|
||||
func (c *Cli) makeRequest(req *http.Request) *http.Response {
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.ua.Do(req)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %s", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
log.Error("response status: %s", resp.Status)
|
||||
resp.Write(os.Stderr)
|
||||
}
|
||||
|
||||
runtime.SetFinalizer(resp, func(r *http.Response) {
|
||||
r.Body.Close()
|
||||
})
|
||||
|
||||
if _, ok := resp.Header["Set-Cookie"]; ok {
|
||||
c.saveCookies(resp.Cookies())
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"bytes"
|
||||
"os"
|
||||
"code.google.com/p/gopass"
|
||||
)
|
||||
|
||||
func (c *Cli) CmdLogin() {
|
||||
uri := fmt.Sprintf("%s/rest/auth/1/session", c.endpoint)
|
||||
resp := c.get(uri)
|
||||
for ; resp.StatusCode != 200 ; {
|
||||
req, _ := http.NewRequest("GET", uri, nil)
|
||||
user, _ := c.opts["user"]
|
||||
|
||||
prompt := fmt.Sprintf("Enter Password for %s: ", user)
|
||||
passwd, _ := gopass.GetPass(prompt);
|
||||
|
||||
req.SetBasicAuth(user, passwd)
|
||||
resp = c.makeRequest(req)
|
||||
if resp.StatusCode == 403 {
|
||||
// probably got this, need to redirect the user to login manually
|
||||
// X-Authentication-Denied-Reason: CAPTCHA_CHALLENGE; login-url=https://jira/login.jsp
|
||||
if reason := resp.Header.Get("X-Authentication-Denied-Reason"); reason != "" {
|
||||
log.Error("Authentication Failed: %s", reason)
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Error("Authentication Failead: Unknown")
|
||||
os.Exit(1)
|
||||
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
log.Error("Login failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cli) CmdFields() {
|
||||
log.Debug("fields called")
|
||||
resp := c.get(fmt.Sprintf("%s/rest/api/2/field", c.endpoint))
|
||||
data := jsonDecode(resp.Body)
|
||||
|
||||
if templateFile, err := FindClosestParentPath(".jira.d/templates/fields"); err != nil {
|
||||
runTemplate(default_fields_template, data)
|
||||
} else {
|
||||
log.Debug("Using Template: %s", templateFile)
|
||||
runTemplate(readFile(templateFile), data)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cli) CmdList() {
|
||||
log.Debug("list called")
|
||||
|
||||
if query, ok := c.opts["query"]; !ok {
|
||||
log.Error("No query argument found, either use --query or set query attribute in .jira file")
|
||||
os.Exit(1)
|
||||
} else {
|
||||
buffer := bytes.NewBuffer(make([]byte, 0, len(query)))
|
||||
enc := json.NewEncoder(buffer)
|
||||
|
||||
enc.Encode(map[string]string{
|
||||
"jql": query,
|
||||
"startAt": "0",
|
||||
"maxResults": "500",
|
||||
})
|
||||
|
||||
resp := c.post(fmt.Sprintf("%s/rest/api/2/search", c.endpoint), buffer)
|
||||
data := jsonDecode(resp.Body)
|
||||
|
||||
if templateFile, err := FindClosestParentPath(".jira.d/templates/list"); err != nil {
|
||||
runTemplate(default_list_template, data)
|
||||
} else {
|
||||
log.Debug("Using Template: %s", templateFile)
|
||||
runTemplate(readFile(templateFile), data)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (c *Cli) CmdView(issue string) {
|
||||
log.Debug("view called")
|
||||
resp := c.get(fmt.Sprintf("%s/rest/api/2/issue/%s", c.endpoint, issue))
|
||||
data := jsonDecode(resp.Body)
|
||||
if templateFile, err := FindClosestParentPath(".jira.d/templates/view"); err != nil {
|
||||
runTemplate(default_view_template, data)
|
||||
} else {
|
||||
log.Debug("Using Template: %s", templateFile)
|
||||
runTemplate(readFile(templateFile), data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package cli
|
||||
|
||||
const default_fields_template = "{{ . | toJson}}\n"
|
||||
|
||||
const default_list_template = "{{ range .issues }}{{ .key | append \":\" | printf \"%-12s\"}} {{ .fields.summary }}\n{{ end }}"
|
||||
|
||||
const default_view_template = `issue: {{ .key }}
|
||||
summary: {{ .fields.summary }}
|
||||
project: {{ .fields.project.key }}
|
||||
components: {{ range .fields.components }}{{ .name }} {{end}}
|
||||
issuetype: {{ .fields.issuetype.name }}
|
||||
assignee: {{ .fields.assignee.name }}
|
||||
reporter: {{ .fields.reporter.name }}
|
||||
watchers: {{ range .fields.customfield_10110 }}{{ .name }} {{end}}
|
||||
blockers: {{ range .fields.issuelinks }}{{if .outwardIssue}}{{ .outwardIssue.key }}{{end}}{{end}}
|
||||
depends: {{ range .fields.issuelinks }}{{if .inwardIssue}}{{ .inwardIssue.key }}{{end}}{{end}}
|
||||
priority: {{ .fields.priority.name }}
|
||||
description: |
|
||||
{{ .fields.description | indent 2 }}
|
||||
|
||||
comments:
|
||||
{{ range .fields.comment.comments }} - | # {{.author.name}} at {{.created}}
|
||||
{{ .body | indent 4}}{{end}}
|
||||
`
|
||||
@@ -0,0 +1,113 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
"fmt"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"text/template"
|
||||
"io"
|
||||
"github.com/mgutz/ansi"
|
||||
)
|
||||
|
||||
func FindParentPaths(fileName string) []string {
|
||||
cwd, _ := os.Getwd()
|
||||
|
||||
paths := make([]string,0)
|
||||
|
||||
var dir string
|
||||
for _, part := range strings.Split(cwd, string(os.PathSeparator)) {
|
||||
if dir == "/" {
|
||||
dir = fmt.Sprintf("/%s", part)
|
||||
} else {
|
||||
dir = fmt.Sprintf("%s/%s", dir, part)
|
||||
}
|
||||
file := fmt.Sprintf("%s/%s", dir, fileName)
|
||||
if _, err := os.Stat(file); err == nil {
|
||||
paths = append(paths, file)
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
func FindClosestParentPath(fileName string) (string,error) {
|
||||
paths := FindParentPaths(fileName)
|
||||
if len(paths) > 0 {
|
||||
return paths[len(paths)-1], nil
|
||||
}
|
||||
return "", errors.New(fmt.Sprintf("%s not found in parent directory hierarchy", fileName))
|
||||
}
|
||||
|
||||
func readFile(file string) string {
|
||||
var bytes []byte
|
||||
var err error
|
||||
if bytes, err = ioutil.ReadFile(file); err != nil {
|
||||
log.Error("Failed to read file %s: %s", file, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
func runTemplate(text string, data interface{}) {
|
||||
funcs := map[string]interface{}{
|
||||
"toJson": func(content interface{}) (string, error) {
|
||||
if bytes, err := json.MarshalIndent(content, "", " "); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
return string(bytes), nil
|
||||
}
|
||||
},
|
||||
"append": func(more string, content interface{}) (string, error) {
|
||||
switch value := content.(type) {
|
||||
case string: return string(append([]byte(content.(string)), []byte(more)...)), nil
|
||||
case []byte: return string(append(content.([]byte), []byte(more)...)), nil
|
||||
default: return "", errors.New(fmt.Sprintf("Unknown type: %s", value))
|
||||
}
|
||||
},
|
||||
"indent": func(spaces int, content string) string {
|
||||
indent := make([]byte, spaces + 1, spaces +1)
|
||||
indent[0] = '\n'
|
||||
for i := 1; i < spaces + 1; i += 1 {
|
||||
indent[i] = ' '
|
||||
}
|
||||
return strings.Replace(content, "\n", string(indent), -1)
|
||||
},
|
||||
"color": func(color string) string {
|
||||
return ansi.ColorCode(color)
|
||||
},
|
||||
}
|
||||
if tmpl, err := template.New("template").Funcs(funcs).Parse(text); err != nil {
|
||||
log.Error("Failed to parse template: %s", err)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
if err := tmpl.Execute(os.Stdout, data); err != nil {
|
||||
log.Error("Failed to execute template: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func jsonDecode(io io.Reader) interface{} {
|
||||
content, err := ioutil.ReadAll(io)
|
||||
var data interface{}
|
||||
err = json.Unmarshal(content, &data)
|
||||
if err != nil {
|
||||
log.Error("JSON Parse Error: %s from %s", err, content)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func jsonWrite(file string, data interface{}) {
|
||||
fh, err := os.OpenFile(file, os.O_WRONLY | os.O_CREATE | os.O_TRUNC, 0600)
|
||||
defer fh.Close()
|
||||
if err != nil {
|
||||
log.Error("Failed to open %s: %s", file, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
enc := json.NewEncoder(fh)
|
||||
enc.Encode(data)
|
||||
}
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/docopt/docopt-go"
|
||||
"github.com/op/go-logging"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"io/ioutil"
|
||||
"gopkg.in/yaml.v2"
|
||||
"github.com/Netflix-Skunkworks/go-jira/jira/cli"
|
||||
)
|
||||
|
||||
var log = logging.MustGetLogger("jira")
|
||||
var format = "%{color}%{time:2006-01-02T15:04:05.000Z07:00} %{level:-5s} [%{shortfile}]%{color:reset} %{message}"
|
||||
|
||||
func parseYaml(file string, opts map[string]string) {
|
||||
if fh, err := ioutil.ReadFile(file); err == nil {
|
||||
log.Debug("Found Config file: %s", file)
|
||||
yaml.Unmarshal(fh, &opts)
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfigs(opts map[string]string) {
|
||||
paths := cli.FindParentPaths(".jira")
|
||||
// prepend
|
||||
paths = append([]string{"/etc/jira-cli.yml"}, paths...)
|
||||
|
||||
for _, file := range(paths) {
|
||||
parseYaml(file, opts)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
user := os.Getenv("USER")
|
||||
usage := fmt.Sprintf(`
|
||||
Usage:
|
||||
jira [-v ...] [-u USER] [-e URI] [-t FILE] fields
|
||||
jira [-v ...] [-u USER] [-e URI] [-t FILE] ls [--query=JQL]
|
||||
jira [-v ...] [-u USER] [-e URI] [-t FILE] view ISSUE
|
||||
jira [-v ...] [-u USER] [-e URI] [-t FILE] ISSUE
|
||||
|
||||
|
||||
General Options:
|
||||
-h --help Show this usage
|
||||
--version Show this version
|
||||
-v --verbose Increase output logging
|
||||
-u --user=USER Username to use for authenticaion (default: %s)
|
||||
-e --endpoint=URI URI to use for jira (default: https://jira)
|
||||
-t --template=FILE Template file to use for output
|
||||
|
||||
List options:
|
||||
-q --query=FILE Template to use for output
|
||||
`, user)
|
||||
|
||||
args, _ := docopt.Parse(usage, nil, true, "0.0.1", false, false)
|
||||
logBackend := logging.NewLogBackend(os.Stderr, "", 0)
|
||||
logging.SetBackend(
|
||||
logging.NewBackendFormatter(
|
||||
logBackend,
|
||||
logging.MustStringFormatter(format),
|
||||
),
|
||||
)
|
||||
logging.SetLevel(logging.NOTICE, "")
|
||||
if verbose, ok := args["--verbose"]; ok {
|
||||
if verbose.(int) > 1 {
|
||||
logging.SetLevel(logging.DEBUG, "")
|
||||
} else if verbose.(int) > 0 {
|
||||
logging.SetLevel(logging.INFO, "")
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Args: %v", args)
|
||||
|
||||
|
||||
opts := make(map[string]string)
|
||||
loadConfigs(opts)
|
||||
|
||||
for key,val := range args {
|
||||
if val != nil && strings.HasPrefix(key, "--") {
|
||||
opt := key[2:]
|
||||
switch v := val.(type) {
|
||||
case string:
|
||||
opts[opt] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := opts["endpoint"]; !ok {
|
||||
opts["endpoint"] = "https://jira"
|
||||
}
|
||||
if _, ok := opts["user"]; !ok {
|
||||
opts["user"] = user
|
||||
}
|
||||
|
||||
c := cli.New(opts)
|
||||
|
||||
log.Debug("opts: %s", opts);
|
||||
|
||||
c.CmdLogin()
|
||||
|
||||
if val, ok := args["fields"]; ok && val.(bool) {
|
||||
c.CmdFields()
|
||||
} else if val, ok := args["ls"]; ok && val.(bool) {
|
||||
c.CmdList()
|
||||
} else if val, ok := args["ISSUE"]; ok {
|
||||
c.CmdView(val.(string))
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
Reference in New Issue
Block a user