mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 12:27:06 +00:00 
			
		
		
		
	Add review request api (#11355)
* Add review request api
* add : POST /repos/{owner}/{repo}/pulls/{index}/requested_reviewers
* Remove : DELET /repos/{owner}/{repo}/pulls/{index}/requested_reviewers
* fix some request review bug
* block delet request review by models/DeleteReview()
Signed-off-by: a1012112796 <1012112796@qq.com>
* make fmt
* fix bug
* fix test code
* fix typo
* Apply suggestion from code review @jonasfranz
* fix swagger ref
* fix typo
Co-authored-by: Lauris BH <lauris@nix.lv>
* fix comment
* Change response message
* chang response so some simplfy
* Add ErrIllLegalReviewRequest
fix some nits
* make fmt
* Apply suggestions from code review
Co-authored-by: silverwind <me@silverwind.io>
* * Add team support
* fix test
* fix an known bug
* fix nit
* fix test
* Apply suggestions from code review
Co-authored-by: zeripath <art27@cantab.net>
* update get api and add test
Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: zeripath <art27@cantab.net>
			
			
This commit is contained in:
		| @@ -153,7 +153,7 @@ func TestAPISearchIssues(t *testing.T) { | |||||||
| 	var apiIssues []*api.Issue | 	var apiIssues []*api.Issue | ||||||
| 	DecodeJSON(t, resp, &apiIssues) | 	DecodeJSON(t, resp, &apiIssues) | ||||||
|  |  | ||||||
| 	assert.Len(t, apiIssues, 9) | 	assert.Len(t, apiIssues, 10) | ||||||
|  |  | ||||||
| 	query := url.Values{} | 	query := url.Values{} | ||||||
| 	query.Add("token", token) | 	query.Add("token", token) | ||||||
| @@ -161,7 +161,7 @@ func TestAPISearchIssues(t *testing.T) { | |||||||
| 	req = NewRequest(t, "GET", link.String()) | 	req = NewRequest(t, "GET", link.String()) | ||||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||||
| 	DecodeJSON(t, resp, &apiIssues) | 	DecodeJSON(t, resp, &apiIssues) | ||||||
| 	assert.Len(t, apiIssues, 9) | 	assert.Len(t, apiIssues, 10) | ||||||
|  |  | ||||||
| 	query.Add("state", "closed") | 	query.Add("state", "closed") | ||||||
| 	link.RawQuery = query.Encode() | 	link.RawQuery = query.Encode() | ||||||
| @@ -182,7 +182,7 @@ func TestAPISearchIssues(t *testing.T) { | |||||||
| 	req = NewRequest(t, "GET", link.String()) | 	req = NewRequest(t, "GET", link.String()) | ||||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||||
| 	DecodeJSON(t, resp, &apiIssues) | 	DecodeJSON(t, resp, &apiIssues) | ||||||
| 	assert.Len(t, apiIssues, 1) | 	assert.Len(t, apiIssues, 2) | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestAPISearchIssuesWithLabels(t *testing.T) { | func TestAPISearchIssuesWithLabels(t *testing.T) { | ||||||
| @@ -197,7 +197,7 @@ func TestAPISearchIssuesWithLabels(t *testing.T) { | |||||||
| 	var apiIssues []*api.Issue | 	var apiIssues []*api.Issue | ||||||
| 	DecodeJSON(t, resp, &apiIssues) | 	DecodeJSON(t, resp, &apiIssues) | ||||||
|  |  | ||||||
| 	assert.Len(t, apiIssues, 9) | 	assert.Len(t, apiIssues, 10) | ||||||
|  |  | ||||||
| 	query := url.Values{} | 	query := url.Values{} | ||||||
| 	query.Add("token", token) | 	query.Add("token", token) | ||||||
| @@ -205,7 +205,7 @@ func TestAPISearchIssuesWithLabels(t *testing.T) { | |||||||
| 	req = NewRequest(t, "GET", link.String()) | 	req = NewRequest(t, "GET", link.String()) | ||||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||||
| 	DecodeJSON(t, resp, &apiIssues) | 	DecodeJSON(t, resp, &apiIssues) | ||||||
| 	assert.Len(t, apiIssues, 9) | 	assert.Len(t, apiIssues, 10) | ||||||
|  |  | ||||||
| 	query.Add("labels", "label1") | 	query.Add("labels", "label1") | ||||||
| 	link.RawQuery = query.Encode() | 	link.RawQuery = query.Encode() | ||||||
|   | |||||||
| @@ -122,4 +122,110 @@ func TestAPIPullReview(t *testing.T) { | |||||||
| 	assert.EqualValues(t, 0, review.CodeCommentsCount) | 	assert.EqualValues(t, 0, review.CodeCommentsCount) | ||||||
| 	req = NewRequestf(t, http.MethodDelete, "/api/v1/repos/%s/%s/pulls/%d/reviews/%d?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, review.ID, token) | 	req = NewRequestf(t, http.MethodDelete, "/api/v1/repos/%s/%s/pulls/%d/reviews/%d?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, review.ID, token) | ||||||
| 	resp = session.MakeRequest(t, req, http.StatusNoContent) | 	resp = session.MakeRequest(t, req, http.StatusNoContent) | ||||||
|  |  | ||||||
|  | 	// test get review requests | ||||||
|  | 	// to make it simple, use same api with get review | ||||||
|  | 	pullIssue12 := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 12}).(*models.Issue) | ||||||
|  | 	assert.NoError(t, pullIssue12.LoadAttributes()) | ||||||
|  | 	repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: pullIssue12.RepoID}).(*models.Repository) | ||||||
|  |  | ||||||
|  | 	req = NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo3.OwnerName, repo3.Name, pullIssue12.Index, token) | ||||||
|  | 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||||
|  | 	DecodeJSON(t, resp, &reviews) | ||||||
|  | 	assert.EqualValues(t, 11, reviews[0].ID) | ||||||
|  | 	assert.EqualValues(t, "REQUEST_REVIEW", reviews[0].State) | ||||||
|  | 	assert.EqualValues(t, 0, reviews[0].CodeCommentsCount) | ||||||
|  | 	assert.EqualValues(t, false, reviews[0].Stale) | ||||||
|  | 	assert.EqualValues(t, true, reviews[0].Official) | ||||||
|  | 	assert.EqualValues(t, "test_team", reviews[0].ReviewerTeam.Name) | ||||||
|  |  | ||||||
|  | 	assert.EqualValues(t, 12, reviews[1].ID) | ||||||
|  | 	assert.EqualValues(t, "REQUEST_REVIEW", reviews[1].State) | ||||||
|  | 	assert.EqualValues(t, 0, reviews[0].CodeCommentsCount) | ||||||
|  | 	assert.EqualValues(t, false, reviews[1].Stale) | ||||||
|  | 	assert.EqualValues(t, true, reviews[1].Official) | ||||||
|  | 	assert.EqualValues(t, 1, reviews[1].Reviewer.ID) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestAPIPullReviewRequest(t *testing.T) { | ||||||
|  | 	defer prepareTestEnv(t)() | ||||||
|  | 	pullIssue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 3}).(*models.Issue) | ||||||
|  | 	assert.NoError(t, pullIssue.LoadAttributes()) | ||||||
|  | 	repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: pullIssue.RepoID}).(*models.Repository) | ||||||
|  |  | ||||||
|  | 	// Test add Review Request | ||||||
|  | 	session := loginUser(t, "user2") | ||||||
|  | 	token := getTokenForLoggedInUser(t, session) | ||||||
|  | 	req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.PullReviewRequestOptions{ | ||||||
|  | 		Reviewers: []string{"user4@example.com", "user8"}, | ||||||
|  | 	}) | ||||||
|  | 	session.MakeRequest(t, req, http.StatusCreated) | ||||||
|  |  | ||||||
|  | 	// poster of pr can't be reviewer | ||||||
|  | 	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.PullReviewRequestOptions{ | ||||||
|  | 		Reviewers: []string{"user1"}, | ||||||
|  | 	}) | ||||||
|  | 	session.MakeRequest(t, req, http.StatusUnprocessableEntity) | ||||||
|  |  | ||||||
|  | 	// test user not exist | ||||||
|  | 	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.PullReviewRequestOptions{ | ||||||
|  | 		Reviewers: []string{"testOther"}, | ||||||
|  | 	}) | ||||||
|  | 	session.MakeRequest(t, req, http.StatusNotFound) | ||||||
|  |  | ||||||
|  | 	// Test Remove Review Request | ||||||
|  | 	session2 := loginUser(t, "user4") | ||||||
|  | 	token2 := getTokenForLoggedInUser(t, session2) | ||||||
|  |  | ||||||
|  | 	req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token2), &api.PullReviewRequestOptions{ | ||||||
|  | 		Reviewers: []string{"user4"}, | ||||||
|  | 	}) | ||||||
|  | 	session.MakeRequest(t, req, http.StatusNoContent) | ||||||
|  |  | ||||||
|  | 	// doer is not admin | ||||||
|  | 	req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token2), &api.PullReviewRequestOptions{ | ||||||
|  | 		Reviewers: []string{"user8"}, | ||||||
|  | 	}) | ||||||
|  | 	session.MakeRequest(t, req, http.StatusUnprocessableEntity) | ||||||
|  |  | ||||||
|  | 	req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.PullReviewRequestOptions{ | ||||||
|  | 		Reviewers: []string{"user8"}, | ||||||
|  | 	}) | ||||||
|  | 	session.MakeRequest(t, req, http.StatusNoContent) | ||||||
|  |  | ||||||
|  | 	// Test team review request | ||||||
|  | 	pullIssue12 := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 12}).(*models.Issue) | ||||||
|  | 	assert.NoError(t, pullIssue12.LoadAttributes()) | ||||||
|  | 	repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: pullIssue12.RepoID}).(*models.Repository) | ||||||
|  |  | ||||||
|  | 	// Test add Team Review Request | ||||||
|  | 	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo3.OwnerName, repo3.Name, pullIssue12.Index, token), &api.PullReviewRequestOptions{ | ||||||
|  | 		TeamReviewers: []string{"team1", "owners"}, | ||||||
|  | 	}) | ||||||
|  | 	session.MakeRequest(t, req, http.StatusCreated) | ||||||
|  |  | ||||||
|  | 	// Test add Team Review Request to not allowned | ||||||
|  | 	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo3.OwnerName, repo3.Name, pullIssue12.Index, token), &api.PullReviewRequestOptions{ | ||||||
|  | 		TeamReviewers: []string{"test_team"}, | ||||||
|  | 	}) | ||||||
|  | 	session.MakeRequest(t, req, http.StatusUnprocessableEntity) | ||||||
|  |  | ||||||
|  | 	// Test add Team Review Request to not exist | ||||||
|  | 	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo3.OwnerName, repo3.Name, pullIssue12.Index, token), &api.PullReviewRequestOptions{ | ||||||
|  | 		TeamReviewers: []string{"not_exist_team"}, | ||||||
|  | 	}) | ||||||
|  | 	session.MakeRequest(t, req, http.StatusNotFound) | ||||||
|  |  | ||||||
|  | 	// Test Remove team Review Request | ||||||
|  | 	req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo3.OwnerName, repo3.Name, pullIssue12.Index, token), &api.PullReviewRequestOptions{ | ||||||
|  | 		TeamReviewers: []string{"team1"}, | ||||||
|  | 	}) | ||||||
|  | 	session.MakeRequest(t, req, http.StatusNoContent) | ||||||
|  |  | ||||||
|  | 	// empty request test | ||||||
|  | 	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo3.OwnerName, repo3.Name, pullIssue12.Index, token), &api.PullReviewRequestOptions{}) | ||||||
|  | 	session.MakeRequest(t, req, http.StatusCreated) | ||||||
|  |  | ||||||
|  | 	req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo3.OwnerName, repo3.Name, pullIssue12.Index, token), &api.PullReviewRequestOptions{}) | ||||||
|  | 	session.MakeRequest(t, req, http.StatusNoContent) | ||||||
| } | } | ||||||
|   | |||||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | d22b4d4daa5be07329fcef6ed458f00cf3392da0 | ||||||
| @@ -2003,7 +2003,7 @@ type ErrNotValidReviewRequest struct { | |||||||
|  |  | ||||||
| // IsErrNotValidReviewRequest checks if an error is a ErrNotValidReviewRequest. | // IsErrNotValidReviewRequest checks if an error is a ErrNotValidReviewRequest. | ||||||
| func IsErrNotValidReviewRequest(err error) bool { | func IsErrNotValidReviewRequest(err error) bool { | ||||||
| 	_, ok := err.(ErrReviewNotExist) | 	_, ok := err.(ErrNotValidReviewRequest) | ||||||
| 	return ok | 	return ok | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -135,3 +135,15 @@ | |||||||
|   is_pull: true |   is_pull: true | ||||||
|   created_unix: 1579194806 |   created_unix: 1579194806 | ||||||
|   updated_unix: 1579194806 |   updated_unix: 1579194806 | ||||||
|  |  | ||||||
|  | - | ||||||
|  |   id: 12 | ||||||
|  |   repo_id: 3 | ||||||
|  |   index: 2 | ||||||
|  |   poster_id: 2 | ||||||
|  |   name: pull6 | ||||||
|  |   content: content for the a pull request | ||||||
|  |   is_closed: false | ||||||
|  |   is_pull: true | ||||||
|  |   created_unix: 1602935696 | ||||||
|  |   updated_unix: 1602935696 | ||||||
|   | |||||||
| @@ -63,3 +63,16 @@ | |||||||
|   base_branch: branch1 |   base_branch: branch1 | ||||||
|   merge_base: 1234567890abcdef |   merge_base: 1234567890abcdef | ||||||
|   has_merged: false |   has_merged: false | ||||||
|  |  | ||||||
|  | - | ||||||
|  |   id: 6 | ||||||
|  |   type: 0 # gitea pull request | ||||||
|  |   status: 2 # mergable | ||||||
|  |   issue_id: 12 | ||||||
|  |   index: 2 | ||||||
|  |   head_repo_id: 3 | ||||||
|  |   base_repo_id: 3 | ||||||
|  |   head_branch: test_branch | ||||||
|  |   base_branch: master | ||||||
|  |   merge_base: 2a47ca4b614a9f5a | ||||||
|  |   has_merged: false | ||||||
|   | |||||||
| @@ -41,7 +41,7 @@ | |||||||
|   is_private: true |   is_private: true | ||||||
|   num_issues: 1 |   num_issues: 1 | ||||||
|   num_closed_issues: 0 |   num_closed_issues: 0 | ||||||
|   num_pulls: 0 |   num_pulls: 1 | ||||||
|   num_closed_pulls: 0 |   num_closed_pulls: 0 | ||||||
|   num_watches: 0 |   num_watches: 0 | ||||||
|   num_projects: 1 |   num_projects: 1 | ||||||
|   | |||||||
| @@ -86,3 +86,22 @@ | |||||||
|   official: true |   official: true | ||||||
|   updated_unix: 946684815 |   updated_unix: 946684815 | ||||||
|   created_unix: 946684815 |   created_unix: 946684815 | ||||||
|  |  | ||||||
|  | - | ||||||
|  |   id: 11 | ||||||
|  |   type: 4 | ||||||
|  |   reviewer_id: 0 | ||||||
|  |   reviewer_team_id: 7 | ||||||
|  |   issue_id: 12 | ||||||
|  |   official: true | ||||||
|  |   updated_unix: 1602936509 | ||||||
|  |   created_unix: 1602936509 | ||||||
|  |  | ||||||
|  | - | ||||||
|  |   id: 12 | ||||||
|  |   type: 4 | ||||||
|  |   reviewer_id: 1 | ||||||
|  |   issue_id: 12 | ||||||
|  |   official: true | ||||||
|  |   updated_unix: 1603196749 | ||||||
|  |   created_unix: 1603196749 | ||||||
| @@ -627,13 +627,14 @@ func AddReviewRequest(issue *Issue, reviewer, doer *User) (*Comment, error) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if _, err = createReview(sess, CreateReviewOptions{ | 	review, err = createReview(sess, CreateReviewOptions{ | ||||||
| 		Type:     ReviewTypeRequest, | 		Type:     ReviewTypeRequest, | ||||||
| 		Issue:    issue, | 		Issue:    issue, | ||||||
| 		Reviewer: reviewer, | 		Reviewer: reviewer, | ||||||
| 		Official: official, | 		Official: official, | ||||||
| 		Stale:    false, | 		Stale:    false, | ||||||
| 	}); err != nil { | 	}) | ||||||
|  | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -644,6 +645,7 @@ func AddReviewRequest(issue *Issue, reviewer, doer *User) (*Comment, error) { | |||||||
| 		Issue:           issue, | 		Issue:           issue, | ||||||
| 		RemovedAssignee: false,       // Use RemovedAssignee as !isRequest | 		RemovedAssignee: false,       // Use RemovedAssignee as !isRequest | ||||||
| 		AssigneeID:      reviewer.ID, // Use AssigneeID as reviewer ID | 		AssigneeID:      reviewer.ID, // Use AssigneeID as reviewer ID | ||||||
|  | 		ReviewID:        review.ID, | ||||||
| 	}) | 	}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| @@ -732,7 +734,7 @@ func AddTeamReviewRequest(issue *Issue, reviewer *Team, doer *User) (*Comment, e | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if _, err = createReview(sess, CreateReviewOptions{ | 	if review, err = createReview(sess, CreateReviewOptions{ | ||||||
| 		Type:         ReviewTypeRequest, | 		Type:         ReviewTypeRequest, | ||||||
| 		Issue:        issue, | 		Issue:        issue, | ||||||
| 		ReviewerTeam: reviewer, | 		ReviewerTeam: reviewer, | ||||||
| @@ -755,6 +757,7 @@ func AddTeamReviewRequest(issue *Issue, reviewer *Team, doer *User) (*Comment, e | |||||||
| 		Issue:           issue, | 		Issue:           issue, | ||||||
| 		RemovedAssignee: false,       // Use RemovedAssignee as !isRequest | 		RemovedAssignee: false,       // Use RemovedAssignee as !isRequest | ||||||
| 		AssigneeTeamID:  reviewer.ID, // Use AssigneeTeamID as reviewer team ID | 		AssigneeTeamID:  reviewer.ID, // Use AssigneeTeamID as reviewer team ID | ||||||
|  | 		ReviewID:        review.ID, | ||||||
| 	}) | 	}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("createComment(): %v", err) | 		return nil, fmt.Errorf("createComment(): %v", err) | ||||||
| @@ -894,6 +897,10 @@ func DeleteReview(r *Review) error { | |||||||
| 		return fmt.Errorf("review is not allowed to be 0") | 		return fmt.Errorf("review is not allowed to be 0") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if r.Type == ReviewTypeRequest { | ||||||
|  | 		return fmt.Errorf("review request can not be deleted using this method") | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	opts := FindCommentsOptions{ | 	opts := FindCommentsOptions{ | ||||||
| 		Type:     CommentTypeCode, | 		Type:     CommentTypeCode, | ||||||
| 		IssueID:  r.IssueID, | 		IssueID:  r.IssueID, | ||||||
|   | |||||||
| @@ -284,6 +284,10 @@ func ToOrganization(org *models.User) *api.Organization { | |||||||
|  |  | ||||||
| // ToTeam convert models.Team to api.Team | // ToTeam convert models.Team to api.Team | ||||||
| func ToTeam(team *models.Team) *api.Team { | func ToTeam(team *models.Team) *api.Team { | ||||||
|  | 	if team == nil { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return &api.Team{ | 	return &api.Team{ | ||||||
| 		ID:                      team.ID, | 		ID:                      team.ID, | ||||||
| 		Name:                    team.Name, | 		Name:                    team.Name, | ||||||
|   | |||||||
| @@ -28,6 +28,7 @@ func ToPullReview(r *models.Review, doer *models.User) (*api.PullReview, error) | |||||||
| 	result := &api.PullReview{ | 	result := &api.PullReview{ | ||||||
| 		ID:                r.ID, | 		ID:                r.ID, | ||||||
| 		Reviewer:          ToUser(r.Reviewer, doer != nil, auth), | 		Reviewer:          ToUser(r.Reviewer, doer != nil, auth), | ||||||
|  | 		ReviewerTeam:      ToTeam(r.ReviewerTeam), | ||||||
| 		State:             api.ReviewStateUnknown, | 		State:             api.ReviewStateUnknown, | ||||||
| 		Body:              r.Content, | 		Body:              r.Content, | ||||||
| 		CommitID:          r.CommitID, | 		CommitID:          r.CommitID, | ||||||
|   | |||||||
| @@ -30,6 +30,7 @@ const ( | |||||||
| type PullReview struct { | type PullReview struct { | ||||||
| 	ID                int64           `json:"id"` | 	ID                int64           `json:"id"` | ||||||
| 	Reviewer          *User           `json:"user"` | 	Reviewer          *User           `json:"user"` | ||||||
|  | 	ReviewerTeam      *Team           `json:"team"` | ||||||
| 	State             ReviewStateType `json:"state"` | 	State             ReviewStateType `json:"state"` | ||||||
| 	Body              string          `json:"body"` | 	Body              string          `json:"body"` | ||||||
| 	CommitID          string          `json:"commit_id"` | 	CommitID          string          `json:"commit_id"` | ||||||
| @@ -90,3 +91,9 @@ type SubmitPullReviewOptions struct { | |||||||
| 	Event ReviewStateType `json:"event"` | 	Event ReviewStateType `json:"event"` | ||||||
| 	Body  string          `json:"body"` | 	Body  string          `json:"body"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // PullReviewRequestOptions are options to add or remove pull review requests | ||||||
|  | type PullReviewRequestOptions struct { | ||||||
|  | 	Reviewers     []string `json:"reviewers"` | ||||||
|  | 	TeamReviewers []string `json:"team_reviewers"` | ||||||
|  | } | ||||||
|   | |||||||
| @@ -827,7 +827,9 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||||
| 									Get(repo.GetPullReviewComments) | 									Get(repo.GetPullReviewComments) | ||||||
| 							}) | 							}) | ||||||
| 						}) | 						}) | ||||||
|  | 						m.Combo("/requested_reviewers"). | ||||||
|  | 							Delete(reqToken(), bind(api.PullReviewRequestOptions{}), repo.DeleteReviewRequests). | ||||||
|  | 							Post(reqToken(), bind(api.PullReviewRequestOptions{}), repo.CreateReviewRequests) | ||||||
| 					}) | 					}) | ||||||
| 				}, mustAllowPulls, reqRepoReader(models.UnitTypeCode), context.ReferencesGitRepo(false)) | 				}, mustAllowPulls, reqRepoReader(models.UnitTypeCode), context.ReferencesGitRepo(false)) | ||||||
| 				m.Group("/statuses", func() { | 				m.Group("/statuses", func() { | ||||||
|   | |||||||
| @@ -15,6 +15,7 @@ import ( | |||||||
| 	"code.gitea.io/gitea/modules/git" | 	"code.gitea.io/gitea/modules/git" | ||||||
| 	api "code.gitea.io/gitea/modules/structs" | 	api "code.gitea.io/gitea/modules/structs" | ||||||
| 	"code.gitea.io/gitea/routers/api/v1/utils" | 	"code.gitea.io/gitea/routers/api/v1/utils" | ||||||
|  | 	issue_service "code.gitea.io/gitea/services/issue" | ||||||
| 	pull_service "code.gitea.io/gitea/services/pull" | 	pull_service "code.gitea.io/gitea/services/pull" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -539,3 +540,214 @@ func prepareSingleReview(ctx *context.APIContext) (*models.Review, *models.PullR | |||||||
|  |  | ||||||
| 	return review, pr, false | 	return review, pr, false | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // CreateReviewRequests create review requests to an pull request | ||||||
|  | func CreateReviewRequests(ctx *context.APIContext, opts api.PullReviewRequestOptions) { | ||||||
|  | 	// swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/requested_reviewers repository repoCreatePullReviewRequests | ||||||
|  | 	// --- | ||||||
|  | 	// summary: create review requests for a pull request | ||||||
|  | 	// produces: | ||||||
|  | 	// - application/json | ||||||
|  | 	// parameters: | ||||||
|  | 	// - name: owner | ||||||
|  | 	//   in: path | ||||||
|  | 	//   description: owner of the repo | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: true | ||||||
|  | 	// - name: repo | ||||||
|  | 	//   in: path | ||||||
|  | 	//   description: name of the repo | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: true | ||||||
|  | 	// - name: index | ||||||
|  | 	//   in: path | ||||||
|  | 	//   description: index of the pull request | ||||||
|  | 	//   type: integer | ||||||
|  | 	//   format: int64 | ||||||
|  | 	//   required: true | ||||||
|  | 	// - name: body | ||||||
|  | 	//   in: body | ||||||
|  | 	//   required: true | ||||||
|  | 	//   schema: | ||||||
|  | 	//     "$ref": "#/definitions/PullReviewRequestOptions" | ||||||
|  | 	// responses: | ||||||
|  | 	//   "201": | ||||||
|  | 	//     "$ref": "#/responses/PullReviewList" | ||||||
|  | 	//   "422": | ||||||
|  | 	//     "$ref": "#/responses/validationError" | ||||||
|  | 	//   "404": | ||||||
|  | 	//     "$ref": "#/responses/notFound" | ||||||
|  | 	apiReviewRequest(ctx, opts, true) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // DeleteReviewRequests delete review requests to an pull request | ||||||
|  | func DeleteReviewRequests(ctx *context.APIContext, opts api.PullReviewRequestOptions) { | ||||||
|  | 	// swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/requested_reviewers repository repoDeletePullReviewRequests | ||||||
|  | 	// --- | ||||||
|  | 	// summary: cancel review requests for a pull request | ||||||
|  | 	// produces: | ||||||
|  | 	// - application/json | ||||||
|  | 	// parameters: | ||||||
|  | 	// - name: owner | ||||||
|  | 	//   in: path | ||||||
|  | 	//   description: owner of the repo | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: true | ||||||
|  | 	// - name: repo | ||||||
|  | 	//   in: path | ||||||
|  | 	//   description: name of the repo | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: true | ||||||
|  | 	// - name: index | ||||||
|  | 	//   in: path | ||||||
|  | 	//   description: index of the pull request | ||||||
|  | 	//   type: integer | ||||||
|  | 	//   format: int64 | ||||||
|  | 	//   required: true | ||||||
|  | 	// - name: body | ||||||
|  | 	//   in: body | ||||||
|  | 	//   required: true | ||||||
|  | 	//   schema: | ||||||
|  | 	//     "$ref": "#/definitions/PullReviewRequestOptions" | ||||||
|  | 	// responses: | ||||||
|  | 	//   "204": | ||||||
|  | 	//     "$ref": "#/responses/empty" | ||||||
|  | 	//   "422": | ||||||
|  | 	//     "$ref": "#/responses/validationError" | ||||||
|  | 	//   "404": | ||||||
|  | 	//     "$ref": "#/responses/notFound" | ||||||
|  | 	apiReviewRequest(ctx, opts, false) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions, isAdd bool) { | ||||||
|  | 	pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if models.IsErrPullRequestNotExist(err) { | ||||||
|  | 			ctx.NotFound("GetPullRequestByIndex", err) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := pr.Issue.LoadRepo(); err != nil { | ||||||
|  | 		ctx.Error(http.StatusInternalServerError, "pr.Issue.LoadRepo", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	reviewers := make([]*models.User, 0, len(opts.Reviewers)) | ||||||
|  |  | ||||||
|  | 	permDoer, err := models.GetUserRepoPermission(pr.Issue.Repo, ctx.User) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, r := range opts.Reviewers { | ||||||
|  | 		var reviewer *models.User | ||||||
|  | 		if strings.Contains(r, "@") { | ||||||
|  | 			reviewer, err = models.GetUserByEmail(r) | ||||||
|  | 		} else { | ||||||
|  | 			reviewer, err = models.GetUserByName(r) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if err != nil { | ||||||
|  | 			if models.IsErrUserNotExist(err) { | ||||||
|  | 				ctx.NotFound("UserNotExist", fmt.Sprintf("User '%s' not exist", r)) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			ctx.Error(http.StatusInternalServerError, "GetUser", err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		err = issue_service.IsValidReviewRequest(reviewer, ctx.User, isAdd, pr.Issue, &permDoer) | ||||||
|  | 		if err != nil { | ||||||
|  | 			if models.IsErrNotValidReviewRequest(err) { | ||||||
|  | 				ctx.Error(http.StatusUnprocessableEntity, "NotValidReviewRequest", err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			ctx.Error(http.StatusInternalServerError, "IsValidReviewRequest", err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		reviewers = append(reviewers, reviewer) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var reviews []*models.Review | ||||||
|  | 	if isAdd { | ||||||
|  | 		reviews = make([]*models.Review, 0, len(reviewers)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, reviewer := range reviewers { | ||||||
|  | 		comment, err := issue_service.ReviewRequest(pr.Issue, ctx.User, reviewer, isAdd) | ||||||
|  | 		if err != nil { | ||||||
|  | 			ctx.Error(http.StatusInternalServerError, "ReviewRequest", err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if comment != nil && isAdd { | ||||||
|  | 			if err = comment.LoadReview(); err != nil { | ||||||
|  | 				ctx.ServerError("ReviewRequest", err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			reviews = append(reviews, comment.Review) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if ctx.Repo.Repository.Owner.IsOrganization() && len(opts.TeamReviewers) > 0 { | ||||||
|  |  | ||||||
|  | 		teamReviewers := make([]*models.Team, 0, len(opts.TeamReviewers)) | ||||||
|  | 		for _, t := range opts.TeamReviewers { | ||||||
|  | 			var teamReviewer *models.Team | ||||||
|  | 			teamReviewer, err = models.GetTeam(ctx.Repo.Owner.ID, t) | ||||||
|  | 			if err != nil { | ||||||
|  | 				if models.IsErrTeamNotExist(err) { | ||||||
|  | 					ctx.NotFound("TeamNotExist", fmt.Sprintf("Team '%s' not exist", t)) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 				ctx.Error(http.StatusInternalServerError, "ReviewRequest", err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			err = issue_service.IsValidTeamReviewRequest(teamReviewer, ctx.User, isAdd, pr.Issue) | ||||||
|  | 			if err != nil { | ||||||
|  | 				if models.IsErrNotValidReviewRequest(err) { | ||||||
|  | 					ctx.Error(http.StatusUnprocessableEntity, "NotValidReviewRequest", err) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 				ctx.Error(http.StatusInternalServerError, "IsValidTeamReviewRequest", err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			teamReviewers = append(teamReviewers, teamReviewer) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for _, teamReviewer := range teamReviewers { | ||||||
|  | 			comment, err := issue_service.TeamReviewRequest(pr.Issue, ctx.User, teamReviewer, isAdd) | ||||||
|  | 			if err != nil { | ||||||
|  | 				ctx.ServerError("TeamReviewRequest", err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if comment != nil && isAdd { | ||||||
|  | 				if err = comment.LoadReview(); err != nil { | ||||||
|  | 					ctx.ServerError("ReviewRequest", err) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 				reviews = append(reviews, comment.Review) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if isAdd { | ||||||
|  | 		apiReviews, err := convert.ToPullReviewList(reviews, ctx.User) | ||||||
|  | 		if err != nil { | ||||||
|  | 			ctx.Error(http.StatusInternalServerError, "convertToPullReviewList", err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		ctx.JSON(http.StatusCreated, apiReviews) | ||||||
|  | 	} else { | ||||||
|  | 		ctx.Status(http.StatusNoContent) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
| @@ -152,4 +152,7 @@ type swaggerParameterBodies struct { | |||||||
|  |  | ||||||
| 	// in:body | 	// in:body | ||||||
| 	MigrateRepoOptions api.MigrateRepoOptions | 	MigrateRepoOptions api.MigrateRepoOptions | ||||||
|  |  | ||||||
|  | 	// in:body | ||||||
|  | 	PullReviewRequestOptions api.PullReviewRequestOptions | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1699,154 +1699,6 @@ func UpdateIssueAssignee(ctx *context.Context) { | |||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
| func isValidReviewRequest(reviewer, doer *models.User, isAdd bool, issue *models.Issue) error { |  | ||||||
| 	if reviewer.IsOrganization() { |  | ||||||
| 		return models.ErrNotValidReviewRequest{ |  | ||||||
| 			Reason: "Organization can't be added as reviewer", |  | ||||||
| 			UserID: doer.ID, |  | ||||||
| 			RepoID: issue.Repo.ID, |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	if doer.IsOrganization() { |  | ||||||
| 		return models.ErrNotValidReviewRequest{ |  | ||||||
| 			Reason: "Organization can't be doer to add reviewer", |  | ||||||
| 			UserID: doer.ID, |  | ||||||
| 			RepoID: issue.Repo.ID, |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	permReviewer, err := models.GetUserRepoPermission(issue.Repo, reviewer) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	permDoer, err := models.GetUserRepoPermission(issue.Repo, doer) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	lastreview, err := models.GetReviewByIssueIDAndUserID(issue.ID, reviewer.ID) |  | ||||||
| 	if err != nil && !models.IsErrReviewNotExist(err) { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var pemResult bool |  | ||||||
| 	if isAdd { |  | ||||||
| 		pemResult = permReviewer.CanAccessAny(models.AccessModeRead, models.UnitTypePullRequests) |  | ||||||
| 		if !pemResult { |  | ||||||
| 			return models.ErrNotValidReviewRequest{ |  | ||||||
| 				Reason: "Reviewer can't read", |  | ||||||
| 				UserID: doer.ID, |  | ||||||
| 				RepoID: issue.Repo.ID, |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if doer.ID == issue.PosterID && issue.OriginalAuthorID == 0 && lastreview != nil && lastreview.Type != models.ReviewTypeRequest { |  | ||||||
| 			return nil |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		pemResult = permDoer.CanAccessAny(models.AccessModeWrite, models.UnitTypePullRequests) |  | ||||||
| 		if !pemResult { |  | ||||||
| 			pemResult, err = models.IsOfficialReviewer(issue, doer) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
| 			if !pemResult { |  | ||||||
| 				return models.ErrNotValidReviewRequest{ |  | ||||||
| 					Reason: "Doer can't choose reviewer", |  | ||||||
| 					UserID: doer.ID, |  | ||||||
| 					RepoID: issue.Repo.ID, |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if doer.ID == reviewer.ID { |  | ||||||
| 			return models.ErrNotValidReviewRequest{ |  | ||||||
| 				Reason: "doer can't be reviewer", |  | ||||||
| 				UserID: doer.ID, |  | ||||||
| 				RepoID: issue.Repo.ID, |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if reviewer.ID == issue.PosterID && issue.OriginalAuthorID == 0 { |  | ||||||
| 			return models.ErrNotValidReviewRequest{ |  | ||||||
| 				Reason: "poster of pr can't be reviewer", |  | ||||||
| 				UserID: doer.ID, |  | ||||||
| 				RepoID: issue.Repo.ID, |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} else { |  | ||||||
| 		if lastreview != nil && lastreview.Type == models.ReviewTypeRequest && lastreview.ReviewerID == doer.ID { |  | ||||||
| 			return nil |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		pemResult = permDoer.IsAdmin() |  | ||||||
| 		if !pemResult { |  | ||||||
| 			return models.ErrNotValidReviewRequest{ |  | ||||||
| 				Reason: "Doer is not admin", |  | ||||||
| 				UserID: doer.ID, |  | ||||||
| 				RepoID: issue.Repo.ID, |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func isValidTeamReviewRequest(reviewer *models.Team, doer *models.User, isAdd bool, issue *models.Issue) error { |  | ||||||
| 	if doer.IsOrganization() { |  | ||||||
| 		return models.ErrNotValidReviewRequest{ |  | ||||||
| 			Reason: "Organization can't be doer to add reviewer", |  | ||||||
| 			UserID: doer.ID, |  | ||||||
| 			RepoID: issue.Repo.ID, |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	permission, err := models.GetUserRepoPermission(issue.Repo, doer) |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Error("Unable to GetUserRepoPermission for %-v in %-v#%d", doer, issue.Repo, issue.Index) |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if isAdd { |  | ||||||
| 		if issue.Repo.IsPrivate { |  | ||||||
| 			hasTeam := models.HasTeamRepo(reviewer.OrgID, reviewer.ID, issue.RepoID) |  | ||||||
|  |  | ||||||
| 			if !hasTeam { |  | ||||||
| 				return models.ErrNotValidReviewRequest{ |  | ||||||
| 					Reason: "Reviewing team can't read repo", |  | ||||||
| 					UserID: doer.ID, |  | ||||||
| 					RepoID: issue.Repo.ID, |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		doerCanWrite := permission.CanAccessAny(models.AccessModeWrite, models.UnitTypePullRequests) |  | ||||||
| 		if !doerCanWrite { |  | ||||||
| 			official, err := models.IsOfficialReviewer(issue, doer) |  | ||||||
| 			if err != nil { |  | ||||||
| 				log.Error("Unable to Check if IsOfficialReviewer for %-v in %-v#%d", doer, issue.Repo, issue.Index) |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
| 			if !official { |  | ||||||
| 				return models.ErrNotValidReviewRequest{ |  | ||||||
| 					Reason: "Doer can't choose reviewer", |  | ||||||
| 					UserID: doer.ID, |  | ||||||
| 					RepoID: issue.Repo.ID, |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} else if !permission.IsAdmin() { |  | ||||||
| 		return models.ErrNotValidReviewRequest{ |  | ||||||
| 			Reason: "Only admin users can remove team requests. Doer is not admin", |  | ||||||
| 			UserID: doer.ID, |  | ||||||
| 			RepoID: issue.Repo.ID, |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // UpdatePullReviewRequest add or remove review request | // UpdatePullReviewRequest add or remove review request | ||||||
| func UpdatePullReviewRequest(ctx *context.Context) { | func UpdatePullReviewRequest(ctx *context.Context) { | ||||||
| 	issues := getActionIssues(ctx) | 	issues := getActionIssues(ctx) | ||||||
| @@ -1907,7 +1759,7 @@ func UpdatePullReviewRequest(ctx *context.Context) { | |||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			err = isValidTeamReviewRequest(team, ctx.User, action == "attach", issue) | 			err = issue_service.IsValidTeamReviewRequest(team, ctx.User, action == "attach", issue) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				if models.IsErrNotValidReviewRequest(err) { | 				if models.IsErrNotValidReviewRequest(err) { | ||||||
| 					log.Warn( | 					log.Warn( | ||||||
| @@ -1918,11 +1770,11 @@ func UpdatePullReviewRequest(ctx *context.Context) { | |||||||
| 					ctx.Status(403) | 					ctx.Status(403) | ||||||
| 					return | 					return | ||||||
| 				} | 				} | ||||||
| 				ctx.ServerError("isValidTeamReviewRequest", err) | 				ctx.ServerError("IsValidTeamReviewRequest", err) | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			err = issue_service.TeamReviewRequest(issue, ctx.User, team, action == "attach") | 			_, err = issue_service.TeamReviewRequest(issue, ctx.User, team, action == "attach") | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				ctx.ServerError("TeamReviewRequest", err) | 				ctx.ServerError("TeamReviewRequest", err) | ||||||
| 				return | 				return | ||||||
| @@ -1945,7 +1797,7 @@ func UpdatePullReviewRequest(ctx *context.Context) { | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		err = isValidReviewRequest(reviewer, ctx.User, action == "attach", issue) | 		err = issue_service.IsValidReviewRequest(reviewer, ctx.User, action == "attach", issue, nil) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			if models.IsErrNotValidReviewRequest(err) { | 			if models.IsErrNotValidReviewRequest(err) { | ||||||
| 				log.Warn( | 				log.Warn( | ||||||
| @@ -1960,7 +1812,7 @@ func UpdatePullReviewRequest(ctx *context.Context) { | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		err = issue_service.ReviewRequest(issue, ctx.User, reviewer, action == "attach") | 		_, err = issue_service.ReviewRequest(issue, ctx.User, reviewer, action == "attach") | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			ctx.ServerError("ReviewRequest", err) | 			ctx.ServerError("ReviewRequest", err) | ||||||
| 			return | 			return | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ package issue | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
|  | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/notification" | 	"code.gitea.io/gitea/modules/notification" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -53,8 +54,7 @@ func ToggleAssignee(issue *models.Issue, doer *models.User, assigneeID int64) (r | |||||||
| } | } | ||||||
|  |  | ||||||
| // ReviewRequest add or remove a review request from a user for this PR, and make comment for it. | // ReviewRequest add or remove a review request from a user for this PR, and make comment for it. | ||||||
| func ReviewRequest(issue *models.Issue, doer *models.User, reviewer *models.User, isAdd bool) (err error) { | func ReviewRequest(issue *models.Issue, doer *models.User, reviewer *models.User, isAdd bool) (comment *models.Comment, err error) { | ||||||
| 	var comment *models.Comment |  | ||||||
| 	if isAdd { | 	if isAdd { | ||||||
| 		comment, err = models.AddReviewRequest(issue, reviewer, doer) | 		comment, err = models.AddReviewRequest(issue, reviewer, doer) | ||||||
| 	} else { | 	} else { | ||||||
| @@ -62,19 +62,171 @@ func ReviewRequest(issue *models.Issue, doer *models.User, reviewer *models.User | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if comment != nil { | 	if comment != nil { | ||||||
| 		notification.NotifyPullReviewRequest(doer, issue, reviewer, isAdd, comment) | 		notification.NotifyPullReviewRequest(doer, issue, reviewer, isAdd, comment) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IsValidReviewRequest Check permission for ReviewRequest | ||||||
|  | func IsValidReviewRequest(reviewer, doer *models.User, isAdd bool, issue *models.Issue, permDoer *models.Permission) error { | ||||||
|  | 	if reviewer.IsOrganization() { | ||||||
|  | 		return models.ErrNotValidReviewRequest{ | ||||||
|  | 			Reason: "Organization can't be added as reviewer", | ||||||
|  | 			UserID: doer.ID, | ||||||
|  | 			RepoID: issue.Repo.ID, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if doer.IsOrganization() { | ||||||
|  | 		return models.ErrNotValidReviewRequest{ | ||||||
|  | 			Reason: "Organization can't be doer to add reviewer", | ||||||
|  | 			UserID: doer.ID, | ||||||
|  | 			RepoID: issue.Repo.ID, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	permReviewer, err := models.GetUserRepoPermission(issue.Repo, reviewer) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if permDoer == nil { | ||||||
|  | 		permDoer = new(models.Permission) | ||||||
|  | 		*permDoer, err = models.GetUserRepoPermission(issue.Repo, doer) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	lastreview, err := models.GetReviewByIssueIDAndUserID(issue.ID, reviewer.ID) | ||||||
|  | 	if err != nil && !models.IsErrReviewNotExist(err) { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var pemResult bool | ||||||
|  | 	if isAdd { | ||||||
|  | 		pemResult = permReviewer.CanAccessAny(models.AccessModeRead, models.UnitTypePullRequests) | ||||||
|  | 		if !pemResult { | ||||||
|  | 			return models.ErrNotValidReviewRequest{ | ||||||
|  | 				Reason: "Reviewer can't read", | ||||||
|  | 				UserID: doer.ID, | ||||||
|  | 				RepoID: issue.Repo.ID, | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if doer.ID == issue.PosterID && issue.OriginalAuthorID == 0 && lastreview != nil && lastreview.Type != models.ReviewTypeRequest { | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		pemResult = permDoer.CanAccessAny(models.AccessModeWrite, models.UnitTypePullRequests) | ||||||
|  | 		if !pemResult { | ||||||
|  | 			pemResult, err = models.IsOfficialReviewer(issue, doer) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 			if !pemResult { | ||||||
|  | 				return models.ErrNotValidReviewRequest{ | ||||||
|  | 					Reason: "Doer can't choose reviewer", | ||||||
|  | 					UserID: doer.ID, | ||||||
|  | 					RepoID: issue.Repo.ID, | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if doer.ID == reviewer.ID { | ||||||
|  | 			return models.ErrNotValidReviewRequest{ | ||||||
|  | 				Reason: "doer can't be reviewer", | ||||||
|  | 				UserID: doer.ID, | ||||||
|  | 				RepoID: issue.Repo.ID, | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if reviewer.ID == issue.PosterID && issue.OriginalAuthorID == 0 { | ||||||
|  | 			return models.ErrNotValidReviewRequest{ | ||||||
|  | 				Reason: "poster of pr can't be reviewer", | ||||||
|  | 				UserID: doer.ID, | ||||||
|  | 				RepoID: issue.Repo.ID, | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		if lastreview != nil && lastreview.Type == models.ReviewTypeRequest && lastreview.ReviewerID == doer.ID { | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		pemResult = permDoer.IsAdmin() | ||||||
|  | 		if !pemResult { | ||||||
|  | 			return models.ErrNotValidReviewRequest{ | ||||||
|  | 				Reason: "Doer is not admin", | ||||||
|  | 				UserID: doer.ID, | ||||||
|  | 				RepoID: issue.Repo.ID, | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IsValidTeamReviewRequest Check permission for ReviewRequest Team | ||||||
|  | func IsValidTeamReviewRequest(reviewer *models.Team, doer *models.User, isAdd bool, issue *models.Issue) error { | ||||||
|  | 	if doer.IsOrganization() { | ||||||
|  | 		return models.ErrNotValidReviewRequest{ | ||||||
|  | 			Reason: "Organization can't be doer to add reviewer", | ||||||
|  | 			UserID: doer.ID, | ||||||
|  | 			RepoID: issue.Repo.ID, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	permission, err := models.GetUserRepoPermission(issue.Repo, doer) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("Unable to GetUserRepoPermission for %-v in %-v#%d", doer, issue.Repo, issue.Index) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if isAdd { | ||||||
|  | 		if issue.Repo.IsPrivate { | ||||||
|  | 			hasTeam := models.HasTeamRepo(reviewer.OrgID, reviewer.ID, issue.RepoID) | ||||||
|  |  | ||||||
|  | 			if !hasTeam { | ||||||
|  | 				return models.ErrNotValidReviewRequest{ | ||||||
|  | 					Reason: "Reviewing team can't read repo", | ||||||
|  | 					UserID: doer.ID, | ||||||
|  | 					RepoID: issue.Repo.ID, | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		doerCanWrite := permission.CanAccessAny(models.AccessModeWrite, models.UnitTypePullRequests) | ||||||
|  | 		if !doerCanWrite { | ||||||
|  | 			official, err := models.IsOfficialReviewer(issue, doer) | ||||||
|  | 			if err != nil { | ||||||
|  | 				log.Error("Unable to Check if IsOfficialReviewer for %-v in %-v#%d", doer, issue.Repo, issue.Index) | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 			if !official { | ||||||
|  | 				return models.ErrNotValidReviewRequest{ | ||||||
|  | 					Reason: "Doer can't choose reviewer", | ||||||
|  | 					UserID: doer.ID, | ||||||
|  | 					RepoID: issue.Repo.ID, | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} else if !permission.IsAdmin() { | ||||||
|  | 		return models.ErrNotValidReviewRequest{ | ||||||
|  | 			Reason: "Only admin users can remove team requests. Doer is not admin", | ||||||
|  | 			UserID: doer.ID, | ||||||
|  | 			RepoID: issue.Repo.ID, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // TeamReviewRequest add or remove a review request from a team for this PR, and make comment for it. | // TeamReviewRequest add or remove a review request from a team for this PR, and make comment for it. | ||||||
| func TeamReviewRequest(issue *models.Issue, doer *models.User, reviewer *models.Team, isAdd bool) (err error) { | func TeamReviewRequest(issue *models.Issue, doer *models.User, reviewer *models.Team, isAdd bool) (comment *models.Comment, err error) { | ||||||
| 	var comment *models.Comment |  | ||||||
| 	if isAdd { | 	if isAdd { | ||||||
| 		comment, err = models.AddTeamReviewRequest(issue, reviewer, doer) | 		comment, err = models.AddTeamReviewRequest(issue, reviewer, doer) | ||||||
| 	} else { | 	} else { | ||||||
| @@ -106,5 +258,5 @@ func TeamReviewRequest(issue *models.Issue, doer *models.User, reviewer *models. | |||||||
| 		notification.NotifyPullReviewRequest(doer, issue, member, isAdd, comment) | 		notification.NotifyPullReviewRequest(doer, issue, member, isAdd, comment) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return | ||||||
| } | } | ||||||
|   | |||||||
| @@ -7164,6 +7164,114 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "/repos/{owner}/{repo}/pulls/{index}/requested_reviewers": { | ||||||
|  |       "post": { | ||||||
|  |         "produces": [ | ||||||
|  |           "application/json" | ||||||
|  |         ], | ||||||
|  |         "tags": [ | ||||||
|  |           "repository" | ||||||
|  |         ], | ||||||
|  |         "summary": "create review requests for a pull request", | ||||||
|  |         "operationId": "repoCreatePullReviewRequests", | ||||||
|  |         "parameters": [ | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "owner of the repo", | ||||||
|  |             "name": "owner", | ||||||
|  |             "in": "path", | ||||||
|  |             "required": true | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "name of the repo", | ||||||
|  |             "name": "repo", | ||||||
|  |             "in": "path", | ||||||
|  |             "required": true | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "integer", | ||||||
|  |             "format": "int64", | ||||||
|  |             "description": "index of the pull request", | ||||||
|  |             "name": "index", | ||||||
|  |             "in": "path", | ||||||
|  |             "required": true | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "name": "body", | ||||||
|  |             "in": "body", | ||||||
|  |             "required": true, | ||||||
|  |             "schema": { | ||||||
|  |               "$ref": "#/definitions/PullReviewRequestOptions" | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         ], | ||||||
|  |         "responses": { | ||||||
|  |           "201": { | ||||||
|  |             "$ref": "#/responses/PullReviewList" | ||||||
|  |           }, | ||||||
|  |           "404": { | ||||||
|  |             "$ref": "#/responses/notFound" | ||||||
|  |           }, | ||||||
|  |           "422": { | ||||||
|  |             "$ref": "#/responses/validationError" | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       "delete": { | ||||||
|  |         "produces": [ | ||||||
|  |           "application/json" | ||||||
|  |         ], | ||||||
|  |         "tags": [ | ||||||
|  |           "repository" | ||||||
|  |         ], | ||||||
|  |         "summary": "cancel review requests for a pull request", | ||||||
|  |         "operationId": "repoDeletePullReviewRequests", | ||||||
|  |         "parameters": [ | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "owner of the repo", | ||||||
|  |             "name": "owner", | ||||||
|  |             "in": "path", | ||||||
|  |             "required": true | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "name of the repo", | ||||||
|  |             "name": "repo", | ||||||
|  |             "in": "path", | ||||||
|  |             "required": true | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "integer", | ||||||
|  |             "format": "int64", | ||||||
|  |             "description": "index of the pull request", | ||||||
|  |             "name": "index", | ||||||
|  |             "in": "path", | ||||||
|  |             "required": true | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "name": "body", | ||||||
|  |             "in": "body", | ||||||
|  |             "required": true, | ||||||
|  |             "schema": { | ||||||
|  |               "$ref": "#/definitions/PullReviewRequestOptions" | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         ], | ||||||
|  |         "responses": { | ||||||
|  |           "204": { | ||||||
|  |             "$ref": "#/responses/empty" | ||||||
|  |           }, | ||||||
|  |           "404": { | ||||||
|  |             "$ref": "#/responses/notFound" | ||||||
|  |           }, | ||||||
|  |           "422": { | ||||||
|  |             "$ref": "#/responses/validationError" | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "/repos/{owner}/{repo}/pulls/{index}/reviews": { |     "/repos/{owner}/{repo}/pulls/{index}/reviews": { | ||||||
|       "get": { |       "get": { | ||||||
|         "produces": [ |         "produces": [ | ||||||
| @@ -14540,6 +14648,9 @@ | |||||||
|           "format": "date-time", |           "format": "date-time", | ||||||
|           "x-go-name": "Submitted" |           "x-go-name": "Submitted" | ||||||
|         }, |         }, | ||||||
|  |         "team": { | ||||||
|  |           "$ref": "#/definitions/Team" | ||||||
|  |         }, | ||||||
|         "user": { |         "user": { | ||||||
|           "$ref": "#/definitions/User" |           "$ref": "#/definitions/User" | ||||||
|         } |         } | ||||||
| @@ -14614,6 +14725,27 @@ | |||||||
|       }, |       }, | ||||||
|       "x-go-package": "code.gitea.io/gitea/modules/structs" |       "x-go-package": "code.gitea.io/gitea/modules/structs" | ||||||
|     }, |     }, | ||||||
|  |     "PullReviewRequestOptions": { | ||||||
|  |       "description": "PullReviewRequestOptions are options to add or remove pull review requests", | ||||||
|  |       "type": "object", | ||||||
|  |       "properties": { | ||||||
|  |         "reviewers": { | ||||||
|  |           "type": "array", | ||||||
|  |           "items": { | ||||||
|  |             "type": "string" | ||||||
|  |           }, | ||||||
|  |           "x-go-name": "Reviewers" | ||||||
|  |         }, | ||||||
|  |         "team_reviewers": { | ||||||
|  |           "type": "array", | ||||||
|  |           "items": { | ||||||
|  |             "type": "string" | ||||||
|  |           }, | ||||||
|  |           "x-go-name": "TeamReviewers" | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       "x-go-package": "code.gitea.io/gitea/modules/structs" | ||||||
|  |     }, | ||||||
|     "Reaction": { |     "Reaction": { | ||||||
|       "description": "Reaction contain one reaction", |       "description": "Reaction contain one reaction", | ||||||
|       "type": "object", |       "type": "object", | ||||||
| @@ -16162,7 +16294,7 @@ | |||||||
|     "parameterBodies": { |     "parameterBodies": { | ||||||
|       "description": "parameterBodies", |       "description": "parameterBodies", | ||||||
|       "schema": { |       "schema": { | ||||||
|         "$ref": "#/definitions/MigrateRepoOptions" |         "$ref": "#/definitions/PullReviewRequestOptions" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "redirect": { |     "redirect": { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 a1012112796
					a1012112796