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>release/v1.15
parent
b50448b286
commit
b9850375fc
|
@ -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": {
|
||||||
|
|
Loading…
Reference in New Issue