mirror of
https://github.com/go-gitea/gitea.git
synced 2024-12-15 01:23:45 +08:00
fe49cb0243
This PR rewrites `GetReviewer` function and move it to service layer. Reviewers should not be watchers, so that this PR removed all watchers from reviewers. When the repository is under an organization, the pull request unit read permission will be checked to resolve the bug of #32394 Fix #32394
445 lines
12 KiB
Go
445 lines
12 KiB
Go
// Copyright 2024 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package repo
|
|
|
|
import (
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"code.gitea.io/gitea/models/db"
|
|
issues_model "code.gitea.io/gitea/models/issues"
|
|
"code.gitea.io/gitea/models/organization"
|
|
project_model "code.gitea.io/gitea/models/project"
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
"code.gitea.io/gitea/modules/container"
|
|
"code.gitea.io/gitea/modules/optional"
|
|
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
|
"code.gitea.io/gitea/services/context"
|
|
issue_service "code.gitea.io/gitea/services/issue"
|
|
pull_service "code.gitea.io/gitea/services/pull"
|
|
)
|
|
|
|
type issueSidebarMilestoneData struct {
|
|
SelectedMilestoneID int64
|
|
OpenMilestones []*issues_model.Milestone
|
|
ClosedMilestones []*issues_model.Milestone
|
|
}
|
|
|
|
type issueSidebarAssigneesData struct {
|
|
SelectedAssigneeIDs string
|
|
CandidateAssignees []*user_model.User
|
|
}
|
|
|
|
type issueSidebarProjectsData struct {
|
|
SelectedProjectID int64
|
|
OpenProjects []*project_model.Project
|
|
ClosedProjects []*project_model.Project
|
|
}
|
|
|
|
type IssuePageMetaData struct {
|
|
RepoLink string
|
|
Repository *repo_model.Repository
|
|
Issue *issues_model.Issue
|
|
IsPullRequest bool
|
|
CanModifyIssueOrPull bool
|
|
|
|
ReviewersData *issueSidebarReviewersData
|
|
LabelsData *issueSidebarLabelsData
|
|
MilestonesData *issueSidebarMilestoneData
|
|
ProjectsData *issueSidebarProjectsData
|
|
AssigneesData *issueSidebarAssigneesData
|
|
}
|
|
|
|
func retrieveRepoIssueMetaData(ctx *context.Context, repo *repo_model.Repository, issue *issues_model.Issue, isPull bool) *IssuePageMetaData {
|
|
data := &IssuePageMetaData{
|
|
RepoLink: ctx.Repo.RepoLink,
|
|
Repository: repo,
|
|
Issue: issue,
|
|
IsPullRequest: isPull,
|
|
|
|
ReviewersData: &issueSidebarReviewersData{},
|
|
LabelsData: &issueSidebarLabelsData{},
|
|
MilestonesData: &issueSidebarMilestoneData{},
|
|
ProjectsData: &issueSidebarProjectsData{},
|
|
AssigneesData: &issueSidebarAssigneesData{},
|
|
}
|
|
ctx.Data["IssuePageMetaData"] = data
|
|
|
|
if isPull {
|
|
data.retrieveReviewersData(ctx)
|
|
if ctx.Written() {
|
|
return data
|
|
}
|
|
}
|
|
data.retrieveLabelsData(ctx)
|
|
if ctx.Written() {
|
|
return data
|
|
}
|
|
|
|
data.CanModifyIssueOrPull = ctx.Repo.CanWriteIssuesOrPulls(isPull) && !ctx.Repo.Repository.IsArchived
|
|
if !data.CanModifyIssueOrPull {
|
|
return data
|
|
}
|
|
|
|
data.retrieveAssigneesDataForIssueWriter(ctx)
|
|
if ctx.Written() {
|
|
return data
|
|
}
|
|
|
|
data.retrieveMilestonesDataForIssueWriter(ctx)
|
|
if ctx.Written() {
|
|
return data
|
|
}
|
|
|
|
data.retrieveProjectsDataForIssueWriter(ctx)
|
|
if ctx.Written() {
|
|
return data
|
|
}
|
|
|
|
PrepareBranchList(ctx)
|
|
if ctx.Written() {
|
|
return data
|
|
}
|
|
|
|
ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx, ctx.Doer, isPull)
|
|
return data
|
|
}
|
|
|
|
func (d *IssuePageMetaData) retrieveMilestonesDataForIssueWriter(ctx *context.Context) {
|
|
var err error
|
|
if d.Issue != nil {
|
|
d.MilestonesData.SelectedMilestoneID = d.Issue.MilestoneID
|
|
}
|
|
d.MilestonesData.OpenMilestones, err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
|
|
RepoID: d.Repository.ID,
|
|
IsClosed: optional.Some(false),
|
|
})
|
|
if err != nil {
|
|
ctx.ServerError("GetMilestones", err)
|
|
return
|
|
}
|
|
d.MilestonesData.ClosedMilestones, err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
|
|
RepoID: d.Repository.ID,
|
|
IsClosed: optional.Some(true),
|
|
})
|
|
if err != nil {
|
|
ctx.ServerError("GetMilestones", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
func (d *IssuePageMetaData) retrieveAssigneesDataForIssueWriter(ctx *context.Context) {
|
|
var err error
|
|
d.AssigneesData.CandidateAssignees, err = repo_model.GetRepoAssignees(ctx, d.Repository)
|
|
if err != nil {
|
|
ctx.ServerError("GetRepoAssignees", err)
|
|
return
|
|
}
|
|
d.AssigneesData.CandidateAssignees = shared_user.MakeSelfOnTop(ctx.Doer, d.AssigneesData.CandidateAssignees)
|
|
if d.Issue != nil {
|
|
_ = d.Issue.LoadAssignees(ctx)
|
|
ids := make([]string, 0, len(d.Issue.Assignees))
|
|
for _, a := range d.Issue.Assignees {
|
|
ids = append(ids, strconv.FormatInt(a.ID, 10))
|
|
}
|
|
d.AssigneesData.SelectedAssigneeIDs = strings.Join(ids, ",")
|
|
}
|
|
// FIXME: this is a tricky part which writes ctx.Data["Mentionable*"]
|
|
handleTeamMentions(ctx)
|
|
}
|
|
|
|
func (d *IssuePageMetaData) retrieveProjectsDataForIssueWriter(ctx *context.Context) {
|
|
if d.Issue != nil && d.Issue.Project != nil {
|
|
d.ProjectsData.SelectedProjectID = d.Issue.Project.ID
|
|
}
|
|
d.ProjectsData.OpenProjects, d.ProjectsData.ClosedProjects = retrieveProjectsInternal(ctx, ctx.Repo.Repository)
|
|
}
|
|
|
|
// repoReviewerSelection items to bee shown
|
|
type repoReviewerSelection struct {
|
|
IsTeam bool
|
|
Team *organization.Team
|
|
User *user_model.User
|
|
Review *issues_model.Review
|
|
CanBeDismissed bool
|
|
CanChange bool
|
|
Requested bool
|
|
ItemID int64
|
|
}
|
|
|
|
type issueSidebarReviewersData struct {
|
|
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 (d *IssuePageMetaData) retrieveReviewersData(ctx *context.Context) {
|
|
data := d.ReviewersData
|
|
repo := d.Repository
|
|
if ctx.Doer != nil && ctx.IsSigned {
|
|
if d.Issue == nil {
|
|
data.CanChooseReviewer = true
|
|
} else {
|
|
data.CanChooseReviewer = issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, repo, d.Issue.PosterID)
|
|
}
|
|
}
|
|
|
|
var posterID int64
|
|
var isClosed bool
|
|
var reviews issues_model.ReviewList
|
|
|
|
if d.Issue == nil {
|
|
posterID = ctx.Doer.ID
|
|
} else {
|
|
posterID = d.Issue.PosterID
|
|
if d.Issue.OriginalAuthorID > 0 {
|
|
posterID = 0 // for migrated PRs, no poster ID
|
|
}
|
|
|
|
isClosed = d.Issue.IsClosed || d.Issue.PullRequest.HasMerged
|
|
|
|
originalAuthorReviews, err := issues_model.GetReviewersFromOriginalAuthorsByIssueID(ctx, d.Issue.ID)
|
|
if err != nil {
|
|
ctx.ServerError("GetReviewersFromOriginalAuthorsByIssueID", err)
|
|
return
|
|
}
|
|
data.OriginalReviews = originalAuthorReviews
|
|
|
|
reviews, err = issues_model.GetReviewsByIssueID(ctx, d.Issue.ID)
|
|
if err != nil {
|
|
ctx.ServerError("GetReviewersByIssueID", err)
|
|
return
|
|
}
|
|
if len(reviews) == 0 && !data.CanChooseReviewer {
|
|
return
|
|
}
|
|
}
|
|
|
|
var (
|
|
pullReviews []*repoReviewerSelection
|
|
reviewersResult []*repoReviewerSelection
|
|
teamReviewersResult []*repoReviewerSelection
|
|
teamReviewers []*organization.Team
|
|
reviewers []*user_model.User
|
|
)
|
|
|
|
if data.CanChooseReviewer {
|
|
var err error
|
|
reviewers, err = pull_service.GetReviewers(ctx, repo, ctx.Doer.ID, posterID)
|
|
if err != nil {
|
|
ctx.ServerError("GetReviewers", err)
|
|
return
|
|
}
|
|
|
|
teamReviewers, err = pull_service.GetReviewerTeams(ctx, repo)
|
|
if err != nil {
|
|
ctx.ServerError("GetReviewerTeams", err)
|
|
return
|
|
}
|
|
|
|
if len(reviewers) > 0 {
|
|
reviewersResult = make([]*repoReviewerSelection, 0, len(reviewers))
|
|
}
|
|
|
|
if len(teamReviewers) > 0 {
|
|
teamReviewersResult = make([]*repoReviewerSelection, 0, len(teamReviewers))
|
|
}
|
|
}
|
|
|
|
pullReviews = make([]*repoReviewerSelection, 0, len(reviews))
|
|
|
|
for _, review := range reviews {
|
|
tmp := &repoReviewerSelection{
|
|
Requested: review.Type == issues_model.ReviewTypeRequest,
|
|
Review: review,
|
|
ItemID: review.ReviewerID,
|
|
}
|
|
if review.ReviewerTeamID > 0 {
|
|
tmp.IsTeam = true
|
|
tmp.ItemID = -review.ReviewerTeamID
|
|
}
|
|
|
|
if data.CanChooseReviewer {
|
|
// Users who can choose reviewers can also remove review requests
|
|
tmp.CanChange = true
|
|
} else if ctx.Doer != nil && ctx.Doer.ID == review.ReviewerID && review.Type == issues_model.ReviewTypeRequest {
|
|
// A user can refuse review requests
|
|
tmp.CanChange = true
|
|
}
|
|
|
|
pullReviews = append(pullReviews, tmp)
|
|
|
|
if data.CanChooseReviewer {
|
|
if tmp.IsTeam {
|
|
teamReviewersResult = append(teamReviewersResult, tmp)
|
|
} else {
|
|
reviewersResult = append(reviewersResult, tmp)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(pullReviews) > 0 {
|
|
// Drop all non-existing users and teams from the reviews
|
|
currentPullReviewers := make([]*repoReviewerSelection, 0, len(pullReviews))
|
|
for _, item := range pullReviews {
|
|
if item.Review.ReviewerID > 0 {
|
|
if err := item.Review.LoadReviewer(ctx); err != nil {
|
|
if user_model.IsErrUserNotExist(err) {
|
|
continue
|
|
}
|
|
ctx.ServerError("LoadReviewer", err)
|
|
return
|
|
}
|
|
item.User = item.Review.Reviewer
|
|
} else if item.Review.ReviewerTeamID > 0 {
|
|
if err := item.Review.LoadReviewerTeam(ctx); err != nil {
|
|
if organization.IsErrTeamNotExist(err) {
|
|
continue
|
|
}
|
|
ctx.ServerError("LoadReviewerTeam", err)
|
|
return
|
|
}
|
|
item.Team = item.Review.ReviewerTeam
|
|
} 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)
|
|
}
|
|
data.CurrentPullReviewers = currentPullReviewers
|
|
}
|
|
|
|
if data.CanChooseReviewer && reviewersResult != nil {
|
|
preadded := len(reviewersResult)
|
|
for _, reviewer := range reviewers {
|
|
found := false
|
|
reviewAddLoop:
|
|
for _, tmp := range reviewersResult[:preadded] {
|
|
if tmp.ItemID == reviewer.ID {
|
|
tmp.User = reviewer
|
|
found = true
|
|
break reviewAddLoop
|
|
}
|
|
}
|
|
|
|
if found {
|
|
continue
|
|
}
|
|
|
|
reviewersResult = append(reviewersResult, &repoReviewerSelection{
|
|
IsTeam: false,
|
|
CanChange: true,
|
|
User: reviewer,
|
|
ItemID: reviewer.ID,
|
|
})
|
|
}
|
|
|
|
data.Reviewers = reviewersResult
|
|
}
|
|
|
|
if data.CanChooseReviewer && teamReviewersResult != nil {
|
|
preadded := len(teamReviewersResult)
|
|
for _, team := range teamReviewers {
|
|
found := false
|
|
teamReviewAddLoop:
|
|
for _, tmp := range teamReviewersResult[:preadded] {
|
|
if tmp.ItemID == -team.ID {
|
|
tmp.Team = team
|
|
found = true
|
|
break teamReviewAddLoop
|
|
}
|
|
}
|
|
|
|
if found {
|
|
continue
|
|
}
|
|
|
|
teamReviewersResult = append(teamReviewersResult, &repoReviewerSelection{
|
|
IsTeam: true,
|
|
CanChange: true,
|
|
Team: team,
|
|
ItemID: -team.ID,
|
|
})
|
|
}
|
|
|
|
data.TeamReviewers = teamReviewersResult
|
|
}
|
|
}
|
|
|
|
type issueSidebarLabelsData struct {
|
|
AllLabels []*issues_model.Label
|
|
RepoLabels []*issues_model.Label
|
|
OrgLabels []*issues_model.Label
|
|
SelectedLabelIDs string
|
|
}
|
|
|
|
func makeSelectedStringIDs[KeyType, ItemType comparable](
|
|
allLabels []*issues_model.Label, candidateKey func(candidate *issues_model.Label) KeyType,
|
|
selectedItems []ItemType, selectedKey func(selected ItemType) KeyType,
|
|
) string {
|
|
selectedIDSet := make(container.Set[string])
|
|
allLabelMap := map[KeyType]*issues_model.Label{}
|
|
for _, label := range allLabels {
|
|
allLabelMap[candidateKey(label)] = label
|
|
}
|
|
for _, item := range selectedItems {
|
|
if label, ok := allLabelMap[selectedKey(item)]; ok {
|
|
label.IsChecked = true
|
|
selectedIDSet.Add(strconv.FormatInt(label.ID, 10))
|
|
}
|
|
}
|
|
ids := selectedIDSet.Values()
|
|
sort.Strings(ids)
|
|
return strings.Join(ids, ",")
|
|
}
|
|
|
|
func (d *issueSidebarLabelsData) SetSelectedLabels(labels []*issues_model.Label) {
|
|
d.SelectedLabelIDs = makeSelectedStringIDs(
|
|
d.AllLabels, func(label *issues_model.Label) int64 { return label.ID },
|
|
labels, func(label *issues_model.Label) int64 { return label.ID },
|
|
)
|
|
}
|
|
|
|
func (d *issueSidebarLabelsData) SetSelectedLabelNames(labelNames []string) {
|
|
d.SelectedLabelIDs = makeSelectedStringIDs(
|
|
d.AllLabels, func(label *issues_model.Label) string { return strings.ToLower(label.Name) },
|
|
labelNames, strings.ToLower,
|
|
)
|
|
}
|
|
|
|
func (d *issueSidebarLabelsData) SetSelectedLabelIDs(labelIDs []int64) {
|
|
d.SelectedLabelIDs = makeSelectedStringIDs(
|
|
d.AllLabels, func(label *issues_model.Label) int64 { return label.ID },
|
|
labelIDs, func(labelID int64) int64 { return labelID },
|
|
)
|
|
}
|
|
|
|
func (d *IssuePageMetaData) retrieveLabelsData(ctx *context.Context) {
|
|
repo := d.Repository
|
|
labelsData := d.LabelsData
|
|
|
|
labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
|
|
if err != nil {
|
|
ctx.ServerError("GetLabelsByRepoID", err)
|
|
return
|
|
}
|
|
labelsData.RepoLabels = labels
|
|
|
|
if repo.Owner.IsOrganization() {
|
|
orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
|
|
if err != nil {
|
|
return
|
|
}
|
|
labelsData.OrgLabels = orgLabels
|
|
}
|
|
labelsData.AllLabels = append(labelsData.AllLabels, labelsData.RepoLabels...)
|
|
labelsData.AllLabels = append(labelsData.AllLabels, labelsData.OrgLabels...)
|
|
}
|