Reimplement `hub issues` commands using simpleApi

- `hub issue` now displays color based on issue open/closed state
- `hub issue` no longer displays HTML URL since it was noisy
- `hub issue` no longer lists pull requests
- `hub issue create` tweaks for compatibility with `hub release create`
This commit is contained in:
Mislav Marohnić 2016-02-03 23:01:13 +11:00
Родитель f412a353b8
Коммит 8563e678d2
4 изменённых файлов: 229 добавлений и 106 удалений

Просмотреть файл

@ -2,8 +2,11 @@ package commands
import (
"fmt"
"os"
"strconv"
"strings"
"github.com/github/hub/git"
"github.com/github/hub/github"
"github.com/github/hub/ui"
"github.com/github/hub/utils"
@ -11,27 +14,24 @@ import (
var (
cmdIssue = &Command{
Run: issue,
Usage: "issue [-a <ASSIGNEE>]",
Long: `List open issues for the current GitHub project.
Run: listIssues,
Usage: `
issue [-a <ASSIGNEE>]
issue create [-m <MESSAGE>|-F <FILE>] [-l <LABELS>]
`,
Long: `Manage GitHub issues for the current project.
## Options:
-a, --assignee <ASSIGNEE>
Display only issues assigned to <ASSIGNEE>
`,
}
Display only issues assigned to <ASSIGNEE>.
cmdCreateIssue = &Command{
Key: "create",
Run: createIssue,
Usage: "issue create [-m <MESSAGE>|-f <FILE>] [-l <LABELS>]",
Long: `File an issue for the current GitHub project.
-s, --state <STATE>
Display issues with state <STATE> (default: "open").
## Options:
-m, --message <MESSAGE>
Use the first line of <MESSAGE> as issue title, and the rest as issue description.
-f, --file <FILE>
-F, --file <FILE>
Read the issue title and description from <FILE>.
-l, --labels <LABELS>
@ -39,7 +39,15 @@ var (
`,
}
cmdCreateIssue = &Command{
Key: "create",
Run: createIssue,
Usage: "issue create [-m <MESSAGE>|-f <FILE>] [-l <LABELS>]",
Long: "File an issue for the current GitHub project.",
}
flagIssueAssignee,
flagIssueState,
flagIssueMessage,
flagIssueFile string
@ -48,80 +56,127 @@ var (
func init() {
cmdCreateIssue.Flag.StringVarP(&flagIssueMessage, "message", "m", "", "MESSAGE")
cmdCreateIssue.Flag.StringVarP(&flagIssueFile, "file", "f", "", "FILE")
cmdCreateIssue.Flag.StringVarP(&flagIssueFile, "file", "F", "", "FILE")
cmdCreateIssue.Flag.VarP(&flagIssueLabels, "label", "l", "LABEL")
cmdIssue.Flag.StringVarP(&flagIssueAssignee, "assignee", "a", "", "ASSIGNEE")
cmdIssue.Flag.StringVarP(&flagIssueState, "state", "s", "", "ASSIGNEE")
cmdIssue.Use(cmdCreateIssue)
CmdRunner.Use(cmdIssue)
}
/*
$ hub issue
*/
func issue(cmd *Command, args *Args) {
runInLocalRepo(func(localRepo *github.GitHubRepo, project *github.Project, gh *github.Client) {
if args.Noop {
ui.Printf("Would request list of issues for %s\n", project)
} else {
issues, err := gh.Issues(project)
utils.Check(err)
for _, issue := range issues {
var url string
// use the pull request URL if we have one
if issue.PullRequest.HTMLURL != "" {
url = issue.PullRequest.HTMLURL
} else {
url = issue.HTMLURL
}
func listIssues(cmd *Command, args *Args) {
localRepo, err := github.LocalRepo()
utils.Check(err)
if flagIssueAssignee == "" ||
strings.EqualFold(issue.Assignee.Login, flagIssueAssignee) {
// "nobody" should have more than 1 million github issues
ui.Printf("% 7d] %s ( %s )\n", issue.Number, issue.Title, url)
}
project, err := localRepo.MainProject()
utils.Check(err)
gh := github.NewClient(project.Host)
if args.Noop {
ui.Printf("Would request list of issues for %s\n", project)
} else {
filters := map[string]interface{}{}
if cmd.FlagPassed("state") {
filters["state"] = flagIssueState
}
if cmd.FlagPassed("assignee") {
filters["assignee"] = flagIssueAssignee
}
issues, err := gh.FetchIssues(project, filters)
utils.Check(err)
maxNumWidth := 0
for _, issue := range issues {
if numWidth := len(strconv.Itoa(issue.Number)); numWidth > maxNumWidth {
maxNumWidth = numWidth
}
}
})
colorize := ui.IsTerminal(os.Stdout)
for _, issue := range issues {
if issue.PullRequest != nil {
continue
}
num := fmt.Sprintf("#%d", issue.Number)
numWidth := len(num)
if colorize {
issueColor := 32
if issue.State == "closed" {
issueColor = 31
}
num = fmt.Sprintf("\033[%dm%s\033[0m", issueColor, num)
}
ui.Printf("%*s%s %s\n", maxNumWidth+1-numWidth, "", num, issue.Title)
}
}
os.Exit(0)
}
func createIssue(cmd *Command, args *Args) {
runInLocalRepo(func(localRepo *github.GitHubRepo, project *github.Project, gh *github.Client) {
if args.Noop {
ui.Printf("Would create an issue for %s\n", project)
} else {
title, body, err := getTitleAndBodyFromFlags(flagIssueMessage, flagIssueFile)
utils.Check(err)
localRepo, err := github.LocalRepo()
utils.Check(err)
if title == "" {
title, body, err = writeIssueTitleAndBody(project)
utils.Check(err)
}
project, err := localRepo.MainProject()
utils.Check(err)
issue, err := gh.CreateIssue(project, title, body, flagIssueLabels)
utils.Check(err)
gh := github.NewClient(project.Host)
ui.Println(issue.HTMLURL)
}
})
}
var title string
var body string
var editor *github.Editor
func writeIssueTitleAndBody(project *github.Project) (string, string, error) {
message := `
# Creating issue for %s.
if cmd.FlagPassed("message") {
title, body = readMsg(flagIssueMessage)
} else if cmd.FlagPassed("file") {
title, body, err = readMsgFromFile(flagIssueFile)
utils.Check(err)
} else {
cs := git.CommentChar()
message := strings.Replace(fmt.Sprintf(`
# Creating an issue for %s
#
# Write a message for this issue. The first block of
# text is the title and the rest is the description.
`
message = fmt.Sprintf(message, project.Name)
`, project), "#", cs, -1)
editor, err := github.NewEditor("ISSUE", "issue", message)
if err != nil {
return "", "", err
editor, err := github.NewEditor("ISSUE", "issue", message)
utils.Check(err)
title, body, err = editor.EditTitleAndBody()
utils.Check(err)
}
defer editor.DeleteFile()
if title == "" {
utils.Check(fmt.Errorf("Aborting creation due to empty issue title"))
}
return editor.EditTitleAndBody()
params := &github.IssueParams{
Title: title,
Body: body,
Labels: flagIssueLabels,
}
if args.Noop {
ui.Printf("Would create issue `%s' for %s\n", params.Title, project)
} else {
issue, err := gh.CreateIssue(project, params)
utils.Check(err)
if editor != nil {
editor.DeleteFile()
}
ui.Println(issue.HtmlUrl)
}
os.Exit(0)
}

Просмотреть файл

@ -7,27 +7,60 @@ Feature: hub issue
Given the GitHub API server:
"""
get('/repos/github/hub/issues') {
json([
assert :assignee => "Cornwe19"
json [
{ :number => 102,
:title => "First issue",
:html_url => "https://github.com/github/hub/issues/102",
:assignee => {
:login => "octokit"
}
:state => "open",
},
{ :number => 103,
{ :number => 13,
:title => "Second issue",
:html_url => "https://github.com/github/hub/issues/103",
:assignee => {
:login => "cornwe19"
}
}
])
:state => "open",
},
]
}
"""
When I run `hub issue -a Cornwe19`
Then the output should contain exactly:
"""
103] Second issue ( https://github.com/github/hub/issues/103 )\n
#102 First issue
#13 Second issue\n
"""
And the exit status should be 0
Scenario: Create an issue
Given the GitHub API server:
"""
post('/repos/github/hub/issues') {
assert :title => "Not workie, pls fix",
:body => "",
:labels => nil
status 201
json :html_url => "https://github.com/github/hub/issues/1337"
}
"""
When I successfully run `hub issue create -m "Not workie, pls fix"`
Then the output should contain exactly:
"""
https://github.com/github/hub/issues/1337\n
"""
Scenario: Create an issue with labels
Given the GitHub API server:
"""
post('/repos/github/hub/issues') {
assert :title => "hello",
:body => "",
:labels => ["wont fix", "docs"]
status 201
json :html_url => "https://github.com/github/hub/issues/1337"
}
"""
When I successfully run `hub issue create -m "hello" -l "wont fix,docs"`
Then the output should contain exactly:
"""
https://github.com/github/hub/issues/1337\n
"""

Просмотреть файл

@ -420,50 +420,79 @@ func (client *Client) ForkRepository(project *Project) (repo *octokit.Repository
return
}
func (client *Client) Issues(project *Project) (issues []octokit.Issue, err error) {
url, err := octokit.RepoIssuesURL.Expand(octokit.M{"owner": project.Owner, "repo": project.Name})
type IssueParams struct {
Title string `json:"title"`
Body string `json:"body"`
Labels []string `json:"labels"`
}
type Issue struct {
Number int `json:"number"`
State string `json:"state"`
Title string `json:"title"`
Body string `json:"body"`
User *User `json:"user"`
Assignee *User `json:"assignee"`
Labels []IssueLabel `json:"labels"`
PullRequest *PullRequest `json:"pull_request"`
HtmlUrl string `json:"html_url"`
}
type IssueLabel struct {
Name string `json:"name"`
Color string `json:"color"`
}
type User struct {
Login string `json:"login"`
}
type PullRequest struct {
ApiUrl string `json:"url"`
HtmlUrl string `json:"html_url"`
}
func (client *Client) FetchIssues(project *Project, filterParams map[string]interface{}) (issues []Issue, err error) {
api, err := client.simpleApi()
if err != nil {
return
}
api, err := client.api()
if err != nil {
err = FormatError("getting issues", err)
return
}
issues, result := api.Issues(client.requestURL(url)).All()
if result.HasError() {
err = FormatError("getting issues", result.Err)
path := fmt.Sprintf("repos/%s/%s/issues", project.Owner, project.Name)
if filterParams != nil {
query := url.Values{}
for key, value := range filterParams {
switch v := value.(type) {
case string:
query.Add(key, v)
}
}
path += "?" + query.Encode()
}
res, err := api.Get(path)
if err = checkStatus(200, "fetching issues", res, err); err != nil {
return
}
issues = []Issue{}
err = res.Unmarshal(&issues)
return
}
func (client *Client) CreateIssue(project *Project, title, body string, labels []string) (issue *octokit.Issue, err error) {
url, err := octokit.RepoIssuesURL.Expand(octokit.M{"owner": project.Owner, "repo": project.Name})
func (client *Client) CreateIssue(project *Project, issueParams *IssueParams) (issue *Issue, err error) {
api, err := client.simpleApi()
if err != nil {
return
}
api, err := client.api()
if err != nil {
err = FormatError("creating issues", err)
return
}
params := octokit.IssueParams{
Title: title,
Body: body,
Labels: labels,
}
issue, result := api.Issues(client.requestURL(url)).Create(params)
if result.HasError() {
err = FormatError("creating issue", result.Err)
res, err := api.PostJSON(fmt.Sprintf("repos/%s/%s/issues", project.Owner, project.Name), issueParams)
if err = checkStatus(201, "creating issue", res, err); err != nil {
return
}
issue = &Issue{}
err = res.Unmarshal(issue)
return
}

Просмотреть файл

@ -69,10 +69,16 @@ func report(reportedError error, stack string) {
gh := NewClient(project.Host)
issue, err := gh.CreateIssue(project, title, body, []string{"Crash Report"})
params := &IssueParams{
Title: title,
Body: body,
Labels: []string{"Crash Report"},
}
issue, err := gh.CreateIssue(project, params)
utils.Check(err)
ui.Println(issue.HTMLURL)
ui.Println(issue.HtmlUrl)
}
const crashReportTmpl = "Crash report - %v\n\n" +