add search feature in listing issues
This commit is contained in:
parent
2ab073d599
commit
f791bbdbcb
6 changed files with 300 additions and 54 deletions
|
|
@ -453,6 +453,113 @@ func IssueByNumber(client *Client, repo ghrepo.Interface, number int) (*Issue, e
|
|||
return &resp.Repository.Issue, nil
|
||||
}
|
||||
|
||||
func IssueSearch(client *Client, repo ghrepo.Interface, searchQuery string, limit int) (*IssuesAndTotalCount, error) {
|
||||
query :=
|
||||
`query IssueSearch($repoName: String!, $owner: String!, $type: SearchType!, $first: Int, $after: String, $searchQuery: String!) {
|
||||
repository(name: $repoName, owner: $owner) {
|
||||
hasIssuesEnabled
|
||||
}
|
||||
search(type: $type, first: $first, after: $after, query: $searchQuery) {
|
||||
issueCount
|
||||
edges {
|
||||
node {
|
||||
... on Issue {
|
||||
repository {
|
||||
hasIssuesEnabled
|
||||
}
|
||||
number
|
||||
title
|
||||
updatedAt
|
||||
state
|
||||
labels(first: 100) {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
type response struct {
|
||||
Repository struct {
|
||||
HasIssuesEnabled bool
|
||||
}
|
||||
Search struct {
|
||||
IssueCount int
|
||||
Edges []struct {
|
||||
Node struct {
|
||||
Number int
|
||||
Title string
|
||||
State string
|
||||
UpdatedAt time.Time
|
||||
Labels Labels
|
||||
}
|
||||
}
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
searchQuery = fmt.Sprintf("is:issue repo:%s/%s %s", repo.RepoOwner(), repo.RepoName(), searchQuery)
|
||||
|
||||
perPage := min(limit, 100)
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"repoName": repo.RepoName(),
|
||||
"owner": repo.RepoOwner(),
|
||||
"type": "ISSUE",
|
||||
"first": perPage,
|
||||
"searchQuery": searchQuery,
|
||||
}
|
||||
|
||||
ic := IssuesAndTotalCount{}
|
||||
|
||||
loop:
|
||||
for {
|
||||
var resp response
|
||||
err := client.GraphQL(repo.RepoHost(), query, variables, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !resp.Repository.HasIssuesEnabled {
|
||||
return nil, fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(repo))
|
||||
}
|
||||
|
||||
ic.TotalCount = resp.Search.IssueCount
|
||||
|
||||
for _, i := range resp.Search.Edges {
|
||||
ic.Issues = append(ic.Issues, Issue{
|
||||
Number: i.Node.Number,
|
||||
Title: i.Node.Title,
|
||||
State: i.Node.State,
|
||||
UpdatedAt: i.Node.UpdatedAt,
|
||||
Labels: i.Node.Labels,
|
||||
})
|
||||
if len(ic.Issues) == limit {
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
if resp.Search.PageInfo.HasNextPage {
|
||||
variables["after"] = resp.Search.PageInfo.EndCursor
|
||||
variables["first"] = min(perPage, limit-len(resp.Search.Edges))
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return &ic, nil
|
||||
}
|
||||
|
||||
func IssueClose(client *Client, repo ghrepo.Interface, issue Issue) error {
|
||||
var mutation struct {
|
||||
CloseIssue struct {
|
||||
|
|
|
|||
|
|
@ -6,49 +6,49 @@
|
|||
"totalCount": 3,
|
||||
"nodes": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "number won",
|
||||
"url": "https://wow.com",
|
||||
"updatedAt": "2011-01-26T19:01:12Z",
|
||||
"labels": {
|
||||
"nodes": [
|
||||
{
|
||||
"name": "label"
|
||||
}
|
||||
],
|
||||
"totalCount": 1
|
||||
}
|
||||
"number": 1,
|
||||
"title": "number won",
|
||||
"url": "https://wow.com",
|
||||
"updatedAt": "2011-01-26T19:01:12Z",
|
||||
"labels": {
|
||||
"nodes": [
|
||||
{
|
||||
"name": "label"
|
||||
}
|
||||
],
|
||||
"totalCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"title": "number too",
|
||||
"url": "https://wow.com",
|
||||
"updatedAt": "2011-01-26T19:01:12Z",
|
||||
"labels": {
|
||||
"nodes": [
|
||||
{
|
||||
"name": "label"
|
||||
}
|
||||
],
|
||||
"totalCount": 1
|
||||
}
|
||||
"number": 2,
|
||||
"title": "number too",
|
||||
"url": "https://wow.com",
|
||||
"updatedAt": "2011-01-26T19:01:12Z",
|
||||
"labels": {
|
||||
"nodes": [
|
||||
{
|
||||
"name": "label"
|
||||
}
|
||||
],
|
||||
"totalCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"number": 4,
|
||||
"title": "number fore",
|
||||
"url": "https://wow.com",
|
||||
"updatedAt": "2011-01-26T19:01:12Z",
|
||||
"labels": {
|
||||
"nodes": [
|
||||
{
|
||||
"name": "label"
|
||||
}
|
||||
],
|
||||
"totalCount": 1
|
||||
}
|
||||
"number": 4,
|
||||
"title": "number fore",
|
||||
"url": "https://wow.com",
|
||||
"updatedAt": "2011-01-26T19:01:12Z",
|
||||
"labels": {
|
||||
"nodes": [
|
||||
{
|
||||
"name": "label"
|
||||
}
|
||||
],
|
||||
"totalCount": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
pkg/cmd/issue/list/fixtures/issueSearch.json
Normal file
60
pkg/cmd/issue/list/fixtures/issueSearch.json
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"data": {
|
||||
"repository": {
|
||||
"hasIssuesEnabled": true
|
||||
},
|
||||
"search": {
|
||||
"issueCount": 3,
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"number": 1,
|
||||
"title": "number won",
|
||||
"url": "https://wow.com",
|
||||
"updatedAt": "2011-01-26T19:01:12Z",
|
||||
"labels": {
|
||||
"nodes": [
|
||||
{
|
||||
"name": "label"
|
||||
}
|
||||
],
|
||||
"totalCount": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"number": 2,
|
||||
"title": "number too",
|
||||
"url": "https://wow.com",
|
||||
"updatedAt": "2011-01-26T19:01:12Z",
|
||||
"labels": {
|
||||
"nodes": [
|
||||
{
|
||||
"name": "label"
|
||||
}
|
||||
],
|
||||
"totalCount": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"number": 4,
|
||||
"title": "number fore",
|
||||
"url": "https://wow.com",
|
||||
"updatedAt": "2011-01-26T19:01:12Z",
|
||||
"labels": {
|
||||
"nodes": [
|
||||
{
|
||||
"name": "label"
|
||||
}
|
||||
],
|
||||
"totalCount": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ type ListOptions struct {
|
|||
Author string
|
||||
Mention string
|
||||
Milestone string
|
||||
Search string
|
||||
}
|
||||
|
||||
func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command {
|
||||
|
|
@ -50,6 +51,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
$ gh issue list -a @me
|
||||
$ gh issue list --web
|
||||
$ gh issue list --milestone 'MVP'
|
||||
$ gh issue list --search "error no:assignee sort:created-asc"
|
||||
`),
|
||||
Args: cmdutil.NoArgsQuoteReminder,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
|
@ -75,7 +77,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
cmd.Flags().StringVarP(&opts.Author, "author", "A", "", "Filter by author")
|
||||
cmd.Flags().StringVar(&opts.Mention, "mention", "", "Filter by mention")
|
||||
cmd.Flags().StringVarP(&opts.Milestone, "milestone", "m", "", "Filter by milestone `number` or `title`")
|
||||
|
||||
cmd.Flags().StringVarP(&opts.Search, "search", "S", "", "Search issues with filter")
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
|
@ -107,29 +109,44 @@ func listRun(opts *ListOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
filterOptions := prShared.FilterOptions{
|
||||
Entity: "issue",
|
||||
State: opts.State,
|
||||
Assignee: filterAssignee,
|
||||
Labels: opts.Labels,
|
||||
Author: filterAuthor,
|
||||
Mention: filterMention,
|
||||
Milestone: opts.Milestone,
|
||||
Search: opts.Search,
|
||||
}
|
||||
|
||||
if opts.WebMode {
|
||||
issueListURL := ghrepo.GenerateRepoURL(baseRepo, "issues")
|
||||
openURL, err := prShared.ListURLWithQuery(issueListURL, prShared.FilterOptions{
|
||||
Entity: "issue",
|
||||
State: opts.State,
|
||||
Assignee: filterAssignee,
|
||||
Labels: opts.Labels,
|
||||
Author: filterAuthor,
|
||||
Mention: filterMention,
|
||||
Milestone: opts.Milestone,
|
||||
})
|
||||
openURL, err := prShared.ListURLWithQuery(issueListURL, filterOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isTerminal {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
|
||||
}
|
||||
return utils.OpenInBrowser(openURL)
|
||||
}
|
||||
|
||||
listResult, err := api.IssueList(apiClient, baseRepo, opts.State, opts.Labels, filterAssignee, opts.LimitResults, filterAuthor, filterMention, opts.Milestone)
|
||||
if err != nil {
|
||||
return err
|
||||
searchQuery := prShared.IssueSearchBuild(filterOptions)
|
||||
|
||||
var listResult *api.IssuesAndTotalCount
|
||||
|
||||
if opts.Search != "" {
|
||||
listResult, err = api.IssueSearch(apiClient, baseRepo, searchQuery, opts.LimitResults)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
listResult, err = api.IssueList(apiClient, baseRepo, opts.State, opts.Labels, filterAssignee, opts.LimitResults, filterAuthor, filterMention, opts.Milestone)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = opts.IO.StartPager()
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, err
|
|||
ErrBuf: stderr,
|
||||
}, err
|
||||
}
|
||||
|
||||
func TestIssueList_nontty(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
|
@ -296,3 +297,52 @@ func TestIssueList_milestoneByNumber(t *testing.T) {
|
|||
t.Fatalf("error running issue list: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueList_Search_tty(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query IssueSearch\b`),
|
||||
httpmock.FileResponse("./fixtures/issueSearch.json"))
|
||||
|
||||
output, err := runCommand(http, true, "--search \"auth bug\"")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `issue list`: %v", err)
|
||||
}
|
||||
|
||||
out := output.String()
|
||||
timeRE := regexp.MustCompile(`\d+ years`)
|
||||
out = timeRE.ReplaceAllString(out, "X years")
|
||||
|
||||
assert.Equal(t, heredoc.Doc(`
|
||||
|
||||
Showing 3 of 3 open issues in OWNER/REPO
|
||||
|
||||
#1 number won (label) about X years ago
|
||||
#2 number too (label) about X years ago
|
||||
#4 number fore (label) about X years ago
|
||||
`), out)
|
||||
}
|
||||
|
||||
func TestIssueList_Search_web(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
cs.Register(`https://github\.com`, 0, "", func(args []string) {
|
||||
url := strings.ReplaceAll(args[len(args)-1], "^", "")
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/issues?q=is%3Aissue+assignee%3Apeter+label%3Abug+label%3Adocs+author%3Ajohn+mentions%3Afrank+milestone%3Av1.1+transfer", url)
|
||||
})
|
||||
|
||||
output, err := runCommand(http, true, "--web -a peter -A john -l bug -l docs -L 10 -s all --mention frank --milestone v1.1 --search transfer")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `issue list` with `--web` flag: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "Opening github.com/OWNER/REPO/issues in your browser.\n", output.Stderr())
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@ type FilterOptions struct {
|
|||
BaseBranch string
|
||||
Mention string
|
||||
Milestone string
|
||||
Search string
|
||||
}
|
||||
|
||||
func ListURLWithQuery(listURL string, options FilterOptions) (string, error) {
|
||||
|
|
@ -156,6 +157,16 @@ func ListURLWithQuery(listURL string, options FilterOptions) (string, error) {
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
query := IssueSearchBuild(options)
|
||||
|
||||
q := u.Query()
|
||||
q.Set("q", strings.TrimSuffix(query, " "))
|
||||
u.RawQuery = q.Encode()
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func IssueSearchBuild(options FilterOptions) string {
|
||||
|
||||
query := fmt.Sprintf("is:%s ", options.Entity)
|
||||
if options.State != "all" {
|
||||
query += fmt.Sprintf("is:%s ", options.State)
|
||||
|
|
@ -178,10 +189,11 @@ func ListURLWithQuery(listURL string, options FilterOptions) (string, error) {
|
|||
if options.Milestone != "" {
|
||||
query += fmt.Sprintf("milestone:%s ", quoteValueForQuery(options.Milestone))
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("q", strings.TrimSuffix(query, " "))
|
||||
u.RawQuery = q.Encode()
|
||||
return u.String(), nil
|
||||
if options.Search != "" {
|
||||
query += options.Search
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
func quoteValueForQuery(v string) string {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue