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