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  }