diff --git a/cmd/jira/main.go b/cmd/jira/main.go index ba85664d..92f29ac3 100644 --- a/cmd/jira/main.go +++ b/cmd/jira/main.go @@ -187,6 +187,10 @@ func main() { Aliases: []string{"prog", "progress"}, Entry: cli.CmdTransitionRegistry("Progress"), }, + jiracli.CommandRegistry{ + Command: "vote", + Entry: cli.CmdVoteRegistry(), + }, } cli.Register(app, registry) @@ -225,21 +229,16 @@ func main() { // } // output := fmt.Sprintf(` // Usage: - // jira vote ISSUE [--down] // jira rank ISSUE (after|before) ISSUE // jira watch ISSUE [-w WATCHER] [--remove] - // jira (trans|transition) TRANSITION ISSUE [--noedit] // jira comment ISSUE [--noedit] // jira (set,add,remove) labels ISSUE [LABEL] ... // jira take ISSUE // jira (assign|give) ISSUE [ASSIGNEE|--default] // jira unassign ISSUE - // jira transmeta ISSUE // jira add component [-p PROJECT] NAME DESCRIPTION LEAD // jira components [-p PROJECT] // 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 request [-M METHOD] URI [DATA] @@ -304,7 +303,6 @@ func main() { // "browse": "browse", // "req": "request", // "request": "request", - // "vote": "vote", // "rank": "rank", // "unassign": "unassign", // } @@ -506,13 +504,6 @@ func main() { // case "unassign": // requireArgs(1) // err = c.CmdUnassign(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 "rank": // requireArgs(3) // if args[1] == "after" { diff --git a/issue.go b/issue.go index 2f62d6da..6b153dca 100644 --- a/issue.go +++ b/issue.go @@ -315,3 +315,33 @@ func (j *Jira) GetIssueLinkTypes() (*jiradata.IssueLinkTypes, error) { } return nil, responseError(resp) } + +// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-addVote +func (j *Jira) IssueAddVote(issue string) error { + uri := fmt.Sprintf("%s/rest/api/2/issue/%s/votes", j.Endpoint, issue) + resp, err := j.UA.Post(uri, "application/json", strings.NewReader("{}")) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode == 204 { + return nil + } + return responseError(resp) +} + +// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-removeVote +func (j *Jira) IssueRemoveVote(issue string) error { + uri := fmt.Sprintf("%s/rest/api/2/issue/%s/votes", j.Endpoint, issue) + resp, err := j.UA.Delete(uri) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode == 204 { + return nil + } + return responseError(resp) +} diff --git a/jiracli/commands.go b/jiracli/commands.go index 530518e3..89739e5f 100644 --- a/jiracli/commands.go +++ b/jiracli/commands.go @@ -63,19 +63,6 @@ func (jc *JiraCli) Register(app *kingpin.Application, reg []CommandRegistry) { } } -// // CmdTransitionMeta will send available transition metadata to the "transmeta" template -// func (c *Cli) CmdTransitionMeta(issue string) error { -// log.Debugf("tranisionMeta called") -// c.Browse(issue) -// uri := fmt.Sprintf("%s/rest/api/2/issue/%s/transitions?expand=transitions.fields", c.endpoint, issue) -// data, err := responseToJSON(c.get(uri)) -// if err != nil { -// return err -// } - -// return runTemplate(c.getTemplate("transmeta"), data, nil) -// } - // // CmdIssueTypes will send issue 'create' metadata to the 'issuetypes' // func (c *Cli) CmdIssueTypes() error { // project := c.opts["project"].(string) @@ -129,140 +116,6 @@ func (jc *JiraCli) Register(app *kingpin.Application, reg []CommandRegistry) { // return runTemplate(c.getTemplate("components"), data, nil) // } -// // ValidTransitions will return a list of valid transitions for given issue. -// func (c *Cli) ValidTransitions(issue string) (jiradata.Transitions, error) { -// uri := fmt.Sprintf("%s/rest/api/2/issue/%s/transitions?expand=transitions.fields", c.endpoint, issue) -// resp, err := c.get(uri) -// if err != nil { -// return nil, err -// } - -// transMeta := &jiradata.TransitionsMeta{} -// content, err := ioutil.ReadAll(resp.Body) -// if err != nil { -// return nil, err -// } -// err = json.Unmarshal(content, transMeta) -// if err != nil { -// return nil, err -// } - -// return transMeta.Transitions, nil -// } - -// // CmdTransitions sends valid transtions for given issue to the "transitions" template -// func (c *Cli) CmdTransitions(issue string) error { -// log.Debugf("Transitions called") -// // FIXME this should just call ValidTransitions then pass that data to templates -// c.Browse(issue) -// uri := fmt.Sprintf("%s/rest/api/2/issue/%s/transitions", c.endpoint, issue) -// data, err := responseToJSON(c.get(uri)) -// if err != nil { -// return err -// } -// return runTemplate(c.getTemplate("transitions"), data, nil) -// } - -// // CmdIssueLinkTypes will send the issue link type data to the "issuelinktypes" template. -// func (c *Cli) CmdIssueLinkTypes() error { -// log.Debugf("Transitions called") -// uri := fmt.Sprintf("%s/rest/api/2/issueLinkType", c.endpoint) -// data, err := responseToJSON(c.get(uri)) -// if err != nil { -// return err -// } -// 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 -// } - -// // CmdDups will update the given issue as being a duplicate by the given dup issue -// // and will attempt to resolve the dup issue -// func (c *Cli) CmdDups(duplicate string, issue string) error { -// log.Debugf("dups called") - -// json, err := jsonEncode(map[string]interface{}{ -// "type": map[string]string{ -// "name": "Duplicate", // TODO This is probably not constant across Jira installs -// }, -// "inwardIssue": map[string]string{ -// "key": duplicate, -// }, -// "outwardIssue": map[string]string{ -// "key": issue, -// }, -// }) -// 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(issue) -// if !c.GetOptBool("quiet", false) { -// fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue) -// } -// } 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 -// } - // // CmdWatch will add the given watcher to the issue (or remove the watcher // // given the 'remove' flag) // func (c *Cli) CmdWatch(issue string, watcher string, remove bool) error { @@ -315,50 +168,6 @@ func (jc *JiraCli) Register(app *kingpin.Application, reg []CommandRegistry) { // return nil // } -// // CmdVote will add or remove a vote on an issue -// func (c *Cli) CmdVote(issue string, up bool) error { -// log.Debugf("vote called, with up: %n", up) - -// uri := fmt.Sprintf("%s/rest/api/2/issue/%s/votes", c.endpoint, issue) -// if c.getOptBool("dryrun", false) { -// if up { -// log.Debugf("POST: %s", "") -// log.Debugf("Dryrun mode, skipping POST") -// } else { -// log.Debugf("DELETE: %s", "") -// log.Debugf("Dryrun mode, skipping DELETE") -// } -// return nil -// } -// var resp *http.Response -// var err error -// if up { -// resp, err = c.post(uri, "") -// } else { -// resp, err = c.delete(uri) -// } -// if err != nil { -// return err -// } -// if resp.StatusCode == 204 { -// c.Browse(issue) -// if !c.GetOptBool("quiet", false) { -// fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue) -// } -// } else { -// logBuffer := bytes.NewBuffer(make([]byte, 0)) -// resp.Write(logBuffer) -// if up { -// err = fmt.Errorf("Unexpected Response From POST") -// } else { -// err = fmt.Errorf("Unexpected Response From DELETE") -// } -// log.Errorf("%s:\n%s", err, logBuffer) -// return err -// } -// return nil -// } - // // CmdRankAfter rank issue after target issue // func (c *Cli) CmdRankAfter(issue, after string) error { // err := c.RankIssue(issue, after, RANKAFTER) @@ -383,102 +192,6 @@ func (jc *JiraCli) Register(app *kingpin.Application, reg []CommandRegistry) { // return nil // } -// // CmdTransition will move state of the given issue to the given transtion -// func (c *Cli) CmdTransition(issue string, trans string) error { -// log.Debugf("transition called") -// uri := fmt.Sprintf("%s/rest/api/2/issue/%s/transitions?expand=transitions.fields", c.endpoint, issue) -// data, err := responseToJSON(c.get(uri)) -// if err != nil { -// return err -// } - -// transitions := data.(map[string]interface{})["transitions"].([]interface{}) -// var transID, transName string -// var transMeta map[string]interface{} -// found := make([]string, 0, len(transitions)) -// for _, transition := range transitions { -// name := transition.(map[string]interface{})["name"].(string) -// id := transition.(map[string]interface{})["id"].(string) -// found = append(found, name) -// if strings.Contains(strings.ToLower(name), strings.ToLower(trans)) { -// transName = name -// transID = id -// transMeta = transition.(map[string]interface{}) -// if strings.ToLower(name) == strings.ToLower(trans) { -// break -// } -// } -// } -// if transID == "" { -// err := fmt.Errorf("Invalid Transition '%s', Available: %s", trans, strings.Join(found, ", ")) -// log.Debugf("%s", err) -// return err -// } - -// handlePost := func(json string) error { -// uri = fmt.Sprintf("%s/rest/api/2/issue/%s/transitions", c.endpoint, issue) -// 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 == 204 { -// c.Browse(issue) -// if !c.GetOptBool("quiet", false) { -// fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue) -// } -// } 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 -// } - -// uri = fmt.Sprintf("%s/rest/api/2/issue/%s", c.endpoint, issue) -// data, err = responseToJSON(c.get(uri)) -// if err != nil { -// return err -// } -// issueData := data.(map[string]interface{}) -// issueData["meta"] = transMeta -// if c.GetOptString("defaultResolution", "") == "" { -// // .meta.fields.resolution.allowedValues -// if fields, ok := transMeta["fields"].(map[string]interface{}); ok { -// if resolution, ok := fields["resolution"].(map[string]interface{}); ok { -// if allowedValues, ok := resolution["allowedValues"].([]interface{}); ok { -// for _, allowedValueRaw := range allowedValues { -// if allowedValues, ok := allowedValueRaw.(map[string]interface{}); ok { -// if allowedValues["name"] == "Fixed" { -// c.opts["defaultResolution"] = "Fixed" -// } else if allowedValues["name"] == "Done" { -// c.opts["defaultResolution"] = "Done" -// } -// } -// } -// } -// } -// } -// } -// issueData["overrides"] = c.opts -// issueData["transition"] = map[string]interface{}{ -// "name": transName, -// "id": transID, -// } -// return c.editTemplate( -// c.getTemplate("transition"), -// fmt.Sprintf("%s-trans-%s-", issue, trans), -// issueData, -// handlePost, -// ) -// } - // // CmdComment will open up editor with "comment" template and submit // // YAML output to jira // func (c *Cli) CmdComment(issue string) error { diff --git a/jiracli/vote.go b/jiracli/vote.go new file mode 100644 index 00000000..a854024a --- /dev/null +++ b/jiracli/vote.go @@ -0,0 +1,66 @@ +package jiracli + +import ( + "fmt" + + kingpin "gopkg.in/alecthomas/kingpin.v2" +) + +type VoteAction int + +const ( + VoteUP VoteAction = iota + VoteDown +) + +type VoteOptions struct { + GlobalOptions + Issue string + Action VoteAction +} + +func (jc *JiraCli) CmdVoteRegistry() *CommandRegistryEntry { + opts := VoteOptions{ + GlobalOptions: GlobalOptions{}, + Action: VoteUP, + } + + return &CommandRegistryEntry{ + "Vote up/down an issue", + func() error { + return jc.CmdVote(&opts) + }, + func(cmd *kingpin.CmdClause) error { + return jc.CmdVoteUsage(cmd, &opts) + }, + } +} + +func (jc *JiraCli) CmdVoteUsage(cmd *kingpin.CmdClause, opts *VoteOptions) error { + if err := jc.GlobalUsage(cmd, &opts.GlobalOptions); err != nil { + return err + } + cmd.Flag("down", "downvote the issue").Short('d').PreAction(func(ctx *kingpin.ParseContext) error { + opts.Action = VoteDown + return nil + }).Bool() + cmd.Arg("ISSUE", "issue id to vote").StringVar(&opts.Issue) + return nil +} + +// Vote will up/down vote an issue +func (jc *JiraCli) CmdVote(opts *VoteOptions) error { + if opts.Action == VoteUP { + if err := jc.IssueAddVote(opts.Issue); err != nil { + return err + } + } else { + if err := jc.IssueRemoveVote(opts.Issue); err != nil { + return err + } + } + fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, jc.Endpoint, opts.Issue) + + // FIXME implement browse + return nil +}