diff --git a/api/queries_issue.go b/api/queries_issue.go index 0070425b4..6ef431ed4 100644 --- a/api/queries_issue.go +++ b/api/queries_issue.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "fmt" + "strconv" "strings" "time" @@ -250,25 +251,24 @@ func IssueList(client *Client, repo ghrepo.Interface, state string, labels []str } if milestoneString != "" { - milestones, err := RepoMilestones(client, repo) - if err != nil { - return nil, err - } - - for i := range milestones { - if strings.EqualFold(milestones[i].Title, milestoneString) { - id, err := milestoneNodeIdToDatabaseId(milestones[i].ID) - if err != nil { - return nil, err - } - variables["milestone"] = id - break + var milestone *RepoMilestone + if milestoneNumber, err := strconv.Atoi(milestoneString); err == nil { + milestone, err = MilestoneByNumber(client, repo, milestoneNumber) + if err != nil { + return nil, err + } + } else { + milestone, err = MilestoneByTitle(client, repo, milestoneString) + if err != nil { + return nil, err } } - if variables["milestone"] == nil { - return nil, fmt.Errorf("no milestone found with title: %q", milestoneString) + milestoneRESTID, err := milestoneNodeIdToDatabaseId(milestone.ID) + if err != nil { + return nil, err } + variables["milestone"] = milestoneRESTID } var response struct { diff --git a/api/queries_repo.go b/api/queries_repo.go index e19bc1cf6..5a9fd587e 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -804,3 +804,43 @@ func RepoMilestones(client *Client, repo ghrepo.Interface) ([]RepoMilestone, err return milestones, nil } + +func MilestoneByTitle(client *Client, repo ghrepo.Interface, title string) (*RepoMilestone, error) { + milestones, err := RepoMilestones(client, repo) + if err != nil { + return nil, err + } + + for i := range milestones { + if strings.EqualFold(milestones[i].Title, title) { + return &milestones[i], nil + } + } + return nil, fmt.Errorf("no milestone found with title %q", title) +} + +func MilestoneByNumber(client *Client, repo ghrepo.Interface, number int) (*RepoMilestone, error) { + var query struct { + Repository struct { + Milestone *RepoMilestone `graphql:"milestone(number: $number)"` + } `graphql:"repository(owner: $owner, name: $name)"` + } + + variables := map[string]interface{}{ + "owner": githubv4.String(repo.RepoOwner()), + "name": githubv4.String(repo.RepoName()), + "number": githubv4.Int(number), + } + + gql := graphQLClient(client.http, repo.RepoHost()) + + err := gql.QueryNamed(context.Background(), "RepositoryMilestoneByNumber", &query, variables) + if err != nil { + return nil, err + } + if query.Repository.Milestone == nil { + return nil, fmt.Errorf("no milestone found with number '%d'", number) + } + + return query.Repository.Milestone, nil +} diff --git a/command/issue.go b/command/issue.go index ffa83f81c..d003e1df2 100644 --- a/command/issue.go +++ b/command/issue.go @@ -46,7 +46,7 @@ func init() { issueListCmd.Flags().IntP("limit", "L", 30, "Maximum number of issues to fetch") issueListCmd.Flags().StringP("author", "A", "", "Filter by author") issueListCmd.Flags().String("mention", "", "Filter by mention") - issueListCmd.Flags().StringP("milestone", "m", "", "Filter by milestone `name`") + issueListCmd.Flags().StringP("milestone", "m", "", "Filter by milestone `number` or title") issueCmd.AddCommand(issueViewCmd) issueViewCmd.Flags().BoolP("web", "w", false, "Open an issue in the browser") diff --git a/command/issue_test.go b/command/issue_test.go index f8d85f50f..27b2b64d5 100644 --- a/command/issue_test.go +++ b/command/issue_test.go @@ -285,11 +285,8 @@ func TestIssueList_web(t *testing.T) { func TestIssueList_milestoneNotFound(t *testing.T) { initBlankContext("", "OWNER/REPO", "master") http := initFakeHTTP() + defer http.Verify(t) http.StubRepoResponse("OWNER", "REPO") - http.Register( - httpmock.GraphQL(`query IssueList\b`), - httpmock.FileResponse("../test/fixtures/issueList.json"), - ) http.Register( httpmock.GraphQL(`query RepositoryMilestoneList\b`), httpmock.StringResponse(` @@ -300,11 +297,39 @@ func TestIssueList_milestoneNotFound(t *testing.T) { `)) _, err := RunCommand("issue list --milestone NotFound") - if err == nil || err.Error() != `no milestone found with title: "NotFound"` { + if err == nil || err.Error() != `no milestone found with title "NotFound"` { t.Errorf("error running command `issue list`: %v", err) } } +func TestIssueList_milestoneByNumber(t *testing.T) { + initBlankContext("", "OWNER/REPO", "master") + http := initFakeHTTP() + defer http.Verify(t) + http.StubRepoResponse("OWNER", "REPO") + http.Register( + httpmock.GraphQL(`query RepositoryMilestoneByNumber\b`), + httpmock.StringResponse(` + { "data": { "repository": { "milestone": { + "id": "MDk6TWlsZXN0b25lMTIzNDU=" + } } } } + `)) + http.Register( + httpmock.GraphQL(`query IssueList\b`), + httpmock.GraphQLQuery(` + { "data": { "repository": { + "hasIssuesEnabled": true, + "issues": { "nodes": [] } + } } }`, func(_ string, params map[string]interface{}) { + assert.Equal(t, "12345", params["milestone"].(string)) // Database ID for the Milestone (see #1462) + })) + + _, err := RunCommand("issue list --milestone 13") + if err != nil { + t.Fatalf("error running issue list: %v", err) + } +} + func TestIssueView_web(t *testing.T) { initBlankContext("", "OWNER/REPO", "master") http := initFakeHTTP()