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