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  }