From c95e66e08104887659665437467d5d41fceeae0d Mon Sep 17 00:00:00 2001 From: Mike Pountney Date: Wed, 13 Jan 2016 21:41:34 +0000 Subject: [PATCH 1/3] Add 'vote' and 'unvote' This adds support for voting on issues via CmdVote() and CmdUnvote() Voting on issues is always done as the logged in user, it appears you can't case a vote for another user: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-addVote This required adding a cli.delete() handler, naturally with no content (as per RFC2616) This is ripe for DRY-ing out, but I will leave that for a future PR. Worth noting is that you cannot vote for your own issues, this results in: 2016-01-13T21:35:41.315Z ERROR [cli.go:184] response status: 404 Not Found 2016-01-13T21:35:41.315Z ERROR [commands.go:439] Unexpected Response From POST: {snip} {"errorMessages":["You cannot vote for an issue you have reported."],"errors":{}} --- cli.go | 18 +++++++++++++++++ commands.go | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++ main/main.go | 8 ++++++++ 3 files changed, 82 insertions(+) diff --git a/cli.go b/cli.go index 54d0fb7..1916449 100644 --- a/cli.go +++ b/cli.go @@ -106,6 +106,24 @@ func (c *Cli) put(uri string, content string) (*http.Response, error) { return c.makeRequestWithContent("PUT", uri, content) } +func (c *Cli) delete(uri string) (*http.Response, error) { + method := "DELETE" + req, _ := http.NewRequest(method, uri, nil) + log.Info("%s %s", req.Method, req.URL.String()) + if resp, err := c.makeRequest(req); err != nil { + return nil, err + } else { + if resp.StatusCode == 401 { + if err := c.CmdLogin(); err != nil { + return nil, err + } + req, _ = http.NewRequest(method, uri, nil) + return c.makeRequest(req) + } + return resp, err + } +} + func (c *Cli) makeRequestWithContent(method string, uri string, content string) (*http.Response, error) { buffer := bytes.NewBufferString(content) req, _ := http.NewRequest(method, uri, buffer) diff --git a/commands.go b/commands.go index 5a85581..6104916 100644 --- a/commands.go +++ b/commands.go @@ -414,6 +414,62 @@ func (c *Cli) CmdWatch(issue string) error { return nil } +func (c *Cli) CmdVote(issue string) error { + log.Debug("vote called") + + uri := fmt.Sprintf("%s/rest/api/2/issue/%s/votes", c.endpoint, issue) + if c.getOptBool("dryrun", false) { + log.Debug("POST: %s", "") + log.Debug("Dryrun mode, skipping POST") + return nil + } + resp, err := c.post(uri, "") + if err != nil { + return err + } + if resp.StatusCode == 204 { + c.Browse(issue) + if !c.opts["quiet"].(bool) { + 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.Error("%s:\n%s", err, logBuffer) + return err + } + return nil +} + +func (c *Cli) CmdUnvote(issue string) error { + log.Debug("unvote called") + + uri := fmt.Sprintf("%s/rest/api/2/issue/%s/votes", c.endpoint, issue) + if c.getOptBool("dryrun", false) { + log.Debug("DELETE: %s", "") + log.Debug("Dryrun mode, skipping DELETE") + return nil + } + resp, err := c.delete(uri) + if err != nil { + return err + } + if resp.StatusCode == 204 { + c.Browse(issue) + if !c.opts["quiet"].(bool) { + 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 DELETE") + log.Error("%s:\n%s", err, logBuffer) + return err + } + return nil +} + func (c *Cli) CmdTransition(issue string, trans string) error { log.Debug("transition called") uri := fmt.Sprintf("%s/rest/api/2/issue/%s/transitions?expand=transitions.fields", c.endpoint, issue) diff --git a/main/main.go b/main/main.go index 8d446d0..4f6f282 100644 --- a/main/main.go +++ b/main/main.go @@ -151,6 +151,8 @@ Command Options: "login": "login", "req": "request", "request": "request", + "vote": "vote", + "unvote": "unvote", } defaults := map[string]interface{}{ @@ -392,6 +394,12 @@ Command Options: case "view": requireArgs(1) err = c.CmdView(args[0]) + case "vote": + requireArgs(1) + err = c.CmdVote(args[0]) + case "unvote": + requireArgs(1) + err = c.CmdUnvote(args[0]) case "request": requireArgs(1) data := "" From 797edefc3fb433678b600ed888dceef560a8f146 Mon Sep 17 00:00:00 2001 From: Mike Pountney Date: Sun, 24 Jan 2016 02:23:08 +0000 Subject: [PATCH 2/3] Amend vote/unvote to be vote/vote --down This simplifies the interface to voting, as per the discussion in https://github.com/Netflix-Skunkworks/go-jira/pull/26 Basically, DRY out the logic into a single CmdVote function based around an 'up' bool. Similarly, make a single CLI command with an option to do the downvote. --- commands.go | 53 ++++++++++++++++++++-------------------------------- main/main.go | 12 +++++++----- 2 files changed, 27 insertions(+), 38 deletions(-) diff --git a/commands.go b/commands.go index 335f216..f628f56 100644 --- a/commands.go +++ b/commands.go @@ -419,16 +419,27 @@ func (c *Cli) CmdWatch(issue string) error { return nil } -func (c *Cli) CmdVote(issue string) error { - log.Debug("vote called") +func (c *Cli) CmdVote(issue string, up bool) error { + log.Debug("vote called, with up: %n", up) uri := fmt.Sprintf("%s/rest/api/2/issue/%s/votes", c.endpoint, issue) if c.getOptBool("dryrun", false) { - log.Debug("POST: %s", "") - log.Debug("Dryrun mode, skipping POST") + if up { + log.Debug("POST: %s", "") + log.Debug("Dryrun mode, skipping POST") + } else { + log.Debug("DELETE: %s", "") + log.Debug("Dryrun mode, skipping DELETE") + } return nil } - resp, err := c.post(uri, "") + 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 } @@ -440,35 +451,11 @@ func (c *Cli) CmdVote(issue string) error { } else { logBuffer := bytes.NewBuffer(make([]byte, 0)) resp.Write(logBuffer) - err := fmt.Errorf("Unexpected Response From POST") - log.Error("%s:\n%s", err, logBuffer) - return err - } - return nil -} - -func (c *Cli) CmdUnvote(issue string) error { - log.Debug("unvote called") - - uri := fmt.Sprintf("%s/rest/api/2/issue/%s/votes", c.endpoint, issue) - if c.getOptBool("dryrun", false) { - log.Debug("DELETE: %s", "") - log.Debug("Dryrun mode, skipping DELETE") - return nil - } - resp, err := c.delete(uri) - if err != nil { - return err - } - if resp.StatusCode == 204 { - c.Browse(issue) - if !c.opts["quiet"].(bool) { - fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue) + if up { + err = fmt.Errorf("Unexpected Response From POST") + } else { + err = fmt.Errorf("Unexpected Response From DELETE") } - } else { - logBuffer := bytes.NewBuffer(make([]byte, 0)) - resp.Write(logBuffer) - err := fmt.Errorf("Unexpected Response From DELETE") log.Error("%s:\n%s", err, logBuffer) return err } diff --git a/main/main.go b/main/main.go index a724c09..ff1f27b 100644 --- a/main/main.go +++ b/main/main.go @@ -57,6 +57,7 @@ Usage: jira DUPLICATE dups ISSUE jira BLOCKER blocks ISSUE jira watch ISSUE [-w WATCHER] + jira vote ISSUE [--down] jira (trans|transition) TRANSITION ISSUE [--noedit] jira ack ISSUE [--edit] jira close ISSUE [--edit] @@ -156,7 +157,6 @@ Command Options: "req": "request", "request": "request", "vote": "vote", - "unvote": "unvote", } defaults := map[string]interface{}{ @@ -208,6 +208,7 @@ Command Options: "M|method=s": setopt, "S|saveFile=s": setopt, "Q|quiet": setopt, + "down": setopt, }) if err := op.ProcessAll(os.Args[1:]); err != nil { @@ -408,10 +409,11 @@ Command Options: err = c.CmdView(args[0]) case "vote": requireArgs(1) - err = c.CmdVote(args[0]) - case "unvote": - requireArgs(1) - err = c.CmdUnvote(args[0]) + 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 := "" From 383847a9d66bcbc539b385c9e4ada44e8558dff1 Mon Sep 17 00:00:00 2001 From: Mike Pountney Date: Sun, 24 Jan 2016 02:56:36 +0000 Subject: [PATCH 3/3] Tweak the CmdWatch contract and add watcher remove support 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) --- cli.go | 8 ++++++++ commands.go | 32 ++++++++++++++++++++++++-------- main/main.go | 7 +++++-- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/cli.go b/cli.go index b4686ad..6d8baad 100644 --- a/cli.go +++ b/cli.go @@ -474,6 +474,10 @@ func (c *Cli) FindIssues() (interface{}, error) { } } +func (c *Cli) GetOptString(optName string, dflt string) string { + return c.getOptString(optName, dflt) +} + func (c *Cli) getOptString(optName string, dflt string) string { if val, ok := c.opts[optName].(string); ok { return val @@ -482,6 +486,10 @@ func (c *Cli) getOptString(optName string, dflt string) string { } } +func (c *Cli) GetOptBool(optName string, dflt bool) bool { + return c.getOptBool(optName, dflt) +} + func (c *Cli) getOptBool(optName string, dflt bool) bool { if val, ok := c.opts[optName].(bool); ok { return val diff --git a/commands.go b/commands.go index f628f56..589f3b6 100644 --- a/commands.go +++ b/commands.go @@ -385,22 +385,34 @@ func (c *Cli) CmdDups(duplicate string, issue string) error { return nil } -func (c *Cli) CmdWatch(issue string) error { - watcher := c.getOptString("watcher", c.opts["user"].(string)) - log.Debug("watch called") +func (c *Cli) CmdWatch(issue string, watcher string, remove bool) error { + log.Debug("watch called: watcher: %q, remove: %n", watcher, remove) + var uri string json, err := jsonEncode(watcher) if err != nil { return err } - uri := fmt.Sprintf("%s/rest/api/2/issue/%s/watchers", c.endpoint, issue) if c.getOptBool("dryrun", false) { - log.Debug("POST: %s", json) - log.Debug("Dryrun mode, skipping POST") + if !remove { + log.Debug("POST: %s", json) + log.Debug("Dryrun mode, skipping POST") + } else { + log.Debug("DELETE: %s", watcher) + log.Debug("Dryrun mode, skipping POST") + } return nil } - resp, err := c.post(uri, json) + + var resp *http.Response + if !remove { + uri = fmt.Sprintf("%s/rest/api/2/issue/%s/watchers", c.endpoint, issue) + resp, err = c.post(uri, json) + } else { + uri = fmt.Sprintf("%s/rest/api/2/issue/%s/watchers?username=%s", c.endpoint, issue, watcher) + resp, err = c.delete(uri) + } if err != nil { return err } @@ -412,7 +424,11 @@ func (c *Cli) CmdWatch(issue string) error { } else { logBuffer := bytes.NewBuffer(make([]byte, 0)) resp.Write(logBuffer) - err := fmt.Errorf("Unexpected Response From POST") + if !remove { + err = fmt.Errorf("Unexpected Response From POST") + } else { + err = fmt.Errorf("Unexpected Response From DELETE") + } log.Error("%s:\n%s", err, logBuffer) return err } diff --git a/main/main.go b/main/main.go index ff1f27b..f58979c 100644 --- a/main/main.go +++ b/main/main.go @@ -56,8 +56,8 @@ Usage: jira create [--noedit] [-p PROJECT] jira DUPLICATE dups ISSUE jira BLOCKER blocks ISSUE - jira watch ISSUE [-w WATCHER] jira vote ISSUE [--down] + jira watch ISSUE [-w WATCHER] [--remove] jira (trans|transition) TRANSITION ISSUE [--noedit] jira ack ISSUE [--edit] jira close ISSUE [--edit] @@ -196,6 +196,7 @@ Command Options: "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, @@ -353,7 +354,9 @@ Command Options: } case "watch": requireArgs(1) - err = c.CmdWatch(args[0]) + 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)