github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/issue/shared/lookup.go (about)

     1  package shared
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"net/http"
     7  	"net/url"
     8  	"regexp"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/ungtb10d/cli/v2/api"
    14  	fd "github.com/ungtb10d/cli/v2/internal/featuredetection"
    15  	"github.com/ungtb10d/cli/v2/internal/ghrepo"
    16  	"github.com/ungtb10d/cli/v2/pkg/set"
    17  )
    18  
    19  // IssueFromArgWithFields loads an issue or pull request with the specified fields. If some of the fields
    20  // could not be fetched by GraphQL, this returns a non-nil issue and a *PartialLoadError.
    21  func IssueFromArgWithFields(httpClient *http.Client, baseRepoFn func() (ghrepo.Interface, error), arg string, fields []string) (*api.Issue, ghrepo.Interface, error) {
    22  	issueNumber, baseRepo := issueMetadataFromURL(arg)
    23  
    24  	if issueNumber == 0 {
    25  		var err error
    26  		issueNumber, err = strconv.Atoi(strings.TrimPrefix(arg, "#"))
    27  		if err != nil {
    28  			return nil, nil, fmt.Errorf("invalid issue format: %q", arg)
    29  		}
    30  	}
    31  
    32  	if baseRepo == nil {
    33  		var err error
    34  		baseRepo, err = baseRepoFn()
    35  		if err != nil {
    36  			return nil, nil, fmt.Errorf("could not determine base repo: %w", err)
    37  		}
    38  	}
    39  
    40  	issue, err := findIssueOrPR(httpClient, baseRepo, issueNumber, fields)
    41  	return issue, baseRepo, err
    42  }
    43  
    44  var issueURLRE = regexp.MustCompile(`^/([^/]+)/([^/]+)/issues/(\d+)`)
    45  
    46  func issueMetadataFromURL(s string) (int, ghrepo.Interface) {
    47  	u, err := url.Parse(s)
    48  	if err != nil {
    49  		return 0, nil
    50  	}
    51  
    52  	if u.Scheme != "https" && u.Scheme != "http" {
    53  		return 0, nil
    54  	}
    55  
    56  	m := issueURLRE.FindStringSubmatch(u.Path)
    57  	if m == nil {
    58  		return 0, nil
    59  	}
    60  
    61  	repo := ghrepo.NewWithHost(m[1], m[2], u.Hostname())
    62  	issueNumber, _ := strconv.Atoi(m[3])
    63  	return issueNumber, repo
    64  }
    65  
    66  // Returns the issue number and repo if the issue URL is provided.
    67  // If only the issue number is provided, returns the number and nil repo.
    68  func IssueNumberAndRepoFromArg(arg string) (int, ghrepo.Interface, error) {
    69  	issueNumber, baseRepo := issueMetadataFromURL(arg)
    70  
    71  	if issueNumber == 0 {
    72  		var err error
    73  		issueNumber, err = strconv.Atoi(strings.TrimPrefix(arg, "#"))
    74  		if err != nil {
    75  			return 0, nil, fmt.Errorf("invalid issue format: %q", arg)
    76  		}
    77  	}
    78  
    79  	return issueNumber, baseRepo, nil
    80  }
    81  
    82  type PartialLoadError struct {
    83  	error
    84  }
    85  
    86  func findIssueOrPR(httpClient *http.Client, repo ghrepo.Interface, number int, fields []string) (*api.Issue, error) {
    87  	fieldSet := set.NewStringSet()
    88  	fieldSet.AddValues(fields)
    89  	if fieldSet.Contains("stateReason") {
    90  		cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24)
    91  		detector := fd.NewDetector(cachedClient, repo.RepoHost())
    92  		features, err := detector.IssueFeatures()
    93  		if err != nil {
    94  			return nil, err
    95  		}
    96  		if !features.StateReason {
    97  			fieldSet.Remove("stateReason")
    98  		}
    99  	}
   100  	fields = fieldSet.ToSlice()
   101  
   102  	type response struct {
   103  		Repository struct {
   104  			HasIssuesEnabled bool
   105  			Issue            *api.Issue
   106  		}
   107  	}
   108  
   109  	query := fmt.Sprintf(`
   110  	query IssueByNumber($owner: String!, $repo: String!, $number: Int!) {
   111  		repository(owner: $owner, name: $repo) {
   112  			hasIssuesEnabled
   113  			issue: issueOrPullRequest(number: $number) {
   114  				__typename
   115  				...on Issue{%[1]s}
   116  				...on PullRequest{%[2]s}
   117  			}
   118  		}
   119  	}`, api.IssueGraphQL(fields), api.PullRequestGraphQL(fields))
   120  
   121  	variables := map[string]interface{}{
   122  		"owner":  repo.RepoOwner(),
   123  		"repo":   repo.RepoName(),
   124  		"number": number,
   125  	}
   126  
   127  	var resp response
   128  	client := api.NewClientFromHTTP(httpClient)
   129  	if err := client.GraphQL(repo.RepoHost(), query, variables, &resp); err != nil {
   130  		var gerr api.GraphQLError
   131  		if errors.As(err, &gerr) {
   132  			if gerr.Match("NOT_FOUND", "repository.issue") && !resp.Repository.HasIssuesEnabled {
   133  				return nil, fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(repo))
   134  			} else if gerr.Match("FORBIDDEN", "repository.issue.projectCards.") {
   135  				issue := resp.Repository.Issue
   136  				// remove nil entries for project cards due to permission issues
   137  				projects := make([]*api.ProjectInfo, 0, len(issue.ProjectCards.Nodes))
   138  				for _, p := range issue.ProjectCards.Nodes {
   139  					if p != nil {
   140  						projects = append(projects, p)
   141  					}
   142  				}
   143  				issue.ProjectCards.Nodes = projects
   144  				return issue, &PartialLoadError{err}
   145  			}
   146  		}
   147  		return nil, err
   148  	}
   149  
   150  	if resp.Repository.Issue == nil {
   151  		return nil, errors.New("issue was not found but GraphQL reported no error")
   152  	}
   153  
   154  	return resp.Repository.Issue, nil
   155  }