github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/pr/shared/params.go (about) 1 package shared 2 3 import ( 4 "fmt" 5 "net/url" 6 "strings" 7 8 "github.com/ungtb10d/cli/v2/api" 9 "github.com/ungtb10d/cli/v2/internal/ghrepo" 10 "github.com/ungtb10d/cli/v2/pkg/search" 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 // Maximum length of a URL: 8192 bytes 48 func ValidURL(urlStr string) bool { 49 return len(urlStr) < 8192 50 } 51 52 // Ensure that tb.MetadataResult object exists and contains enough pre-fetched API data to be able 53 // to resolve all object listed in tb to GraphQL IDs. 54 func fillMetadata(client *api.Client, baseRepo ghrepo.Interface, tb *IssueMetadataState) error { 55 resolveInput := api.RepoResolveInput{} 56 57 if len(tb.Assignees) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.AssignableUsers) == 0) { 58 resolveInput.Assignees = tb.Assignees 59 } 60 61 if len(tb.Reviewers) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.AssignableUsers) == 0) { 62 resolveInput.Reviewers = tb.Reviewers 63 } 64 65 if len(tb.Labels) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.Labels) == 0) { 66 resolveInput.Labels = tb.Labels 67 } 68 69 if len(tb.Projects) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.Projects) == 0) { 70 resolveInput.Projects = tb.Projects 71 } 72 73 if len(tb.Milestones) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.Milestones) == 0) { 74 resolveInput.Milestones = tb.Milestones 75 } 76 77 metadataResult, err := api.RepoResolveMetadataIDs(client, baseRepo, resolveInput) 78 if err != nil { 79 return err 80 } 81 82 if tb.MetadataResult == nil { 83 tb.MetadataResult = metadataResult 84 } else { 85 tb.MetadataResult.Merge(metadataResult) 86 } 87 88 return nil 89 } 90 91 func AddMetadataToIssueParams(client *api.Client, baseRepo ghrepo.Interface, params map[string]interface{}, tb *IssueMetadataState) error { 92 if !tb.HasMetadata() { 93 return nil 94 } 95 96 if err := fillMetadata(client, baseRepo, tb); err != nil { 97 return err 98 } 99 100 assigneeIDs, err := tb.MetadataResult.MembersToIDs(tb.Assignees) 101 if err != nil { 102 return fmt.Errorf("could not assign user: %w", err) 103 } 104 params["assigneeIds"] = assigneeIDs 105 106 labelIDs, err := tb.MetadataResult.LabelsToIDs(tb.Labels) 107 if err != nil { 108 return fmt.Errorf("could not add label: %w", err) 109 } 110 params["labelIds"] = labelIDs 111 112 projectIDs, err := tb.MetadataResult.ProjectsToIDs(tb.Projects) 113 if err != nil { 114 return fmt.Errorf("could not add to project: %w", err) 115 } 116 params["projectIds"] = projectIDs 117 118 if len(tb.Milestones) > 0 { 119 milestoneID, err := tb.MetadataResult.MilestoneToID(tb.Milestones[0]) 120 if err != nil { 121 return fmt.Errorf("could not add to milestone '%s': %w", tb.Milestones[0], err) 122 } 123 params["milestoneId"] = milestoneID 124 } 125 126 if len(tb.Reviewers) == 0 { 127 return nil 128 } 129 130 var userReviewers []string 131 var teamReviewers []string 132 for _, r := range tb.Reviewers { 133 if strings.ContainsRune(r, '/') { 134 teamReviewers = append(teamReviewers, r) 135 } else { 136 userReviewers = append(userReviewers, r) 137 } 138 } 139 140 userReviewerIDs, err := tb.MetadataResult.MembersToIDs(userReviewers) 141 if err != nil { 142 return fmt.Errorf("could not request reviewer: %w", err) 143 } 144 params["userReviewerIds"] = userReviewerIDs 145 146 teamReviewerIDs, err := tb.MetadataResult.TeamsToIDs(teamReviewers) 147 if err != nil { 148 return fmt.Errorf("could not request reviewer: %w", err) 149 } 150 params["teamReviewerIds"] = teamReviewerIDs 151 152 return nil 153 } 154 155 type FilterOptions struct { 156 Assignee string 157 Author string 158 BaseBranch string 159 Draft *bool 160 Entity string 161 Fields []string 162 HeadBranch string 163 Labels []string 164 Mention string 165 Milestone string 166 Repo string 167 Search string 168 State string 169 } 170 171 func (opts *FilterOptions) IsDefault() bool { 172 if opts.State != "open" { 173 return false 174 } 175 if len(opts.Labels) > 0 { 176 return false 177 } 178 if opts.Assignee != "" { 179 return false 180 } 181 if opts.Author != "" { 182 return false 183 } 184 if opts.BaseBranch != "" { 185 return false 186 } 187 if opts.HeadBranch != "" { 188 return false 189 } 190 if opts.Mention != "" { 191 return false 192 } 193 if opts.Milestone != "" { 194 return false 195 } 196 if opts.Search != "" { 197 return false 198 } 199 return true 200 } 201 202 func ListURLWithQuery(listURL string, options FilterOptions) (string, error) { 203 u, err := url.Parse(listURL) 204 if err != nil { 205 return "", err 206 } 207 208 params := u.Query() 209 params.Set("q", SearchQueryBuild(options)) 210 u.RawQuery = params.Encode() 211 212 return u.String(), nil 213 } 214 215 func SearchQueryBuild(options FilterOptions) string { 216 var is, state string 217 switch options.State { 218 case "open", "closed": 219 state = options.State 220 case "merged": 221 is = "merged" 222 } 223 q := search.Query{ 224 Qualifiers: search.Qualifiers{ 225 Assignee: options.Assignee, 226 Author: options.Author, 227 Base: options.BaseBranch, 228 Draft: options.Draft, 229 Head: options.HeadBranch, 230 Label: options.Labels, 231 Mentions: options.Mention, 232 Milestone: options.Milestone, 233 Repo: []string{options.Repo}, 234 State: state, 235 Is: []string{is}, 236 Type: options.Entity, 237 }, 238 } 239 if options.Search != "" { 240 return fmt.Sprintf("%s %s", options.Search, q.String()) 241 } 242 return q.String() 243 } 244 245 func QueryHasStateClause(searchQuery string) bool { 246 argv, err := shlex.Split(searchQuery) 247 if err != nil { 248 return false 249 } 250 251 for _, arg := range argv { 252 if arg == "is:closed" || arg == "is:merged" || arg == "state:closed" || arg == "state:merged" || strings.HasPrefix(arg, "merged:") || strings.HasPrefix(arg, "closed:") { 253 return true 254 } 255 } 256 257 return false 258 } 259 260 // MeReplacer resolves usages of `@me` to the handle of the currently logged in user. 261 type MeReplacer struct { 262 apiClient *api.Client 263 hostname string 264 login string 265 } 266 267 func NewMeReplacer(apiClient *api.Client, hostname string) *MeReplacer { 268 return &MeReplacer{ 269 apiClient: apiClient, 270 hostname: hostname, 271 } 272 } 273 274 func (r *MeReplacer) currentLogin() (string, error) { 275 if r.login != "" { 276 return r.login, nil 277 } 278 login, err := api.CurrentLoginName(r.apiClient, r.hostname) 279 if err != nil { 280 return "", fmt.Errorf("failed resolving `@me` to your user handle: %w", err) 281 } 282 r.login = login 283 return login, nil 284 } 285 286 func (r *MeReplacer) Replace(handle string) (string, error) { 287 if handle == "@me" { 288 return r.currentLogin() 289 } 290 return handle, nil 291 } 292 293 func (r *MeReplacer) ReplaceSlice(handles []string) ([]string, error) { 294 res := make([]string, len(handles)) 295 for i, h := range handles { 296 var err error 297 res[i], err = r.Replace(h) 298 if err != nil { 299 return nil, err 300 } 301 } 302 return res, nil 303 }