Compare commits

..

16 Commits

Author SHA1 Message Date
Cory Bennett 37b138376b Updated Changelog 2017-05-10 09:05:30 -07:00
Cory Bennett 8a5e588ce2 fix unsafe casting for --quiet flag 2017-05-10 09:03:47 -07:00
Cory Bennett 67c86e4858 [#80] add jira unassign and jira give ISSUE --default commands 2017-05-10 08:58:53 -07:00
Cory Bennett 445f8f1f84 Updated Changelog 2017-04-24 11:06:39 -07:00
Cory Bennett 485f73181c revert update 2017-04-24 11:06:31 -07:00
Cory Bennett 4d321ec202 work around github.com/tmc/keyring compile error for windows 2017-04-24 10:50:30 -07:00
Cory Bennett d7bce222b6 Updated Changelog 2017-04-24 10:30:33 -07:00
coryb f231f55d74 Merge pull request #78 from davidreuss/generic-issuelink
Added generic issuelink command
2017-04-24 09:10:31 -07:00
coryb 28242c9c7e Merge pull request #77 from davidreuss/fix-start-parameter-for-pagination
Added --start parameter for pagination on results
2017-04-24 09:07:05 -07:00
David Reuss 05951f1c0d Added generic issuelink command
This allows adding generic links, and could replace 'blocks', and 'dups'
command, since it's pretty much just a copy/paste job.

Usage will be something like:

$ jira issuelink $INWARDISSUE "Relates" OUTWARDISSUE

Pulling the list of the names, for your issuelinktypes

$ jira issuelinktypes | jq '.issueLinkTypes | map(.name)'
[
  "Blocks",
  "Bonfire testing",
  "Clones",
  "Deprecates",
  "Duplicate",
  "Relates",
  "Risks"
]
2017-04-24 12:45:24 +02:00
David Reuss f47563048b Added --start parameter for pagination on results 2017-04-24 11:33:17 +02:00
Cory Bennett cd9976ae4e Updated Changelog 2017-03-22 22:25:25 -07:00
coryb f3aa2f4c1a Merge pull request #74 from clausb/BrowseOnWindows
Implement "browse" subcommand on Windows
2017-03-22 22:24:50 -07:00
Claus Brod f6230ca8c6 Implement "browse" subcommand on Windows 2017-03-22 22:36:21 +01:00
Cory Bennett 412174f8a9 Updated Changelog 2017-02-26 22:58:28 -08:00
Cory Bennett 52085417e6 [#69] add subtask command 2017-02-26 22:38:47 -08:00
8 changed files with 274 additions and 46 deletions
+19
View File
@@ -1,5 +1,24 @@
# Changelog
## 0.1.14 - 2017-05-10
* fix unsafe casting for --quiet flag [Cory Bennett] [[6f29f43](https://github.com/Netflix-Skunkworks/go-jira/commit/6f29f43)]
* [[#80](https://github.com/Netflix-Skunkworks/go-jira/issues/80)] add `jira unassign` and `jira give ISSUE --default` commands [Cory Bennett] [[03d8633](https://github.com/Netflix-Skunkworks/go-jira/commit/03d8633)]
## 0.1.13 - 2017-04-24
* work around `github.com/tmc/keyring` compile error for windows [Cory Bennett] [[85298e9](https://github.com/Netflix-Skunkworks/go-jira/commit/85298e9)]
* Added generic issuelink command [David Reuss] [[cc54d11](https://github.com/Netflix-Skunkworks/go-jira/commit/cc54d11)]
* Added --start parameter for pagination on results [David Reuss] [[9b94d9e](https://github.com/Netflix-Skunkworks/go-jira/commit/9b94d9e)]
## 0.1.12 - 2017-03-22
* Implement "browse" subcommand on Windows [Claus Brod] [[ca333d8](https://github.com/Netflix-Skunkworks/go-jira/commit/ca333d8)]
## 0.1.11 - 2017-02-26
* [[#69](https://github.com/Netflix-Skunkworks/go-jira/issues/69)] add subtask command [Cory Bennett] [[21a2ed5](https://github.com/Netflix-Skunkworks/go-jira/commit/21a2ed5)]
## 0.1.10 - 2017-02-08
* set GPG_TTY in .bashrc [Cory Bennett] [[b1e552f](https://github.com/Netflix-Skunkworks/go-jira/commit/b1e552f)]
+3 -1
View File
@@ -451,6 +451,8 @@ func (c *Cli) Browse(issue string) error {
return exec.Command("open", fmt.Sprintf("%s/browse/%s", c.endpoint, issue)).Run()
} else if runtime.GOOS == "linux" {
return exec.Command("xdg-open", fmt.Sprintf("%s/browse/%s", c.endpoint, issue)).Run()
} else if runtime.GOOS == "windows" {
return exec.Command("cmd", "/c", "start", fmt.Sprintf("%s/browse/%s", c.endpoint, issue)).Run()
}
}
return nil
@@ -542,7 +544,7 @@ func (c *Cli) FindIssues() (interface{}, error) {
json, err := jsonEncode(map[string]interface{}{
"jql": query,
"startAt": "0",
"startAt": c.opts["start_at"],
"maxResults": c.opts["max_results"],
"fields": fields,
"expand": c.expansions(),
+167 -35
View File
@@ -157,7 +157,7 @@ func (c *Cli) CmdWorklog(action string, issue string) error {
if resp.StatusCode == 201 {
c.Browse(issue)
if !c.opts["quiet"].(bool) {
if !c.GetOptBool("quiet", false) {
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
}
return nil
@@ -210,7 +210,7 @@ func (c *Cli) CmdEdit(issue string) error {
if resp.StatusCode == 204 {
c.Browse(issueData["key"].(string))
if !c.opts["quiet"].(bool) {
if !c.GetOptBool("quiet", false) {
fmt.Printf("OK %s %s/browse/%s\n", issueData["key"], c.endpoint, issueData["key"])
}
return nil
@@ -376,31 +376,15 @@ func (c *Cli) CmdCreate() error {
issuetype = c.defaultIssueType()
}
uri := fmt.Sprintf("%s/rest/api/2/issue/createmeta?projectKeys=%s&issuetypeNames=%s&expand=projects.issuetypes.fields", c.endpoint, project, url.QueryEscape(issuetype))
data, err := responseToJSON(c.get(uri))
if err != nil {
return err
}
issueData := make(map[string]interface{})
issueData["overrides"] = c.opts
issueData["overrides"].(map[string]interface{})["issuetype"] = issuetype
if val, ok := data.(map[string]interface{})["projects"]; ok {
if len(val.([]interface{})) == 0 {
err = fmt.Errorf("Project '%s' or issuetype '%s' unknown. Unable to create issue.", project, issuetype)
log.Errorf("%s", err)
return err
}
if val, ok = val.([]interface{})[0].(map[string]interface{})["issuetypes"]; ok {
if len(val.([]interface{})) == 0 {
err = fmt.Errorf("Project '%s' does not support issuetype '%s'. Unable to create issue.", project, issuetype)
log.Errorf("%s", err)
return err
}
issueData["meta"] = val.([]interface{})[0]
}
meta, err := c.createIssueMetaData(project, issuetype)
if err != nil {
return err
}
issueData["meta"] = meta
sanitizedType := strings.ToLower(strings.Replace(issuetype, " ", "", -1))
return c.editTemplate(
@@ -432,7 +416,97 @@ func (c *Cli) CmdCreate() error {
"issue": key,
"link": link,
})
if !c.opts["quiet"].(bool) {
if !c.GetOptBool("quiet", false) {
fmt.Printf("OK %s %s\n", key, link)
}
return nil
}
logBuffer := bytes.NewBuffer(make([]byte, 0))
resp.Write(logBuffer)
err = fmt.Errorf("Unexpected Response From POST")
log.Errorf("%s:\n%s", err, logBuffer)
return err
},
)
}
func (c *Cli) createIssueMetaData(project, issuetype string) (interface{}, error) {
uri := fmt.Sprintf("%s/rest/api/2/issue/createmeta?projectKeys=%s&issuetypeNames=%s&expand=projects.issuetypes.fields", c.endpoint, project, url.QueryEscape(issuetype))
metaData, err := responseToJSON(c.get(uri))
if err != nil {
return nil, err
}
if val, ok := metaData.(map[string]interface{})["projects"]; ok {
if len(val.([]interface{})) == 0 {
err = fmt.Errorf("Project '%s' or issuetype '%s' unknown. Unable to create issue.", project, issuetype)
log.Errorf("%s", err)
return nil, err
}
if val, ok = val.([]interface{})[0].(map[string]interface{})["issuetypes"]; ok {
if len(val.([]interface{})) == 0 {
err = fmt.Errorf("Project '%s' does not support issuetype '%s'. Unable to create issue.", project, issuetype)
log.Errorf("%s", err)
return nil, err
}
return val.([]interface{})[0], nil
}
}
return nil, nil
}
// CmdSubtask sends the create-metadata to the "subtask" template for editing, then
// will parse the edited document as YAML and submit the document to jira.
func (c *Cli) CmdSubtask(issue string) error {
log.Debugf("subtask called")
uri := fmt.Sprintf("%s/rest/api/2/issue/%s", c.endpoint, issue)
parentData, err := responseToJSON(c.get(uri))
if err != nil {
return err
}
subtaskData := make(map[string]interface{})
subtaskData["parent"] = parentData
subtaskData["overrides"] = c.opts
project := parentData.(map[string]interface{})["fields"].(map[string]interface{})["project"].(map[string]interface{})["key"].(string)
meta, err := c.createIssueMetaData(project, "Sub-task")
if err != nil {
return err
}
subtaskData["meta"] = meta
return c.editTemplate(
c.getTemplate("subtask"),
"subtask-",
subtaskData,
func(json string) error {
uri := fmt.Sprintf("%s/rest/api/2/issue", c.endpoint)
if c.getOptBool("dryrun", false) {
log.Debugf("POST: %s", json)
log.Debugf("Dryrun mode, skipping POST")
return nil
}
resp, err := c.post(uri, json)
if err != nil {
return err
}
if resp.StatusCode == 201 {
// response: {"id":"410836","key":"PROJ-238","self":"https://jira/rest/api/2/issue/410836"}
json, err := responseToJSON(resp, nil)
if err != nil {
return err
}
key := json.(map[string]interface{})["key"].(string)
link := fmt.Sprintf("%s/browse/%s", c.endpoint, key)
c.Browse(key)
c.SaveData(map[string]string{
"issue": key,
"link": link,
})
if !c.GetOptBool("quiet", false) {
fmt.Printf("OK %s %s\n", key, link)
}
return nil
@@ -457,6 +531,50 @@ func (c *Cli) CmdIssueLinkTypes() error {
return runTemplate(c.getTemplate("issuelinktypes"), data, nil)
}
// CmdIssueLink is a generic function for adding a link type to an issue
func (c *Cli) CmdIssueLink(inwardIssue string, issueLinkTypeName string, outwardIssue string) error {
log.Debugf("issuelink called")
json, err := jsonEncode(map[string]interface{}{
"type": map[string]string{
"name": issueLinkTypeName,
},
"inwardIssue": map[string]string{
"key": inwardIssue,
},
"outwardIssue": map[string]string{
"key": outwardIssue,
},
})
if err != nil {
return err
}
uri := fmt.Sprintf("%s/rest/api/2/issueLink", c.endpoint)
if c.getOptBool("dryrun", false) {
log.Debugf("POST: %s", json)
log.Debugf("Dryrun mode, skipping POST")
return nil
}
resp, err := c.post(uri, json)
if err != nil {
return err
}
if resp.StatusCode == 201 {
c.Browse(inwardIssue)
if !c.GetOptBool("quiet", false) {
fmt.Printf("OK %s %s/browse/%s\n", inwardIssue, c.endpoint, inwardIssue)
}
} else {
logBuffer := bytes.NewBuffer(make([]byte, 0))
resp.Write(logBuffer)
err := fmt.Errorf("Unexpected Response From POST")
log.Errorf("%s:\n%s", err, logBuffer)
return err
}
return nil
}
// CmdBlocks will update the given issue as being "blocked" by the given blocker
func (c *Cli) CmdBlocks(blocker string, issue string) error {
log.Debugf("blocks called")
@@ -488,7 +606,7 @@ func (c *Cli) CmdBlocks(blocker string, issue string) error {
}
if resp.StatusCode == 201 {
c.Browse(issue)
if !c.opts["quiet"].(bool) {
if !c.GetOptBool("quiet", false) {
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
}
} else {
@@ -533,7 +651,7 @@ func (c *Cli) CmdDups(duplicate string, issue string) error {
}
if resp.StatusCode == 201 {
c.Browse(issue)
if !c.opts["quiet"].(bool) {
if !c.GetOptBool("quiet", false) {
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
}
} else {
@@ -581,7 +699,7 @@ func (c *Cli) CmdWatch(issue string, watcher string, remove bool) error {
}
if resp.StatusCode == 204 {
c.Browse(issue)
if !c.opts["quiet"].(bool) {
if !c.GetOptBool("quiet", false) {
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
}
} else {
@@ -625,7 +743,7 @@ func (c *Cli) CmdVote(issue string, up bool) error {
}
if resp.StatusCode == 204 {
c.Browse(issue)
if !c.opts["quiet"].(bool) {
if !c.GetOptBool("quiet", false) {
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
}
} else {
@@ -648,7 +766,7 @@ func (c *Cli) CmdRankAfter(issue, after string) error {
if err != nil {
return nil
}
if !c.opts["quiet"].(bool) {
if !c.GetOptBool("quiet", false) {
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
}
return nil
@@ -660,7 +778,7 @@ func (c *Cli) CmdRankBefore(issue, before string) error {
if err != nil {
return nil
}
if !c.opts["quiet"].(bool) {
if !c.GetOptBool("quiet", false) {
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
}
return nil
@@ -711,7 +829,7 @@ func (c *Cli) CmdTransition(issue string, trans string) error {
}
if resp.StatusCode == 204 {
c.Browse(issue)
if !c.opts["quiet"].(bool) {
if !c.GetOptBool("quiet", false) {
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
}
} else {
@@ -781,7 +899,7 @@ func (c *Cli) CmdComment(issue string) error {
if resp.StatusCode == 201 {
c.Browse(issue)
if !c.opts["quiet"].(bool) {
if !c.GetOptBool("quiet", false) {
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
}
return nil
@@ -841,7 +959,7 @@ func (c *Cli) CmdComponent(action string, project string, name string, desc stri
return err
}
if resp.StatusCode == 201 {
if !c.opts["quiet"].(bool) {
if !c.GetOptBool("quiet", false) {
fmt.Printf("OK %s %s\n", project, name)
}
} else {
@@ -876,7 +994,7 @@ func (c *Cli) CmdLabels(action string, issue string, labels []string) error {
if resp.StatusCode == 204 {
c.Browse(issue)
if !c.opts["quiet"].(bool) {
if !c.GetOptBool("quiet", false) {
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
}
return nil
@@ -922,8 +1040,18 @@ func (c *Cli) CmdLabels(action string, issue string, labels []string) error {
func (c *Cli) CmdAssign(issue string, user string) error {
log.Debugf("assign called")
var userVal interface{} = user
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-assign
// If the name is "-1" automatic assignee is used. A null name will remove the assignee.
if user == "" {
userVal = nil
}
if c.GetOptBool("default", false) {
userVal = "-1"
}
json, err := jsonEncode(map[string]interface{}{
"name": user,
"name": userVal,
})
if err != nil {
return err
@@ -941,7 +1069,7 @@ func (c *Cli) CmdAssign(issue string, user string) error {
}
if resp.StatusCode == 204 {
c.Browse(issue)
if !c.opts["quiet"].(bool) {
if !c.GetOptBool("quiet", false) {
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
}
} else {
@@ -954,6 +1082,10 @@ func (c *Cli) CmdAssign(issue string, user string) error {
return nil
}
func (c *Cli) CmdUnassign(issue string) error {
return c.CmdAssign(issue, "")
}
// CmdExportTemplates will export the default templates to the template directory.
func (c *Cli) CmdExportTemplates() error {
dir := c.opts["directory"].(string)
+13
View File
@@ -0,0 +1,13 @@
// +build !windows
package jira
import "github.com/tmc/keyring"
func keyringGet(user string) (string, error) {
return keyring.Get("go-jira", user)
}
func keyringSet(user, passwd string) error {
return keyring.Set("go-jira", user, passwd)
}
+11
View File
@@ -0,0 +1,11 @@
package jira
import "fmt"
func keyringGet(user string) (string, error) {
return "", fmt.Errorf("Keyring is not supported for Windows, see: https://github.com/tmc/keyring")
}
func keyringSet(user, passwd string) error {
return fmt.Errorf("Keyring is not supported for Windows, see: https://github.com/tmc/keyring")
}
+30 -7
View File
@@ -3,14 +3,15 @@ package main
import (
"bytes"
"fmt"
"gopkg.in/Netflix-Skunkworks/go-jira.v0"
"github.com/coryb/optigo"
"gopkg.in/coryb/yaml.v2"
"gopkg.in/op/go-logging.v1"
"io/ioutil"
"os"
"os/exec"
"strings"
"github.com/coryb/optigo"
"gopkg.in/Netflix-Skunkworks/go-jira.v0"
"gopkg.in/coryb/yaml.v2"
"gopkg.in/op/go-logging.v1"
)
var (
@@ -60,8 +61,10 @@ Usage:
jira add worklog ISSUE <Worklog Options>
jira edit [--noedit] <Edit Options> [ISSUE | <Query Options>]
jira create [--noedit] [-p PROJECT] <Create Options>
jira subtask ISSUE [--noedit] <Create Options>
jira DUPLICATE dups ISSUE
jira BLOCKER blocks ISSUE
jira issuelink OUTWARDISSUE ISSUELINKTYPE INWARDISSUE
jira vote ISSUE [--down]
jira rank ISSUE (after|before) ISSUE
jira watch ISSUE [-w WATCHER] [--remove]
@@ -79,7 +82,8 @@ Usage:
jira comment ISSUE [--noedit] <Edit Options>
jira (set,add,remove) labels ISSUE [LABEL] ...
jira take ISSUE
jira (assign|give) ISSUE ASSIGNEE
jira (assign|give) ISSUE [ASSIGNEE|--default]
jira unassign ISSUE
jira fields
jira issuelinktypes
jira transmeta ISSUE
@@ -113,6 +117,7 @@ Query Options:
-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)
--start=START Start parameter for pagination
-p --project=PROJECT Project to Search for
-q --query=JQL Jira Query Language expression for the search
-r --reporter=USER Reporter to search for
@@ -145,8 +150,10 @@ Command Options:
"view": "view",
"edit": "edit",
"create": "create",
"subtask": "subtask",
"dups": "dups",
"blocks": "blocks",
"issuelink": "issuelink",
"watch": "watch",
"trans": "transition",
"transition": "transition",
@@ -188,6 +195,7 @@ Command Options:
"rank": "rank",
"worklog": "worklog",
"addworklog": "addworklog",
"unassign": "unassign",
}
defaults := map[string]interface{}{
@@ -233,6 +241,7 @@ Command Options:
"x|expand=s": setopt,
"s|sort=s": setopt,
"l|limit|max_results=i": setopt,
"start|start_at=i": setopt,
"o|override=s%": &opts,
"noedit": setopt,
"edit": setopt,
@@ -244,6 +253,7 @@ Command Options:
"Q|quiet": setopt,
"unixproxy": setopt,
"down": setopt,
"default": setopt,
})
if err := op.ProcessAll(os.Args[1:]); err != nil {
@@ -332,6 +342,9 @@ Command Options:
var err error
switch command {
case "issuelink":
requireArgs(3)
err = c.CmdIssueLink(args[0], args[1], args[2])
case "login":
err = c.CmdLogin()
case "logout":
@@ -376,6 +389,9 @@ Command Options:
case "create":
setEditing(true)
err = c.CmdCreate()
case "subtask":
setEditing(true)
err = c.CmdSubtask(args[0])
case "transitions":
requireArgs(1)
err = c.CmdTransitions(args[0])
@@ -490,8 +506,15 @@ Command Options:
case "export-templates":
err = c.CmdExportTemplates()
case "assign":
requireArgs(2)
err = c.CmdAssign(args[0], args[1])
requireArgs(1)
assignee := ""
if len(args) > 1 {
assignee = args[1]
}
err = c.CmdAssign(args[0], assignee)
case "unassign":
requireArgs(1)
err = c.CmdUnassign(args[0])
case "view":
requireArgs(1)
err = c.CmdView(args[0])
+6 -3
View File
@@ -7,14 +7,17 @@ import (
"strings"
"github.com/howeyc/gopass"
"github.com/tmc/keyring"
)
func (c *Cli) GetPass(user string) string {
passwd := ""
if source, ok := c.opts["password-source"].(string); ok {
if source == "keyring" {
passwd, _ = keyring.Get("go-jira", user)
var err error
passwd, err = keyringGet(user)
if err != nil {
panic(err)
}
} else if source == "pass" {
if bin, err := exec.LookPath("pass"); err == nil {
buf := bytes.NewBufferString("")
@@ -48,7 +51,7 @@ func (c *Cli) SetPass(user, passwd string) error {
log.Debugf("password-source: %s", source)
if source == "keyring" {
// save password in keychain so that it can be used for subsequent http requests
err := keyring.Set("go-jira", user, passwd)
err := keyringSet(user, passwd)
if err != nil {
log.Errorf("Failed to set password in keyring: %s", err)
return err
+25
View File
@@ -15,6 +15,7 @@ var allTemplates = map[string]string{
"components": defaultComponentsTemplate,
"issuetypes": defaultIssuetypesTemplate,
"create": defaultCreateTemplate,
"subtask": defaultSubtaskTemplate,
"comment": defaultCommentTemplate,
"transition": defaultTransitionTemplate,
"request": defaultDebugTemplate,
@@ -140,6 +141,30 @@ fields:
- name: {{.}}{{end}}
- name:{{end}}`
const defaultSubtaskTemplate = `{{/* create subtask template */ -}}
fields:
project:
key: {{ .parent.fields.project.key }}
summary: {{ or .overrides.summary "" }}{{if .meta.fields.priority.allowedValues}}
priority: # Values: {{ range .meta.fields.priority.allowedValues }}{{.name}}, {{end}}
name: {{ or .overrides.priority ""}}{{end}}{{if .meta.fields.components.allowedValues}}
components: # Values: {{ range .meta.fields.components.allowedValues }}{{.name}}, {{end}}{{ range split "," (or .overrides.components "")}}
- name: {{ . }}{{end}}{{end}}
description: |~
{{ or .overrides.description "" | indent 4 }}{{if .meta.fields.assignee}}
assignee:
name: {{ or .overrides.assignee "" }}{{end}}{{if .meta.fields.reporter}}
reporter:
name: {{ or .overrides.reporter .overrides.user }}{{end}}{{if .meta.fields.customfield_10110}}
# watchers
customfield_10110: {{ range split "," (or .overrides.watchers "")}}
- name: {{.}}{{end}}
- name:{{end}}
issuetype:
name: Sub-task
parent:
key: {{ .parent.key }}`
const defaultCommentTemplate = `body: |~
{{ or .overrides.comment "" | indent 2 }}
`