github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/pr/shared/params.go (about) 1 package shared 2 3 import ( 4 "fmt" 5 "net/url" 6 "strings" 7 8 "github.com/cli/cli/api" 9 "github.com/cli/cli/internal/ghrepo" 10 "github.com/cli/cli/pkg/githubsearch" 11 "github.com/google/shlex" 12 ) 13 14 func WithPrAndIssueQueryParams(client *api.Client, baseRepo ghrepo.Interface, baseURL string, state IssueMetadataState) (string, error) { 15 u, err := url.Parse(baseURL) 16 if err != nil { 17 return "", err 18 } 19 q := u.Query() 20 if state.Title != "" { 21 q.Set("title", state.Title) 22 } 23 // We always want to send the body parameter, even if it's empty, to prevent the web interface from 24 // applying the default template. Since the user has the option to select a template in the terminal, 25 // assume that empty body here means that the user either skipped it or erased its contents. 26 q.Set("body", state.Body) 27 if len(state.Assignees) > 0 { 28 q.Set("assignees", strings.Join(state.Assignees, ",")) 29 } 30 if len(state.Labels) > 0 { 31 q.Set("labels", strings.Join(state.Labels, ",")) 32 } 33 if len(state.Projects) > 0 { 34 projectPaths, err := api.ProjectNamesToPaths(client, baseRepo, state.Projects) 35 if err != nil { 36 return "", fmt.Errorf("could not add to project: %w", err) 37 } 38 q.Set("projects", strings.Join(projectPaths, ",")) 39 } 40 if len(state.Milestones) > 0 { 41 q.Set("milestone", state.Milestones[0]) 42 } 43 u.RawQuery = q.Encode() 44 return u.String(), nil 45 } 46 47 // Ensure that tb.MetadataResult object exists and contains enough pre-fetched API data to be able 48 // to resolve all object listed in tb to GraphQL IDs. 49 func fillMetadata(client *api.Client, baseRepo ghrepo.Interface, tb *IssueMetadataState) error { 50 resolveInput := api.RepoResolveInput{} 51 52 if len(tb.Assignees) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.AssignableUsers) == 0) { 53 resolveInput.Assignees = tb.Assignees 54 } 55 56 if len(tb.Reviewers) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.AssignableUsers) == 0) { 57 resolveInput.Reviewers = tb.Reviewers 58 } 59 60 if len(tb.Labels) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.Labels) == 0) { 61 resolveInput.Labels = tb.Labels 62 } 63 64 if len(tb.Projects) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.Projects) == 0) { 65 resolveInput.Projects = tb.Projects 66 } 67 68 if len(tb.Milestones) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.Milestones) == 0) { 69 resolveInput.Milestones = tb.Milestones 70 } 71 72 metadataResult, err := api.RepoResolveMetadataIDs(client, baseRepo, resolveInput) 73 if err != nil { 74 return err 75 } 76 77 if tb.MetadataResult == nil { 78 tb.MetadataResult = metadataResult 79 } else { 80 tb.MetadataResult.Merge(metadataResult) 81 } 82 83 return nil 84 } 85 86 func AddMetadataToIssueParams(client *api.Client, baseRepo ghrepo.Interface, params map[string]interface{}, tb *IssueMetadataState) error { 87 if !tb.HasMetadata() { 88 return nil 89 } 90 91 if err := fillMetadata(client, baseRepo, tb); err != nil { 92 return err 93 } 94 95 assigneeIDs, err := tb.MetadataResult.MembersToIDs(tb.Assignees) 96 if err != nil { 97 return fmt.Errorf("could not assign user: %w", err) 98 } 99 params["assigneeIds"] = assigneeIDs 100 101 labelIDs, err := tb.MetadataResult.LabelsToIDs(tb.Labels) 102 if err != nil { 103 return fmt.Errorf("could not add label: %w", err) 104 } 105 params["labelIds"] = labelIDs 106 107 projectIDs, err := tb.MetadataResult.ProjectsToIDs(tb.Projects) 108 if err != nil { 109 return fmt.Errorf("could not add to project: %w", err) 110 } 111 params["projectIds"] = projectIDs 112 113 if len(tb.Milestones) > 0 { 114 milestoneID, err := tb.MetadataResult.MilestoneToID(tb.Milestones[0]) 115 if err != nil { 116 return fmt.Errorf("could not add to milestone '%s': %w", tb.Milestones[0], err) 117 } 118 params["milestoneId"] = milestoneID 119 } 120 121 if len(tb.Reviewers) == 0 { 122 return nil 123 } 124 125 var userReviewers []string 126 var teamReviewers []string 127 for _, r := range tb.Reviewers { 128 if strings.ContainsRune(r, '/') { 129 teamReviewers = append(teamReviewers, r) 130 } else { 131 userReviewers = append(userReviewers, r) 132 } 133 } 134 135 userReviewerIDs, err := tb.MetadataResult.MembersToIDs(userReviewers) 136 if err != nil { 137 return fmt.Errorf("could not request reviewer: %w", err) 138 } 139 params["userReviewerIds"] = userReviewerIDs 140 141 teamReviewerIDs, err := tb.MetadataResult.TeamsToIDs(teamReviewers) 142 if err != nil { 143 return fmt.Errorf("could not request reviewer: %w", err) 144 } 145 params["teamReviewerIds"] = teamReviewerIDs 146 147 return nil 148 } 149 150 type FilterOptions struct { 151 Entity string 152 State string 153 Assignee string 154 Labels []string 155 Author string 156 BaseBranch string 157 Mention string 158 Milestone string 159 Search string 160 161 Fields []string 162 } 163 164 func (opts *FilterOptions) IsDefault() bool { 165 if opts.State != "open" { 166 return false 167 } 168 if len(opts.Labels) > 0 { 169 return false 170 } 171 if opts.Assignee != "" { 172 return false 173 } 174 if opts.Author != "" { 175 return false 176 } 177 if opts.BaseBranch != "" { 178 return false 179 } 180 if opts.Mention != "" { 181 return false 182 } 183 if opts.Milestone != "" { 184 return false 185 } 186 if opts.Search != "" { 187 return false 188 } 189 return true 190 } 191 192 func ListURLWithQuery(listURL string, options FilterOptions) (string, error) { 193 u, err := url.Parse(listURL) 194 if err != nil { 195 return "", err 196 } 197 198 params := u.Query() 199 params.Set("q", SearchQueryBuild(options)) 200 u.RawQuery = params.Encode() 201 202 return u.String(), nil 203 } 204 205 func SearchQueryBuild(options FilterOptions) string { 206 q := githubsearch.NewQuery() 207 switch options.Entity { 208 case "issue": 209 q.SetType(githubsearch.Issue) 210 case "pr": 211 q.SetType(githubsearch.PullRequest) 212 } 213 214 switch options.State { 215 case "open": 216 q.SetState(githubsearch.Open) 217 case "closed": 218 q.SetState(githubsearch.Closed) 219 case "merged": 220 q.SetState(githubsearch.Merged) 221 } 222 223 if options.Assignee != "" { 224 q.AssignedTo(options.Assignee) 225 } 226 for _, label := range options.Labels { 227 q.AddLabel(label) 228 } 229 if options.Author != "" { 230 q.AuthoredBy(options.Author) 231 } 232 if options.BaseBranch != "" { 233 q.SetBaseBranch(options.BaseBranch) 234 } 235 if options.Mention != "" { 236 q.Mentions(options.Mention) 237 } 238 if options.Milestone != "" { 239 q.InMilestone(options.Milestone) 240 } 241 if options.Search != "" { 242 q.AddQuery(options.Search) 243 } 244 245 return q.String() 246 } 247 248 func QueryHasStateClause(searchQuery string) bool { 249 argv, err := shlex.Split(searchQuery) 250 if err != nil { 251 return false 252 } 253 254 for _, arg := range argv { 255 if arg == "is:closed" || arg == "is:merged" || arg == "state:closed" || arg == "state:merged" || strings.HasPrefix(arg, "merged:") || strings.HasPrefix(arg, "closed:") { 256 return true 257 } 258 } 259 260 return false 261 } 262 263 // MeReplacer resolves usages of `@me` to the handle of the currently logged in user. 264 type MeReplacer struct { 265 apiClient *api.Client 266 hostname string 267 login string 268 } 269 270 func NewMeReplacer(apiClient *api.Client, hostname string) *MeReplacer { 271 return &MeReplacer{ 272 apiClient: apiClient, 273 hostname: hostname, 274 } 275 } 276 277 func (r *MeReplacer) currentLogin() (string, error) { 278 if r.login != "" { 279 return r.login, nil 280 } 281 login, err := api.CurrentLoginName(r.apiClient, r.hostname) 282 if err != nil { 283 return "", fmt.Errorf("failed resolving `@me` to your user handle: %w", err) 284 } 285 r.login = login 286 return login, nil 287 } 288 289 func (r *MeReplacer) Replace(handle string) (string, error) { 290 if handle == "@me" { 291 return r.currentLogin() 292 } 293 return handle, nil 294 } 295 296 func (r *MeReplacer) ReplaceSlice(handles []string) ([]string, error) { 297 res := make([]string, len(handles)) 298 for i, h := range handles { 299 var err error 300 res[i], err = r.Replace(h) 301 if err != nil { 302 return nil, err 303 } 304 } 305 return res, nil 306 }