github.com/andrewhsu/cli/v2@v2.0.1-0.20210910131313-d4b4061f5b89/pkg/cmd/issue/list/http.go (about)

     1  package list
     2  
     3  import (
     4  	"encoding/base64"
     5  	"fmt"
     6  	"strconv"
     7  	"strings"
     8  
     9  	"github.com/andrewhsu/cli/v2/api"
    10  	"github.com/andrewhsu/cli/v2/internal/ghrepo"
    11  	prShared "github.com/andrewhsu/cli/v2/pkg/cmd/pr/shared"
    12  )
    13  
    14  func listIssues(client *api.Client, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.IssuesAndTotalCount, error) {
    15  	var states []string
    16  	switch filters.State {
    17  	case "open", "":
    18  		states = []string{"OPEN"}
    19  	case "closed":
    20  		states = []string{"CLOSED"}
    21  	case "all":
    22  		states = []string{"OPEN", "CLOSED"}
    23  	default:
    24  		return nil, fmt.Errorf("invalid state: %s", filters.State)
    25  	}
    26  
    27  	fragments := fmt.Sprintf("fragment issue on Issue {%s}", api.PullRequestGraphQL(filters.Fields))
    28  	query := fragments + `
    29  	query IssueList($owner: String!, $repo: String!, $limit: Int, $endCursor: String, $states: [IssueState!] = OPEN, $assignee: String, $author: String, $mention: String, $milestone: String) {
    30  		repository(owner: $owner, name: $repo) {
    31  			hasIssuesEnabled
    32  			issues(first: $limit, after: $endCursor, orderBy: {field: CREATED_AT, direction: DESC}, states: $states, filterBy: {assignee: $assignee, createdBy: $author, mentioned: $mention, milestone: $milestone}) {
    33  				totalCount
    34  				nodes {
    35  					...issue
    36  				}
    37  				pageInfo {
    38  					hasNextPage
    39  					endCursor
    40  				}
    41  			}
    42  		}
    43  	}
    44  	`
    45  
    46  	variables := map[string]interface{}{
    47  		"owner":  repo.RepoOwner(),
    48  		"repo":   repo.RepoName(),
    49  		"states": states,
    50  	}
    51  	if filters.Assignee != "" {
    52  		variables["assignee"] = filters.Assignee
    53  	}
    54  	if filters.Author != "" {
    55  		variables["author"] = filters.Author
    56  	}
    57  	if filters.Mention != "" {
    58  		variables["mention"] = filters.Mention
    59  	}
    60  
    61  	if filters.Milestone != "" {
    62  		var milestone *api.RepoMilestone
    63  		if milestoneNumber, err := strconv.ParseInt(filters.Milestone, 10, 32); err == nil {
    64  			milestone, err = api.MilestoneByNumber(client, repo, int32(milestoneNumber))
    65  			if err != nil {
    66  				return nil, err
    67  			}
    68  		} else {
    69  			milestone, err = api.MilestoneByTitle(client, repo, "all", filters.Milestone)
    70  			if err != nil {
    71  				return nil, err
    72  			}
    73  		}
    74  
    75  		milestoneRESTID, err := milestoneNodeIdToDatabaseId(milestone.ID)
    76  		if err != nil {
    77  			return nil, err
    78  		}
    79  		variables["milestone"] = milestoneRESTID
    80  	}
    81  
    82  	type responseData struct {
    83  		Repository struct {
    84  			Issues struct {
    85  				TotalCount int
    86  				Nodes      []api.Issue
    87  				PageInfo   struct {
    88  					HasNextPage bool
    89  					EndCursor   string
    90  				}
    91  			}
    92  			HasIssuesEnabled bool
    93  		}
    94  	}
    95  
    96  	var issues []api.Issue
    97  	var totalCount int
    98  	pageLimit := min(limit, 100)
    99  
   100  loop:
   101  	for {
   102  		var response responseData
   103  		variables["limit"] = pageLimit
   104  		err := client.GraphQL(repo.RepoHost(), query, variables, &response)
   105  		if err != nil {
   106  			return nil, err
   107  		}
   108  		if !response.Repository.HasIssuesEnabled {
   109  			return nil, fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(repo))
   110  		}
   111  		totalCount = response.Repository.Issues.TotalCount
   112  
   113  		for _, issue := range response.Repository.Issues.Nodes {
   114  			issues = append(issues, issue)
   115  			if len(issues) == limit {
   116  				break loop
   117  			}
   118  		}
   119  
   120  		if response.Repository.Issues.PageInfo.HasNextPage {
   121  			variables["endCursor"] = response.Repository.Issues.PageInfo.EndCursor
   122  			pageLimit = min(pageLimit, limit-len(issues))
   123  		} else {
   124  			break
   125  		}
   126  	}
   127  
   128  	res := api.IssuesAndTotalCount{Issues: issues, TotalCount: totalCount}
   129  	return &res, nil
   130  }
   131  
   132  func searchIssues(client *api.Client, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.IssuesAndTotalCount, error) {
   133  	fragments := fmt.Sprintf("fragment issue on Issue {%s}", api.PullRequestGraphQL(filters.Fields))
   134  	query := fragments +
   135  		`query IssueSearch($repo: String!, $owner: String!, $type: SearchType!, $limit: Int, $after: String, $query: String!) {
   136  			repository(name: $repo, owner: $owner) {
   137  				hasIssuesEnabled
   138  			}
   139  			search(type: $type, last: $limit, after: $after, query: $query) {
   140  				issueCount
   141  				nodes { ...issue }
   142  				pageInfo {
   143  					hasNextPage
   144  					endCursor
   145  				}
   146  			}
   147  		}`
   148  
   149  	type response struct {
   150  		Repository struct {
   151  			HasIssuesEnabled bool
   152  		}
   153  		Search struct {
   154  			IssueCount int
   155  			Nodes      []api.Issue
   156  			PageInfo   struct {
   157  				HasNextPage bool
   158  				EndCursor   string
   159  			}
   160  		}
   161  	}
   162  
   163  	perPage := min(limit, 100)
   164  	searchQuery := fmt.Sprintf("repo:%s/%s %s", repo.RepoOwner(), repo.RepoName(), prShared.SearchQueryBuild(filters))
   165  
   166  	variables := map[string]interface{}{
   167  		"owner": repo.RepoOwner(),
   168  		"repo":  repo.RepoName(),
   169  		"type":  "ISSUE",
   170  		"limit": perPage,
   171  		"query": searchQuery,
   172  	}
   173  
   174  	ic := api.IssuesAndTotalCount{}
   175  
   176  loop:
   177  	for {
   178  		var resp response
   179  		err := client.GraphQL(repo.RepoHost(), query, variables, &resp)
   180  		if err != nil {
   181  			return nil, err
   182  		}
   183  
   184  		if !resp.Repository.HasIssuesEnabled {
   185  			return nil, fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(repo))
   186  		}
   187  
   188  		ic.TotalCount = resp.Search.IssueCount
   189  
   190  		for _, issue := range resp.Search.Nodes {
   191  			ic.Issues = append(ic.Issues, issue)
   192  			if len(ic.Issues) == limit {
   193  				break loop
   194  			}
   195  		}
   196  
   197  		if !resp.Search.PageInfo.HasNextPage {
   198  			break
   199  		}
   200  		variables["after"] = resp.Search.PageInfo.EndCursor
   201  		variables["perPage"] = min(perPage, limit-len(ic.Issues))
   202  	}
   203  
   204  	return &ic, nil
   205  }
   206  
   207  // milestoneNodeIdToDatabaseId extracts the REST Database ID from the GraphQL Node ID
   208  // This conversion is necessary since the GraphQL API requires the use of the milestone's database ID
   209  // for querying the related issues.
   210  func milestoneNodeIdToDatabaseId(nodeId string) (string, error) {
   211  	// The Node ID is Base64 obfuscated, with an underlying pattern:
   212  	// "09:Milestone12345", where "12345" is the database ID
   213  	decoded, err := base64.StdEncoding.DecodeString(nodeId)
   214  	if err != nil {
   215  		return "", err
   216  	}
   217  	splitted := strings.Split(string(decoded), "Milestone")
   218  	if len(splitted) != 2 {
   219  		return "", fmt.Errorf("couldn't get database id from node id")
   220  	}
   221  	return splitted[1], nil
   222  }
   223  
   224  func min(a, b int) int {
   225  	if a < b {
   226  		return a
   227  	}
   228  	return b
   229  }