mirror of
https://github.com/Threnklyn/jira.git
synced 2026-05-24 14:48:30 +02:00
f7eb04e36d
This adjusts the CmdWatch interface as per discussion in https://github.com/Netflix-Skunkworks/go-jira/pull/26 It also exposes public versions of the c.getOptString and c.getOptBool utility functions, again as discussed. The interface to CmdWatch now includes the user to be watched (rather than depending on the opt[] map. This makes CmdWatch more useful externally. A '--remove' option has been created, to allow for removal of a given watcher. This was deliberately not included in the defaults map, as it is specifically only used for 'watch' command right now. It should be moved up to a default if it becomes a more common option, I guess (as 'remove is false' isn't a bad default)
503 lines
13 KiB
Go
503 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"github.com/Netflix-Skunkworks/go-jira"
|
|
"github.com/coryb/optigo"
|
|
"github.com/op/go-logging"
|
|
"gopkg.in/coryb/yaml.v2"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
log = logging.MustGetLogger("jira")
|
|
format = "%{color}%{time:2006-01-02T15:04:05.000Z07:00} %{level:-5s} [%{shortfile}]%{color:reset} %{message}"
|
|
)
|
|
|
|
func main() {
|
|
logBackend := logging.NewLogBackend(os.Stderr, "", 0)
|
|
logging.SetBackend(
|
|
logging.NewBackendFormatter(
|
|
logBackend,
|
|
logging.MustStringFormatter(format),
|
|
),
|
|
)
|
|
logging.SetLevel(logging.NOTICE, "")
|
|
|
|
user := os.Getenv("USER")
|
|
home := os.Getenv("HOME")
|
|
defaultQueryFields := "summary,created,updated,priority,status,reporter,assignee"
|
|
defaultSort := "priority asc, created"
|
|
defaultMaxResults := 500
|
|
|
|
usage := func(ok bool) {
|
|
printer := fmt.Printf
|
|
if !ok {
|
|
printer = func(format string, args ...interface{}) (int, error) {
|
|
return fmt.Fprintf(os.Stderr, format, args...)
|
|
}
|
|
defer func() {
|
|
os.Exit(1)
|
|
}()
|
|
} else {
|
|
defer func() {
|
|
os.Exit(0)
|
|
}()
|
|
}
|
|
output := fmt.Sprintf(`
|
|
Usage:
|
|
jira (ls|list) <Query Options>
|
|
jira view ISSUE
|
|
jira edit [--noedit] <Edit Options> [ISSUE | <Query Options>]
|
|
jira create [--noedit] [-p PROJECT] <Create Options>
|
|
jira DUPLICATE dups ISSUE
|
|
jira BLOCKER blocks ISSUE
|
|
jira vote ISSUE [--down]
|
|
jira watch ISSUE [-w WATCHER] [--remove]
|
|
jira (trans|transition) TRANSITION ISSUE [--noedit] <Edit Options>
|
|
jira ack ISSUE [--edit] <Edit Options>
|
|
jira close ISSUE [--edit] <Edit Options>
|
|
jira resolve ISSUE [--edit] <Edit Options>
|
|
jira reopen ISSUE [--edit] <Edit Options>
|
|
jira start ISSUE [--edit] <Edit Options>
|
|
jira stop ISSUE [--edit] <Edit Options>
|
|
jira comment ISSUE [--noedit] <Edit Options>
|
|
jira (set,add,remove) labels ISSUE [LABEL] ...
|
|
jira take ISSUE
|
|
jira (assign|give) ISSUE ASSIGNEE
|
|
jira fields
|
|
jira issuelinktypes
|
|
jira transmeta ISSUE
|
|
jira editmeta ISSUE
|
|
jira issuetypes [-p PROJECT]
|
|
jira createmeta [-p PROJECT] [-i ISSUETYPE]
|
|
jira transitions ISSUE
|
|
jira export-templates [-d DIR] [-t template]
|
|
jira (b|browse) ISSUE
|
|
jira login
|
|
jira request [-M METHOD] URI [DATA]
|
|
jira ISSUE
|
|
|
|
General Options:
|
|
-b --browse Open your browser to the Jira issue
|
|
-e --endpoint=URI URI to use for jira
|
|
-k --insecure disable TLS certificate verification
|
|
-h --help Show this usage
|
|
-t --template=FILE Template file to use for output/editing
|
|
-u --user=USER Username to use for authenticaion (default: %s)
|
|
-v --verbose Increase output logging
|
|
--version Print version
|
|
|
|
Query Options:
|
|
-a --assignee=USER Username assigned the issue
|
|
-c --component=COMPONENT Component to Search for
|
|
-f --queryfields=FIELDS Fields that are used in "list" template: (default: %s)
|
|
-i --issuetype=ISSUETYPE The Issue Type
|
|
-l --limit=VAL Maximum number of results to return in query (default: %d)
|
|
-p --project=PROJECT Project to Search for
|
|
-q --query=JQL Jira Query Language expression for the search
|
|
-r --reporter=USER Reporter to search for
|
|
-s --sort=ORDER For list operations, sort issues (default: %s)
|
|
-w --watcher=USER Watcher to add to issue (default: %s)
|
|
or Watcher to search for
|
|
|
|
Edit Options:
|
|
-m --comment=COMMENT Comment message for transition
|
|
-o --override=KEY=VAL Set custom key/value pairs
|
|
|
|
Create Options:
|
|
-i --issuetype=ISSUETYPE Jira Issue Type (default: Bug)
|
|
-m --comment=COMMENT Comment message for transition
|
|
-o --override=KEY=VAL Set custom key/value pairs
|
|
|
|
Command Options:
|
|
-d --directory=DIR Directory to export templates to (default: %s)
|
|
`, user, defaultQueryFields, defaultMaxResults, defaultSort, user, fmt.Sprintf("%s/.jira.d/templates", home))
|
|
printer(output)
|
|
}
|
|
|
|
jiraCommands := map[string]string{
|
|
"list": "list",
|
|
"ls": "list",
|
|
"view": "view",
|
|
"edit": "edit",
|
|
"create": "create",
|
|
"dups": "dups",
|
|
"blocks": "blocks",
|
|
"watch": "watch",
|
|
"trans": "transition",
|
|
"transition": "transition",
|
|
"ack": "acknowledge",
|
|
"acknowledge": "acknowledge",
|
|
"close": "close",
|
|
"resolve": "resolve",
|
|
"reopen": "reopen",
|
|
"start": "start",
|
|
"stop": "stop",
|
|
"comment": "comment",
|
|
"label": "labels",
|
|
"labels": "labels",
|
|
"take": "take",
|
|
"assign": "assign",
|
|
"give": "assign",
|
|
"fields": "fields",
|
|
"issuelinktypes": "issuelinktypes",
|
|
"transmeta": "transmeta",
|
|
"editmeta": "editmeta",
|
|
"issuetypes": "issuetypes",
|
|
"createmeta": "createmeta",
|
|
"transitions": "transitions",
|
|
"export-templates": "export-templates",
|
|
"browse": "browse",
|
|
"login": "login",
|
|
"req": "request",
|
|
"request": "request",
|
|
"vote": "vote",
|
|
}
|
|
|
|
defaults := map[string]interface{}{
|
|
"user": user,
|
|
"queryfields": defaultQueryFields,
|
|
"directory": fmt.Sprintf("%s/.jira.d/templates", home),
|
|
"sort": defaultSort,
|
|
"max_results": defaultMaxResults,
|
|
"method": "GET",
|
|
"quiet": false,
|
|
}
|
|
opts := make(map[string]interface{})
|
|
|
|
setopt := func(name string, value interface{}) {
|
|
opts[name] = value
|
|
}
|
|
|
|
op := optigo.NewDirectAssignParser(map[string]interface{}{
|
|
"h|help": usage,
|
|
"version": func() {
|
|
fmt.Println(fmt.Sprintf("version: %s", jira.VERSION))
|
|
os.Exit(0)
|
|
},
|
|
"v|verbose+": func() {
|
|
logging.SetLevel(logging.GetLevel("")+1, "")
|
|
},
|
|
"dryrun": setopt,
|
|
"b|browse": setopt,
|
|
"editor=s": setopt,
|
|
"u|user=s": setopt,
|
|
"endpoint=s": setopt,
|
|
"k|insecure": setopt,
|
|
"t|template=s": setopt,
|
|
"q|query=s": setopt,
|
|
"p|project=s": setopt,
|
|
"c|component=s": setopt,
|
|
"a|assignee=s": setopt,
|
|
"i|issuetype=s": setopt,
|
|
"w|watcher=s": setopt,
|
|
"remove": setopt,
|
|
"r|reporter=s": setopt,
|
|
"f|queryfields=s": setopt,
|
|
"s|sort=s": setopt,
|
|
"l|limit|max_results=i": setopt,
|
|
"o|override=s%": &opts,
|
|
"noedit": setopt,
|
|
"edit": setopt,
|
|
"m|comment=s": setopt,
|
|
"d|dir|directory=s": setopt,
|
|
"M|method=s": setopt,
|
|
"S|saveFile=s": setopt,
|
|
"Q|quiet": setopt,
|
|
"down": setopt,
|
|
})
|
|
|
|
if err := op.ProcessAll(os.Args[1:]); err != nil {
|
|
log.Error("%s", err)
|
|
usage(false)
|
|
}
|
|
args := op.Args
|
|
|
|
var command string
|
|
if len(args) > 0 {
|
|
if alias, ok := jiraCommands[args[0]]; ok {
|
|
command = alias
|
|
args = args[1:]
|
|
} else if len(args) > 1 {
|
|
// look at second arg for "dups" and "blocks" commands
|
|
// also for 'set/add/remove' actions like 'labels'
|
|
if alias, ok := jiraCommands[args[1]]; ok {
|
|
command = alias
|
|
args = append(args[:1], args[2:]...)
|
|
}
|
|
}
|
|
}
|
|
|
|
if command == "" && len(args) > 0 {
|
|
command = args[0]
|
|
args = args[1:]
|
|
}
|
|
|
|
os.Setenv("JIRA_OPERATION", command)
|
|
loadConfigs(opts)
|
|
|
|
// check to see if it was set in the configs:
|
|
if value, ok := opts["command"].(string); ok {
|
|
command = value
|
|
} else if _, ok := jiraCommands[command]; !ok || command == "" {
|
|
if command != "" {
|
|
args = append([]string{command}, args...)
|
|
}
|
|
command = "view"
|
|
}
|
|
|
|
// apply defaults
|
|
for k, v := range defaults {
|
|
if _, ok := opts[k]; !ok {
|
|
log.Debug("Setting %q to %#v from defaults", k, v)
|
|
opts[k] = v
|
|
}
|
|
}
|
|
|
|
log.Debug("opts: %v", opts)
|
|
log.Debug("args: %v", args)
|
|
|
|
if _, ok := opts["endpoint"]; !ok {
|
|
log.Error("endpoint option required. Either use --endpoint or set a endpoint option in your ~/.jira.d/config.yml file")
|
|
os.Exit(1)
|
|
}
|
|
|
|
c := jira.New(opts)
|
|
|
|
log.Debug("opts: %s", opts)
|
|
|
|
setEditing := func(dflt bool) {
|
|
log.Debug("Default Editing: %t", dflt)
|
|
if dflt {
|
|
if val, ok := opts["noedit"].(bool); ok && val {
|
|
log.Debug("Setting edit = false")
|
|
opts["edit"] = false
|
|
} else {
|
|
log.Debug("Setting edit = true")
|
|
opts["edit"] = true
|
|
}
|
|
} else {
|
|
if _, ok := opts["edit"].(bool); !ok {
|
|
log.Debug("Setting edit = %t", dflt)
|
|
opts["edit"] = dflt
|
|
}
|
|
}
|
|
}
|
|
|
|
requireArgs := func(count int) {
|
|
if len(args) < count {
|
|
log.Error("Not enough arguments. %d required, %d provided", count, len(args))
|
|
usage(false)
|
|
}
|
|
}
|
|
|
|
var err error
|
|
switch command {
|
|
case "login":
|
|
err = c.CmdLogin()
|
|
case "fields":
|
|
err = c.CmdFields()
|
|
case "list":
|
|
err = c.CmdList()
|
|
case "edit":
|
|
setEditing(true)
|
|
if len(args) > 0 {
|
|
err = c.CmdEdit(args[0])
|
|
} else {
|
|
var data interface{}
|
|
if data, err = c.FindIssues(); err == nil {
|
|
issues := data.(map[string]interface{})["issues"].([]interface{})
|
|
for _, issue := range issues {
|
|
if err = c.CmdEdit(issue.(map[string]interface{})["key"].(string)); err != nil {
|
|
switch err.(type) {
|
|
case jira.NoChangesFound:
|
|
log.Warning("No Changes found: %s", err)
|
|
err = nil
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
case "editmeta":
|
|
requireArgs(1)
|
|
err = c.CmdEditMeta(args[0])
|
|
case "transmeta":
|
|
requireArgs(1)
|
|
err = c.CmdTransitionMeta(args[0])
|
|
case "issuelinktypes":
|
|
err = c.CmdIssueLinkTypes()
|
|
case "issuetypes":
|
|
err = c.CmdIssueTypes()
|
|
case "createmeta":
|
|
err = c.CmdCreateMeta()
|
|
case "create":
|
|
setEditing(true)
|
|
err = c.CmdCreate()
|
|
case "transitions":
|
|
requireArgs(1)
|
|
err = c.CmdTransitions(args[0])
|
|
case "blocks":
|
|
requireArgs(2)
|
|
err = c.CmdBlocks(args[0], args[1])
|
|
case "dups":
|
|
requireArgs(2)
|
|
if err = c.CmdDups(args[0], args[1]); err == nil {
|
|
opts["resolution"] = "Duplicate"
|
|
err = c.CmdTransition(args[0], "close")
|
|
}
|
|
case "watch":
|
|
requireArgs(1)
|
|
watcher := c.GetOptString("watcher", opts["user"].(string))
|
|
remove := c.GetOptBool("remove", false)
|
|
err = c.CmdWatch(args[0], watcher, remove)
|
|
case "transition":
|
|
requireArgs(2)
|
|
setEditing(true)
|
|
err = c.CmdTransition(args[1], args[0])
|
|
case "close":
|
|
requireArgs(1)
|
|
setEditing(false)
|
|
err = c.CmdTransition(args[0], "close")
|
|
case "acknowledge":
|
|
requireArgs(1)
|
|
setEditing(false)
|
|
err = c.CmdTransition(args[0], "acknowledge")
|
|
case "reopen":
|
|
requireArgs(1)
|
|
setEditing(false)
|
|
err = c.CmdTransition(args[0], "reopen")
|
|
case "resolve":
|
|
requireArgs(1)
|
|
setEditing(false)
|
|
err = c.CmdTransition(args[0], "resolve")
|
|
case "start":
|
|
requireArgs(1)
|
|
setEditing(false)
|
|
err = c.CmdTransition(args[0], "start")
|
|
case "stop":
|
|
requireArgs(1)
|
|
setEditing(false)
|
|
err = c.CmdTransition(args[0], "stop")
|
|
case "comment":
|
|
requireArgs(1)
|
|
setEditing(true)
|
|
err = c.CmdComment(args[0])
|
|
case "labels":
|
|
requireArgs(2)
|
|
action := args[0]
|
|
issue := args[1]
|
|
labels := args[2:]
|
|
err = c.CmdLabels(action, issue, labels)
|
|
case "take":
|
|
requireArgs(1)
|
|
err = c.CmdAssign(args[0], opts["user"].(string))
|
|
case "browse":
|
|
requireArgs(1)
|
|
opts["browse"] = true
|
|
err = c.Browse(args[0])
|
|
case "export-templates":
|
|
err = c.CmdExportTemplates()
|
|
case "assign":
|
|
requireArgs(2)
|
|
err = c.CmdAssign(args[0], args[1])
|
|
case "view":
|
|
requireArgs(1)
|
|
err = c.CmdView(args[0])
|
|
case "vote":
|
|
requireArgs(1)
|
|
if val, ok := opts["down"]; ok {
|
|
err = c.CmdVote(args[0], !val.(bool))
|
|
} else {
|
|
err = c.CmdVote(args[0], true)
|
|
}
|
|
case "request":
|
|
requireArgs(1)
|
|
data := ""
|
|
if len(args) > 1 {
|
|
data = args[1]
|
|
}
|
|
err = c.CmdRequest(args[0], data)
|
|
default:
|
|
log.Error("Unknown command %s", command)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if err != nil {
|
|
log.Error("%s", err)
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
|
|
func parseYaml(file string, opts map[string]interface{}) {
|
|
if fh, err := ioutil.ReadFile(file); err == nil {
|
|
log.Debug("Found Config file: %s", file)
|
|
yaml.Unmarshal(fh, &opts)
|
|
}
|
|
}
|
|
|
|
func populateEnv(opts map[string]interface{}) {
|
|
for k, v := range opts {
|
|
envName := fmt.Sprintf("JIRA_%s", strings.ToUpper(k))
|
|
var val string
|
|
switch t := v.(type) {
|
|
case string:
|
|
val = t
|
|
case int, int8, int16, int32, int64:
|
|
val = fmt.Sprintf("%d", t)
|
|
case float32, float64:
|
|
val = fmt.Sprintf("%f", t)
|
|
case bool:
|
|
val = fmt.Sprintf("%t", t)
|
|
default:
|
|
val = fmt.Sprintf("%v", t)
|
|
}
|
|
os.Setenv(envName, val)
|
|
}
|
|
}
|
|
|
|
func loadConfigs(opts map[string]interface{}) {
|
|
populateEnv(opts)
|
|
paths := jira.FindParentPaths(".jira.d/config.yml")
|
|
// prepend
|
|
paths = append([]string{"/etc/go-jira.yml"}, paths...)
|
|
|
|
// iterate paths in reverse
|
|
for i := len(paths) - 1; i >= 0; i-- {
|
|
file := paths[i]
|
|
if stat, err := os.Stat(file); err == nil {
|
|
tmp := make(map[string]interface{})
|
|
// check to see if config file is exectuable
|
|
if stat.Mode()&0111 == 0 {
|
|
parseYaml(file, tmp)
|
|
} else {
|
|
log.Debug("Found Executable Config file: %s", file)
|
|
// it is executable, so run it and try to parse the output
|
|
cmd := exec.Command(file)
|
|
stdout := bytes.NewBufferString("")
|
|
cmd.Stdout = stdout
|
|
cmd.Stderr = bytes.NewBufferString("")
|
|
if err := cmd.Run(); err != nil {
|
|
log.Error("%s is exectuable, but it failed to execute: %s\n%s", file, err, cmd.Stderr)
|
|
os.Exit(1)
|
|
}
|
|
yaml.Unmarshal(stdout.Bytes(), &tmp)
|
|
}
|
|
for k, v := range tmp {
|
|
if _, ok := opts[k]; !ok {
|
|
log.Debug("Setting %q to %#v from %s", k, v, file)
|
|
opts[k] = v
|
|
}
|
|
}
|
|
populateEnv(opts)
|
|
}
|
|
}
|
|
}
|