2024-11-10 21:28:54 -07:00
|
|
|
// 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"
|
2024-11-22 08:44:48 -07:00
|
|
|
pull_service "code.gitea.io/gitea/services/pull"
|
2024-11-10 21:28:54 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
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*"]
|
2024-12-04 07:57:50 -07:00
|
|
|
handleMentionableAssigneesAndTeams(ctx, d.AssigneesData.CandidateAssignees)
|
2024-11-10 21:28:54 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2024-11-22 08:44:48 -07:00
|
|
|
data.CanChooseReviewer = issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, repo, d.Issue.PosterID)
|
2024-11-10 21:28:54 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2024-11-22 08:44:48 -07:00
|
|
|
reviewers, err = pull_service.GetReviewers(ctx, repo, ctx.Doer.ID, posterID)
|
2024-11-10 21:28:54 -07:00
|
|
|
if err != nil {
|
|
|
|
ctx.ServerError("GetReviewers", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-11-22 08:44:48 -07:00
|
|
|
teamReviewers, err = pull_service.GetReviewerTeams(ctx, repo)
|
2024-11-10 21:28:54 -07:00
|
|
|
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...)
|
|
|
|
}
|