1// Copyright 2013 The go-github AUTHORS. All rights reserved. 2// 3// Use of this source code is governed by a BSD-style 4// license that can be found in the LICENSE file. 5 6package github 7 8import ( 9 "bytes" 10 "context" 11 "fmt" 12 "strings" 13 "time" 14) 15 16// PullRequestsService handles communication with the pull request related 17// methods of the GitHub API. 18// 19// GitHub API docs: https://developer.github.com/v3/pulls/ 20type PullRequestsService service 21 22// PullRequest represents a GitHub pull request on a repository. 23type PullRequest struct { 24 ID *int64 `json:"id,omitempty"` 25 Number *int `json:"number,omitempty"` 26 State *string `json:"state,omitempty"` 27 Locked *bool `json:"locked,omitempty"` 28 Title *string `json:"title,omitempty"` 29 Body *string `json:"body,omitempty"` 30 CreatedAt *time.Time `json:"created_at,omitempty"` 31 UpdatedAt *time.Time `json:"updated_at,omitempty"` 32 ClosedAt *time.Time `json:"closed_at,omitempty"` 33 MergedAt *time.Time `json:"merged_at,omitempty"` 34 Labels []*Label `json:"labels,omitempty"` 35 User *User `json:"user,omitempty"` 36 Draft *bool `json:"draft,omitempty"` 37 Merged *bool `json:"merged,omitempty"` 38 Mergeable *bool `json:"mergeable,omitempty"` 39 MergeableState *string `json:"mergeable_state,omitempty"` 40 MergedBy *User `json:"merged_by,omitempty"` 41 MergeCommitSHA *string `json:"merge_commit_sha,omitempty"` 42 Rebaseable *bool `json:"rebaseable,omitempty"` 43 Comments *int `json:"comments,omitempty"` 44 Commits *int `json:"commits,omitempty"` 45 Additions *int `json:"additions,omitempty"` 46 Deletions *int `json:"deletions,omitempty"` 47 ChangedFiles *int `json:"changed_files,omitempty"` 48 URL *string `json:"url,omitempty"` 49 HTMLURL *string `json:"html_url,omitempty"` 50 IssueURL *string `json:"issue_url,omitempty"` 51 StatusesURL *string `json:"statuses_url,omitempty"` 52 DiffURL *string `json:"diff_url,omitempty"` 53 PatchURL *string `json:"patch_url,omitempty"` 54 CommitsURL *string `json:"commits_url,omitempty"` 55 CommentsURL *string `json:"comments_url,omitempty"` 56 ReviewCommentsURL *string `json:"review_comments_url,omitempty"` 57 ReviewCommentURL *string `json:"review_comment_url,omitempty"` 58 ReviewComments *int `json:"review_comments,omitempty"` 59 Assignee *User `json:"assignee,omitempty"` 60 Assignees []*User `json:"assignees,omitempty"` 61 Milestone *Milestone `json:"milestone,omitempty"` 62 MaintainerCanModify *bool `json:"maintainer_can_modify,omitempty"` 63 AuthorAssociation *string `json:"author_association,omitempty"` 64 NodeID *string `json:"node_id,omitempty"` 65 RequestedReviewers []*User `json:"requested_reviewers,omitempty"` 66 67 // RequestedTeams is populated as part of the PullRequestEvent. 68 // See, https://developer.github.com/v3/activity/events/types/#pullrequestevent for an example. 69 RequestedTeams []*Team `json:"requested_teams,omitempty"` 70 71 Links *PRLinks `json:"_links,omitempty"` 72 Head *PullRequestBranch `json:"head,omitempty"` 73 Base *PullRequestBranch `json:"base,omitempty"` 74 75 // ActiveLockReason is populated only when LockReason is provided while locking the pull request. 76 // Possible values are: "off-topic", "too heated", "resolved", and "spam". 77 ActiveLockReason *string `json:"active_lock_reason,omitempty"` 78} 79 80func (p PullRequest) String() string { 81 return Stringify(p) 82} 83 84// PRLink represents a single link object from Github pull request _links. 85type PRLink struct { 86 HRef *string `json:"href,omitempty"` 87} 88 89// PRLinks represents the "_links" object in a Github pull request. 90type PRLinks struct { 91 Self *PRLink `json:"self,omitempty"` 92 HTML *PRLink `json:"html,omitempty"` 93 Issue *PRLink `json:"issue,omitempty"` 94 Comments *PRLink `json:"comments,omitempty"` 95 ReviewComments *PRLink `json:"review_comments,omitempty"` 96 ReviewComment *PRLink `json:"review_comment,omitempty"` 97 Commits *PRLink `json:"commits,omitempty"` 98 Statuses *PRLink `json:"statuses,omitempty"` 99} 100 101// PullRequestBranch represents a base or head branch in a GitHub pull request. 102type PullRequestBranch struct { 103 Label *string `json:"label,omitempty"` 104 Ref *string `json:"ref,omitempty"` 105 SHA *string `json:"sha,omitempty"` 106 Repo *Repository `json:"repo,omitempty"` 107 User *User `json:"user,omitempty"` 108} 109 110// PullRequestListOptions specifies the optional parameters to the 111// PullRequestsService.List method. 112type PullRequestListOptions struct { 113 // State filters pull requests based on their state. Possible values are: 114 // open, closed, all. Default is "open". 115 State string `url:"state,omitempty"` 116 117 // Head filters pull requests by head user and branch name in the format of: 118 // "user:ref-name". 119 Head string `url:"head,omitempty"` 120 121 // Base filters pull requests by base branch name. 122 Base string `url:"base,omitempty"` 123 124 // Sort specifies how to sort pull requests. Possible values are: created, 125 // updated, popularity, long-running. Default is "created". 126 Sort string `url:"sort,omitempty"` 127 128 // Direction in which to sort pull requests. Possible values are: asc, desc. 129 // If Sort is "created" or not specified, Default is "desc", otherwise Default 130 // is "asc" 131 Direction string `url:"direction,omitempty"` 132 133 ListOptions 134} 135 136// List the pull requests for the specified repository. 137// 138// GitHub API docs: https://developer.github.com/v3/pulls/#list-pull-requests 139func (s *PullRequestsService) List(ctx context.Context, owner string, repo string, opt *PullRequestListOptions) ([]*PullRequest, *Response, error) { 140 u := fmt.Sprintf("repos/%v/%v/pulls", owner, repo) 141 u, err := addOptions(u, opt) 142 if err != nil { 143 return nil, nil, err 144 } 145 146 req, err := s.client.NewRequest("GET", u, nil) 147 if err != nil { 148 return nil, nil, err 149 } 150 151 // TODO: remove custom Accept header when this API fully launches. 152 acceptHeaders := []string{mediaTypeLabelDescriptionSearchPreview, mediaTypeLockReasonPreview, mediaTypeDraftPreview} 153 req.Header.Set("Accept", strings.Join(acceptHeaders, ", ")) 154 155 var pulls []*PullRequest 156 resp, err := s.client.Do(ctx, req, &pulls) 157 if err != nil { 158 return nil, resp, err 159 } 160 161 return pulls, resp, nil 162} 163 164// ListPullRequestsWithCommit returns pull requests associated with a commit SHA. 165// 166// The results will include open and closed pull requests. 167// 168// GitHub API docs: https://developer.github.com/v3/repos/commits/#list-pull-requests-associated-with-commit 169func (s *PullRequestsService) ListPullRequestsWithCommit(ctx context.Context, owner, repo, sha string, opt *PullRequestListOptions) ([]*PullRequest, *Response, error) { 170 u := fmt.Sprintf("repos/%v/%v/commits/%v/pulls", owner, repo, sha) 171 u, err := addOptions(u, opt) 172 if err != nil { 173 return nil, nil, err 174 } 175 176 req, err := s.client.NewRequest("GET", u, nil) 177 if err != nil { 178 return nil, nil, err 179 } 180 181 // TODO: remove custom Accept header when this API fully launches. 182 acceptHeaders := []string{mediaTypeListPullsOrBranchesForCommitPreview, mediaTypeDraftPreview, mediaTypeLabelDescriptionSearchPreview, mediaTypeLockReasonPreview} 183 req.Header.Set("Accept", strings.Join(acceptHeaders, ", ")) 184 var pulls []*PullRequest 185 resp, err := s.client.Do(ctx, req, &pulls) 186 if err != nil { 187 return nil, resp, err 188 } 189 190 return pulls, resp, nil 191} 192 193// Get a single pull request. 194// 195// GitHub API docs: https://developer.github.com/v3/pulls/#get-a-single-pull-request 196func (s *PullRequestsService) Get(ctx context.Context, owner string, repo string, number int) (*PullRequest, *Response, error) { 197 u := fmt.Sprintf("repos/%v/%v/pulls/%d", owner, repo, number) 198 req, err := s.client.NewRequest("GET", u, nil) 199 if err != nil { 200 return nil, nil, err 201 } 202 203 // TODO: remove custom Accept header when this API fully launches. 204 acceptHeaders := []string{mediaTypeLabelDescriptionSearchPreview, mediaTypeLockReasonPreview, mediaTypeDraftPreview} 205 req.Header.Set("Accept", strings.Join(acceptHeaders, ", ")) 206 207 pull := new(PullRequest) 208 resp, err := s.client.Do(ctx, req, pull) 209 if err != nil { 210 return nil, resp, err 211 } 212 213 return pull, resp, nil 214} 215 216// GetRaw gets a single pull request in raw (diff or patch) format. 217func (s *PullRequestsService) GetRaw(ctx context.Context, owner string, repo string, number int, opt RawOptions) (string, *Response, error) { 218 u := fmt.Sprintf("repos/%v/%v/pulls/%d", owner, repo, number) 219 req, err := s.client.NewRequest("GET", u, nil) 220 if err != nil { 221 return "", nil, err 222 } 223 224 switch opt.Type { 225 case Diff: 226 req.Header.Set("Accept", mediaTypeV3Diff) 227 case Patch: 228 req.Header.Set("Accept", mediaTypeV3Patch) 229 default: 230 return "", nil, fmt.Errorf("unsupported raw type %d", opt.Type) 231 } 232 233 var buf bytes.Buffer 234 resp, err := s.client.Do(ctx, req, &buf) 235 if err != nil { 236 return "", resp, err 237 } 238 239 return buf.String(), resp, nil 240} 241 242// NewPullRequest represents a new pull request to be created. 243type NewPullRequest struct { 244 Title *string `json:"title,omitempty"` 245 Head *string `json:"head,omitempty"` 246 Base *string `json:"base,omitempty"` 247 Body *string `json:"body,omitempty"` 248 Issue *int `json:"issue,omitempty"` 249 MaintainerCanModify *bool `json:"maintainer_can_modify,omitempty"` 250 Draft *bool `json:"draft,omitempty"` 251} 252 253// Create a new pull request on the specified repository. 254// 255// GitHub API docs: https://developer.github.com/v3/pulls/#create-a-pull-request 256func (s *PullRequestsService) Create(ctx context.Context, owner string, repo string, pull *NewPullRequest) (*PullRequest, *Response, error) { 257 u := fmt.Sprintf("repos/%v/%v/pulls", owner, repo) 258 req, err := s.client.NewRequest("POST", u, pull) 259 if err != nil { 260 return nil, nil, err 261 } 262 263 // TODO: remove custom Accept header when this API fully launches. 264 acceptHeaders := []string{mediaTypeLabelDescriptionSearchPreview, mediaTypeDraftPreview} 265 req.Header.Set("Accept", strings.Join(acceptHeaders, ", ")) 266 267 p := new(PullRequest) 268 resp, err := s.client.Do(ctx, req, p) 269 if err != nil { 270 return nil, resp, err 271 } 272 273 return p, resp, nil 274} 275 276// PullReqestBranchUpdateOptions specifies the optional parameters to the 277// PullRequestsService.UpdateBranch method. 278type PullReqestBranchUpdateOptions struct { 279 // ExpectedHeadSHA specifies the most recent commit on the pull request's branch. 280 // Default value is the SHA of the pull request's current HEAD ref. 281 ExpectedHeadSHA *string `json:"expected_head_sha,omitempty"` 282} 283 284// PullRequestBranchUpdateResponse specifies the response of pull request branch update. 285type PullRequestBranchUpdateResponse struct { 286 Message *string `json:"message,omitempty"` 287 URL *string `json:"url,omitempty"` 288} 289 290// UpdateBranch updates the pull request branch with latest upstream changes. 291// 292// This method might return an AcceptedError and a status code of 293// 202. This is because this is the status that GitHub returns to signify that 294// it has now scheduled the update of the pull request branch in a background task. 295// A follow up request, after a delay of a second or so, should result 296// in a successful request. 297// 298// GitHub API docs: https://developer.github.com/v3/pulls/#update-a-pull-request-branch 299func (s *PullRequestsService) UpdateBranch(ctx context.Context, owner, repo string, number int, opts *PullReqestBranchUpdateOptions) (*PullRequestBranchUpdateResponse, *Response, error) { 300 u := fmt.Sprintf("repos/%v/%v/pulls/%d/update-branch", owner, repo, number) 301 302 req, err := s.client.NewRequest("PUT", u, opts) 303 if err != nil { 304 return nil, nil, err 305 } 306 307 // TODO: remove custom Accept header when this API fully launches. 308 req.Header.Set("Accept", mediaTypeUpdatePullRequestBranchPreview) 309 310 p := new(PullRequestBranchUpdateResponse) 311 resp, err := s.client.Do(ctx, req, p) 312 if err != nil { 313 return nil, resp, err 314 } 315 316 return p, resp, nil 317} 318 319type pullRequestUpdate struct { 320 Title *string `json:"title,omitempty"` 321 Body *string `json:"body,omitempty"` 322 State *string `json:"state,omitempty"` 323 Base *string `json:"base,omitempty"` 324 MaintainerCanModify *bool `json:"maintainer_can_modify,omitempty"` 325} 326 327// Edit a pull request. 328// pull must not be nil. 329// 330// The following fields are editable: Title, Body, State, Base.Ref and MaintainerCanModify. 331// Base.Ref updates the base branch of the pull request. 332// 333// GitHub API docs: https://developer.github.com/v3/pulls/#update-a-pull-request 334func (s *PullRequestsService) Edit(ctx context.Context, owner string, repo string, number int, pull *PullRequest) (*PullRequest, *Response, error) { 335 if pull == nil { 336 return nil, nil, fmt.Errorf("pull must be provided") 337 } 338 339 u := fmt.Sprintf("repos/%v/%v/pulls/%d", owner, repo, number) 340 341 update := &pullRequestUpdate{ 342 Title: pull.Title, 343 Body: pull.Body, 344 State: pull.State, 345 MaintainerCanModify: pull.MaintainerCanModify, 346 } 347 if pull.Base != nil { 348 update.Base = pull.Base.Ref 349 } 350 351 req, err := s.client.NewRequest("PATCH", u, update) 352 if err != nil { 353 return nil, nil, err 354 } 355 356 // TODO: remove custom Accept header when this API fully launches. 357 acceptHeaders := []string{mediaTypeLabelDescriptionSearchPreview, mediaTypeLockReasonPreview} 358 req.Header.Set("Accept", strings.Join(acceptHeaders, ", ")) 359 360 p := new(PullRequest) 361 resp, err := s.client.Do(ctx, req, p) 362 if err != nil { 363 return nil, resp, err 364 } 365 366 return p, resp, nil 367} 368 369// ListCommits lists the commits in a pull request. 370// 371// GitHub API docs: https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request 372func (s *PullRequestsService) ListCommits(ctx context.Context, owner string, repo string, number int, opt *ListOptions) ([]*RepositoryCommit, *Response, error) { 373 u := fmt.Sprintf("repos/%v/%v/pulls/%d/commits", owner, repo, number) 374 u, err := addOptions(u, opt) 375 if err != nil { 376 return nil, nil, err 377 } 378 379 req, err := s.client.NewRequest("GET", u, nil) 380 if err != nil { 381 return nil, nil, err 382 } 383 384 var commits []*RepositoryCommit 385 resp, err := s.client.Do(ctx, req, &commits) 386 if err != nil { 387 return nil, resp, err 388 } 389 390 return commits, resp, nil 391} 392 393// ListFiles lists the files in a pull request. 394// 395// GitHub API docs: https://developer.github.com/v3/pulls/#list-pull-requests-files 396func (s *PullRequestsService) ListFiles(ctx context.Context, owner string, repo string, number int, opt *ListOptions) ([]*CommitFile, *Response, error) { 397 u := fmt.Sprintf("repos/%v/%v/pulls/%d/files", owner, repo, number) 398 u, err := addOptions(u, opt) 399 if err != nil { 400 return nil, nil, err 401 } 402 403 req, err := s.client.NewRequest("GET", u, nil) 404 if err != nil { 405 return nil, nil, err 406 } 407 408 var commitFiles []*CommitFile 409 resp, err := s.client.Do(ctx, req, &commitFiles) 410 if err != nil { 411 return nil, resp, err 412 } 413 414 return commitFiles, resp, nil 415} 416 417// IsMerged checks if a pull request has been merged. 418// 419// GitHub API docs: https://developer.github.com/v3/pulls/#get-if-a-pull-request-has-been-merged 420func (s *PullRequestsService) IsMerged(ctx context.Context, owner string, repo string, number int) (bool, *Response, error) { 421 u := fmt.Sprintf("repos/%v/%v/pulls/%d/merge", owner, repo, number) 422 req, err := s.client.NewRequest("GET", u, nil) 423 if err != nil { 424 return false, nil, err 425 } 426 427 resp, err := s.client.Do(ctx, req, nil) 428 merged, err := parseBoolResponse(err) 429 return merged, resp, err 430} 431 432// PullRequestMergeResult represents the result of merging a pull request. 433type PullRequestMergeResult struct { 434 SHA *string `json:"sha,omitempty"` 435 Merged *bool `json:"merged,omitempty"` 436 Message *string `json:"message,omitempty"` 437} 438 439// PullRequestOptions lets you define how a pull request will be merged. 440type PullRequestOptions struct { 441 CommitTitle string // Extra detail to append to automatic commit message. (Optional.) 442 SHA string // SHA that pull request head must match to allow merge. (Optional.) 443 444 // The merge method to use. Possible values include: "merge", "squash", and "rebase" with the default being merge. (Optional.) 445 MergeMethod string 446} 447 448type pullRequestMergeRequest struct { 449 CommitMessage string `json:"commit_message"` 450 CommitTitle string `json:"commit_title,omitempty"` 451 MergeMethod string `json:"merge_method,omitempty"` 452 SHA string `json:"sha,omitempty"` 453} 454 455// Merge a pull request (Merge Button™). 456// commitMessage is the title for the automatic commit message. 457// 458// GitHub API docs: https://developer.github.com/v3/pulls/#merge-a-pull-request-merge-buttontrade 459func (s *PullRequestsService) Merge(ctx context.Context, owner string, repo string, number int, commitMessage string, options *PullRequestOptions) (*PullRequestMergeResult, *Response, error) { 460 u := fmt.Sprintf("repos/%v/%v/pulls/%d/merge", owner, repo, number) 461 462 pullRequestBody := &pullRequestMergeRequest{CommitMessage: commitMessage} 463 if options != nil { 464 pullRequestBody.CommitTitle = options.CommitTitle 465 pullRequestBody.MergeMethod = options.MergeMethod 466 pullRequestBody.SHA = options.SHA 467 } 468 req, err := s.client.NewRequest("PUT", u, pullRequestBody) 469 if err != nil { 470 return nil, nil, err 471 } 472 473 mergeResult := new(PullRequestMergeResult) 474 resp, err := s.client.Do(ctx, req, mergeResult) 475 if err != nil { 476 return nil, resp, err 477 } 478 479 return mergeResult, resp, nil 480} 481