cli/pkg/cmd/pr/shared/params.go
Mislav Marohnić f6b33572fd
Merge pull request #4279 from SiarheiFedartsou/sf-pr-list-head-filter
Add `--head` filter to `gh pr list`
2021-10-14 20:54:56 +02:00

315 lines
7.5 KiB
Go

package shared
import (
"fmt"
"net/url"
"strings"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/githubsearch"
"github.com/google/shlex"
)
func WithPrAndIssueQueryParams(client *api.Client, baseRepo ghrepo.Interface, baseURL string, state IssueMetadataState) (string, error) {
u, err := url.Parse(baseURL)
if err != nil {
return "", err
}
q := u.Query()
if state.Title != "" {
q.Set("title", state.Title)
}
// We always want to send the body parameter, even if it's empty, to prevent the web interface from
// applying the default template. Since the user has the option to select a template in the terminal,
// assume that empty body here means that the user either skipped it or erased its contents.
q.Set("body", state.Body)
if len(state.Assignees) > 0 {
q.Set("assignees", strings.Join(state.Assignees, ","))
}
if len(state.Labels) > 0 {
q.Set("labels", strings.Join(state.Labels, ","))
}
if len(state.Projects) > 0 {
projectPaths, err := api.ProjectNamesToPaths(client, baseRepo, state.Projects)
if err != nil {
return "", fmt.Errorf("could not add to project: %w", err)
}
q.Set("projects", strings.Join(projectPaths, ","))
}
if len(state.Milestones) > 0 {
q.Set("milestone", state.Milestones[0])
}
u.RawQuery = q.Encode()
return u.String(), nil
}
// Ensure that tb.MetadataResult object exists and contains enough pre-fetched API data to be able
// to resolve all object listed in tb to GraphQL IDs.
func fillMetadata(client *api.Client, baseRepo ghrepo.Interface, tb *IssueMetadataState) error {
resolveInput := api.RepoResolveInput{}
if len(tb.Assignees) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.AssignableUsers) == 0) {
resolveInput.Assignees = tb.Assignees
}
if len(tb.Reviewers) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.AssignableUsers) == 0) {
resolveInput.Reviewers = tb.Reviewers
}
if len(tb.Labels) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.Labels) == 0) {
resolveInput.Labels = tb.Labels
}
if len(tb.Projects) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.Projects) == 0) {
resolveInput.Projects = tb.Projects
}
if len(tb.Milestones) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.Milestones) == 0) {
resolveInput.Milestones = tb.Milestones
}
metadataResult, err := api.RepoResolveMetadataIDs(client, baseRepo, resolveInput)
if err != nil {
return err
}
if tb.MetadataResult == nil {
tb.MetadataResult = metadataResult
} else {
tb.MetadataResult.Merge(metadataResult)
}
return nil
}
func AddMetadataToIssueParams(client *api.Client, baseRepo ghrepo.Interface, params map[string]interface{}, tb *IssueMetadataState) error {
if !tb.HasMetadata() {
return nil
}
if err := fillMetadata(client, baseRepo, tb); err != nil {
return err
}
assigneeIDs, err := tb.MetadataResult.MembersToIDs(tb.Assignees)
if err != nil {
return fmt.Errorf("could not assign user: %w", err)
}
params["assigneeIds"] = assigneeIDs
labelIDs, err := tb.MetadataResult.LabelsToIDs(tb.Labels)
if err != nil {
return fmt.Errorf("could not add label: %w", err)
}
params["labelIds"] = labelIDs
projectIDs, err := tb.MetadataResult.ProjectsToIDs(tb.Projects)
if err != nil {
return fmt.Errorf("could not add to project: %w", err)
}
params["projectIds"] = projectIDs
if len(tb.Milestones) > 0 {
milestoneID, err := tb.MetadataResult.MilestoneToID(tb.Milestones[0])
if err != nil {
return fmt.Errorf("could not add to milestone '%s': %w", tb.Milestones[0], err)
}
params["milestoneId"] = milestoneID
}
if len(tb.Reviewers) == 0 {
return nil
}
var userReviewers []string
var teamReviewers []string
for _, r := range tb.Reviewers {
if strings.ContainsRune(r, '/') {
teamReviewers = append(teamReviewers, r)
} else {
userReviewers = append(userReviewers, r)
}
}
userReviewerIDs, err := tb.MetadataResult.MembersToIDs(userReviewers)
if err != nil {
return fmt.Errorf("could not request reviewer: %w", err)
}
params["userReviewerIds"] = userReviewerIDs
teamReviewerIDs, err := tb.MetadataResult.TeamsToIDs(teamReviewers)
if err != nil {
return fmt.Errorf("could not request reviewer: %w", err)
}
params["teamReviewerIds"] = teamReviewerIDs
return nil
}
type FilterOptions struct {
Entity string
State string
Assignee string
Labels []string
Author string
BaseBranch string
HeadBranch string
Mention string
Milestone string
Search string
Draft string
Fields []string
}
func (opts *FilterOptions) IsDefault() bool {
if opts.State != "open" {
return false
}
if len(opts.Labels) > 0 {
return false
}
if opts.Assignee != "" {
return false
}
if opts.Author != "" {
return false
}
if opts.BaseBranch != "" {
return false
}
if opts.HeadBranch != "" {
return false
}
if opts.Mention != "" {
return false
}
if opts.Milestone != "" {
return false
}
if opts.Search != "" {
return false
}
return true
}
func ListURLWithQuery(listURL string, options FilterOptions) (string, error) {
u, err := url.Parse(listURL)
if err != nil {
return "", err
}
params := u.Query()
params.Set("q", SearchQueryBuild(options))
u.RawQuery = params.Encode()
return u.String(), nil
}
func SearchQueryBuild(options FilterOptions) string {
q := githubsearch.NewQuery()
switch options.Entity {
case "issue":
q.SetType(githubsearch.Issue)
case "pr":
q.SetType(githubsearch.PullRequest)
}
switch options.State {
case "open":
q.SetState(githubsearch.Open)
case "closed":
q.SetState(githubsearch.Closed)
case "merged":
q.SetState(githubsearch.Merged)
}
if options.Assignee != "" {
q.AssignedTo(options.Assignee)
}
for _, label := range options.Labels {
q.AddLabel(label)
}
if options.Author != "" {
q.AuthoredBy(options.Author)
}
if options.BaseBranch != "" {
q.SetBaseBranch(options.BaseBranch)
}
if options.HeadBranch != "" {
q.SetHeadBranch(options.HeadBranch)
}
if options.Mention != "" {
q.Mentions(options.Mention)
}
if options.Milestone != "" {
q.InMilestone(options.Milestone)
}
if options.Search != "" {
q.AddQuery(options.Search)
}
if options.Draft != "" {
q.SetDraft(options.Draft)
}
return q.String()
}
func QueryHasStateClause(searchQuery string) bool {
argv, err := shlex.Split(searchQuery)
if err != nil {
return false
}
for _, arg := range argv {
if arg == "is:closed" || arg == "is:merged" || arg == "state:closed" || arg == "state:merged" || strings.HasPrefix(arg, "merged:") || strings.HasPrefix(arg, "closed:") {
return true
}
}
return false
}
// MeReplacer resolves usages of `@me` to the handle of the currently logged in user.
type MeReplacer struct {
apiClient *api.Client
hostname string
login string
}
func NewMeReplacer(apiClient *api.Client, hostname string) *MeReplacer {
return &MeReplacer{
apiClient: apiClient,
hostname: hostname,
}
}
func (r *MeReplacer) currentLogin() (string, error) {
if r.login != "" {
return r.login, nil
}
login, err := api.CurrentLoginName(r.apiClient, r.hostname)
if err != nil {
return "", fmt.Errorf("failed resolving `@me` to your user handle: %w", err)
}
r.login = login
return login, nil
}
func (r *MeReplacer) Replace(handle string) (string, error) {
if handle == "@me" {
return r.currentLogin()
}
return handle, nil
}
func (r *MeReplacer) ReplaceSlice(handles []string) ([]string, error) {
res := make([]string, len(handles))
for i, h := range handles {
var err error
res[i], err = r.Replace(h)
if err != nil {
return nil, err
}
}
return res, nil
}