add search feature in listing issues

This commit is contained in:
Gowtham Munukutla 2021-03-10 16:14:32 +05:30 committed by Mislav Marohnić
parent 2ab073d599
commit f791bbdbcb
6 changed files with 300 additions and 54 deletions

View file

@ -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 {

View file

@ -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
}
}
]
}
}
}
}
}

View 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
}
}
}
]
}
}
}

View file

@ -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()

View file

@ -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())
}

View file

@ -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 {