github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/api/queries_pr.go (about)

     1  package api
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"net/url"
     7  	"time"
     8  
     9  	"github.com/ungtb10d/cli/v2/internal/ghrepo"
    10  	"github.com/shurcooL/githubv4"
    11  )
    12  
    13  type PullRequestAndTotalCount struct {
    14  	TotalCount   int
    15  	PullRequests []PullRequest
    16  	SearchCapped bool
    17  }
    18  
    19  type PullRequest struct {
    20  	ID                  string
    21  	Number              int
    22  	Title               string
    23  	State               string
    24  	Closed              bool
    25  	URL                 string
    26  	BaseRefName         string
    27  	HeadRefName         string
    28  	HeadRefOid          string
    29  	Body                string
    30  	Mergeable           string
    31  	Additions           int
    32  	Deletions           int
    33  	ChangedFiles        int
    34  	MergeStateStatus    string
    35  	IsInMergeQueue      bool
    36  	IsMergeQueueEnabled bool // Indicates whether the pull request's base ref has a merge queue enabled.
    37  	CreatedAt           time.Time
    38  	UpdatedAt           time.Time
    39  	ClosedAt            *time.Time
    40  	MergedAt            *time.Time
    41  
    42  	MergeCommit          *Commit
    43  	PotentialMergeCommit *Commit
    44  
    45  	Files struct {
    46  		Nodes []PullRequestFile
    47  	}
    48  
    49  	Author              Author
    50  	MergedBy            *Author
    51  	HeadRepositoryOwner Owner
    52  	HeadRepository      *PRRepository
    53  	IsCrossRepository   bool
    54  	IsDraft             bool
    55  	MaintainerCanModify bool
    56  
    57  	BaseRef struct {
    58  		BranchProtectionRule struct {
    59  			RequiresStrictStatusChecks   bool
    60  			RequiredApprovingReviewCount int
    61  		}
    62  	}
    63  
    64  	ReviewDecision string
    65  
    66  	Commits struct {
    67  		TotalCount int
    68  		Nodes      []PullRequestCommit
    69  	}
    70  	StatusCheckRollup struct {
    71  		Nodes []StatusCheckRollupNode
    72  	}
    73  
    74  	Assignees      Assignees
    75  	Labels         Labels
    76  	ProjectCards   ProjectCards
    77  	Milestone      *Milestone
    78  	Comments       Comments
    79  	ReactionGroups ReactionGroups
    80  	Reviews        PullRequestReviews
    81  	LatestReviews  PullRequestReviews
    82  	ReviewRequests ReviewRequests
    83  }
    84  
    85  type StatusCheckRollupNode struct {
    86  	Commit StatusCheckRollupCommit
    87  }
    88  
    89  type StatusCheckRollupCommit struct {
    90  	StatusCheckRollup CommitStatusCheckRollup
    91  }
    92  
    93  type CommitStatusCheckRollup struct {
    94  	Contexts CheckContexts
    95  }
    96  
    97  type CheckContexts struct {
    98  	Nodes    []CheckContext
    99  	PageInfo struct {
   100  		HasNextPage bool
   101  		EndCursor   string
   102  	}
   103  }
   104  
   105  type CheckContext struct {
   106  	TypeName   string `json:"__typename"`
   107  	Name       string `json:"name"`
   108  	IsRequired bool   `json:"isRequired"`
   109  	CheckSuite struct {
   110  		WorkflowRun struct {
   111  			Workflow struct {
   112  				Name string `json:"name"`
   113  			} `json:"workflow"`
   114  		} `json:"workflowRun"`
   115  	} `json:"checkSuite"`
   116  	// QUEUED IN_PROGRESS COMPLETED WAITING PENDING REQUESTED
   117  	Status string `json:"status"`
   118  	// ACTION_REQUIRED TIMED_OUT CANCELLED FAILURE SUCCESS NEUTRAL SKIPPED STARTUP_FAILURE STALE
   119  	Conclusion  string    `json:"conclusion"`
   120  	StartedAt   time.Time `json:"startedAt"`
   121  	CompletedAt time.Time `json:"completedAt"`
   122  	DetailsURL  string    `json:"detailsUrl"`
   123  
   124  	/* StatusContext fields */
   125  
   126  	Context string `json:"context"`
   127  	// EXPECTED ERROR FAILURE PENDING SUCCESS
   128  	State     string    `json:"state"`
   129  	TargetURL string    `json:"targetUrl"`
   130  	CreatedAt time.Time `json:"createdAt"`
   131  }
   132  
   133  type PRRepository struct {
   134  	ID   string `json:"id"`
   135  	Name string `json:"name"`
   136  }
   137  
   138  // Commit loads just the commit SHA and nothing else
   139  type Commit struct {
   140  	OID string `json:"oid"`
   141  }
   142  
   143  type PullRequestCommit struct {
   144  	Commit PullRequestCommitCommit
   145  }
   146  
   147  // PullRequestCommitCommit contains full information about a commit
   148  type PullRequestCommitCommit struct {
   149  	OID     string `json:"oid"`
   150  	Authors struct {
   151  		Nodes []struct {
   152  			Name  string
   153  			Email string
   154  			User  GitHubUser
   155  		}
   156  	}
   157  	MessageHeadline string
   158  	MessageBody     string
   159  	CommittedDate   time.Time
   160  	AuthoredDate    time.Time
   161  }
   162  
   163  type PullRequestFile struct {
   164  	Path      string `json:"path"`
   165  	Additions int    `json:"additions"`
   166  	Deletions int    `json:"deletions"`
   167  }
   168  
   169  type ReviewRequests struct {
   170  	Nodes []struct {
   171  		RequestedReviewer RequestedReviewer
   172  	}
   173  }
   174  
   175  type RequestedReviewer struct {
   176  	TypeName     string `json:"__typename"`
   177  	Login        string `json:"login"`
   178  	Name         string `json:"name"`
   179  	Slug         string `json:"slug"`
   180  	Organization struct {
   181  		Login string `json:"login"`
   182  	} `json:"organization"`
   183  }
   184  
   185  func (r RequestedReviewer) LoginOrSlug() string {
   186  	if r.TypeName == teamTypeName {
   187  		return fmt.Sprintf("%s/%s", r.Organization.Login, r.Slug)
   188  	}
   189  	return r.Login
   190  }
   191  
   192  const teamTypeName = "Team"
   193  
   194  func (r ReviewRequests) Logins() []string {
   195  	logins := make([]string, len(r.Nodes))
   196  	for i, r := range r.Nodes {
   197  		logins[i] = r.RequestedReviewer.LoginOrSlug()
   198  	}
   199  	return logins
   200  }
   201  
   202  func (pr PullRequest) HeadLabel() string {
   203  	if pr.IsCrossRepository {
   204  		return fmt.Sprintf("%s:%s", pr.HeadRepositoryOwner.Login, pr.HeadRefName)
   205  	}
   206  	return pr.HeadRefName
   207  }
   208  
   209  func (pr PullRequest) Link() string {
   210  	return pr.URL
   211  }
   212  
   213  func (pr PullRequest) Identifier() string {
   214  	return pr.ID
   215  }
   216  
   217  func (pr PullRequest) CurrentUserComments() []Comment {
   218  	return pr.Comments.CurrentUserComments()
   219  }
   220  
   221  func (pr PullRequest) IsOpen() bool {
   222  	return pr.State == "OPEN"
   223  }
   224  
   225  type PullRequestReviewStatus struct {
   226  	ChangesRequested bool
   227  	Approved         bool
   228  	ReviewRequired   bool
   229  }
   230  
   231  func (pr *PullRequest) ReviewStatus() PullRequestReviewStatus {
   232  	var status PullRequestReviewStatus
   233  	switch pr.ReviewDecision {
   234  	case "CHANGES_REQUESTED":
   235  		status.ChangesRequested = true
   236  	case "APPROVED":
   237  		status.Approved = true
   238  	case "REVIEW_REQUIRED":
   239  		status.ReviewRequired = true
   240  	}
   241  	return status
   242  }
   243  
   244  type PullRequestChecksStatus struct {
   245  	Pending int
   246  	Failing int
   247  	Passing int
   248  	Total   int
   249  }
   250  
   251  func (pr *PullRequest) ChecksStatus() (summary PullRequestChecksStatus) {
   252  	if len(pr.StatusCheckRollup.Nodes) == 0 {
   253  		return
   254  	}
   255  	commit := pr.StatusCheckRollup.Nodes[0].Commit
   256  	for _, c := range commit.StatusCheckRollup.Contexts.Nodes {
   257  		state := c.State // StatusContext
   258  		if state == "" {
   259  			// CheckRun
   260  			if c.Status == "COMPLETED" {
   261  				state = c.Conclusion
   262  			} else {
   263  				state = c.Status
   264  			}
   265  		}
   266  		switch state {
   267  		case "SUCCESS", "NEUTRAL", "SKIPPED":
   268  			summary.Passing++
   269  		case "ERROR", "FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED":
   270  			summary.Failing++
   271  		default: // "EXPECTED", "REQUESTED", "WAITING", "QUEUED", "PENDING", "IN_PROGRESS", "STALE"
   272  			summary.Pending++
   273  		}
   274  		summary.Total++
   275  	}
   276  
   277  	return
   278  }
   279  
   280  func (pr *PullRequest) DisplayableReviews() PullRequestReviews {
   281  	published := []PullRequestReview{}
   282  	for _, prr := range pr.Reviews.Nodes {
   283  		//Dont display pending reviews
   284  		//Dont display commenting reviews without top level comment body
   285  		if prr.State != "PENDING" && !(prr.State == "COMMENTED" && prr.Body == "") {
   286  			published = append(published, prr)
   287  		}
   288  	}
   289  	return PullRequestReviews{Nodes: published, TotalCount: len(published)}
   290  }
   291  
   292  // CreatePullRequest creates a pull request in a GitHub repository
   293  func CreatePullRequest(client *Client, repo *Repository, params map[string]interface{}) (*PullRequest, error) {
   294  	query := `
   295  		mutation PullRequestCreate($input: CreatePullRequestInput!) {
   296  			createPullRequest(input: $input) {
   297  				pullRequest {
   298  					id
   299  					url
   300  				}
   301  			}
   302  	}`
   303  
   304  	inputParams := map[string]interface{}{
   305  		"repositoryId": repo.ID,
   306  	}
   307  	for key, val := range params {
   308  		switch key {
   309  		case "title", "body", "draft", "baseRefName", "headRefName", "maintainerCanModify":
   310  			inputParams[key] = val
   311  		}
   312  	}
   313  	variables := map[string]interface{}{
   314  		"input": inputParams,
   315  	}
   316  
   317  	result := struct {
   318  		CreatePullRequest struct {
   319  			PullRequest PullRequest
   320  		}
   321  	}{}
   322  
   323  	err := client.GraphQL(repo.RepoHost(), query, variables, &result)
   324  	if err != nil {
   325  		return nil, err
   326  	}
   327  	pr := &result.CreatePullRequest.PullRequest
   328  
   329  	// metadata parameters aren't currently available in `createPullRequest`,
   330  	// but they are in `updatePullRequest`
   331  	updateParams := make(map[string]interface{})
   332  	for key, val := range params {
   333  		switch key {
   334  		case "assigneeIds", "labelIds", "projectIds", "milestoneId":
   335  			if !isBlank(val) {
   336  				updateParams[key] = val
   337  			}
   338  		}
   339  	}
   340  	if len(updateParams) > 0 {
   341  		updateQuery := `
   342  		mutation PullRequestCreateMetadata($input: UpdatePullRequestInput!) {
   343  			updatePullRequest(input: $input) { clientMutationId }
   344  		}`
   345  		updateParams["pullRequestId"] = pr.ID
   346  		variables := map[string]interface{}{
   347  			"input": updateParams,
   348  		}
   349  		err := client.GraphQL(repo.RepoHost(), updateQuery, variables, &result)
   350  		if err != nil {
   351  			return pr, err
   352  		}
   353  	}
   354  
   355  	// reviewers are requested in yet another additional mutation
   356  	reviewParams := make(map[string]interface{})
   357  	if ids, ok := params["userReviewerIds"]; ok && !isBlank(ids) {
   358  		reviewParams["userIds"] = ids
   359  	}
   360  	if ids, ok := params["teamReviewerIds"]; ok && !isBlank(ids) {
   361  		reviewParams["teamIds"] = ids
   362  	}
   363  
   364  	//TODO: How much work to extract this into own method and use for create and edit?
   365  	if len(reviewParams) > 0 {
   366  		reviewQuery := `
   367  		mutation PullRequestCreateRequestReviews($input: RequestReviewsInput!) {
   368  			requestReviews(input: $input) { clientMutationId }
   369  		}`
   370  		reviewParams["pullRequestId"] = pr.ID
   371  		reviewParams["union"] = true
   372  		variables := map[string]interface{}{
   373  			"input": reviewParams,
   374  		}
   375  		err := client.GraphQL(repo.RepoHost(), reviewQuery, variables, &result)
   376  		if err != nil {
   377  			return pr, err
   378  		}
   379  	}
   380  
   381  	return pr, nil
   382  }
   383  
   384  func UpdatePullRequestReviews(client *Client, repo ghrepo.Interface, params githubv4.RequestReviewsInput) error {
   385  	var mutation struct {
   386  		RequestReviews struct {
   387  			PullRequest struct {
   388  				ID string
   389  			}
   390  		} `graphql:"requestReviews(input: $input)"`
   391  	}
   392  	variables := map[string]interface{}{"input": params}
   393  	err := client.Mutate(repo.RepoHost(), "PullRequestUpdateRequestReviews", &mutation, variables)
   394  	return err
   395  }
   396  
   397  func isBlank(v interface{}) bool {
   398  	switch vv := v.(type) {
   399  	case string:
   400  		return vv == ""
   401  	case []string:
   402  		return len(vv) == 0
   403  	default:
   404  		return true
   405  	}
   406  }
   407  
   408  func PullRequestClose(httpClient *http.Client, repo ghrepo.Interface, prID string) error {
   409  	var mutation struct {
   410  		ClosePullRequest struct {
   411  			PullRequest struct {
   412  				ID githubv4.ID
   413  			}
   414  		} `graphql:"closePullRequest(input: $input)"`
   415  	}
   416  
   417  	variables := map[string]interface{}{
   418  		"input": githubv4.ClosePullRequestInput{
   419  			PullRequestID: prID,
   420  		},
   421  	}
   422  
   423  	client := NewClientFromHTTP(httpClient)
   424  	return client.Mutate(repo.RepoHost(), "PullRequestClose", &mutation, variables)
   425  }
   426  
   427  func PullRequestReopen(httpClient *http.Client, repo ghrepo.Interface, prID string) error {
   428  	var mutation struct {
   429  		ReopenPullRequest struct {
   430  			PullRequest struct {
   431  				ID githubv4.ID
   432  			}
   433  		} `graphql:"reopenPullRequest(input: $input)"`
   434  	}
   435  
   436  	variables := map[string]interface{}{
   437  		"input": githubv4.ReopenPullRequestInput{
   438  			PullRequestID: prID,
   439  		},
   440  	}
   441  
   442  	client := NewClientFromHTTP(httpClient)
   443  	return client.Mutate(repo.RepoHost(), "PullRequestReopen", &mutation, variables)
   444  }
   445  
   446  func PullRequestReady(client *Client, repo ghrepo.Interface, pr *PullRequest) error {
   447  	var mutation struct {
   448  		MarkPullRequestReadyForReview struct {
   449  			PullRequest struct {
   450  				ID githubv4.ID
   451  			}
   452  		} `graphql:"markPullRequestReadyForReview(input: $input)"`
   453  	}
   454  
   455  	variables := map[string]interface{}{
   456  		"input": githubv4.MarkPullRequestReadyForReviewInput{
   457  			PullRequestID: pr.ID,
   458  		},
   459  	}
   460  
   461  	return client.Mutate(repo.RepoHost(), "PullRequestReadyForReview", &mutation, variables)
   462  }
   463  
   464  func ConvertPullRequestToDraft(client *Client, repo ghrepo.Interface, pr *PullRequest) error {
   465  	var mutation struct {
   466  		ConvertPullRequestToDraft struct {
   467  			PullRequest struct {
   468  				ID githubv4.ID
   469  			}
   470  		} `graphql:"convertPullRequestToDraft(input: $input)"`
   471  	}
   472  
   473  	variables := map[string]interface{}{
   474  		"input": githubv4.ConvertPullRequestToDraftInput{
   475  			PullRequestID: pr.ID,
   476  		},
   477  	}
   478  
   479  	return client.Mutate(repo.RepoHost(), "ConvertPullRequestToDraft", &mutation, variables)
   480  }
   481  
   482  func BranchDeleteRemote(client *Client, repo ghrepo.Interface, branch string) error {
   483  	path := fmt.Sprintf("repos/%s/%s/git/refs/heads/%s", repo.RepoOwner(), repo.RepoName(), url.PathEscape(branch))
   484  	return client.REST(repo.RepoHost(), "DELETE", path, nil, nil)
   485  }