aboutsummaryrefslogtreecommitdiffstats
path: root/routers/web/repo/issue_page_meta.go
diff options
context:
space:
mode:
Diffstat (limited to 'routers/web/repo/issue_page_meta.go')
-rw-r--r--routers/web/repo/issue_page_meta.go444
1 files changed, 444 insertions, 0 deletions
diff --git a/routers/web/repo/issue_page_meta.go b/routers/web/repo/issue_page_meta.go
new file mode 100644
index 0000000000..ac0b1c6425
--- /dev/null
+++ b/routers/web/repo/issue_page_meta.go
@@ -0,0 +1,444 @@
+// 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"
+ repo_service "code.gitea.io/gitea/services/repository"
+)
+
+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)
+ }
+ }
+
+ 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 = repo_model.GetReviewers(ctx, repo, ctx.Doer.ID, posterID)
+ if err != nil {
+ ctx.ServerError("GetReviewers", err)
+ return
+ }
+
+ teamReviewers, err = repo_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...)
+}