1
1
mirror of https://github.com/go-gitea/gitea synced 2025-07-03 09:07:19 +00:00

Add reviewers selection to new pull request (#32403)

Users could add reviewers when creating new PRs.

---------

Co-authored-by: splitt3r <splitt3r@users.noreply.github.com>
Co-authored-by: Sebastian Sauer <sauer.sebastian@gmail.com>
Co-authored-by: bb-ben <70356237+bboerben@users.noreply.github.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Calvin K
2024-11-09 12:48:31 +08:00
committed by GitHub
parent d80f99ef04
commit 18aeca5320
26 changed files with 503 additions and 271 deletions

View File

@ -554,7 +554,19 @@ func CreatePullRequest(ctx *context.APIContext) {
}
}
if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs); err != nil {
prOpts := &pull_service.NewPullRequestOptions{
Repo: repo,
Issue: prIssue,
LabelIDs: labelIDs,
PullRequest: pr,
AssigneeIDs: assigneeIDs,
}
prOpts.Reviewers, prOpts.TeamReviewers = parseReviewersByNames(ctx, form.Reviewers, form.TeamReviewers)
if ctx.Written() {
return
}
if err := pull_service.NewPullRequest(ctx, prOpts); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
} else if errors.Is(err, user_model.ErrBlockedUser) {

View File

@ -656,6 +656,47 @@ func DeleteReviewRequests(ctx *context.APIContext) {
apiReviewRequest(ctx, *opts, false)
}
func parseReviewersByNames(ctx *context.APIContext, reviewerNames, teamReviewerNames []string) (reviewers []*user_model.User, teamReviewers []*organization.Team) {
var err error
for _, r := range reviewerNames {
var reviewer *user_model.User
if strings.Contains(r, "@") {
reviewer, err = user_model.GetUserByEmail(ctx, r)
} else {
reviewer, err = user_model.GetUserByName(ctx, r)
}
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.NotFound("UserNotExist", fmt.Sprintf("User '%s' not exist", r))
return nil, nil
}
ctx.Error(http.StatusInternalServerError, "GetUser", err)
return nil, nil
}
reviewers = append(reviewers, reviewer)
}
if ctx.Repo.Repository.Owner.IsOrganization() && len(teamReviewerNames) > 0 {
for _, t := range teamReviewerNames {
var teamReviewer *organization.Team
teamReviewer, err = organization.GetTeam(ctx, ctx.Repo.Owner.ID, t)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.NotFound("TeamNotExist", fmt.Sprintf("Team '%s' not exist", t))
return nil, nil
}
ctx.Error(http.StatusInternalServerError, "ReviewRequest", err)
return nil, nil
}
teamReviewers = append(teamReviewers, teamReviewer)
}
}
return reviewers, teamReviewers
}
func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions, isAdd bool) {
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index"))
if err != nil {
@ -672,42 +713,15 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
return
}
reviewers := make([]*user_model.User, 0, len(opts.Reviewers))
permDoer, err := access_model.GetUserRepoPermission(ctx, pr.Issue.Repo, ctx.Doer)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
return
}
for _, r := range opts.Reviewers {
var reviewer *user_model.User
if strings.Contains(r, "@") {
reviewer, err = user_model.GetUserByEmail(ctx, r)
} else {
reviewer, err = user_model.GetUserByName(ctx, r)
}
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.NotFound("UserNotExist", fmt.Sprintf("User '%s' not exist", r))
return
}
ctx.Error(http.StatusInternalServerError, "GetUser", err)
return
}
err = issue_service.IsValidReviewRequest(ctx, reviewer, ctx.Doer, isAdd, pr.Issue, &permDoer)
if err != nil {
if issues_model.IsErrNotValidReviewRequest(err) {
ctx.Error(http.StatusUnprocessableEntity, "NotValidReviewRequest", err)
return
}
ctx.Error(http.StatusInternalServerError, "IsValidReviewRequest", err)
return
}
reviewers = append(reviewers, reviewer)
reviewers, teamReviewers := parseReviewersByNames(ctx, opts.Reviewers, opts.TeamReviewers)
if ctx.Written() {
return
}
var reviews []*issues_model.Review
@ -716,12 +730,16 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
}
for _, reviewer := range reviewers {
comment, err := issue_service.ReviewRequest(ctx, pr.Issue, ctx.Doer, reviewer, isAdd)
comment, err := issue_service.ReviewRequest(ctx, pr.Issue, ctx.Doer, &permDoer, reviewer, isAdd)
if err != nil {
if issues_model.IsErrReviewRequestOnClosedPR(err) {
ctx.Error(http.StatusForbidden, "", err)
return
}
if issues_model.IsErrNotValidReviewRequest(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
return
}
ctx.Error(http.StatusInternalServerError, "ReviewRequest", err)
return
}
@ -736,35 +754,17 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
}
if ctx.Repo.Repository.Owner.IsOrganization() && len(opts.TeamReviewers) > 0 {
teamReviewers := make([]*organization.Team, 0, len(opts.TeamReviewers))
for _, t := range opts.TeamReviewers {
var teamReviewer *organization.Team
teamReviewer, err = organization.GetTeam(ctx, ctx.Repo.Owner.ID, t)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.NotFound("TeamNotExist", fmt.Sprintf("Team '%s' not exist", t))
return
}
ctx.Error(http.StatusInternalServerError, "ReviewRequest", err)
return
}
err = issue_service.IsValidTeamReviewRequest(ctx, teamReviewer, ctx.Doer, isAdd, pr.Issue)
if err != nil {
if issues_model.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(ctx, pr.Issue, ctx.Doer, teamReviewer, isAdd)
if err != nil {
if issues_model.IsErrReviewRequestOnClosedPR(err) {
ctx.Error(http.StatusForbidden, "", err)
return
}
if issues_model.IsErrNotValidReviewRequest(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
return
}
ctx.ServerError("TeamReviewRequest", err)
return
}

View File

@ -792,6 +792,10 @@ func CompareDiff(ctx *context.Context) {
if ctx.Written() {
return
}
RetrieveRepoReviewers(ctx, ctx.Repo.Repository, nil, true)
if ctx.Written() {
return
}
}
}
beforeCommitID := ctx.Data["BeforeCommitID"].(string)

