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