diff options
Diffstat (limited to 'routers/web/repo/issue_page_meta.go')
-rw-r--r-- | routers/web/repo/issue_page_meta.go | 444 |
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...) +} |