View File

@ -654,34 +654,66 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
// repoReviewerSelection items to bee shown
type repoReviewerSelection struct {
IsTeam bool
Team *organization.Team
User *user_model.User
Review *issues_model.Review
CanChange bool
Checked bool
ItemID int64
IsTeam bool
Team *organization.Team
User *user_model.User
Review *issues_model.Review
CanBeDismissed bool
CanChange bool
Requested bool
ItemID int64
}
// RetrieveRepoReviewers find all reviewers of a repository
type issueSidebarReviewersData struct {
Repository *repo_model.Repository
RepoOwnerName string
RepoLink string
IssueID int64
CanChooseReviewer bool
OriginalReviews issues_model.ReviewList
TeamReviewers []*repoReviewerSelection
Reviewers []*repoReviewerSelection
CurrentPullReviewers []*repoReviewerSelection
}
// RetrieveRepoReviewers find all reviewers of a repository. If issue is nil, it means the doer is creating a new PR.
func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, issue *issues_model.Issue, canChooseReviewer bool) {
ctx.Data["CanChooseReviewer"] = canChooseReviewer
data := &issueSidebarReviewersData{}
data.RepoLink = ctx.Repo.RepoLink
data.Repository = repo
data.RepoOwnerName = repo.OwnerName
data.CanChooseReviewer = canChooseReviewer
originalAuthorReviews, err := issues_model.GetReviewersFromOriginalAuthorsByIssueID(ctx, issue.ID)
if err != nil {
ctx.ServerError("GetReviewersFromOriginalAuthorsByIssueID", err)
return
}
ctx.Data["OriginalReviews"] = originalAuthorReviews
var posterID int64
var isClosed bool
var reviews issues_model.ReviewList
reviews, err := issues_model.GetReviewsByIssueID(ctx, issue.ID)
if err != nil {
ctx.ServerError("GetReviewersByIssueID", err)
return
}
if issue == nil {
posterID = ctx.Doer.ID
} else {
posterID = issue.PosterID
if issue.OriginalAuthorID > 0 {
posterID = 0 // for migrated PRs, no poster ID
}
if len(reviews) == 0 && !canChooseReviewer {
return
data.IssueID = issue.ID
isClosed = issue.IsClosed || issue.PullRequest.HasMerged
originalAuthorReviews, err := issues_model.GetReviewersFromOriginalAuthorsByIssueID(ctx, issue.ID)
if err != nil {
ctx.ServerError("GetReviewersFromOriginalAuthorsByIssueID", err)
return
}
data.OriginalReviews = originalAuthorReviews
reviews, err = issues_model.GetReviewsByIssueID(ctx, issue.ID)
if err != nil {
ctx.ServerError("GetReviewersByIssueID", err)
return
}
if len(reviews) == 0 && !canChooseReviewer {
return
}
}
var (
@ -693,11 +725,7 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
)
if canChooseReviewer {
posterID := issue.PosterID
if issue.OriginalAuthorID > 0 {
posterID = 0
}
var err error
reviewers, err = repo_model.GetReviewers(ctx, repo, ctx.Doer.ID, posterID)
if err != nil {
ctx.ServerError("GetReviewers", err)
@ -723,9 +751,9 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
for _, review := range reviews {
tmp := &repoReviewerSelection{
Checked: review.Type == issues_model.ReviewTypeRequest,
Review: review,
ItemID: review.ReviewerID,
Requested: review.Type == issues_model.ReviewTypeRequest,
Review: review,
ItemID: review.ReviewerID,
}
if review.ReviewerTeamID > 0 {
tmp.IsTeam = true
@ -756,7 +784,7 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
currentPullReviewers := make([]*repoReviewerSelection, 0, len(pullReviews))
for _, item := range pullReviews {
if item.Review.ReviewerID > 0 {
if err = item.Review.LoadReviewer(ctx); err != nil {
if err := item.Review.LoadReviewer(ctx); err != nil {
if user_model.IsErrUserNotExist(err) {
continue
}
@ -765,7 +793,7 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
}
item.User = item.Review.Reviewer
} else if item.Review.ReviewerTeamID > 0 {
if err = item.Review.LoadReviewerTeam(ctx); err != nil {
if err := item.Review.LoadReviewerTeam(ctx); err != nil {
if organization.IsErrTeamNotExist(err) {
continue
}
@ -776,10 +804,11 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
} else {
continue
}
item.CanBeDismissed = ctx.Repo.Permission.IsAdmin() && !isClosed &&
(item.Review.Type == issues_model.ReviewTypeApprove || item.Review.Type == issues_model.ReviewTypeReject)
currentPullReviewers = append(currentPullReviewers, item)
}
ctx.Data["PullReviewers"] = currentPullReviewers
data.CurrentPullReviewers = currentPullReviewers
}
if canChooseReviewer && reviewersResult != nil {
@ -807,7 +836,7 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
})
}
ctx.Data["Reviewers"] = reviewersResult
data.Reviewers = reviewersResult
}
if canChooseReviewer && teamReviewersResult != nil {
@ -835,8 +864,10 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
})
}
ctx.Data["TeamReviewers"] = teamReviewersResult
data.TeamReviewers = teamReviewersResult
}
ctx.Data["IssueSidebarReviewersData"] = data
}
// RetrieveRepoMetas find all the meta information of a repository
@ -1117,7 +1148,14 @@ func DeleteIssue(ctx *context.Context) {
}
// ValidateRepoMetas check and returns repository's meta information
func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull bool) ([]int64, []int64, int64, int64) {
func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull bool) (ret struct {
LabelIDs, AssigneeIDs []int64
MilestoneID, ProjectID int64
Reviewers []*user_model.User
TeamReviewers []*organization.Team
},
) {
var (
repo = ctx.Repo.Repository
err error
@ -1125,7 +1163,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository, isPull)
if ctx.Written() {
return nil, nil, 0, 0
return ret
}
var labelIDs []int64
@ -1134,7 +1172,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
if len(form.LabelIDs) > 0 {
labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
if err != nil {
return nil, nil, 0, 0
return ret
}
labelIDMark := make(container.Set[int64])
labelIDMark.AddMultiple(labelIDs...)
@ -1157,11 +1195,11 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID)
if err != nil {
ctx.ServerError("GetMilestoneByID", err)
return nil, nil, 0, 0
return ret
}
if milestone.RepoID != repo.ID {
ctx.ServerError("GetMilestoneByID", err)
return nil, nil, 0, 0
return ret
}
ctx.Data["Milestone"] = milestone
ctx.Data["milestone_id"] = milestoneID
@ -1171,11 +1209,11 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
p, err := project_model.GetProjectByID(ctx, form.ProjectID)
if err != nil {
ctx.ServerError("GetProjectByID", err)
return nil, nil, 0, 0
return ret
}
if p.RepoID != ctx.Repo.Repository.ID && p.OwnerID != ctx.Repo.Repository.OwnerID {
ctx.NotFound("", nil)
return nil, nil, 0, 0
return ret
}
ctx.Data["Project"] = p
@ -1187,7 +1225,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
if len(form.AssigneeIDs) > 0 {
assigneeIDs, err = base.StringsToInt64s(strings.Split(form.AssigneeIDs, ","))
if err != nil {
return nil, nil, 0, 0
return ret
}
// Check if the passed assignees actually exists and is assignable
@ -1195,18 +1233,18 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
assignee, err := user_model.GetUserByID(ctx, aID)
if err != nil {
ctx.ServerError("GetUserByID", err)
return nil, nil, 0, 0
return ret
}
valid, err := access_model.CanBeAssigned(ctx, assignee, repo, isPull)
if err != nil {
ctx.ServerError("CanBeAssigned", err)
return nil, nil, 0, 0
return ret
}
if !valid {
ctx.ServerError("canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
return nil, nil, 0, 0
return ret
}
}
}
@ -1216,7 +1254,39 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
assigneeIDs = append(assigneeIDs, form.AssigneeID)
}
return labelIDs, assigneeIDs, milestoneID, form.ProjectID
// Check reviewers
var reviewers []*user_model.User
var teamReviewers []*organization.Team
if isPull && len(form.ReviewerIDs) > 0 {
reviewerIDs, err := base.StringsToInt64s(strings.Split(form.ReviewerIDs, ","))
if err != nil {
return ret
}
// Check if the passed reviewers (user/team) actually exist
for _, rID := range reviewerIDs {
// negative reviewIDs represent team requests
if rID < 0 {
teamReviewer, err := organization.GetTeamByID(ctx, -rID)
if err != nil {
ctx.ServerError("GetTeamByID", err)
return ret
}
teamReviewers = append(teamReviewers, teamReviewer)
continue
}
reviewer, err := user_model.GetUserByID(ctx, rID)
if err != nil {
ctx.ServerError("GetUserByID", err)
return ret
}
reviewers = append(reviewers, reviewer)
}
}
ret.LabelIDs, ret.AssigneeIDs, ret.MilestoneID, ret.ProjectID = labelIDs, assigneeIDs, milestoneID, form.ProjectID
ret.Reviewers, ret.TeamReviewers = reviewers, teamReviewers
return ret
}
// NewIssuePost response for creating new issue
@ -1234,11 +1304,13 @@ func NewIssuePost(ctx *context.Context) {
attachments []string
)
labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, false)
validateRet := ValidateRepoMetas(ctx, *form, false)
if ctx.Written() {
return
}
labelIDs, assigneeIDs, milestoneID, projectID := validateRet.LabelIDs, validateRet.AssigneeIDs, validateRet.MilestoneID, validateRet.ProjectID
if projectID > 0 {
if !ctx.Repo.CanRead(unit.TypeProjects) {
// User must also be able to see the project.
@ -2479,7 +2551,7 @@ func UpdatePullReviewRequest(ctx *context.Context) {
return
}
err = issue_service.IsValidTeamReviewRequest(ctx, team, ctx.Doer, action == "attach", issue)
_, err = issue_service.TeamReviewRequest(ctx, issue, ctx.Doer, team, action == "attach")
if err != nil {
if issues_model.IsErrNotValidReviewRequest(err) {
log.Warn(
@ -2490,12 +2562,6 @@ func UpdatePullReviewRequest(ctx *context.Context) {
ctx.Status(http.StatusForbidden)
return
}
ctx.ServerError("IsValidTeamReviewRequest", err)
return
}
_, err = issue_service.TeamReviewRequest(ctx, issue, ctx.Doer, team, action == "attach")
if err != nil {
ctx.ServerError("TeamReviewRequest", err)
return
}
@ -2517,7 +2583,7 @@ func UpdatePullReviewRequest(ctx *context.Context) {
return
}
err = issue_service.IsValidReviewRequest(ctx, reviewer, ctx.Doer, action == "attach", issue, nil)
_, err = issue_service.ReviewRequest(ctx, issue, ctx.Doer, &ctx.Repo.Permission, reviewer, action == "attach")
if err != nil {
if issues_model.IsErrNotValidReviewRequest(err) {
log.Warn(
@ -2528,12 +2594,6 @@ func UpdatePullReviewRequest(ctx *context.Context) {
ctx.Status(http.StatusForbidden)
return
}
ctx.ServerError("isValidReviewRequest", err)
return
}
_, err = issue_service.ReviewRequest(ctx, issue, ctx.Doer, reviewer, action == "attach")
if err != nil {
if issues_model.IsErrReviewRequestOnClosedPR(err) {
ctx.Status(http.StatusForbidden)
return

View File

@ -1269,11 +1269,13 @@ func CompareAndPullRequestPost(ctx *context.Context) {
return
}
labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, true)
validateRet := ValidateRepoMetas(ctx, *form, true)
if ctx.Written() {
return
}
labelIDs, assigneeIDs, milestoneID, projectID := validateRet.LabelIDs, validateRet.AssigneeIDs, validateRet.MilestoneID, validateRet.ProjectID
if setting.Attachment.Enabled {
attachments = form.Files
}
@ -1318,8 +1320,17 @@ func CompareAndPullRequestPost(ctx *context.Context) {
}
// FIXME: check error in the case two people send pull request at almost same time, give nice error prompt
// instead of 500.
if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil {
prOpts := &pull_service.NewPullRequestOptions{
Repo: repo,
Issue: pullIssue,
LabelIDs: labelIDs,
AttachmentUUIDs: attachments,
PullRequest: pullRequest,
AssigneeIDs: assigneeIDs,
Reviewers: validateRet.Reviewers,
TeamReviewers: validateRet.TeamReviewers,
}
if err := pull_service.NewPullRequest(ctx, prOpts); err != nil {
switch {
case repo_model.IsErrUserDoesNotHaveAccessToRepo(err):
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())