mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-17 21:00:38 +02:00
Move review request functions to a standalone file (#37358)
Assignee functions should be different from review request functions.
This commit is contained in:
parent
1af16679c3
commit
aedf4e84f5
@ -7,13 +7,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
"code.gitea.io/gitea/models/organization"
|
|
||||||
"code.gitea.io/gitea/models/perm"
|
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unit"
|
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/container"
|
||||||
notify_service "code.gitea.io/gitea/services/notify"
|
notify_service "code.gitea.io/gitea/services/notify"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -62,267 +59,85 @@ func ToggleAssigneeWithNotify(ctx context.Context, issue *issues_model.Issue, do
|
|||||||
return removed, comment, err
|
return removed, comment, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReviewRequest add or remove a review request from a user for this PR, and make comment for it.
|
// UpdateAssignees is a helper function to add or delete one or multiple issue assignee(s)
|
||||||
func ReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, permDoer *access_model.Permission, reviewer *user_model.User, isAdd bool) (comment *issues_model.Comment, err error) {
|
// Deleting is done the GitHub way (quote from their api documentation):
|
||||||
err = isValidReviewRequest(ctx, reviewer, doer, isAdd, issue, permDoer)
|
// https://developer.github.com/v3/issues/#edit-an-issue
|
||||||
if err != nil {
|
// "assignees" (array): Logins for Users to assign to this issue.
|
||||||
return nil, err
|
// Pass one or more user logins to replace the set of assignees on this Issue.
|
||||||
|
// Send an empty array ([]) to clear all assignees from the Issue.
|
||||||
|
func UpdateAssignees(ctx context.Context, issue *issues_model.Issue, oneAssignee string, multipleAssignees []string, doer *user_model.User) (err error) {
|
||||||
|
uniqueAssignees := container.SetOf(multipleAssignees...)
|
||||||
|
|
||||||
|
// Keep the old assignee thingy for compatibility reasons
|
||||||
|
if oneAssignee != "" {
|
||||||
|
uniqueAssignees.Add(oneAssignee)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isAdd {
|
// Loop through all assignees to add them
|
||||||
comment, err = issues_model.AddReviewRequest(ctx, issue, reviewer, doer, false)
|
allNewAssignees := make([]*user_model.User, 0, len(uniqueAssignees))
|
||||||
} else {
|
for _, assigneeName := range uniqueAssignees.Values() {
|
||||||
comment, err = issues_model.RemoveReviewRequest(ctx, issue, reviewer, doer)
|
assignee, err := user_model.GetUserByName(ctx, assigneeName)
|
||||||
}
|
if err != nil {
|
||||||
|
return err
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if comment != nil {
|
|
||||||
notify_service.PullRequestReviewRequest(ctx, doer, issue, reviewer, isAdd, comment)
|
|
||||||
}
|
|
||||||
|
|
||||||
return comment, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// isValidReviewRequest Check permission for ReviewRequest
|
|
||||||
func isValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User, isAdd bool, issue *issues_model.Issue, permDoer *access_model.Permission) error {
|
|
||||||
if reviewer.IsOrganization() {
|
|
||||||
return issues_model.ErrNotValidReviewRequest{
|
|
||||||
Reason: "Organization can't be added as reviewer",
|
|
||||||
UserID: doer.ID,
|
|
||||||
RepoID: issue.Repo.ID,
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if doer.IsOrganization() {
|
if user_model.IsUserBlockedBy(ctx, doer, assignee.ID) {
|
||||||
return issues_model.ErrNotValidReviewRequest{
|
return user_model.ErrBlockedUser
|
||||||
Reason: "Organization can't be doer to add reviewer",
|
|
||||||
UserID: doer.ID,
|
|
||||||
RepoID: issue.Repo.ID,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allNewAssignees = append(allNewAssignees, assignee)
|
||||||
}
|
}
|
||||||
|
|
||||||
permReviewer, err := access_model.GetIndividualUserRepoPermission(ctx, issue.Repo, reviewer)
|
// Delete all old assignees not passed
|
||||||
if err != nil {
|
if err = DeleteNotPassedAssignee(ctx, issue, doer, allNewAssignees); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if permDoer == nil {
|
// Add all new assignees
|
||||||
permDoer = new(access_model.Permission)
|
// Update the assignee. The function will check if the user exists, is already
|
||||||
*permDoer, err = access_model.GetDoerRepoPermission(ctx, issue.Repo, doer)
|
// assigned (which he shouldn't as we deleted all assignees before) and
|
||||||
|
// has access to the repo.
|
||||||
|
for _, assignee := range allNewAssignees {
|
||||||
|
// Extra method to prevent double adding (which would result in removing)
|
||||||
|
_, err = AddAssigneeIfNotAssigned(ctx, issue, doer, assignee.ID, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lastReview, err := issues_model.GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID)
|
|
||||||
if err != nil && !issues_model.IsErrReviewNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue.PosterID)
|
|
||||||
|
|
||||||
if isAdd {
|
|
||||||
if !permReviewer.CanAccessAny(perm.AccessModeRead, unit.TypePullRequests) {
|
|
||||||
return issues_model.ErrNotValidReviewRequest{
|
|
||||||
Reason: "Reviewer can't read",
|
|
||||||
UserID: doer.ID,
|
|
||||||
RepoID: issue.Repo.ID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if reviewer.ID == issue.PosterID && issue.OriginalAuthorID == 0 {
|
|
||||||
return issues_model.ErrNotValidReviewRequest{
|
|
||||||
Reason: "poster of pr can't be reviewer",
|
|
||||||
UserID: doer.ID,
|
|
||||||
RepoID: issue.Repo.ID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if canDoerChangeReviewRequests {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if doer.ID == issue.PosterID && issue.OriginalAuthorID == 0 && lastReview != nil && lastReview.Type != issues_model.ReviewTypeRequest {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return issues_model.ErrNotValidReviewRequest{
|
|
||||||
Reason: "Doer can't choose reviewer",
|
|
||||||
UserID: doer.ID,
|
|
||||||
RepoID: issue.Repo.ID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if canDoerChangeReviewRequests {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if lastReview != nil && lastReview.Type == issues_model.ReviewTypeRequest && lastReview.ReviewerID == doer.ID {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return issues_model.ErrNotValidReviewRequest{
|
|
||||||
Reason: "Doer can't remove reviewer",
|
|
||||||
UserID: doer.ID,
|
|
||||||
RepoID: issue.Repo.ID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// isValidTeamReviewRequest Check permission for ReviewRequest Team
|
|
||||||
func isValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team, doer *user_model.User, isAdd bool, issue *issues_model.Issue) error {
|
|
||||||
if doer.IsOrganization() {
|
|
||||||
return issues_model.ErrNotValidReviewRequest{
|
|
||||||
Reason: "Organization can't be doer to add reviewer",
|
|
||||||
UserID: doer.ID,
|
|
||||||
RepoID: issue.Repo.ID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue.PosterID)
|
|
||||||
|
|
||||||
if isAdd {
|
|
||||||
if issue.Repo.IsPrivate {
|
|
||||||
hasTeam := organization.HasTeamRepo(ctx, reviewer.OrgID, reviewer.ID, issue.RepoID)
|
|
||||||
|
|
||||||
if !hasTeam {
|
|
||||||
return issues_model.ErrNotValidReviewRequest{
|
|
||||||
Reason: "Reviewing team can't read repo",
|
|
||||||
UserID: doer.ID,
|
|
||||||
RepoID: issue.Repo.ID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if canDoerChangeReviewRequests {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return issues_model.ErrNotValidReviewRequest{
|
|
||||||
Reason: "Doer can't choose reviewer",
|
|
||||||
UserID: doer.ID,
|
|
||||||
RepoID: issue.Repo.ID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if canDoerChangeReviewRequests {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return issues_model.ErrNotValidReviewRequest{
|
|
||||||
Reason: "Doer can't remove reviewer",
|
|
||||||
UserID: doer.ID,
|
|
||||||
RepoID: issue.Repo.ID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TeamReviewRequest add or remove a review request from a team for this PR, and make comment for it.
|
|
||||||
func TeamReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewer *organization.Team, isAdd bool) (comment *issues_model.Comment, err error) {
|
|
||||||
err = isValidTeamReviewRequest(ctx, reviewer, doer, isAdd, issue)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if isAdd {
|
|
||||||
comment, err = issues_model.AddTeamReviewRequest(ctx, issue, reviewer, doer, false)
|
|
||||||
} else {
|
|
||||||
comment, err = issues_model.RemoveTeamReviewRequest(ctx, issue, reviewer, doer)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if comment == nil || !isAdd {
|
|
||||||
return nil, nil //nolint:nilnil // return nil because no comment was created or it is a removal
|
|
||||||
}
|
|
||||||
|
|
||||||
return comment, teamReviewRequestNotify(ctx, issue, doer, reviewer, isAdd, comment)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReviewRequestNotify(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewNotifiers []*ReviewRequestNotifier) {
|
|
||||||
for _, reviewNotifier := range reviewNotifiers {
|
|
||||||
if reviewNotifier.Reviewer != nil {
|
|
||||||
notify_service.PullRequestReviewRequest(ctx, issue.Poster, issue, reviewNotifier.Reviewer, reviewNotifier.IsAdd, reviewNotifier.Comment)
|
|
||||||
} else if reviewNotifier.ReviewTeam != nil {
|
|
||||||
if err := teamReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifier.ReviewTeam, reviewNotifier.IsAdd, reviewNotifier.Comment); err != nil {
|
|
||||||
log.Error("teamReviewRequestNotify: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// teamReviewRequestNotify notify all user in this team
|
|
||||||
func teamReviewRequestNotify(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewer *organization.Team, isAdd bool, comment *issues_model.Comment) error {
|
|
||||||
// notify all user in this team
|
|
||||||
if err := comment.LoadIssue(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
members, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{
|
|
||||||
TeamID: reviewer.ID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, member := range members {
|
|
||||||
if member.ID == comment.Issue.PosterID {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
comment.AssigneeID = member.ID
|
|
||||||
notify_service.PullRequestReviewRequest(ctx, doer, issue, member, isAdd, comment)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanDoerChangeReviewRequests returns if the doer can add/remove review requests of a PR
|
// AddAssigneeIfNotAssigned adds an assignee only if he isn't already assigned to the issue.
|
||||||
func CanDoerChangeReviewRequests(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, posterID int64) bool {
|
// Also checks for access of assigned user
|
||||||
if repo.IsArchived {
|
func AddAssigneeIfNotAssigned(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, assigneeID int64, notify bool) (comment *issues_model.Comment, err error) {
|
||||||
return false
|
assignee, err := user_model.GetUserByID(ctx, assigneeID)
|
||||||
}
|
|
||||||
// The poster of the PR can change the reviewers
|
|
||||||
if doer.ID == posterID {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// The owner of the repo can change the reviewers
|
|
||||||
if doer.ID == repo.OwnerID {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collaborators of the repo can change the reviewers
|
|
||||||
isCollaborator, err := repo_model.IsCollaborator(ctx, repo.ID, doer.ID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("IsCollaborator: %v", err)
|
return nil, err
|
||||||
return false
|
|
||||||
}
|
|
||||||
if isCollaborator {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the repo's owner is an organization, members of teams with read permission on pull requests can change reviewers
|
// Check if the user is already assigned
|
||||||
if repo.Owner.IsOrganization() {
|
isAssigned, err := issues_model.IsUserAssignedToIssue(ctx, issue, assignee)
|
||||||
teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypePullRequests)
|
if err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
log.Error("GetTeamsWithAccessToRepo: %v", err)
|
}
|
||||||
return false
|
if isAssigned {
|
||||||
}
|
// nothing to do
|
||||||
for _, team := range teams {
|
return nil, nil //nolint:nilnil // return nil because the user is already assigned
|
||||||
if !team.UnitEnabled(ctx, unit.TypePullRequests) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
isMember, err := organization.IsTeamMember(ctx, repo.OwnerID, team.ID, doer.ID)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("IsTeamMember: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if isMember {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
valid, err := access_model.CanBeAssigned(ctx, assignee, issue.Repo, issue.IsPull)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !valid {
|
||||||
|
return nil, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name}
|
||||||
|
}
|
||||||
|
|
||||||
|
if notify {
|
||||||
|
_, comment, err = ToggleAssigneeWithNotify(ctx, issue, doer, assigneeID)
|
||||||
|
return comment, err
|
||||||
|
}
|
||||||
|
_, comment, err = issues_model.ToggleIssueAssignee(ctx, issue, doer, assigneeID)
|
||||||
|
return comment, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,6 @@ import (
|
|||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
system_model "code.gitea.io/gitea/models/system"
|
system_model "code.gitea.io/gitea/models/system"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/container"
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
@ -131,55 +130,6 @@ func ChangeIssueRef(ctx context.Context, issue *issues_model.Issue, doer *user_m
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAssignees is a helper function to add or delete one or multiple issue assignee(s)
|
|
||||||
// Deleting is done the GitHub way (quote from their api documentation):
|
|
||||||
// https://developer.github.com/v3/issues/#edit-an-issue
|
|
||||||
// "assignees" (array): Logins for Users to assign to this issue.
|
|
||||||
// Pass one or more user logins to replace the set of assignees on this Issue.
|
|
||||||
// Send an empty array ([]) to clear all assignees from the Issue.
|
|
||||||
func UpdateAssignees(ctx context.Context, issue *issues_model.Issue, oneAssignee string, multipleAssignees []string, doer *user_model.User) (err error) {
|
|
||||||
uniqueAssignees := container.SetOf(multipleAssignees...)
|
|
||||||
|
|
||||||
// Keep the old assignee thingy for compatibility reasons
|
|
||||||
if oneAssignee != "" {
|
|
||||||
uniqueAssignees.Add(oneAssignee)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop through all assignees to add them
|
|
||||||
allNewAssignees := make([]*user_model.User, 0, len(uniqueAssignees))
|
|
||||||
for _, assigneeName := range uniqueAssignees.Values() {
|
|
||||||
assignee, err := user_model.GetUserByName(ctx, assigneeName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if user_model.IsUserBlockedBy(ctx, doer, assignee.ID) {
|
|
||||||
return user_model.ErrBlockedUser
|
|
||||||
}
|
|
||||||
|
|
||||||
allNewAssignees = append(allNewAssignees, assignee)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete all old assignees not passed
|
|
||||||
if err = DeleteNotPassedAssignee(ctx, issue, doer, allNewAssignees); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add all new assignees
|
|
||||||
// Update the assignee. The function will check if the user exists, is already
|
|
||||||
// assigned (which he shouldn't as we deleted all assignees before) and
|
|
||||||
// has access to the repo.
|
|
||||||
for _, assignee := range allNewAssignees {
|
|
||||||
// Extra method to prevent double adding (which would result in removing)
|
|
||||||
_, err = AddAssigneeIfNotAssigned(ctx, issue, doer, assignee.ID, true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteIssue deletes an issue
|
// DeleteIssue deletes an issue
|
||||||
func DeleteIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) error {
|
func DeleteIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) error {
|
||||||
// load issue before deleting it
|
// load issue before deleting it
|
||||||
@ -214,40 +164,6 @@ func DeleteIssue(ctx context.Context, doer *user_model.User, issue *issues_model
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddAssigneeIfNotAssigned adds an assignee only if he isn't already assigned to the issue.
|
|
||||||
// Also checks for access of assigned user
|
|
||||||
func AddAssigneeIfNotAssigned(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, assigneeID int64, notify bool) (comment *issues_model.Comment, err error) {
|
|
||||||
assignee, err := user_model.GetUserByID(ctx, assigneeID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the user is already assigned
|
|
||||||
isAssigned, err := issues_model.IsUserAssignedToIssue(ctx, issue, assignee)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if isAssigned {
|
|
||||||
// nothing to do
|
|
||||||
return nil, nil //nolint:nilnil // return nil because the user is already assigned
|
|
||||||
}
|
|
||||||
|
|
||||||
valid, err := access_model.CanBeAssigned(ctx, assignee, issue.Repo, issue.IsPull)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !valid {
|
|
||||||
return nil, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name}
|
|
||||||
}
|
|
||||||
|
|
||||||
if notify {
|
|
||||||
_, comment, err = ToggleAssigneeWithNotify(ctx, issue, doer, assigneeID)
|
|
||||||
return comment, err
|
|
||||||
}
|
|
||||||
_, comment, err = issues_model.ToggleIssueAssignee(ctx, issue, doer, assigneeID)
|
|
||||||
return comment, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRefEndNamesAndURLs retrieves the ref end names (e.g. refs/heads/branch-name -> branch-name)
|
// GetRefEndNamesAndURLs retrieves the ref end names (e.g. refs/heads/branch-name -> branch-name)
|
||||||
// and their respective URLs.
|
// and their respective URLs.
|
||||||
func GetRefEndNamesAndURLs(issues []*issues_model.Issue, repoLink string) (map[int64]string, map[int64]string) {
|
func GetRefEndNamesAndURLs(issues []*issues_model.Issue, repoLink string) (map[int64]string, map[int64]string) {
|
||||||
|
|||||||
@ -133,7 +133,7 @@ func PullRequestCodeOwnersReview(ctx context.Context, pr *issues_model.PullReque
|
|||||||
if u.ID != issue.Poster.ID && !contain(latestReviews, u) {
|
if u.ID != issue.Poster.ID && !contain(latestReviews, u) {
|
||||||
comment, err := issues_model.AddReviewRequest(ctx, issue, u, issue.Poster, true)
|
comment, err := issues_model.AddReviewRequest(ctx, issue, u, issue.Poster, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Failed add assignee user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err)
|
log.Warn("Failed add review user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if comment == nil { // comment maybe nil if review type is ReviewTypeRequest
|
if comment == nil { // comment maybe nil if review type is ReviewTypeRequest
|
||||||
@ -150,7 +150,7 @@ func PullRequestCodeOwnersReview(ctx context.Context, pr *issues_model.PullReque
|
|||||||
for _, t := range uniqTeams {
|
for _, t := range uniqTeams {
|
||||||
comment, err := issues_model.AddTeamReviewRequest(ctx, issue, t, issue.Poster, true)
|
comment, err := issues_model.AddTeamReviewRequest(ctx, issue, t, issue.Poster, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Failed add assignee team: %s to PR review: %s#%d, error: %s", t.Name, pr.BaseRepo.Name, pr.ID, err)
|
log.Warn("Failed add reviewer team: %s to PR review: %s#%d, error: %s", t.Name, pr.BaseRepo.Name, pr.ID, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if comment == nil { // comment maybe nil if review type is ReviewTypeRequest
|
if comment == nil { // comment maybe nil if review type is ReviewTypeRequest
|
||||||
|
|||||||
283
services/issue/review_request.go
Normal file
283
services/issue/review_request.go
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package issue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
|
"code.gitea.io/gitea/models/organization"
|
||||||
|
"code.gitea.io/gitea/models/perm"
|
||||||
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/models/unit"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
notify_service "code.gitea.io/gitea/services/notify"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReviewRequest add or remove a review request from a user for this PR, and make comment for it.
|
||||||
|
func ReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, permDoer *access_model.Permission, reviewer *user_model.User, isAdd bool) (comment *issues_model.Comment, err error) {
|
||||||
|
err = isValidReviewRequest(ctx, reviewer, doer, isAdd, issue, permDoer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if isAdd {
|
||||||
|
comment, err = issues_model.AddReviewRequest(ctx, issue, reviewer, doer, false)
|
||||||
|
} else {
|
||||||
|
comment, err = issues_model.RemoveReviewRequest(ctx, issue, reviewer, doer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if comment != nil {
|
||||||
|
notify_service.PullRequestReviewRequest(ctx, doer, issue, reviewer, isAdd, comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
return comment, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidReviewRequest Check permission for ReviewRequest
|
||||||
|
func isValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User, isAdd bool, issue *issues_model.Issue, permDoer *access_model.Permission) error {
|
||||||
|
if reviewer.IsOrganization() {
|
||||||
|
return issues_model.ErrNotValidReviewRequest{
|
||||||
|
Reason: "Organization can't be added as reviewer",
|
||||||
|
UserID: doer.ID,
|
||||||
|
RepoID: issue.Repo.ID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if doer.IsOrganization() {
|
||||||
|
return issues_model.ErrNotValidReviewRequest{
|
||||||
|
Reason: "Organization can't be doer to add reviewer",
|
||||||
|
UserID: doer.ID,
|
||||||
|
RepoID: issue.Repo.ID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
permReviewer, err := access_model.GetIndividualUserRepoPermission(ctx, issue.Repo, reviewer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if permDoer == nil {
|
||||||
|
permDoer = new(access_model.Permission)
|
||||||
|
*permDoer, err = access_model.GetDoerRepoPermission(ctx, issue.Repo, doer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastReview, err := issues_model.GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID)
|
||||||
|
if err != nil && !issues_model.IsErrReviewNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue.PosterID)
|
||||||
|
|
||||||
|
if isAdd {
|
||||||
|
if !permReviewer.CanAccessAny(perm.AccessModeRead, unit.TypePullRequests) {
|
||||||
|
return issues_model.ErrNotValidReviewRequest{
|
||||||
|
Reason: "Reviewer can't read",
|
||||||
|
UserID: doer.ID,
|
||||||
|
RepoID: issue.Repo.ID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if reviewer.ID == issue.PosterID && issue.OriginalAuthorID == 0 {
|
||||||
|
return issues_model.ErrNotValidReviewRequest{
|
||||||
|
Reason: "poster of pr can't be reviewer",
|
||||||
|
UserID: doer.ID,
|
||||||
|
RepoID: issue.Repo.ID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if canDoerChangeReviewRequests {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if doer.ID == issue.PosterID && issue.OriginalAuthorID == 0 && lastReview != nil && lastReview.Type != issues_model.ReviewTypeRequest {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return issues_model.ErrNotValidReviewRequest{
|
||||||
|
Reason: "Doer can't choose reviewer",
|
||||||
|
UserID: doer.ID,
|
||||||
|
RepoID: issue.Repo.ID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if canDoerChangeReviewRequests {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastReview != nil && lastReview.Type == issues_model.ReviewTypeRequest && lastReview.ReviewerID == doer.ID {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return issues_model.ErrNotValidReviewRequest{
|
||||||
|
Reason: "Doer can't remove reviewer",
|
||||||
|
UserID: doer.ID,
|
||||||
|
RepoID: issue.Repo.ID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidTeamReviewRequest Check permission for ReviewRequest Team
|
||||||
|
func isValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team, doer *user_model.User, isAdd bool, issue *issues_model.Issue) error {
|
||||||
|
if doer.IsOrganization() {
|
||||||
|
return issues_model.ErrNotValidReviewRequest{
|
||||||
|
Reason: "Organization can't be doer to add reviewer",
|
||||||
|
UserID: doer.ID,
|
||||||
|
RepoID: issue.Repo.ID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue.PosterID)
|
||||||
|
|
||||||
|
if isAdd {
|
||||||
|
if issue.Repo.IsPrivate {
|
||||||
|
hasTeam := organization.HasTeamRepo(ctx, reviewer.OrgID, reviewer.ID, issue.RepoID)
|
||||||
|
|
||||||
|
if !hasTeam {
|
||||||
|
return issues_model.ErrNotValidReviewRequest{
|
||||||
|
Reason: "Reviewing team can't read repo",
|
||||||
|
UserID: doer.ID,
|
||||||
|
RepoID: issue.Repo.ID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if canDoerChangeReviewRequests {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return issues_model.ErrNotValidReviewRequest{
|
||||||
|
Reason: "Doer can't choose reviewer",
|
||||||
|
UserID: doer.ID,
|
||||||
|
RepoID: issue.Repo.ID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if canDoerChangeReviewRequests {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return issues_model.ErrNotValidReviewRequest{
|
||||||
|
Reason: "Doer can't remove reviewer",
|
||||||
|
UserID: doer.ID,
|
||||||
|
RepoID: issue.Repo.ID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TeamReviewRequest add or remove a review request from a team for this PR, and make comment for it.
|
||||||
|
func TeamReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewer *organization.Team, isAdd bool) (comment *issues_model.Comment, err error) {
|
||||||
|
err = isValidTeamReviewRequest(ctx, reviewer, doer, isAdd, issue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if isAdd {
|
||||||
|
comment, err = issues_model.AddTeamReviewRequest(ctx, issue, reviewer, doer, false)
|
||||||
|
} else {
|
||||||
|
comment, err = issues_model.RemoveTeamReviewRequest(ctx, issue, reviewer, doer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if comment == nil || !isAdd {
|
||||||
|
return nil, nil //nolint:nilnil // return nil because no comment was created or it is a removal
|
||||||
|
}
|
||||||
|
|
||||||
|
return comment, teamReviewRequestNotify(ctx, issue, doer, reviewer, isAdd, comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReviewRequestNotify(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewNotifiers []*ReviewRequestNotifier) {
|
||||||
|
for _, reviewNotifier := range reviewNotifiers {
|
||||||
|
if reviewNotifier.Reviewer != nil {
|
||||||
|
notify_service.PullRequestReviewRequest(ctx, issue.Poster, issue, reviewNotifier.Reviewer, reviewNotifier.IsAdd, reviewNotifier.Comment)
|
||||||
|
} else if reviewNotifier.ReviewTeam != nil {
|
||||||
|
if err := teamReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifier.ReviewTeam, reviewNotifier.IsAdd, reviewNotifier.Comment); err != nil {
|
||||||
|
log.Error("teamReviewRequestNotify: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// teamReviewRequestNotify notify all user in this team
|
||||||
|
func teamReviewRequestNotify(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewer *organization.Team, isAdd bool, comment *issues_model.Comment) error {
|
||||||
|
// notify all user in this team
|
||||||
|
if err := comment.LoadIssue(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
members, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{
|
||||||
|
TeamID: reviewer.ID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, member := range members {
|
||||||
|
if member.ID == comment.Issue.PosterID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
comment.AssigneeID = member.ID
|
||||||
|
notify_service.PullRequestReviewRequest(ctx, doer, issue, member, isAdd, comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanDoerChangeReviewRequests returns if the doer can add/remove review requests of a PR
|
||||||
|
func CanDoerChangeReviewRequests(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, posterID int64) bool {
|
||||||
|
if repo.IsArchived {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// The poster of the PR can change the reviewers
|
||||||
|
if doer.ID == posterID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// The owner of the repo can change the reviewers
|
||||||
|
if doer.ID == repo.OwnerID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collaborators of the repo can change the reviewers
|
||||||
|
isCollaborator, err := repo_model.IsCollaborator(ctx, repo.ID, doer.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("IsCollaborator: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if isCollaborator {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the repo's owner is an organization, members of teams with read permission on pull requests can change reviewers
|
||||||
|
if repo.Owner.IsOrganization() {
|
||||||
|
teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypePullRequests)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("GetTeamsWithAccessToRepo: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, team := range teams {
|
||||||
|
if !team.UnitEnabled(ctx, unit.TypePullRequests) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
isMember, err := organization.IsTeamMember(ctx, repo.OwnerID, team.ID, doer.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("IsTeamMember: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isMember {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user