commit d2fa05283082197455aecfaa93287d441bb0c452
parent 7dcedec32d126c09b98d35f104bf239f4105ea9f
Author: Russ Cox <rsc@golang.org>
Date: Sat, 25 Apr 2015 15:41:29 -0400
issue: add -json, -token flags
Diffstat:
2 files changed, 345 insertions(+), 198 deletions(-)
diff --git a/issue/edit.go b/issue/edit.go
@@ -73,8 +73,8 @@ func runEditor(filename string) error {
// The magic list of characters and the idea of running
// sh -c this way is taken from git/run-command.c.
var cmd *exec.Cmd
- if strings.Contains(ed, "|&;<>()$`\\\"' \t\n*?[#~=%") {
- cmd = exec.Command("sh", "-c", ed+` "$@"`, filename)
+ if strings.ContainsAny(ed, "|&;<>()$`\\\"' \t\n*?[#~=%") {
+ cmd = exec.Command("sh", "-c", ed+` "$@"`, "$EDITOR", filename)
} else {
cmd = exec.Command(ed, filename)
}
@@ -319,7 +319,7 @@ func findMilestone(w io.Writer, name *string) *int {
}
}
- fmt.Fprintf(w, "Ignoring unknown milestone: %s\n", *name)
+ fmt.Fprintf(w, "Unknown milestone: %s\n", *name)
return nil
}
diff --git a/issue/issue.go b/issue/issue.go
@@ -2,203 +2,234 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// Issue is a client for reading and updating issues in a GitHub project issue tracker.
-//
-// usage: issue [-a] [-e] [-p owner/repo] <query>
-//
-// Issue runs the query against the given project's issue tracker and
-// prints a table of matching issues, sorted by issue summary.
-// The default owner/repo is golang/go.
-//
-// If multiple arguments are given as the query, issue joins them by
-// spaces to form a single issue search. These two commands are equivalent:
-//
-// issue assignee:rsc author:robpike
-// issue "assignee:rsc author:robpike"
-//
-// Searches are always limited to open issues.
-//
-// If the query is a single number, issue prints that issue in detail,
-// including all comments.
-//
-// Authentication
-//
-// Issue expects to find a GitHub "personal access token" in
-// $HOME/.github-issue-token and will use that token to authenticate
-// to GitHub when reading or writing issue data.
-// A token can be created by visiting https://github.com/settings/tokens/new.
-// The token only needs the 'repo' scope checkbox, and optionally 'private_repo'
-// if you want to work with issue trackers for private repositories.
-// It does not need any other permissions.
-//
-// Acme Editor Integration
-//
-// If the -a flag is specified, issue runs as a collection of acme windows
-// instead of a command-line tool. In this mode, the query is optional.
-// If no query is given, issue uses "state:open".
-//
-// There are three kinds of acme windows: issue, issue creation, issue list,
-// search result, and milestone list.
-//
-// The following text forms can be looked for (right clicked on)
-// and open a window (or navigate to an existing one).
-//
-// nnnn issue #nnnn
-// #nnnn issue #nnnn
-// all the issue list
-// milestone(s) the milestone list
-// <milestone-name> the named milestone (e.g., Go1.5)
-//
-// Executing "New" opens an issue creation window.
-//
-// Executing "Search <query>" opens a new window showing the
-// results of that search.
-//
-// Issue Window
-//
-// An issue window, opened by loading an issue number,
-// displays full detail about an issue, a header followed by each comment.
-// For example:
-//
-// Title: time: Duration should implement fmt.Formatter
-// State: closed
-// Assignee: robpike
-// Closed: 2015-01-08 05:20:00
-// Labels: release-none repo-main size-m
-// Milestone:
-// URL: https://github.com/golang/go/issues/8786
-//
-// Reported by dsymonds (2014-09-21 23:02:50)
-//
-// It'd be nice if http://play.golang.org/p/KCnUQOPyol
-// printed "[+3us]", which would require time.Duration
-// implementing fmt.Formatter to get the '+' flag.
-//
-// Comment by rsc (2015-01-08 05:17:06)
-//
-// time must not depend on fmt.
-//
-// Executing "Get" reloads the issue data.
-//
-// Executing "Put" updates an issue. It saves any changes to the issue header
-// and, if any text has been entered between the header and the "Reported by" line,
-// posts that text as a new comment. If both succeed, Put then reloads the issue data.
-// The "Closed" and "URL" headers cannot be changed.
-//
-// Issue Creation Window
-//
-// An issue creation window, opened by executing "New", is like an issue window
-// but displays only an empty issue template:
-//
-// Title:
-// Assignee:
-// Labels:
-// Milestone:
-//
-// <describe issue here>
-//
-// Once the template has been completed (only the title is required), executing "Put"
-// creates the issue and converts the window into a issue window for the new issue.
-//
-// Issue List Window
-//
-// An issue list window displays a list of all open issue numbers and titles.
-// If the project has any open milestones, they are listed in a header line.
-// For example:
-//
-// Milestones: Go1.4.1 Go1.5 Go1.5Maybe
-//
-// 9027 archive/tar: round-trip of Header misses values
-// 8669 archive/zip: not possible to a start writing zip at offset other than zero
-// 8359 archive/zip: not possible to specify deflate compression level
-// ...
-//
-// As in any window, right clicking on an issue number opens a window for that issue.
-//
-// Search Result Window
-//
-// A search result window, opened by executing "Search <query>", displays a list of issues
-// matching a search query. It shows the query in a header line. For example:
-//
-// Search author:rsc
-//
-// 9131 bench: no documentation
-// 599 cmd/5c, 5g, 8c, 8g: make 64-bit fields 64-bit aligned
-// 6699 cmd/5l: use m to store div/mod denominator
-// 4997 cmd/6a, cmd/8a: MOVL $x-8(SP) and LEAL x-8(SP) are different
-// ...
-//
-// Executing "Sort" in a search result window toggles between sorting by title
-// and sorting by decreasing issue number.
-//
-// Bulk Edit Window
-//
-// Executing "Bulk" in an issue list or search result window opens a new
-// bulk edit window applying to the displayed issues. If there is a non-empty
-// text selection in the issue list or search result list, the bulk edit window
-// is restricted to issues in the selection.
-//
-// The bulk edit window consists of a metadata header followed by a list of issues, like:
-//
-// State: open
-// Assignee:
-// Labels:
-// Milestone: Go1.4.3
-//
-// 10219 cmd/gc: internal compiler error: agen: unknown op
-// 9711 net/http: Testing timeout on Go1.4.1
-// 9576 runtime: crash in checkdead
-// 9954 runtime: invalid heap pointer found in bss on openbsd/386
-//
-// The metadata header shows only metadata shared by all the issues.
-// In the above example, all four issues are open and have milestone Go1.4.3,
-// but they have no common labels nor a common assignee.
-//
-// The bulk edit applies to the issues listed in the window text; adding or removing
-// issue lines changes the set of issues affected by Get or Put operations.
-//
-// Executing "Get" refreshes the metadata header and issue summaries.
-//
-// Executing "Put" updates all the listed issues. It applies any changes made to
-// the metadata header and, if any text has been entered between the header
-// and the first issue line, posts that text as a comment. If all operations succeed,
-// Put then refreshes the window as Get does.
-//
-// Milestone List Window
-//
-// The milestone list window, opened by loading any of the names
-// "milestone", "Milestone", or "Milestones", displays the open project
-// milestones, sorted by due date, along with the number of open issues in each.
-// For example:
-//
-// 2015-01-15 Go1.4.1 1
-// 2015-07-31 Go1.5 215
-// 2015-07-31 Go1.5Maybe 5
-//
-// Loading one of the listed milestone names opens a search for issues
-// in that milestone.
-//
-// Alternate Editor Integration
-//
-// The -e flag enables basic editing of issues with editors other than acme.
-// The editor invoked is $VISUAL if set, $EDITOR if set, or else ed.
-// Issue prepares a textual representation of issue data in a temporary file,
-// opens that file in the editor, waits for the editor to exit, and then applies any
-// changes from the file to the actual issues.
-//
-// When <query> is a single number, issue -e edits a single issue.
-// See the ``Issue Window'' section above.
-//
-// If the <query> is the text "new", issue -e creates a new issue.
-// See the ``Issue Creation Window'' section above.
-//
-// Otherwise, for general queries, issue -e edits multiple issues in bulk.
-// See the ``Bulk Edit Window'' section above.
-//
+/*
+Issue is a client for reading and updating issues in a GitHub project issue tracker.
+
+ usage: issue [-a] [-e] [-p owner/repo] <query>
+
+Issue runs the query against the given project's issue tracker and
+prints a table of matching issues, sorted by issue summary.
+The default owner/repo is golang/go.
+
+If multiple arguments are given as the query, issue joins them by
+spaces to form a single issue search. These two commands are equivalent:
+
+ issue assignee:rsc author:robpike
+ issue "assignee:rsc author:robpike"
+
+Searches are always limited to open issues.
+
+If the query is a single number, issue prints that issue in detail,
+including all comments.
+
+Authentication
+
+Issue expects to find a GitHub "personal access token" in
+$HOME/.github-issue-token and will use that token to authenticate
+to GitHub when reading or writing issue data.
+A token can be created by visiting https://github.com/settings/tokens/new.
+The token only needs the 'repo' scope checkbox, and optionally 'private_repo'
+if you want to work with issue trackers for private repositories.
+It does not need any other permissions.
+The -token flag specifies an alternate file from which to read the token.
+
+Acme Editor Integration
+
+If the -a flag is specified, issue runs as a collection of acme windows
+instead of a command-line tool. In this mode, the query is optional.
+If no query is given, issue uses "state:open".
+
+There are three kinds of acme windows: issue, issue creation, issue list,
+search result, and milestone list.
+
+The following text forms can be looked for (right clicked on)
+and open a window (or navigate to an existing one).
+
+ nnnn issue #nnnn
+ #nnnn issue #nnnn
+ all the issue list
+ milestone(s) the milestone list
+ <milestone-name> the named milestone (e.g., Go1.5)
+
+Executing "New" opens an issue creation window.
+
+Executing "Search <query>" opens a new window showing the
+results of that search.
+
+Issue Window
+
+An issue window, opened by loading an issue number,
+displays full detail about an issue, a header followed by each comment.
+For example:
+
+ Title: time: Duration should implement fmt.Formatter
+ State: closed
+ Assignee: robpike
+ Closed: 2015-01-08 05:20:00
+ Labels: release-none repo-main size-m
+ Milestone:
+ URL: https://github.com/golang/go/issues/8786
+
+ Reported by dsymonds (2014-09-21 23:02:50)
+
+ It'd be nice if http://play.golang.org/p/KCnUQOPyol
+ printed "[+3us]", which would require time.Duration
+ implementing fmt.Formatter to get the '+' flag.
+
+ Comment by rsc (2015-01-08 05:17:06)
+
+ time must not depend on fmt.
+
+Executing "Get" reloads the issue data.
+
+Executing "Put" updates an issue. It saves any changes to the issue header
+and, if any text has been entered between the header and the "Reported by" line,
+posts that text as a new comment. If both succeed, Put then reloads the issue data.
+The "Closed" and "URL" headers cannot be changed.
+
+Issue Creation Window
+
+An issue creation window, opened by executing "New", is like an issue window
+but displays only an empty issue template:
+
+ Title:
+ Assignee:
+ Labels:
+ Milestone:
+
+ <describe issue here>
+
+Once the template has been completed (only the title is required), executing "Put"
+creates the issue and converts the window into a issue window for the new issue.
+
+Issue List Window
+
+An issue list window displays a list of all open issue numbers and titles.
+If the project has any open milestones, they are listed in a header line.
+For example:
+
+ Milestones: Go1.4.1 Go1.5 Go1.5Maybe
+
+ 9027 archive/tar: round-trip of Header misses values
+ 8669 archive/zip: not possible to a start writing zip at offset other than zero
+ 8359 archive/zip: not possible to specify deflate compression level
+ ...
+
+As in any window, right clicking on an issue number opens a window for that issue.
+
+Search Result Window
+
+A search result window, opened by executing "Search <query>", displays a list of issues
+matching a search query. It shows the query in a header line. For example:
+
+ Search author:rsc
+
+ 9131 bench: no documentation
+ 599 cmd/5c, 5g, 8c, 8g: make 64-bit fields 64-bit aligned
+ 6699 cmd/5l: use m to store div/mod denominator
+ 4997 cmd/6a, cmd/8a: MOVL $x-8(SP) and LEAL x-8(SP) are different
+ ...
+
+Executing "Sort" in a search result window toggles between sorting by title
+and sorting by decreasing issue number.
+
+Bulk Edit Window
+
+Executing "Bulk" in an issue list or search result window opens a new
+bulk edit window applying to the displayed issues. If there is a non-empty
+text selection in the issue list or search result list, the bulk edit window
+is restricted to issues in the selection.
+
+The bulk edit window consists of a metadata header followed by a list of issues, like:
+
+ State: open
+ Assignee:
+ Labels:
+ Milestone: Go1.4.3
+
+ 10219 cmd/gc: internal compiler error: agen: unknown op
+ 9711 net/http: Testing timeout on Go1.4.1
+ 9576 runtime: crash in checkdead
+ 9954 runtime: invalid heap pointer found in bss on openbsd/386
+
+The metadata header shows only metadata shared by all the issues.
+In the above example, all four issues are open and have milestone Go1.4.3,
+but they have no common labels nor a common assignee.
+
+The bulk edit applies to the issues listed in the window text; adding or removing
+issue lines changes the set of issues affected by Get or Put operations.
+
+Executing "Get" refreshes the metadata header and issue summaries.
+
+Executing "Put" updates all the listed issues. It applies any changes made to
+the metadata header and, if any text has been entered between the header
+and the first issue line, posts that text as a comment. If all operations succeed,
+Put then refreshes the window as Get does.
+
+Milestone List Window
+
+The milestone list window, opened by loading any of the names
+"milestone", "Milestone", or "Milestones", displays the open project
+milestones, sorted by due date, along with the number of open issues in each.
+For example:
+
+ 2015-01-15 Go1.4.1 1
+ 2015-07-31 Go1.5 215
+ 2015-07-31 Go1.5Maybe 5
+
+Loading one of the listed milestone names opens a search for issues
+in that milestone.
+
+Alternate Editor Integration
+
+The -e flag enables basic editing of issues with editors other than acme.
+The editor invoked is $VISUAL if set, $EDITOR if set, or else ed.
+Issue prepares a textual representation of issue data in a temporary file,
+opens that file in the editor, waits for the editor to exit, and then applies any
+changes from the file to the actual issues.
+
+When <query> is a single number, issue -e edits a single issue.
+See the ``Issue Window'' section above.
+
+If the <query> is the text "new", issue -e creates a new issue.
+See the ``Issue Creation Window'' section above.
+
+Otherwise, for general queries, issue -e edits multiple issues in bulk.
+See the ``Bulk Edit Window'' section above.
+
+JSON Output
+
+The -json flag causes issue to print the results in JSON format
+using these data structures:
+
+ type Issue struct {
+ Title string
+ State string
+ Assignee string
+ Closed time.Time
+ Labels []string
+ Milestone string
+ URL string
+ Reporter string
+ Created time.Time
+ Text string
+ Comments []*Comment
+ }
+
+ type Comment struct {
+ Author string
+ Time time.Time
+ Text string
+ }
+
+If asked for a specific issue, the output is an Issue with Comments.
+Otherwise, the result is an array of Issues without Comments.
+*/
package main // import "rsc.io/github/issue"
import (
"bytes"
+ "encoding/json"
"flag"
"fmt"
"io"
@@ -220,7 +251,9 @@ import (
var (
acmeFlag = flag.Bool("a", false, "open in new acme window")
editFlag = flag.Bool("e", false, "edit in system editor")
+ jsonFlag = flag.Bool("json", false, "write JSON output")
project = flag.String("p", "golang/go", "GitHub owner/repo name")
+ tokenFile = flag.String("token", "", "read GitHub token personal access token from `file` (default $HOME/.github-issue-token)")
projectOwner = ""
projectRepo = ""
)
@@ -231,6 +264,7 @@ func usage() {
If query is a single number, prints the full history for the issue.
Otherwise, prints a table of matching results.
`)
+ flag.PrintDefaults()
os.Exit(2)
}
@@ -244,6 +278,13 @@ func main() {
usage()
}
+ if *jsonFlag && *acmeFlag {
+ log.Fatal("cannot use -a with -json")
+ }
+ if *jsonFlag && *editFlag {
+ log.Fatal("cannot use -e with -acme")
+ }
+
f := strings.Split(*project, "/")
if len(f) != 2 {
log.Fatal("invalid form for -p argument: must be owner/repo, like golang/go")
@@ -311,6 +352,11 @@ func showIssue(w io.Writer, n int) (*github.Issue, error) {
const timeFormat = "2006-01-02 15:04:05"
func printIssue(w io.Writer, issue *github.Issue) error {
+ if *jsonFlag {
+ showJSONIssue(w, issue)
+ return nil
+ }
+
fmt.Fprintf(w, "Title: %s\n", getString(issue.Title))
fmt.Fprintf(w, "State: %s\n", getString(issue.State))
fmt.Fprintf(w, "Assignee: %s\n", getUserLogin(issue.Assignee))
@@ -362,6 +408,10 @@ func showQuery(w io.Writer, q string) error {
return err
}
sort.Sort(issuesByTitle(all))
+ if *jsonFlag {
+ showJSONList(all)
+ return nil
+ }
for _, issue := range all {
fmt.Fprintf(w, "%v\t%v\n", getInt(issue.Number), getString(issue.Title))
}
@@ -559,11 +609,16 @@ var authToken string
func loadAuth() {
const short = ".github-issue-token"
filename := filepath.Clean(os.Getenv("HOME") + "/" + short)
+ shortFilename := filepath.Clean("$HOME/" + short)
+ if *tokenFile != "" {
+ filename = *tokenFile
+ shortFilename = *tokenFile
+ }
data, err := ioutil.ReadFile(filename)
if err != nil {
log.Fatal("reading token: ", err, "\n\n"+
"Please create a personal access token at https://github.com/settings/tokens/new\n"+
- "and write it to ", filepath.Clean("$HOME/"+short), " to use this program.\n"+
+ "and write it to ", shortFilename, " to use this program.\n"+
"The token only needs the repo scope, or private_repo if you want to\n"+
"view or edit issues for private repositories.\n"+
"The benefit of using a personal access token over using your GitHub\n"+
@@ -571,7 +626,7 @@ func loadAuth() {
}
fi, err := os.Stat(filename)
if fi.Mode()&0077 != 0 {
- log.Fatalf("reading token: %s mode is %#o, want %#o", filepath.Clean("$HOME/"+short), fi.Mode()&0777, fi.Mode()&0700)
+ log.Fatalf("reading token: %s mode is %#o, want %#o", shortFilename, fi.Mode()&0777, fi.Mode()&0700)
}
authToken = strings.TrimSpace(string(data))
t := &oauth2.Transport{
@@ -674,3 +729,95 @@ func bulkReadIssuesCached(ids []int) ([]*github.Issue, error) {
}
return all, err
}
+
+// JSON output
+// If you make changes to the structs, copy them back into the doc comment.
+
+type Issue struct {
+ Title string
+ State string
+ Assignee string
+ Closed time.Time
+ Labels []string
+ Milestone string
+ URL string
+ Reporter string
+ Created time.Time
+ Text string
+ Comments []*Comment
+}
+
+type Comment struct {
+ Author string
+ Time time.Time
+ Text string
+}
+
+func showJSONIssue(w io.Writer, issue *github.Issue) {
+ data, err := json.MarshalIndent(toJSONWithComments(issue), "", "\t")
+ if err != nil {
+ log.Fatal(err)
+ }
+ data = append(data, '\n')
+ w.Write(data)
+}
+
+func showJSONList(all []*github.Issue) {
+ j := []*Issue{} // non-nil for json
+ for _, issue := range all {
+ j = append(j, toJSON(issue))
+ }
+ data, err := json.MarshalIndent(j, "", "\t")
+ if err != nil {
+ log.Fatal(err)
+ }
+ data = append(data, '\n')
+ os.Stdout.Write(data)
+}
+
+func toJSON(issue *github.Issue) *Issue {
+ j := &Issue{
+ Title: getString(issue.Title),
+ State: getString(issue.State),
+ Assignee: getUserLogin(issue.Assignee),
+ Closed: getTime(issue.ClosedAt),
+ Labels: getLabelNames(issue.Labels),
+ Milestone: getMilestoneTitle(issue.Milestone),
+ URL: fmt.Sprintf("https://github.com/%s/%s/issues/%d\n", projectOwner, projectRepo, getInt(issue.Number)),
+ Reporter: getUserLogin(issue.User),
+ Created: getTime(issue.CreatedAt),
+ Text: getString(issue.Body),
+ Comments: []*Comment{},
+ }
+ if j.Labels == nil {
+ j.Labels = []string{}
+ }
+ return j
+}
+
+func toJSONWithComments(issue *github.Issue) *Issue {
+ j := toJSON(issue)
+ for page := 1; ; {
+ list, resp, err := client.Issues.ListComments(projectOwner, projectRepo, getInt(issue.Number), &github.IssueListCommentsOptions{
+ ListOptions: github.ListOptions{
+ Page: page,
+ PerPage: 100,
+ },
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ for _, com := range list {
+ j.Comments = append(j.Comments, &Comment{
+ Author: getUserLogin(com.User),
+ Time: getTime(com.CreatedAt),
+ Text: getString(com.Body),
+ })
+ }
+ if resp.NextPage < page {
+ break
+ }
+ page = resp.NextPage
+ }
+ return j
+}