зеркало из https://github.com/mislav/hub.git
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:
Родитель
f412a353b8
Коммит
8563e678d2
|
@ -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" +
|
||||
|
|
Загрузка…
Ссылка в новой задаче