| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798 |
- // Copyright 2024 The Gitea Authors. All rights reserved.
- // SPDX-License-Identifier: MIT
-
- package repo
-
- import (
- "bytes"
- "maps"
- "net/http"
- "slices"
- "sort"
- "strconv"
- "strings"
-
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/organization"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
- db_indexer "code.gitea.io/gitea/modules/indexer/issues/db"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/routers/web/shared/issue"
- shared_user "code.gitea.io/gitea/routers/web/shared/user"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/convert"
- issue_service "code.gitea.io/gitea/services/issue"
- pull_service "code.gitea.io/gitea/services/pull"
- )
-
- func retrieveProjectsForIssueList(ctx *context.Context, repo *repo_model.Repository) {
- ctx.Data["OpenProjects"], ctx.Data["ClosedProjects"] = retrieveProjectsInternal(ctx, repo)
- }
-
- // SearchIssues searches for issues across the repositories that the user has access to
- func SearchIssues(ctx *context.Context) {
- before, since, err := context.GetQueryBeforeSince(ctx.Base)
- if err != nil {
- ctx.HTTPError(http.StatusUnprocessableEntity, err.Error())
- return
- }
-
- var isClosed optional.Option[bool]
- switch ctx.FormString("state") {
- case "closed":
- isClosed = optional.Some(true)
- case "all":
- isClosed = optional.None[bool]()
- default:
- isClosed = optional.Some(false)
- }
-
- var (
- repoIDs []int64
- allPublic bool
- )
- {
- // find repos user can access (for issue search)
- opts := repo_model.SearchRepoOptions{
- Private: false,
- AllPublic: true,
- TopicOnly: false,
- Collaborate: optional.None[bool](),
- // This needs to be a column that is not nil in fixtures or
- // MySQL will return different results when sorting by null in some cases
- OrderBy: db.SearchOrderByAlphabetically,
- Actor: ctx.Doer,
- }
- if ctx.IsSigned {
- opts.Private = true
- opts.AllLimited = true
- }
- if ctx.FormString("owner") != "" {
- owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
- if err != nil {
- if user_model.IsErrUserNotExist(err) {
- ctx.HTTPError(http.StatusBadRequest, "Owner not found", err.Error())
- } else {
- ctx.HTTPError(http.StatusInternalServerError, "GetUserByName", err.Error())
- }
- return
- }
- opts.OwnerID = owner.ID
- opts.AllLimited = false
- opts.AllPublic = false
- opts.Collaborate = optional.Some(false)
- }
- if ctx.FormString("team") != "" {
- if ctx.FormString("owner") == "" {
- ctx.HTTPError(http.StatusBadRequest, "", "Owner organisation is required for filtering on team")
- return
- }
- team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team"))
- if err != nil {
- if organization.IsErrTeamNotExist(err) {
- ctx.HTTPError(http.StatusBadRequest, "Team not found", err.Error())
- } else {
- ctx.HTTPError(http.StatusInternalServerError, "GetUserByName", err.Error())
- }
- return
- }
- opts.TeamID = team.ID
- }
-
- if opts.AllPublic {
- allPublic = true
- opts.AllPublic = false // set it false to avoid returning too many repos, we could filter by indexer
- }
- repoIDs, _, err = repo_model.SearchRepositoryIDs(ctx, opts)
- if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, "SearchRepositoryIDs", err.Error())
- return
- }
- if len(repoIDs) == 0 {
- // no repos found, don't let the indexer return all repos
- repoIDs = []int64{0}
- }
- }
-
- keyword := ctx.FormTrim("q")
- if strings.IndexByte(keyword, 0) >= 0 {
- keyword = ""
- }
-
- isPull := optional.None[bool]()
- switch ctx.FormString("type") {
- case "pulls":
- isPull = optional.Some(true)
- case "issues":
- isPull = optional.Some(false)
- }
-
- var includedAnyLabels []int64
- {
- labels := ctx.FormTrim("labels")
- var includedLabelNames []string
- if len(labels) > 0 {
- includedLabelNames = strings.Split(labels, ",")
- }
- includedAnyLabels, err = issues_model.GetLabelIDsByNames(ctx, includedLabelNames)
- if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, "GetLabelIDsByNames", err.Error())
- return
- }
- }
-
- var includedMilestones []int64
- {
- milestones := ctx.FormTrim("milestones")
- var includedMilestoneNames []string
- if len(milestones) > 0 {
- includedMilestoneNames = strings.Split(milestones, ",")
- }
- includedMilestones, err = issues_model.GetMilestoneIDsByNames(ctx, includedMilestoneNames)
- if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, "GetMilestoneIDsByNames", err.Error())
- return
- }
- }
-
- projectID := optional.None[int64]()
- if v := ctx.FormInt64("project"); v > 0 {
- projectID = optional.Some(v)
- }
-
- // this api is also used in UI,
- // so the default limit is set to fit UI needs
- limit := ctx.FormInt("limit")
- if limit == 0 {
- limit = setting.UI.IssuePagingNum
- } else if limit > setting.API.MaxResponseItems {
- limit = setting.API.MaxResponseItems
- }
-
- searchOpt := &issue_indexer.SearchOptions{
- Paginator: &db.ListOptions{
- Page: ctx.FormInt("page"),
- PageSize: limit,
- },
- Keyword: keyword,
- RepoIDs: repoIDs,
- AllPublic: allPublic,
- IsPull: isPull,
- IsClosed: isClosed,
- IncludedAnyLabelIDs: includedAnyLabels,
- MilestoneIDs: includedMilestones,
- ProjectID: projectID,
- SortBy: issue_indexer.SortByCreatedDesc,
- }
-
- if since != 0 {
- searchOpt.UpdatedAfterUnix = optional.Some(since)
- }
- if before != 0 {
- searchOpt.UpdatedBeforeUnix = optional.Some(before)
- }
-
- if ctx.IsSigned {
- ctxUserID := ctx.Doer.ID
- if ctx.FormBool("created") {
- searchOpt.PosterID = strconv.FormatInt(ctxUserID, 10)
- }
- if ctx.FormBool("assigned") {
- searchOpt.AssigneeID = strconv.FormatInt(ctxUserID, 10)
- }
- if ctx.FormBool("mentioned") {
- searchOpt.MentionID = optional.Some(ctxUserID)
- }
- if ctx.FormBool("review_requested") {
- searchOpt.ReviewRequestedID = optional.Some(ctxUserID)
- }
- if ctx.FormBool("reviewed") {
- searchOpt.ReviewedID = optional.Some(ctxUserID)
- }
- }
-
- // FIXME: It's unsupported to sort by priority repo when searching by indexer,
- // it's indeed an regression, but I think it is worth to support filtering by indexer first.
- _ = ctx.FormInt64("priority_repo_id")
-
- ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
- if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, "SearchIssues", err.Error())
- return
- }
- issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
- if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, "FindIssuesByIDs", err.Error())
- return
- }
-
- ctx.SetTotalCountHeader(total)
- ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, ctx.Doer, issues))
- }
-
- func getUserIDForFilter(ctx *context.Context, queryName string) int64 {
- userName := ctx.FormString(queryName)
- if len(userName) == 0 {
- return 0
- }
-
- user, err := user_model.GetUserByName(ctx, userName)
- if user_model.IsErrUserNotExist(err) {
- ctx.NotFound(err)
- return 0
- }
-
- if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
- return 0
- }
-
- return user.ID
- }
-
- // SearchRepoIssuesJSON lists the issues of a repository
- // This function was copied from API (decouple the web and API routes),
- // it is only used by frontend to search some dependency or related issues
- func SearchRepoIssuesJSON(ctx *context.Context) {
- before, since, err := context.GetQueryBeforeSince(ctx.Base)
- if err != nil {
- ctx.HTTPError(http.StatusUnprocessableEntity, err.Error())
- return
- }
-
- var isClosed optional.Option[bool]
- switch ctx.FormString("state") {
- case "closed":
- isClosed = optional.Some(true)
- case "all":
- isClosed = optional.None[bool]()
- default:
- isClosed = optional.Some(false)
- }
-
- keyword := ctx.FormTrim("q")
- if strings.IndexByte(keyword, 0) >= 0 {
- keyword = ""
- }
-
- var mileIDs []int64
- if part := strings.Split(ctx.FormString("milestones"), ","); len(part) > 0 {
- for i := range part {
- // uses names and fall back to ids
- // non-existent milestones are discarded
- mile, err := issues_model.GetMilestoneByRepoIDANDName(ctx, ctx.Repo.Repository.ID, part[i])
- if err == nil {
- mileIDs = append(mileIDs, mile.ID)
- continue
- }
- if !issues_model.IsErrMilestoneNotExist(err) {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
- return
- }
- id, err := strconv.ParseInt(part[i], 10, 64)
- if err != nil {
- continue
- }
- mile, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, id)
- if err == nil {
- mileIDs = append(mileIDs, mile.ID)
- continue
- }
- if issues_model.IsErrMilestoneNotExist(err) {
- continue
- }
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
- }
- }
-
- projectID := optional.None[int64]()
- if v := ctx.FormInt64("project"); v > 0 {
- projectID = optional.Some(v)
- }
-
- isPull := optional.None[bool]()
- switch ctx.FormString("type") {
- case "pulls":
- isPull = optional.Some(true)
- case "issues":
- isPull = optional.Some(false)
- }
-
- // FIXME: we should be more efficient here
- createdByID := getUserIDForFilter(ctx, "created_by")
- if ctx.Written() {
- return
- }
- assignedByID := getUserIDForFilter(ctx, "assigned_by")
- if ctx.Written() {
- return
- }
- mentionedByID := getUserIDForFilter(ctx, "mentioned_by")
- if ctx.Written() {
- return
- }
-
- searchOpt := &issue_indexer.SearchOptions{
- Paginator: &db.ListOptions{
- Page: ctx.FormInt("page"),
- PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
- },
- Keyword: keyword,
- RepoIDs: []int64{ctx.Repo.Repository.ID},
- IsPull: isPull,
- IsClosed: isClosed,
- ProjectID: projectID,
- SortBy: issue_indexer.SortByCreatedDesc,
- }
- if since != 0 {
- searchOpt.UpdatedAfterUnix = optional.Some(since)
- }
- if before != 0 {
- searchOpt.UpdatedBeforeUnix = optional.Some(before)
- }
-
- // TODO: the "labels" query parameter is never used, so no need to handle it
-
- if len(mileIDs) == 1 && mileIDs[0] == db.NoConditionID {
- searchOpt.MilestoneIDs = []int64{0}
- } else {
- searchOpt.MilestoneIDs = mileIDs
- }
-
- if createdByID > 0 {
- searchOpt.PosterID = strconv.FormatInt(createdByID, 10)
- }
- if assignedByID > 0 {
- searchOpt.AssigneeID = strconv.FormatInt(assignedByID, 10)
- }
- if mentionedByID > 0 {
- searchOpt.MentionID = optional.Some(mentionedByID)
- }
-
- ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
- if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, "SearchIssues", err.Error())
- return
- }
- issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
- if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, "FindIssuesByIDs", err.Error())
- return
- }
-
- ctx.SetTotalCountHeader(total)
- ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, ctx.Doer, issues))
- }
-
- func BatchDeleteIssues(ctx *context.Context) {
- issues := getActionIssues(ctx)
- if ctx.Written() {
- return
- }
- for _, issue := range issues {
- if err := issue_service.DeleteIssue(ctx, ctx.Doer, issue); err != nil {
- ctx.ServerError("DeleteIssue", err)
- return
- }
- }
- ctx.JSONOK()
- }
-
- // UpdateIssueStatus change issue's status
- func UpdateIssueStatus(ctx *context.Context) {
- issues := getActionIssues(ctx)
- if ctx.Written() {
- return
- }
-
- action := ctx.FormString("action")
- if action != "open" && action != "close" {
- log.Warn("Unrecognized action: %s", action)
- ctx.JSONOK()
- return
- }
-
- if _, err := issues.LoadRepositories(ctx); err != nil {
- ctx.ServerError("LoadRepositories", err)
- return
- }
- if err := issues.LoadPullRequests(ctx); err != nil {
- ctx.ServerError("LoadPullRequests", err)
- return
- }
-
- for _, issue := range issues {
- if issue.IsPull && issue.PullRequest.HasMerged {
- continue
- }
- if action == "close" && !issue.IsClosed {
- if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
- if issues_model.IsErrDependenciesLeft(err) {
- ctx.JSON(http.StatusPreconditionFailed, map[string]any{
- "error": ctx.Tr("repo.issues.dependency.issue_batch_close_blocked", issue.Index),
- })
- return
- }
- ctx.ServerError("CloseIssue", err)
- return
- }
- } else if action == "open" && issue.IsClosed {
- if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil {
- ctx.ServerError("ReopenIssue", err)
- return
- }
- }
- }
- ctx.JSONOK()
- }
-
- func prepareIssueFilterExclusiveOrderScopes(ctx *context.Context, allLabels []*issues_model.Label) {
- scopeSet := make(map[string]bool)
- for _, label := range allLabels {
- scope := label.ExclusiveScope()
- if len(scope) > 0 && label.ExclusiveOrder > 0 {
- scopeSet[scope] = true
- }
- }
- scopes := slices.Collect(maps.Keys(scopeSet))
- sort.Strings(scopes)
- ctx.Data["ExclusiveLabelScopes"] = scopes
- }
-
- func renderMilestones(ctx *context.Context) {
- // Get milestones
- milestones, err := db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
- RepoID: ctx.Repo.Repository.ID,
- })
- if err != nil {
- ctx.ServerError("GetAllRepoMilestones", err)
- return
- }
-
- openMilestones, closedMilestones := issues_model.MilestoneList{}, issues_model.MilestoneList{}
- for _, milestone := range milestones {
- if milestone.IsClosed {
- closedMilestones = append(closedMilestones, milestone)
- } else {
- openMilestones = append(openMilestones, milestone)
- }
- }
- ctx.Data["OpenMilestones"] = openMilestones
- ctx.Data["ClosedMilestones"] = closedMilestones
- }
-
- func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int64, isPullOption optional.Option[bool]) {
- var err error
- viewType := ctx.FormString("type")
- sortType := ctx.FormString("sort")
- types := []string{"all", "your_repositories", "assigned", "created_by", "mentioned", "review_requested", "reviewed_by"}
- if !util.SliceContainsString(types, viewType, true) {
- viewType = "all"
- }
-
- assigneeID := ctx.FormString("assignee")
- posterUsername := ctx.FormString("poster")
- posterUserID := shared_user.GetFilterUserIDByName(ctx, posterUsername)
- var mentionedID, reviewRequestedID, reviewedID int64
-
- if ctx.IsSigned {
- switch viewType {
- case "created_by":
- posterUserID = strconv.FormatInt(ctx.Doer.ID, 10)
- case "mentioned":
- mentionedID = ctx.Doer.ID
- case "assigned":
- assigneeID = strconv.FormatInt(ctx.Doer.ID, 10)
- case "review_requested":
- reviewRequestedID = ctx.Doer.ID
- case "reviewed_by":
- reviewedID = ctx.Doer.ID
- }
- }
-
- repo := ctx.Repo.Repository
- keyword := strings.Trim(ctx.FormString("q"), " ")
- if bytes.Contains([]byte(keyword), []byte{0x00}) {
- keyword = ""
- }
-
- var mileIDs []int64
- if milestoneID > 0 || milestoneID == db.NoConditionID { // -1 to get those issues which have no any milestone assigned
- mileIDs = []int64{milestoneID}
- }
-
- preparedLabelFilter := issue.PrepareFilterIssueLabels(ctx, repo.ID, ctx.Repo.Owner)
- if ctx.Written() {
- return
- }
-
- prepareIssueFilterExclusiveOrderScopes(ctx, preparedLabelFilter.AllLabels)
-
- var keywordMatchedIssueIDs []int64
- var issueStats *issues_model.IssueStats
- statsOpts := &issues_model.IssuesOptions{
- RepoIDs: []int64{repo.ID},
- LabelIDs: preparedLabelFilter.SelectedLabelIDs,
- MilestoneIDs: mileIDs,
- ProjectID: projectID,
- AssigneeID: assigneeID,
- MentionedID: mentionedID,
- PosterID: posterUserID,
- ReviewRequestedID: reviewRequestedID,
- ReviewedID: reviewedID,
- IsPull: isPullOption,
- IssueIDs: nil,
- }
- if keyword != "" {
- keywordMatchedIssueIDs, _, err = issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, statsOpts))
- if err != nil {
- if issue_indexer.IsAvailable(ctx) {
- ctx.ServerError("issueIDsFromSearch", err)
- return
- }
- ctx.Data["IssueIndexerUnavailable"] = true
- return
- }
- if len(keywordMatchedIssueIDs) == 0 {
- // It did search with the keyword, but no issue found, just set issueStats to empty, then no need to do query again.
- issueStats = &issues_model.IssueStats{}
- // set keywordMatchedIssueIDs to empty slice, so we can distinguish it from "nil"
- keywordMatchedIssueIDs = []int64{}
- }
- statsOpts.IssueIDs = keywordMatchedIssueIDs
- }
-
- if issueStats == nil {
- // Either it did search with the keyword, and found some issues, it needs to get issueStats of these issues.
- // Or the keyword is empty, so it doesn't need issueIDs as filter, just get issueStats with statsOpts.
- issueStats, err = issues_model.GetIssueStats(ctx, statsOpts)
- if err != nil {
- ctx.ServerError("GetIssueStats", err)
- return
- }
- }
-
- var isShowClosed optional.Option[bool]
- switch ctx.FormString("state") {
- case "closed":
- isShowClosed = optional.Some(true)
- case "all":
- isShowClosed = optional.None[bool]()
- default:
- isShowClosed = optional.Some(false)
- }
- // if there are closed issues and no open issues, default to showing all issues
- if len(ctx.FormString("state")) == 0 && issueStats.OpenCount == 0 && issueStats.ClosedCount != 0 {
- isShowClosed = optional.None[bool]()
- }
-
- if repo.IsTimetrackerEnabled(ctx) {
- totalTrackedTime, err := issues_model.GetIssueTotalTrackedTime(ctx, statsOpts, isShowClosed)
- if err != nil {
- ctx.ServerError("GetIssueTotalTrackedTime", err)
- return
- }
- ctx.Data["TotalTrackedTime"] = totalTrackedTime
- }
-
- // prepare pager
- total := int(issueStats.OpenCount + issueStats.ClosedCount)
- if isShowClosed.Has() {
- total = util.Iif(isShowClosed.Value(), int(issueStats.ClosedCount), int(issueStats.OpenCount))
- }
- page := max(ctx.FormInt("page"), 1)
- pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
-
- // prepare real issue list:
- var issues issues_model.IssueList
- if keywordMatchedIssueIDs == nil || len(keywordMatchedIssueIDs) > 0 {
- // Either it did search with the keyword, and found some issues, then keywordMatchedIssueIDs is not null, it needs to use db indexer.
- // Or the keyword is empty, it also needs to usd db indexer.
- // In either case, no need to use keyword anymore
- searchResult, err := db_indexer.GetIndexer().FindWithIssueOptions(ctx, &issues_model.IssuesOptions{
- Paginator: &db.ListOptions{
- Page: pager.Paginater.Current(),
- PageSize: setting.UI.IssuePagingNum,
- },
- RepoIDs: []int64{repo.ID},
- AssigneeID: assigneeID,
- PosterID: posterUserID,
- MentionedID: mentionedID,
- ReviewRequestedID: reviewRequestedID,
- ReviewedID: reviewedID,
- MilestoneIDs: mileIDs,
- ProjectID: projectID,
- IsClosed: isShowClosed,
- IsPull: isPullOption,
- LabelIDs: preparedLabelFilter.SelectedLabelIDs,
- SortType: sortType,
- IssueIDs: keywordMatchedIssueIDs,
- })
- if err != nil {
- ctx.ServerError("DBIndexer.Search", err)
- return
- }
- issueIDs := issue_indexer.SearchResultToIDSlice(searchResult)
- issues, err = issues_model.GetIssuesByIDs(ctx, issueIDs, true)
- if err != nil {
- ctx.ServerError("GetIssuesByIDs", err)
- return
- }
- }
-
- approvalCounts, err := issues.GetApprovalCounts(ctx)
- if err != nil {
- ctx.ServerError("ApprovalCounts", err)
- return
- }
-
- if ctx.IsSigned {
- if err := issues.LoadIsRead(ctx, ctx.Doer.ID); err != nil {
- ctx.ServerError("LoadIsRead", err)
- return
- }
- } else {
- for i := range issues {
- issues[i].IsRead = true
- }
- }
-
- commitStatuses, lastStatus, err := pull_service.GetIssuesAllCommitStatus(ctx, issues)
- if err != nil {
- ctx.ServerError("GetIssuesAllCommitStatus", err)
- return
- }
- if !ctx.Repo.CanRead(unit.TypeActions) {
- for key := range commitStatuses {
- git_model.CommitStatusesHideActionsURL(ctx, commitStatuses[key])
- }
- }
-
- if err := issues.LoadAttributes(ctx); err != nil {
- ctx.ServerError("issues.LoadAttributes", err)
- return
- }
-
- ctx.Data["Issues"] = issues
- ctx.Data["CommitLastStatus"] = lastStatus
- ctx.Data["CommitStatuses"] = commitStatuses
-
- // Get assignees.
- assigneeUsers, err := repo_model.GetRepoAssignees(ctx, repo)
- if err != nil {
- ctx.ServerError("GetRepoAssignees", err)
- return
- }
- handleMentionableAssigneesAndTeams(ctx, shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers))
- if ctx.Written() {
- return
- }
-
- ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, ctx.Repo.RepoLink)
-
- ctx.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 {
- counts, ok := approvalCounts[issueID]
- if !ok || len(counts) == 0 {
- return 0
- }
- reviewTyp := issues_model.ReviewTypeApprove
- switch typ {
- case "reject":
- reviewTyp = issues_model.ReviewTypeReject
- case "waiting":
- reviewTyp = issues_model.ReviewTypeRequest
- }
- for _, count := range counts {
- if count.Type == reviewTyp {
- return count.Count
- }
- }
- return 0
- }
-
- retrieveProjectsForIssueList(ctx, repo)
- if ctx.Written() {
- return
- }
-
- pinned, err := issues_model.GetPinnedIssues(ctx, repo.ID, isPullOption.Value())
- if err != nil {
- ctx.ServerError("GetPinnedIssues", err)
- return
- }
-
- showArchivedLabels := ctx.FormBool("archived_labels")
- ctx.Data["ShowArchivedLabels"] = showArchivedLabels
- ctx.Data["PinnedIssues"] = pinned
- ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.Doer.IsAdmin)
- ctx.Data["IssueStats"] = issueStats
- ctx.Data["OpenCount"] = issueStats.OpenCount
- ctx.Data["ClosedCount"] = issueStats.ClosedCount
- ctx.Data["SelLabelIDs"] = preparedLabelFilter.SelectedLabelIDs
- ctx.Data["ViewType"] = viewType
- ctx.Data["SortType"] = sortType
- ctx.Data["MilestoneID"] = milestoneID
- ctx.Data["ProjectID"] = projectID
- ctx.Data["AssigneeID"] = assigneeID
- ctx.Data["PosterUsername"] = posterUsername
- ctx.Data["Keyword"] = keyword
- ctx.Data["IsShowClosed"] = isShowClosed
- switch {
- case isShowClosed.Value():
- ctx.Data["State"] = "closed"
- case !isShowClosed.Has():
- ctx.Data["State"] = "all"
- default:
- ctx.Data["State"] = "open"
- }
- pager.AddParamFromRequest(ctx.Req)
- ctx.Data["Page"] = pager
- }
-
- // Issues render issues page
- func Issues(ctx *context.Context) {
- isPullList := ctx.PathParam("type") == "pulls"
- if isPullList {
- MustAllowPulls(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["Title"] = ctx.Tr("repo.pulls")
- ctx.Data["PageIsPullList"] = true
- prepareRecentlyPushedNewBranches(ctx)
- if ctx.Written() {
- return
- }
- } else {
- MustEnableIssues(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["Title"] = ctx.Tr("repo.issues")
- ctx.Data["PageIsIssueList"] = true
- ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
- }
-
- prepareIssueFilterAndList(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), optional.Some(isPullList))
- if ctx.Written() {
- return
- }
-
- renderMilestones(ctx)
- if ctx.Written() {
- return
- }
-
- ctx.Data["CanWriteIssuesOrPulls"] = ctx.Repo.CanWriteIssuesOrPulls(isPullList)
-
- ctx.HTML(http.StatusOK, tplIssues)
- }
|