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 }